@driftengine/package 4.13.0 → 4.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -59,7 +59,7 @@ Beside your `package.json`, in the project that produces the web build:
59
59
  "splash": { "show": true, "minMs": 1400 },
60
60
  "targets": ["linux-x64", "win-x64", "mac-arm64"],
61
61
  "steam": { "appId": null },
62
- "android": { "permissions": [], "cleartextTraffic": false }
62
+ "android": { "permissions": [], "cleartextTraffic": false, "orientation": "unspecified" }
63
63
  }
64
64
  ```
65
65
 
@@ -280,6 +280,23 @@ renderer cannot disagree — which they would if only the manifest moved.
280
280
  Set it only for the case it exists for: a relay on a LAN, which cannot hold a certificate for an
281
281
  address that is not a name. A public relay should be `wss://` and this should stay false.
282
282
 
283
+ ### Which way the app is held
284
+
285
+ `orientation` is the activity's `android:screenOrientation`, and it is `unspecified` by default,
286
+ which leaves it to the phone. A game played sideways says so:
287
+
288
+ ```json
289
+ "android": { "orientation": "sensorLandscape" }
290
+ ```
291
+
292
+ Any value Android documents is accepted (`landscape`, `sensorLandscape`, `portrait`,
293
+ `sensorPortrait`, `userLandscape`, `fullSensor` and the rest), and one it does not is refused when
294
+ the manifest is read, since Android ignores an attribute value it does not know and the build would
295
+ quietly be the phone's choice again. `sensorLandscape` lets a player turn the phone over;
296
+ `landscape` does not. It is written into the copied manifest like the permissions, so the activity
297
+ starts the right way round before any of the game's code runs, which a `lockOrientation` taken at
298
+ run time cannot do.
299
+
283
300
  **Signing is not optional here and there is no unsigned option to offer**: Android refuses to
284
301
  install an unsigned APK at all. Without a keystore the packager generates one and says so on every
285
302
  build. That APK installs on a device with developer mode on; it cannot go to the Play Store.
package/dist/android.js CHANGED
@@ -85,6 +85,9 @@ export async function buildAndroid(manifest, projectDir, outDir, options = {}) {
85
85
  */
86
86
  console.log('[drift-package] android: cleartext traffic permitted (http:// and ws:// allowed)');
87
87
  }
