@driftengine/package 3.61.0 → 3.61.1

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.
Files changed (41) hide show
  1. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  2. package/android/.gradle/8.9/checksums/md5-checksums.bin +0 -0
  3. package/android/.gradle/8.9/checksums/sha1-checksums.bin +0 -0
  4. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  5. package/android/.gradle/8.9/executionHistory/executionHistory.lock +0 -0
  6. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  7. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  8. package/android/.gradle/8.9/gc.properties +0 -0
  9. package/android/.gradle/9.2.0/checksums/checksums.lock +0 -0
  10. package/android/.gradle/9.2.0/checksums/md5-checksums.bin +0 -0
  11. package/android/.gradle/9.2.0/checksums/sha1-checksums.bin +0 -0
  12. package/android/.gradle/9.2.0/fileChanges/last-build.bin +0 -0
  13. package/android/.gradle/9.2.0/fileHashes/fileHashes.bin +0 -0
  14. package/android/.gradle/9.2.0/fileHashes/fileHashes.lock +0 -0
  15. package/android/.gradle/9.2.0/gc.properties +0 -0
  16. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  17. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  18. package/android/.gradle/vcs-1/gc.properties +0 -0
  19. package/android/app/build.gradle.kts +91 -0
  20. package/android/app/src/main/AndroidManifest.xml +33 -0
  21. package/android/app/src/main/java/dev/driftengine/host/HostBridge.java +334 -0
  22. package/android/app/src/main/java/dev/driftengine/host/MainActivity.java +347 -0
  23. package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  24. package/android/app/src/main/res/values/strings.xml +11 -0
  25. package/android/build/reports/problems/problems-report.html +659 -0
  26. package/android/build.gradle.kts +3 -0
  27. package/android/gradle.properties +4 -0
  28. package/android/settings.gradle.kts +24 -0
  29. package/assets/icon.png +0 -0
  30. package/assets/lockup.svg +45 -0
  31. package/assets/mark-plain.svg +23 -0
  32. package/assets/mark-small.svg +22 -0
  33. package/assets/mark.svg +24 -0
  34. package/assets/originProbe.html +117 -0
  35. package/assets/splash.html +119 -0
  36. package/ios/DriftHost/AppDelegate.swift +23 -0
  37. package/ios/DriftHost/HostBridge.swift +159 -0
  38. package/ios/DriftHost/SchemeHandler.swift +130 -0
  39. package/ios/DriftHost/ViewController.swift +126 -0
  40. package/ios/project.yml +57 -0
  41. package/package.json +6 -2