88
+ if (manifest.android.orientation !== 'unspecified') {
89
+ console.log(`[drift-package] android: held ${manifest.android.orientation}`);
90
+ }
88
91
  if (manifest.icon !== null) {
89
92
  /* One PNG at one density: Android scales it for the rest, and a game that wants a full set
90
93
  supplies its own `res/` through its own build rather than through this. */
package/dist/cli.js CHANGED
@@ -115,6 +115,9 @@ async function doctor(cwd, options = {}) {
115
115
  console.log(` android: cleartext ${manifest.android.cleartextTraffic
116
116
  ? 'permitted — http:// and ws:// are allowed, and mixed content with them'
117
117
  : 'refused — a relay must be wss://'}`);
118
+ console.log(` android: orientation ${manifest.android.orientation === 'unspecified'
119
+ ? "unspecified — the phone's own choice"
120
+ : manifest.android.orientation}`);
118
121
  }
119
122
  /*
120
123
  * **What each target still needs, and the command that fetches it.** A `doctor` that only
@@ -1,3 +1,16 @@
1
+ /**
2
+ * `drift.package.json`, parsed once and trusted afterwards.
3
+ *
4
+ * **Every rejection names its field.** A packaging manifest is written by hand, usually once, and
5
+ * a wrong value surfaces at signing or at first launch on a machine the author does not have. The
6
+ * cost of validating here is a few dozen lines; the cost of not validating is a build.
7
+ *
8
+ * **What is defaulted and what is not** is the only interesting decision in this file. Window size
9
+ * and features default, because a short manifest is easier to read and a wrong window is visible
10
+ * immediately. The id, the entry, the targets and the backend policy do not, because each of them
11
+ * is silent when wrong: the wrong id writes saves to a directory nobody looks in.
12
+ */
13
+ import { type AndroidOrientation } from './mobile/androidManifest.ts';
1
14
  export type WebGpuPolicy = 'prefer' | 'require' | 'off';
2
15
  export type Target = 'win-x64' | 'mac-arm64' | 'mac-x64' | 'linux-x64' | 'native-linux-x64' | 'android' | 'ios';
3
16
  /** The two that are a phone rather than a desktop, and differ in almost every way that matters. */
@@ -49,10 +62,16 @@ export interface PackageManifest {
49
62
  * sets `android:usesCleartextTraffic` *and* is what `MainActivity` reads at runtime to allow
50
63
  * mixed content in the WebView, so the platform and the renderer cannot disagree about it. A
51
64
  * relay on a LAN cannot hold a certificate, which is the case it exists for.
65
+ *
66
+ * **`orientation` is `unspecified` by default**, which leaves it to the phone, and takes any
67
+ * value Android documents for an activity (`landscape`, `sensorLandscape`, `portrait`, ...). A
68
+ * game played sideways says so here, and the activity starts that way before any of its code
69
+ * runs; one Android does not know is refused rather than written.
52
70
  */
53
71
  readonly android: {
54
72
  readonly permissions: readonly string[];
55
73
  readonly cleartextTraffic: boolean;
74
+ readonly orientation: AndroidOrientation;
56
75
  };
57
76
  /**
58
77
  * Who an installer says this is from.
package/dist/manifest.js CHANGED
@@ -10,7 +10,7 @@
10
10
  * immediately. The id, the entry, the targets and the backend policy do not, because each of them
11
11
  * is silent when wrong: the wrong id writes saves to a directory nobody looks in.
12
12
  */
13
- import { checkPermission, fullPermissionName } from './mobile/androidManifest.js';
13
+ import { checkOrientation, checkPermission, fullPermissionName, } from './mobile/androidManifest.js';
14
14
  /** The two that are a phone rather than a desktop, and differ in almost every way that matters. */
15
15
  export const MOBILE_TARGETS = ['android', 'ios'];
16
16
  const POLICIES = ['prefer', 'require', 'off'];
@@ -117,6 +117,9 @@ export function parseManifest(raw) {
117
117
  android: {
118
118
  permissions,
119
119
  cleartextTraffic: androidRaw.cleartextTraffic === true,
120
+ orientation: androidRaw.orientation === undefined
121
+ ? 'unspecified'
122
+ : checkOrientation(androidRaw.orientation),
120
123
  },
121
124
  splash: {
122
125
  show: splashRaw.show !== false,
@@ -5,9 +5,24 @@ export interface AndroidOptions {
5
5
  readonly permissions: readonly string[];
6
6
  /** Whether the app may open `http://` and `ws://` at all. */
7
7
  readonly cleartextTraffic: boolean;
8
+ /** The activity's `android:screenOrientation`, one of `ANDROID_ORIENTATIONS`. */
9
+ readonly orientation: AndroidOrientation;
8
10
  }
11
+ /**
12
+ * Every value Android documents for an activity's `android:screenOrientation`.
13
+ *
14
+ * **The whole list, not the two a landscape game wants**, because which way a game is held is the
15
+ * game's decision and the engine has no reason to prefer one: a card game is held upright, a
16
+ * racing game sideways, and `sensorLandscape` against `landscape` is whether a player may turn
17
+ * the phone over. What the list buys is a refusal at parse time, since the value is written into
18
+ * an XML attribute and Android answers one it does not know by ignoring it.
19
+ */
20
+ export declare const ANDROID_ORIENTATIONS: readonly ['unspecified', 'behind', 'landscape', 'portrait', 'reverseLandscape', 'reversePortrait', 'sensorLandscape', 'sensorPortrait', 'userLandscape', 'userPortrait', 'sensor', 'fullSensor', 'nosensor', 'user', 'fullUser', 'locked'];
21
+ export type AndroidOrientation = (typeof ANDROID_ORIENTATIONS)[number];
9
22
  /** Nothing asked for: what every build got before this existed, and still gets. */
10
23
  export declare const NO_ANDROID_OPTIONS: AndroidOptions;
24
+ /** Refuse an orientation Android does not document, at parse time where a consumer can fix it. */
25
+ export declare function checkOrientation(value: unknown): AndroidOrientation;
11
26
  /**
12
27
  * `INTERNET` and `android.permission.INTERNET` are the same thing, written two ways.
13
28
  *
@@ -20,7 +35,8 @@ export declare function fullPermissionName(name: string): string;
20
35
  /** Refuse a name that cannot go into the XML, at parse time where a consumer can fix it. */
21
36
  export declare function checkPermission(name: string): void;
22
37
  /**
23
- * The template manifest with a game's permissions in it, and its cleartext policy set.
38
+ * The template manifest with a game's permissions in it, its cleartext policy set, and its
39
+ * activity held the way the game asked.
24
40
  *
25
41
  * **Written into the copy, not into the template**, the way the icon already is: `buildAndroid`
26
42
  * stages `packages/package/android/` into `out/android/project` and edits it there, so this
@@ -1,9 +1,46 @@
1
1
  /** What a game asks the Android manifest for, written into the copied template before Gradle runs. */
2
+ /**
3
+ * Every value Android documents for an activity's `android:screenOrientation`.
4
+ *
5
+ * **The whole list, not the two a landscape game wants**, because which way a game is held is the
6
+ * game's decision and the engine has no reason to prefer one: a card game is held upright, a
7
+ * racing game sideways, and `sensorLandscape` against `landscape` is whether a player may turn
8
+ * the phone over. What the list buys is a refusal at parse time, since the value is written into
9
+ * an XML attribute and Android answers one it does not know by ignoring it.
10
+ */
11
+ export const ANDROID_ORIENTATIONS = [
12
+ 'unspecified',
13
+ 'behind',
14
+ 'landscape',
15
+ 'portrait',
16
+ 'reverseLandscape',
17
+ 'reversePortrait',
18
+ 'sensorLandscape',
19
+ 'sensorPortrait',
20
+ 'userLandscape',
21
+ 'userPortrait',
22
+ 'sensor',
23
+ 'fullSensor',
24
+ 'nosensor',
25
+ 'user',
26
+ 'fullUser',
27
+ 'locked',
28
+ ];
2
29
  /** Nothing asked for: what every build got before this existed, and still gets. */
3
30
  export const NO_ANDROID_OPTIONS = Object.freeze({
4
31
  permissions: Object.freeze([]),
5
32
  cleartextTraffic: false,
33
+ orientation: 'unspecified',
6
34
  });
35
+ /** Refuse an orientation Android does not document, at parse time where a consumer can fix it. */
36
+ export function checkOrientation(value) {
37
+ const known = ANDROID_ORIENTATIONS.find((name) => name === value);
38
+ if (known === undefined) {
39
+ throw new Error(`drift.package.json: "android.orientation" is ${JSON.stringify(value)}, expected one of ` +
40
+ `${ANDROID_ORIENTATIONS.join(', ')}.`);
41
+ }
42
+ return known;
43
+ }
7
44
  /**
8
45
  * `INTERNET` and `android.permission.INTERNET` are the same thing, written two ways.
9
46
  *
@@ -32,7 +69,8 @@ export function checkPermission(name) {
32
69
  }
33
70
  }
34
71
  /**
35
- * The template manifest with a game's permissions in it, and its cleartext policy set.
72
+ * The template manifest with a game's permissions in it, its cleartext policy set, and its
73
+ * activity held the way the game asked.
36
74
  *
37
75
  * **Written into the copy, not into the template**, the way the icon already is: `buildAndroid`
38
76
  * stages `packages/package/android/` into `out/android/project` and edits it there, so this
@@ -83,7 +121,19 @@ export function withAndroidManifest(xml, options) {
83
121
  throw new Error('the Android manifest template no longer declares android:usesCleartextTraffic, so asking ' +
84
122
  'for cleartext traffic would do nothing');
85
123
  }
86
- return replaced;
124
+ /*
125
+ * **Rewritten on the activity for the same reason**: the template carries `unspecified`, which
126
+ * leaves the choice to the phone. Held by the manifest rather than by `lockOrientation` at run
127
+ * time, because the activity is laid out before any of the game's code runs, and a lock taken
128
+ * then turns a picture the player has already seen.
129
+ */
130
+ const oriented = replaced.replace(/android:screenOrientation="[A-Za-z]+"/, `android:screenOrientation="${options.orientation}"`);
131
+ if (!/android:screenOrientation="[A-Za-z]+"/.test(replaced) &&
132
+ options.orientation !== 'unspecified') {
133
+ throw new Error('the Android manifest template no longer declares android:screenOrientation, so asking for ' +
134
+ `"${options.orientation}" would do nothing`);
135
+ }
136
+ return oriented;
87
137
  }
88
138
  /** The whitespace at the start of the line `at` falls on, so an inserted element lines up. */
89
139
  function indentOf(xml, at) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftengine/package",
3
- "version": "4.13.0",
3
+ "version": "4.13.2",
4
4
  "description": "Builds an installable application from a game built on the engine",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -49,7 +49,7 @@
49
49
  "drift-package": "bin/drift-package.mjs"
50
50
  },
51
51
  "peerDependencies": {
52
- "@driftengine/core": "4.13.0",
52
+ "@driftengine/core": "4.13.2",
53
53
  "steamworks.js": ">=0.4.0"
54
54
  },
55
55
  "dependencies": {
package/src/android.ts CHANGED
@@ -101,6 +101,9 @@ export async function buildAndroid(
101
101
  */
102
102
  console.log('[drift-package] android: cleartext traffic permitted (http:// and ws:// allowed)');
103
103
  }
104
+ if (manifest.android.orientation !== 'unspecified') {
105
+ console.log(`[drift-package] android: held ${manifest.android.orientation}`);
106
+ }
104
107
 
105
108
  if (manifest.icon !== null) {
106
109
  /* One PNG at one density: Android scales it for the rest, and a game that wants a full set
package/src/cli.ts CHANGED
@@ -125,6 +125,13 @@ async function doctor(cwd: string, options: ResourceOptions = {}): Promise<Packa
125
125
  : 'refused — a relay must be wss://'
126
126
  }`,
127
127
  );
128
+ console.log(
129
+ ` android: orientation ${
130
+ manifest.android.orientation === 'unspecified'
131
+ ? "unspecified — the phone's own choice"
132
+ : manifest.android.orientation
133
+ }`,
134
+ );
128
135
  }
129
136
 
130
137
  /*
package/src/manifest.ts CHANGED
@@ -10,7 +10,12 @@
10
10
  * immediately. The id, the entry, the targets and the backend policy do not, because each of them
11
11
  * is silent when wrong: the wrong id writes saves to a directory nobody looks in.
12
12
  */
13
- import { checkPermission, fullPermissionName } from './mobile/androidManifest.ts';
13
+ import {
14
+ checkOrientation,
15
+ checkPermission,
16
+ fullPermissionName,
17
+ type AndroidOrientation,
18
+ } from './mobile/androidManifest.ts';
14
19
 
15
20
  export type WebGpuPolicy = 'prefer' | 'require' | 'off';
16
21
  export type Target =
@@ -77,10 +82,16 @@ export interface PackageManifest {
77
82
  * sets `android:usesCleartextTraffic` *and* is what `MainActivity` reads at runtime to allow
78
83
  * mixed content in the WebView, so the platform and the renderer cannot disagree about it. A
79
84
  * relay on a LAN cannot hold a certificate, which is the case it exists for.
85
+ *
86
+ * **`orientation` is `unspecified` by default**, which leaves it to the phone, and takes any
87
+ * value Android documents for an activity (`landscape`, `sensorLandscape`, `portrait`, ...). A
88
+ * game played sideways says so here, and the activity starts that way before any of its code
89
+ * runs; one Android does not know is refused rather than written.
80
90
  */
81
91
  readonly android: {
82
92
  readonly permissions: readonly string[];
83
93
  readonly cleartextTraffic: boolean;
94
+ readonly orientation: AndroidOrientation;
84
95
  };
85
96
  /**
86
97
  * Who an installer says this is from.
@@ -202,6 +213,10 @@ export function parseManifest(raw: unknown): PackageManifest {
202
213
  android: {
203
214
  permissions,
204
215
  cleartextTraffic: androidRaw.cleartextTraffic === true,
216
+ orientation:
217
+ androidRaw.orientation === undefined
218
+ ? 'unspecified'
219
+ : checkOrientation(androidRaw.orientation),
205
220
  },
206
221
  splash: {
207
222
  show: splashRaw.show !== false,
@@ -6,14 +6,59 @@ export interface AndroidOptions {
6
6
  readonly permissions: readonly string[];
7
7
  /** Whether the app may open `http://` and `ws://` at all. */
8
8
  readonly cleartextTraffic: boolean;
9
+ /** The activity's `android:screenOrientation`, one of `ANDROID_ORIENTATIONS`. */
10
+ readonly orientation: AndroidOrientation;
9
11
  }
10
12
 
13
+ /**
14
+ * Every value Android documents for an activity's `android:screenOrientation`.
15
+ *
16
+ * **The whole list, not the two a landscape game wants**, because which way a game is held is the
17
+ * game's decision and the engine has no reason to prefer one: a card game is held upright, a
18
+ * racing game sideways, and `sensorLandscape` against `landscape` is whether a player may turn
19
+ * the phone over. What the list buys is a refusal at parse time, since the value is written into
20
+ * an XML attribute and Android answers one it does not know by ignoring it.
21
+ */
22
+ export const ANDROID_ORIENTATIONS = [
23
+ 'unspecified',
24
+ 'behind',
25
+ 'landscape',
26
+ 'portrait',
27
+ 'reverseLandscape',
28
+ 'reversePortrait',
29
+ 'sensorLandscape',
30
+ 'sensorPortrait',
31
+ 'userLandscape',
32
+ 'userPortrait',
33
+ 'sensor',
34
+ 'fullSensor',
35
+ 'nosensor',
36
+ 'user',
37
+ 'fullUser',
38
+ 'locked',
39
+ ] as const;
40
+
41
+ export type AndroidOrientation = (typeof ANDROID_ORIENTATIONS)[number];
42
+
11
43
  /** Nothing asked for: what every build got before this existed, and still gets. */
12
44
  export const NO_ANDROID_OPTIONS: AndroidOptions = Object.freeze({
13
45
  permissions: Object.freeze([]) as readonly string[],
14
46
  cleartextTraffic: false,
47
+ orientation: 'unspecified',
15
48
  });
16
49
 
50
+ /** Refuse an orientation Android does not document, at parse time where a consumer can fix it. */
51
+ export function checkOrientation(value: unknown): AndroidOrientation {
52
+ const known = ANDROID_ORIENTATIONS.find((name) => name === value);
53
+ if (known === undefined) {
54
+ throw new Error(
55
+ `drift.package.json: "android.orientation" is ${JSON.stringify(value)}, expected one of ` +
56
+ `${ANDROID_ORIENTATIONS.join(', ')}.`,
57
+ );
58
+ }
59
+ return known;
60
+ }
61
+
17
62
  /**
18
63
  * `INTERNET` and `android.permission.INTERNET` are the same thing, written two ways.
19
64
  *
@@ -47,7 +92,8 @@ export function checkPermission(name: string): void {
47
92
  }
48
93
 
49
94
  /**
50
- * The template manifest with a game's permissions in it, and its cleartext policy set.
95
+ * The template manifest with a game's permissions in it, its cleartext policy set, and its
96
+ * activity held the way the game asked.
51
97
  *
52
98
  * **Written into the copy, not into the template**, the way the icon already is: `buildAndroid`
53
99
  * stages `packages/package/android/` into `out/android/project` and edits it there, so this
@@ -105,7 +151,27 @@ export function withAndroidManifest(xml: string, options: AndroidOptions): strin
105
151
  'for cleartext traffic would do nothing',
106
152
  );
107
153
  }
108
- return replaced;
154
+
155
+ /*
156
+ * **Rewritten on the activity for the same reason**: the template carries `unspecified`, which
157
+ * leaves the choice to the phone. Held by the manifest rather than by `lockOrientation` at run
158
+ * time, because the activity is laid out before any of the game's code runs, and a lock taken
159
+ * then turns a picture the player has already seen.
160
+ */
161
+ const oriented = replaced.replace(
162
+ /android:screenOrientation="[A-Za-z]+"/,
163
+ `android:screenOrientation="${options.orientation}"`,
164
+ );
165
+ if (
166
+ !/android:screenOrientation="[A-Za-z]+"/.test(replaced) &&
167
+ options.orientation !== 'unspecified'
168
+ ) {
169
+ throw new Error(
170
+ 'the Android manifest template no longer declares android:screenOrientation, so asking for ' +
171
+ `"${options.orientation}" would do nothing`,
172
+ );
173
+ }
174
+ return oriented;
109
175
  }
110
176
 
111
177
  /** The whitespace at the start of the line `at` falls on, so an inserted element lines up. */