@@ -0,0 +1,3 @@
1
+ plugins {
2
+ id("com.android.application") version "8.7.3" apply false
3
+ }
@@ -0,0 +1,4 @@
1
+ # The defaults Android Studio writes, minus the ones that are only about its own UI.
2
+ android.useAndroidX=true
3
+ android.nonTransitiveRClass=true
4
+ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
@@ -0,0 +1,24 @@
1
+ /*
2
+ * The Android host, as a one-module Gradle project.
3
+ *
4
+ * It is a *template*: the packager copies this whole directory into a staging area, drops the
5
+ * game's build into `app/src/main/assets/www`, and builds there. Nothing a consumer owns is ever
6
+ * written into this directory, which is what keeps a 70 MB game out of the engine's repository.
7
+ */
8
+ pluginManagement {
9
+ repositories {
10
+ google()
11
+ mavenCentral()
12
+ gradlePluginPortal()
13
+ }
14
+ }
15
+
16
+ dependencyResolutionManagement {
17
+ repositories {
18
+ google()
19
+ mavenCentral()
20
+ }
21
+ }
22
+
23
+ rootProject.name = "drift-android-host"
24
+ include(":app")
Binary file
@@ -0,0 +1,45 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 233.30 207.08" color="#ececf0">
2
+ <!--
3
+ The vertical lockup: the mark over the word.
4
+
5
+ The word is outlines rather than text, so this file renders identically on a machine with no
6
+ fonts installed and inside a shell that has no access to any. Drawn from Fira Sans (SIL Open
7
+ Font License 1.1) at Light for "Drift" and SemiBold for "Engine": the weight break is what
8
+ carries the compound word without a space or a capital doing the work, and the colour break
9
+ puts the engine's own accent on the half that names it.
10
+
11
+ Nothing here is a font dependency: no font is embedded, distributed or required. These are
12
+ curves.
13
+
14
+ "Drift" is `currentColor` so the half that is not the accent follows whatever surface this is
15
+ placed on; the root carries the dark-surface default, which is what a standalone view or an
16
+ `<img>` gets. The mark is centred on the *cube* rather than on the composition's bounding box:
17
+ the streaks drag that box left, and centring on it puts the cube visibly off the word's axis. -->
18
+ <style>
19
+ /* The half that is not the accent follows its surface. A file referenced as an image gets
20
+ these defaults; an inline copy can be overridden by setting `color` on any ancestor.
21
+ No angle brackets in here: this is XML, and a tag name inside a CSS comment is a tag. */
22
+ :root { color: #ececf0; }
23
+ @media (prefers-color-scheme: light) { :root { color: #14141a; } }
24
+ </style>
25
+ <g transform="translate(27.96,0)">
26
+ <polygon points="38.04,54.65 4.00,71.67 4.00,82.07 38.04,65.05" fill="#2ac79c" />
27
+ <polygon points="38.04,83.02 4.00,100.04 4.00,110.44 38.04,93.42" fill="#116352" />
28
+ <polygon points="49.39,43.31 88.69,66.00 88.69,111.39 49.39,88.69" fill="#0f5b49" />
29
+ <polygon points="88.69,66.00 128.00,43.31 128.00,88.69 88.69,111.39" fill="#1c9b7c" />
30
+ <polygon points="88.69,20.61 128.00,43.31 88.69,66.00 49.39,43.31" fill="#35e0b0" />
31
+ </g>
32
+ <g transform="translate(0.00,196.96)">
33
+ <path transform="translate(0.00,0.00) scale(0.04600,-0.04600)" d="M247 685H111V0H258C399 0 566 64 566 348C566 630 406 685 247 685ZM255 635C375 635 503 599 503 348C503 102 380 50 262 50H171V635Z" fill="currentColor" />
34
+ <path transform="translate(29.37,0.00) scale(0.04600,-0.04600)" d="M309 534C236 534 189 492 161 406L156 523H106V0H164V320C192 427 231 479 304 479C322 479 335 477 350 473L361 528C347 532 329 534 309 534Z" fill="currentColor" />
35
+ <path transform="translate(46.83,0.00) scale(0.04600,-0.04600)" d="M134 753C108 753 91 734 91 711C91 688 108 669 134 669C161 669 178 688 178 711C178 734 161 753 134 753ZM164 523H106V0H164Z" fill="currentColor" />
36
+ <path transform="translate(59.55,0.00) scale(0.04600,-0.04600)" d="M260 695C293 695 325 687 359 672L379 715C340 733 302 743 257 743C166 743 112 691 112 609V523H21V476H112V0H170V476H300L308 523H170V607C170 669 201 695 260 695Z" fill="currentColor" />
37
+ <path transform="translate(74.62,0.00) scale(0.04600,-0.04600)" d="M321 62C295 47 270 39 241 39C188 39 164 69 164 130V476H292L299 523H164V655L106 648V523H18V476H106V127C106 36 155 -11 234 -11C276 -11 312 1 344 22Z" fill="currentColor" />
38
+ <path transform="translate(90.60,0.00) scale(0.04600,-0.04600)" d="M486 692H73V0H488V108H220V299H438V405H220V585H470Z" fill="#35e0b0" />
39
+ <path transform="translate(115.19,0.00) scale(0.04600,-0.04600)" d="M366 546C298 546 247 516 204 463L193 530H70V0H212V366C241 413 273 440 314 440C350 440 372 423 372 362V0H514V386C514 486 459 546 366 546Z" fill="#35e0b0" />
40
+ <path transform="translate(142.12,0.00) scale(0.04600,-0.04600)" d="M509 610C445 574 383 543 261 546C128 546 35 471 35 354C35 286 63 239 129 203C91 179 67 142 67 104C67 48 113 0 224 0H302C358 0 385 -22 385 -58C385 -96 359 -123 261 -123C160 -123 138 -101 138 -51H11C11 -157 64 -220 258 -220C435 -220 528 -155 528 -48C528 40 451 104 331 104H250C198 104 189 121 189 139C189 153 197 166 209 174C228 169 248 167 271 167C406 167 489 242 489 345C489 412 456 452 391 481C455 481 505 486 544 501ZM264 451C323 451 352 421 352 358C352 293 322 259 265 259C211 259 177 295 177 356C177 414 210 451 264 451Z" fill="#35e0b0" />
41
+ <path transform="translate(167.45,0.00) scale(0.04600,-0.04600)" d="M140 803C89 803 54 767 54 720C54 673 89 637 140 637C191 637 227 673 227 720C227 767 191 803 140 803ZM212 530H70V0H212Z" fill="#35e0b0" />
42
+ <path transform="translate(180.72,0.00) scale(0.04600,-0.04600)" d="M366 546C298 546 247 516 204 463L193 530H70V0H212V366C241 413 273 440 314 440C350 440 372 423 372 362V0H514V386C514 486 459 546 366 546Z" fill="#35e0b0" />
43
+ <path transform="translate(207.65,0.00) scale(0.04600,-0.04600)" d="M518 277C518 446 429 546 279 546C124 546 38 422 38 262C38 96 127 -16 297 -16C380 -16 446 14 498 56L439 136C393 104 355 90 310 90C242 90 194 120 184 222H515C516 237 518 259 518 277ZM377 311H184C191 411 226 449 282 449C350 449 377 397 377 317Z" fill="#35e0b0" />
44
+ </g>
45
+ </svg>
@@ -0,0 +1,23 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2
+ <!--
3
+ DriftEngine: a cube in isometric, with the streaks of its own motion behind it.
4
+
5
+ A cube because that is what a 3D engine draws before it draws anything else, and three flat
6
+ facets because that is what this one's renderer does to it — one tone a face, no gradient, no
7
+ smooth Bezier anywhere in the file.
8
+
9
+ The isometric is exact rather than eyeballed: half-diagonals of side*cos30 and side*sin30 with
10
+ a vertical edge of side, so it reads as a cube instead of as a box. The streaks carry the name.
11
+ They were arrived at by elimination, and what was tried first is worth recording, because it is
12
+ the obvious thing: echoes of the cube itself, offset along its axis. Stacked triangles read as
13
+ a pine tree at every size that was tried. Two parallel streaks, near one bright and far one
14
+ dim, read as motion at 128 pixels and as motion at 32.
15
+
16
+ Below about 24 pixels the streaks merge into the cube. `mark-small.svg` is the same mark with
17
+ them removed, which is what a favicon should use. -->
18
+ <polygon points="19.82,27.06 5.00,34.47 5.00,39.00 19.82,31.59" fill="#2ac79c" />
19
+ <polygon points="19.82,39.41 5.00,46.82 5.00,51.35 19.82,43.94" fill="#116352" />
20
+ <polygon points="24.77,22.12 41.88,32.00 41.88,51.77 24.77,41.88" fill="#0f5b49" />
21
+ <polygon points="41.88,32.00 59.00,22.12 59.00,41.88 41.88,51.77" fill="#1c9b7c" />
22
+ <polygon points="41.88,12.23 59.00,22.12 41.88,32.00 24.77,22.12" fill="#35e0b0" />
23
+ </svg>
@@ -0,0 +1,22 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2
+ <!--
3
+ DriftEngine: a cube in isometric, with the streaks of its own motion behind it.
4
+
5
+ A cube because that is what a 3D engine draws before it draws anything else, and three flat
6
+ facets because that is what this one's renderer does to it — one tone a face, no gradient, no
7
+ smooth Bezier anywhere in the file.
8
+
9
+ The isometric is exact rather than eyeballed: half-diagonals of side*cos30 and side*sin30 with
10
+ a vertical edge of side, so it reads as a cube instead of as a box. The streaks carry the name.
11
+ They were arrived at by elimination, and what was tried first is worth recording, because it is
12
+ the obvious thing: echoes of the cube itself, offset along its axis. Stacked triangles read as
13
+ a pine tree at every size that was tried. Two parallel streaks, near one bright and far one
14
+ dim, read as motion at 128 pixels and as motion at 32.
15
+
16
+ Below about 24 pixels the streaks merge into the cube. `mark-small.svg` is the same mark with
17
+ them removed, which is what a favicon should use. -->
18
+ <rect width="64" height="64" rx="12" fill="#0b0b0d" />
19
+ <polygon points="9.48,19.00 32.00,32.00 32.00,58.00 9.48,45.00" fill="#0f5b49" />
20
+ <polygon points="32.00,32.00 54.52,19.00 54.52,45.00 32.00,58.00" fill="#1c9b7c" />
21
+ <polygon points="32.00,6.00 54.52,19.00 32.00,32.00 9.48,19.00" fill="#35e0b0" />
22
+ </svg>
@@ -0,0 +1,24 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2
+ <!--
3
+ DriftEngine: a cube in isometric, with the streaks of its own motion behind it.
4
+
5
+ A cube because that is what a 3D engine draws before it draws anything else, and three flat
6
+ facets because that is what this one's renderer does to it — one tone a face, no gradient, no
7
+ smooth Bezier anywhere in the file.
8
+
9
+ The isometric is exact rather than eyeballed: half-diagonals of side*cos30 and side*sin30 with
10
+ a vertical edge of side, so it reads as a cube instead of as a box. The streaks carry the name.
11
+ They were arrived at by elimination, and what was tried first is worth recording, because it is
12
+ the obvious thing: echoes of the cube itself, offset along its axis. Stacked triangles read as
13
+ a pine tree at every size that was tried. Two parallel streaks, near one bright and far one
14
+ dim, read as motion at 128 pixels and as motion at 32.
15
+
16
+ Below about 24 pixels the streaks merge into the cube. `mark-small.svg` is the same mark with
17
+ them removed, which is what a favicon should use. -->
18
+ <rect width="64" height="64" rx="12" fill="#0b0b0d" />
19
+ <polygon points="19.82,27.06 5.00,34.47 5.00,39.00 19.82,31.59" fill="#2ac79c" />
20
+ <polygon points="19.82,39.41 5.00,46.82 5.00,51.35 19.82,43.94" fill="#116352" />
21
+ <polygon points="24.77,22.12 41.88,32.00 41.88,51.77 24.77,41.88" fill="#0f5b49" />
22
+ <polygon points="41.88,32.00 59.00,22.12 59.00,41.88 41.88,51.77" fill="#1c9b7c" />
23
+ <polygon points="41.88,12.23 59.00,22.12 41.88,32.00 24.77,22.12" fill="#35e0b0" />
24
+ </svg>
@@ -0,0 +1,117 @@
1
+ <!doctype html>
2
+ <meta charset="utf-8" />
3
+ <title>Origin probe</title>
4
+ <!--
5
+ The one measurement a mobile host cannot be written without.
6
+
7
+ **Whether a WebView calls its origin a secure context decides the design.** `crossOriginIsolated`,
8
+ `SharedArrayBuffer` and anything threaded depend on it, and the answer is not the same on the two
9
+ platforms: Android serves from an `https://` origin through `WebViewAssetLoader`, which is a
10
+ secure context by definition, while iOS serves from a custom scheme through `WKURLSchemeHandler`,
11
+ where it is genuinely unclear. If iOS says no, the answer is a loopback server on 127.0.0.1 and
12
+ `SchemeHandler.swift` is the only file that changes.
13
+
14
+ Load this in the host — `drift://localhost/__drift/originProbe.html` on iOS,
15
+ `/originProbe.html` on Android — and read the table. A simulator is evidence for the origin
16
+ rows and not for the GPU ones.
17
+
18
+ The iOS host is `localhost` and the prefix is `__drift/`, and both are load-bearing:
19
+ `SchemeHandler.swift` serves shell assets from under that prefix alone, and `localhost` is the
20
+ whole reason the origin is trustworthy. `drift://shell/` is the desktop shell's spelling and
21
+ resolves here against the game's bundle root, where this file is not.
22
+ -->
23
+ <style>
24
+ body {
25
+ font:
26
+ 15px/1.5 system-ui,
27
+ sans-serif;
28
+ margin: 0;
29
+ padding: 16px;
30
+ background: #0b0b0d;
31
+ color: #ececf0;
32
+ }
33
+ h1 {
34
+ font-size: 1rem;
35
+ color: #9a9aa8;
36
+ margin: 0 0 12px;
37
+ }
38
+ table {
39
+ border-collapse: collapse;
40
+ width: 100%;
41
+ }
42
+ td {
43
+ padding: 6px 8px;
44
+ border-bottom: 1px solid #26262d;
45
+ vertical-align: top;
46
+ }
47
+ td:first-child {
48
+ color: #9a9aa8;
49
+ white-space: nowrap;
50
+ }
51
+ .yes {
52
+ color: #35e0b0;
53
+ }
54
+ .no {
55
+ color: #ff9b9b;
56
+ }
57
+ </style>
58
+ <h1>Origin probe</h1>
59
+ <table id="t">
60
+ <tbody>
61
+ <tr>
62
+ <td>running…</td>
63
+ <td></td>
64
+ </tr>
65
+ </tbody>
66
+ </table>
67
+ <script type="module">
68
+ /* That this script runs at all is the first result: a module script fails from `file://`. */
69
+ const rows = {
70
+ origin: location.origin,
71
+ isSecureContext: String(globalThis.isSecureContext),
72
+ crossOriginIsolated: String(globalThis.crossOriginIsolated),
73
+ sharedArrayBuffer: String(typeof SharedArrayBuffer !== 'undefined'),
74
+ moduleScript: 'yes — this script is one',
75
+ host: String(globalThis.__driftHost !== undefined),
76
+ webgpu: String(navigator.gpu !== undefined),
77
+ webgl2: String(document.createElement('canvas').getContext('webgl2') !== null),
78
+ videoEncoder: String(typeof VideoEncoder !== 'undefined'),
79
+ devicePixelRatio: String(devicePixelRatio),
80
+ screen: `${screen.width}x${screen.height}`,
81
+ userAgent: navigator.userAgent,
82
+ };
83
+
84
+ try {
85
+ const worker = new Worker(
86
+ URL.createObjectURL(new Blob(['self.postMessage(1)'], { type: 'text/javascript' })),
87
+ );
88
+ rows.blobWorker = await new Promise((done) => {
89
+ worker.onmessage = () => done('yes');
90
+ setTimeout(() => done('no'), 500);
91
+ });
92
+ } catch (cause) {
93
+ rows.blobWorker = `no — ${cause}`;
94
+ }
95
+
96
+ if (navigator.gpu !== undefined) {
97
+ try {
98
+ const adapter = await navigator.gpu.requestAdapter();
99
+ rows.adapter = adapter === null ? 'none offered' : (adapter.info?.description ?? 'unnamed');
100
+ } catch (cause) {
101
+ rows.adapter = `threw — ${cause}`;
102
+ }
103
+ }
104
+
105
+ globalThis.__originReport = rows;
106
+ document.querySelector('#t tbody').innerHTML = Object.entries(rows)
107
+ .map(([key, value]) => {
108
+ const cls =
109
+ value === 'true' || value === 'yes'
110
+ ? 'yes'
111
+ : value === 'false' || value === 'no'
112
+ ? 'no'
113
+ : '';
114
+ return `<tr><td>${key}</td><td class="${cls}">${String(value)}</td></tr>`;
115
+ })
116
+ .join('');
117
+ </script>
@@ -0,0 +1,119 @@
1
+ <!doctype html>
2
+ <meta charset="utf-8" />
3
+ <title>DriftEngine</title>
4
+ <!--
5
+ The badge the shell shows while the game builds its first frame.
6
+
7
+ **It covers a real wait rather than manufacturing one.** A packaged game has to start Chromium,
8
+ read a world off disk and compile shaders before it can paint, and the alternative to this is a
9
+ black window for that whole time — which reads as a crash. The minimum duration in the manifest
10
+ is what stops it flickering on a machine that boots fast, and the cap in `splashTiming.ts` is
11
+ what stops it outliving a boot that never finishes.
12
+
13
+ No script, and nothing loaded from anywhere but the shell's own assets: this page has no reason
14
+ to run code, and a splash that can fail to render is worse than none.
15
+ -->
16
+ <style>
17
+ :root {
18
+ color-scheme: dark;
19
+ }
20
+ html,
21
+ body {
22
+ height: 100%;
23
+ }
24
+ body {
25
+ margin: 0;
26
+ display: grid;
27
+ place-items: center;
28
+ background: #0b0b0d;
29
+ /* The plate is flat except for one very soft lift behind the mark, which is what stops a
30
+ 420x300 rectangle of near-black reading as a window that failed to paint. */
31
+ background-image: radial-gradient(60% 55% at 50% 42%, #12161a 0%, #0b0b0d 70%);
32
+ overflow: hidden;
33
+ }
34
+
35
+ .stack {
36
+ display: grid;
37
+ justify-items: center;
38
+ gap: 26px;
39
+ }
40
+
41
+ /*
42
+ * The entrance is the mark's own idea, applied to the whole lockup: it arrives from the left
43
+ * and settles. Eased hard out, so it is quick at the start and lands rather than stops.
44
+ */
45
+ img {
46
+ width: 208px;
47
+ height: auto;
48
+ animation: arrive 640ms cubic-bezier(0.16, 0.84, 0.44, 1) both;
49
+ }
50
+
51
+ @keyframes arrive {
52
+ from {
53
+ opacity: 0;
54
+ transform: translateX(-16px);
55
+ }
56
+ to {
57
+ opacity: 1;
58
+ transform: translateX(0);
59
+ }
60
+ }
61
+
62
+ /* A hairline that says something is happening, without claiming to know how much is left.
63
+ A progress bar that cannot measure progress is a lie told in pixels. */
64
+ .line {
65
+ width: 168px;
66
+ height: 2px;
67
+ border-radius: 2px;
68
+ background: #1a2320;
69
+ overflow: hidden;
70
+ animation: fade 700ms 260ms both;
71
+ }
72
+
73
+ .line::after {
74
+ content: '';
75
+ display: block;
76
+ width: 42%;
77
+ height: 100%;
78
+ border-radius: 2px;
79
+ background: linear-gradient(90deg, rgba(53, 224, 176, 0), #35e0b0, rgba(53, 224, 176, 0));
80
+ animation: sweep 1500ms cubic-bezier(0.45, 0, 0.55, 1) infinite;
81
+ }
82
+
83
+ @keyframes fade {
84
+ from {
85
+ opacity: 0;
86
+ }
87
+ to {
88
+ opacity: 1;
89
+ }
90
+ }
91
+ @keyframes sweep {
92
+ from {
93
+ transform: translateX(-100%);
94
+ }
95
+ to {
96
+ transform: translateX(340%);
97
+ }
98
+ }
99
+
100
+ /*
101
+ * Movement is the first thing to go for somebody who asked for less of it. The badge still
102
+ * appears and still says the same thing; it simply does not travel, and the hairline holds
103
+ * still rather than sweeping.
104
+ */
105
+ @media (prefers-reduced-motion: reduce) {
106
+ img {
107
+ animation: fade 300ms both;
108
+ }
109
+ .line::after {
110
+ animation: none;
111
+ width: 100%;
112
+ opacity: 0.5;
113
+ }
114
+ }
115
+ </style>
116
+ <div class="stack">
117
+ <img src="./lockup.svg" alt="DriftEngine" />
118
+ <div class="line"></div>
119
+ </div>
@@ -0,0 +1,23 @@
1
+ import UIKit
2
+
3
+ /// The application, which is one window holding one view controller.
4
+ ///
5
+ /// No storyboard: a game has one screen and a storyboard for it is a file nobody reads and a merge
6
+ /// conflict nobody wants. The launch screen is declared in the generated Info.plist as an empty
7
+ /// `UILaunchScreen`, which gives a black screen for the moment before the first frame — the same
8
+ /// thing the desktop shell's splash covers, and the most iOS offers without a bundled image.
9
+ @main
10
+ final class AppDelegate: UIResponder, UIApplicationDelegate {
11
+ var window: UIWindow?
12
+
13
+ func application(
14
+ _ application: UIApplication,
15
+ didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]? = nil
16
+ ) -> Bool {
17
+ let window = UIWindow(frame: UIScreen.main.bounds)
18
+ window.rootViewController = ViewController()
19
+ window.makeKeyAndVisible()
20
+ self.window = window
21
+ return true
22
+ }
23
+ }
@@ -0,0 +1,159 @@
1
+ import UIKit
2
+ import WebKit
3
+
4
+ /// The half of the bridge that iOS can expose, which is less than Android's.
5
+ ///
6
+ /// **There is no synchronous return from native code to JavaScript on this platform.** A
7
+ /// `WKScriptMessageHandler` is one-way; `WKScriptMessageHandlerWithReply` — iOS 14 and up, which
8
+ /// is why the deployment target is 15 — gives a promise back, and that is the most this can do. So
9
+ /// everything a game must read *synchronously* is injected before its first line of script runs,
10
+ /// as a literal object, and only the mutations arrive here.
11
+ ///
12
+ /// `bridgeShim.ts`'s iOS half assembles both into the same `DriftHostBridge` the desktop shell
13
+ /// exposes, so the engine sees one seam with three shells behind it.
14
+ final class HostBridge: NSObject, WKScriptMessageHandlerWithReply {
15
+ static let name = "driftHost"
16
+ private static let storePrefix = "drift.store."
17
+
18
+ private weak var controller: ViewController?
19
+ private let defaults = UserDefaults.standard
20
+
21
+ init(controller: ViewController) {
22
+ self.controller = controller
23
+ }
24
+
25
+ /// Everything the page can read the instant it starts, as JSON for the injected literal.
26
+ ///
27
+ /// Rebuilt whenever the host has something new to say — a rotation, a display change — and
28
+ /// pushed into the page by assigning over the same object, so a bridge that read it at boot is
29
+ /// not left reporting the previous orientation for the rest of the session.
30
+ func stateJson() -> String {
31
+ var store: [String: String] = [:]
32
+ for (key, value) in defaults.dictionaryRepresentation() {
33
+ guard key.hasPrefix(Self.storePrefix), let text = value as? String else { continue }
34
+ store[String(key.dropFirst(Self.storePrefix.count))] = text
35
+ }
36
+
37
+ let screen = controller?.view.window?.windowScene?.screen ?? UIScreen.main
38
+ let bounds = screen.bounds
39
+ // A real number, which is the whole point of a shell: 60 on most devices and 120 on a
40
+ // ProMotion one, and a game pacing itself against a guess would be wrong on both.
41
+ let hz = screen.maximumFramesPerSecond
42
+
43
+ let state: [String: Any] = [
44
+ "snapshot": store,
45
+ "refreshHz": hz > 0 ? hz : NSNull(),
46
+ "mode": "fullscreen",
47
+ "size": ["width": Int(bounds.width), "height": Int(bounds.height)],
48
+ "displays": [[
49
+ "id": "0",
50
+ "label": "built-in",
51
+ "width": Int(bounds.width),
52
+ "height": Int(bounds.height),
53
+ "scale": screen.scale,
54
+ "refreshHz": hz > 0 ? hz : NSNull(),
55
+ "primary": true,
56
+ ]],
57
+ "fullscreen": true,
58
+ // An iOS application does not exit itself and Apple asks that it not try, so a game
59
+ // reads this and draws no exit button rather than one that does nothing.
60
+ "canQuit": false,
61
+ ]
62
+ guard let data = try? JSONSerialization.data(withJSONObject: state),
63
+ let json = String(data: data, encoding: .utf8) else {
64
+ return "{}"
65
+ }
66
+ return json
67
+ }
68
+
69
+ func userContentController(
70
+ _ controller: WKUserContentController,
71
+ didReceive message: WKScriptMessage,
72
+ replyHandler: @escaping (Any?, String?) -> Void
73
+ ) {
74
+ guard let body = message.body as? [String: Any],
75
+ let name = body["name"] as? String else {
76
+ replyHandler(false, nil)
77
+ return
78
+ }
79
+ let payload = body["payload"] as? [String: Any] ?? [:]
80
+
81
+ switch name {
82
+ case "write":
83
+ if let key = payload["key"] as? String, let value = payload["value"] as? String {
84
+ defaults.set(value, forKey: Self.storePrefix + key)
85
+ }
86
+ replyHandler(true, nil)
87
+
88
+ case "remove":
89
+ if let key = payload["key"] as? String {
90
+ defaults.removeObject(forKey: Self.storePrefix + key)
91
+ }
92
+ replyHandler(true, nil)
93
+
94
+ case "fullscreen", "mode":
95
+ // The status bar is the only thing there is to hide here, and the view controller
96
+ // hides it for the whole life of the application. Answered rather than acted on.
97
+ replyHandler(true, nil)
98
+
99
+ case "save":
100
+ save(payload: payload, reply: replyHandler)
101
+
102
+ case "openExternal":
103
+ if let raw = payload["url"] as? String, let url = URL(string: raw),
104
+ ["http", "https", "mailto"].contains(url.scheme ?? "") {
105
+ UIApplication.shared.open(url)
106
+ }
107
+ replyHandler(true, nil)
108
+
109
+ default:
110
+ replyHandler(false, "unknown message: \(name)")
111
+ }
112
+ }
113
+
114
+ /// Write a file and offer it to the share sheet, which is where a file goes on this platform.
115
+ ///
116
+ /// **There is no Downloads folder to save into.** A clip that a player cannot send anywhere is
117
+ /// a clip that does not exist, so the file is written to the app's temporary directory and
118
+ /// handed straight to `UIActivityViewController` — Photos, Files, Messages, whatever they
119
+ /// choose. The temporary copy is the system's to reclaim.
120
+ private func save(payload: [String: Any], reply: @escaping (Any?, String?) -> Void) {
121
+ guard let name = payload["name"] as? String,
122
+ let pieces = payload["data"] as? [String] else {
123
+ reply(false, nil)
124
+ return
125
+ }
126
+ var bytes = Data()
127
+ for piece in pieces {
128
+ guard let part = Data(base64Encoded: piece) else {
129
+ reply(false, nil)
130
+ return
131
+ }
132
+ bytes.append(part)
133
+ }
134
+
135
+ let safe = name.replacingOccurrences(of: "/", with: "_")
136
+ let url = FileManager.default.temporaryDirectory.appendingPathComponent(safe)
137
+ do {
138
+ try bytes.write(to: url, options: .atomic)
139
+ } catch {
140
+ reply(false, nil)
141
+ return
142
+ }
143
+
144
+ DispatchQueue.main.async { [weak self] in
145
+ guard let host = self?.controller else {
146
+ reply(false, nil)
147
+ return
148
+ }
149
+ let sheet = UIActivityViewController(activityItems: [url], applicationActivities: nil)
150
+ // An iPad presents this from a point rather than from the bottom, and one with no
151
+ // anchor throws rather than appearing.
152
+ sheet.popoverPresentationController?.sourceView = host.view
153
+ sheet.popoverPresentationController?.sourceRect = CGRect(
154
+ x: host.view.bounds.midX, y: host.view.bounds.midY, width: 0, height: 0)
155
+ host.present(sheet, animated: true)
156
+ reply(true, nil)
157
+ }
158
+ }
159
+ }