@driftengine/package 3.61.0 → 3.61.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.
Files changed (42) 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 +7 -2
  42. package/scripts/build-macos.sh +71 -0
@@ -0,0 +1,130 @@
1
+ import Foundation
2
+ import WebKit
3
+
4
+ /// Serves the game out of the app bundle over a scheme that is an origin.
5
+ ///
6
+ /// **`file://` is not an origin**, and a game loaded from one fails to import its own modules —
7
+ /// which reads as a broken build rather than as a missing origin. A custom scheme handled here is
8
+ /// one, and it costs no listening socket inside a game.
9
+ ///
10
+ /// **The host is `localhost`, and that is the whole of why this is a secure context.**
11
+ ///
12
+ /// A custom scheme is *not* trustworthy to WebKit on its own. What rescues this one is the host:
13
+ /// W3C's "is origin potentially trustworthy" decides on the **host** before it considers any
14
+ /// scheme beyond https — step 4 returns trustworthy for a host in `127.0.0.0/8` or `::1/128`, and
15
+ /// step 5 for a host of `localhost`, and **neither step mentions the scheme**. So
16
+ /// `drift://localhost/…` is a secure context and `drift://app/…` is not, and the two differ by six
17
+ /// characters.
18
+ ///
19
+ /// **Do not confuse this with the other well-known custom-scheme problem, which is real and is not
20
+ /// this one.** An `https://` page cannot fetch a subresource from a custom scheme — WebKit blocks
21
+ /// it as mixed content — and the workaround written up for that is `_registerURLSchemeAsSecure:`,
22
+ /// a private selector on `WKProcessPool` that risks a review rejection. That is a question about a
23
+ /// custom scheme used as a *subresource origin underneath an https document*. This is a question
24
+ /// about the *document's own* origin, and the answers are different. Nothing private is used here,
25
+ /// and nothing needs to be.
26
+ ///
27
+ /// This is what Capacitor has shipped on iOS for years — `capacitor://localhost`, with their own
28
+ /// documentation recommending the hostname stay `localhost` precisely because it "allows the use of
29
+ /// Web APIs that would otherwise require a secure context". Millions of installed applications are
30
+ /// the evidence; no device here was needed to read it.
31
+ ///
32
+ /// **So there is no loopback server**, which was the alternative this was going to cost: a
33
+ /// listening socket inside a game, a port that can be taken, and a permission prompt on some
34
+ /// platforms.
35
+ ///
36
+ /// **The containment check is not optional.** A scheme handler that resolves a path and serves it
37
+ /// is a filesystem the page can walk, so every request is resolved and then checked to be inside
38
+ /// the bundle root — the same rule the desktop shell's `resolveWithinRoot` enforces.
39
+ final class SchemeHandler: NSObject, WKURLSchemeHandler {
40
+ static let scheme = "drift"
41
+
42
+ /// The reserved prefix the packager's own assets are served under.
43
+ ///
44
+ /// One host rather than two, unlike the desktop shell's `app` and `shell`: a second host would
45
+ /// not be `localhost`, and would therefore not be a secure context. A path prefix costs
46
+ /// nothing and keeps the whole application on the one origin that is trustworthy.
47
+ static let shellPrefix = "/__drift/"
48
+
49
+ private let root: URL
50
+ private let shell: URL
51
+
52
+ init(root: URL, shell: URL) {
53
+ self.root = root.standardizedFileURL
54
+ self.shell = shell.standardizedFileURL
55
+ }
56
+
57
+ func webView(_ webView: WKWebView, start task: WKURLSchemeTask) {
58
+ guard let url = task.request.url else {
59
+ task.didFailWithError(URLError(.badURL))
60
+ return
61
+ }
62
+
63
+ let path = url.path.isEmpty || url.path == "/" ? "/index.html" : url.path
64
+ let shellAsset = path.hasPrefix(Self.shellPrefix)
65
+ let base = shellAsset ? shell : root
66
+ let relative = shellAsset ? String(path.dropFirst(Self.shellPrefix.count - 1)) : path
67
+ let resolved = base.appendingPathComponent(relative).standardizedFileURL
68
+
69
+ // A prefix check on the string alone accepts a sibling directory whose name starts with
70
+ // the root's; the separator is what makes this a directory check.
71
+ guard resolved.path == base.path || resolved.path.hasPrefix(base.path + "/") else {
72
+ task.didFailWithError(URLError(.noPermissionsToReadFile))
73
+ return
74
+ }
75
+
76
+ guard let data = try? Data(contentsOf: resolved) else {
77
+ task.didFailWithError(URLError(.fileDoesNotExist))
78
+ return
79
+ }
80
+
81
+ let response = HTTPURLResponse(
82
+ url: url,
83
+ statusCode: 200,
84
+ httpVersion: "HTTP/1.1",
85
+ headerFields: [
86
+ "Content-Type": Self.contentType(for: resolved.pathExtension.lowercased()),
87
+ "Content-Length": String(data.count),
88
+ // The same policy the desktop shell serves: everything a game does is allowed and
89
+ // a script from somewhere else is not.
90
+ "Content-Security-Policy":
91
+ "default-src 'self' drift: blob: data:; "
92
+ + "script-src 'self' drift: blob: 'unsafe-inline' 'wasm-unsafe-eval'; "
93
+ + "style-src 'self' drift: 'unsafe-inline'; "
94
+ + "img-src 'self' drift: blob: data: https:; "
95
+ + "media-src 'self' drift: blob: data: https:; "
96
+ + "connect-src 'self' drift: blob: data: https: wss:",
97
+ ]
98
+ )!
99
+
100
+ task.didReceive(response)
101
+ task.didReceive(data)
102
+ task.didFinish()
103
+ }
104
+
105
+ func webView(_ webView: WKWebView, stop task: WKURLSchemeTask) {
106
+ // Nothing is held between start and finish, so there is nothing to cancel.
107
+ }
108
+
109
+ /// **A wrong type is a silent load failure**, not a wrong picture: a module served as anything
110
+ /// but JavaScript is refused by the loader, and an SVG served as bytes renders as a broken
111
+ /// image — which is exactly how the desktop shell's splash failed once.
112
+ static func contentType(for ext: String) -> String {
113
+ switch ext {
114
+ case "html": return "text/html; charset=utf-8"
115
+ case "js", "mjs": return "text/javascript; charset=utf-8"
116
+ case "css": return "text/css; charset=utf-8"
117
+ case "json": return "application/json; charset=utf-8"
118
+ case "wasm": return "application/wasm"
119
+ case "svg": return "image/svg+xml"
120
+ case "png": return "image/png"
121
+ case "jpg", "jpeg": return "image/jpeg"
122
+ case "webp": return "image/webp"
123
+ case "ktx2": return "image/ktx2"
124
+ case "ogg": return "audio/ogg"
125
+ case "mp3": return "audio/mpeg"
126
+ case "wav": return "audio/wav"
127
+ default: return "application/octet-stream"
128
+ }
129
+ }
130
+ }
@@ -0,0 +1,126 @@
1
+ import UIKit
2
+ import WebKit
3
+
4
+ /// One view controller, one `WKWebView`, and nothing else.
5
+ ///
6
+ /// **The renderer here is WebKit and there is no choice about that.** Apple requires it for
7
+ /// anything that browses the web, so a bundled Chromium is not an option on this platform — which
8
+ /// is also why the engine's WebGL2 baseline matters: what the device offers is what the game gets,
9
+ /// and the acceptance probe decides whether WebGPU is among it.
10
+ ///
11
+ /// **The bridge is injected at document start**, before the game's own scripts, the same timing
12
+ /// the desktop preload and Android's document-start script have. Everything readable synchronously
13
+ /// goes in as a literal; see `HostBridge` for why that is not a shortcut but the only shape this
14
+ /// platform allows.
15
+ final class ViewController: UIViewController, WKNavigationDelegate, WKUIDelegate {
16
+ private var web: WKWebView!
17
+ private var bridge: HostBridge!
18
+
19
+ override var prefersStatusBarHidden: Bool { true }
20
+ override var prefersHomeIndicatorAutoHidden: Bool { true }
21
+
22
+ override func viewDidLoad() {
23
+ super.viewDidLoad()
24
+
25
+ guard let root = Bundle.main.url(forResource: "www", withExtension: nil) else {
26
+ assertionFailure("the game was not copied into the bundle; see ios.ts")
27
+ return
28
+ }
29
+ let shell = Bundle.main.url(forResource: "shell", withExtension: nil) ?? root
30
+
31
+ bridge = HostBridge(controller: self)
32
+
33
+ let configuration = WKWebViewConfiguration()
34
+ configuration.setURLSchemeHandler(
35
+ SchemeHandler(root: root, shell: shell), forURLScheme: SchemeHandler.scheme)
36
+ // A game's own audio is not an autoplaying advert, and the engine's mixer starts on the
37
+ // first input anyway — so the gesture requirement only ever delays the first sound.
38
+ configuration.allowsInlineMediaPlayback = true
39
+ configuration.mediaTypesRequiringUserActionForPlayback = []
40
+
41
+ let content = configuration.userContentController
42
+ content.addScriptMessageHandler(bridge, contentWorld: .page, name: HostBridge.name)
43
+ content.addUserScript(WKUserScript(
44
+ source: "globalThis.__driftHostState = \(bridge.stateJson());",
45
+ injectionTime: .atDocumentStart,
46
+ forMainFrameOnly: true
47
+ ))
48
+ if let shim = Bundle.main.url(forResource: "drift-shim", withExtension: "js"),
49
+ let source = try? String(contentsOf: shim, encoding: .utf8) {
50
+ content.addUserScript(WKUserScript(
51
+ source: source, injectionTime: .atDocumentStart, forMainFrameOnly: true))
52
+ } else {
53
+ // Loud rather than silent: the game runs and believes it is in a plain browser, which
54
+ // is a defined state and a surprising one to debug from the outside.
55
+ NSLog("[drift] the bridge shim is missing from the bundle; the game has no host")
56
+ }
57
+
58
+ web = WKWebView(frame: view.bounds, configuration: configuration)
59
+ web.autoresizingMask = [.flexibleWidth, .flexibleHeight]
60
+ web.navigationDelegate = self
61
+ web.uiDelegate = self
62
+ // A game is not a document: rubber-banding at its edges reads as the whole screen coming
63
+ // loose, and the scroll view is the only thing that does it.
64
+ web.scrollView.bounces = false
65
+ web.scrollView.isScrollEnabled = false
66
+ web.isOpaque = false
67
+ web.backgroundColor = .black
68
+ view.backgroundColor = .black
69
+ view.addSubview(web)
70
+
71
+ /*
72
+ * **`localhost`, not `app`.** The host is what makes this a secure context — see the note
73
+ * in `SchemeHandler` — and the difference between the two is `crossOriginIsolated`,
74
+ * `SharedArrayBuffer`, the wake lock, and everything else gated on one.
75
+ */
76
+ web.load(URLRequest(url: URL(string: "\(SchemeHandler.scheme)://localhost/index.html")!))
77
+ }
78
+
79
+ override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
80
+ super.viewWillTransition(to: size, with: coordinator)
81
+ // A rotation changes what the injected state says, and a bridge reading a stale object
82
+ // would report the previous orientation for the rest of the session.
83
+ coordinator.animate(alongsideTransition: nil) { [weak self] _ in
84
+ guard let self else { return }
85
+ self.web.evaluateJavaScript(
86
+ "Object.assign(globalThis.__driftHostState ?? {}, \(self.bridge.stateJson()))")
87
+ }
88
+ }
89
+
90
+ /// A link to the outside opens in Safari, never inside the game.
91
+ ///
92
+ /// The same rule the desktop shell follows: their browser has their session, their bookmarks
93
+ /// and a way back, and an in-app view is a browser nobody is maintaining. Only `http`, `https`
94
+ /// and `mailto` leave at all.
95
+ func webView(
96
+ _ webView: WKWebView,
97
+ decidePolicyFor action: WKNavigationAction,
98
+ decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
99
+ ) {
100
+ guard let url = action.request.url else {
101
+ decisionHandler(.cancel)
102
+ return
103
+ }
104
+ if url.scheme == SchemeHandler.scheme {
105
+ decisionHandler(.allow)
106
+ return
107
+ }
108
+ if ["http", "https", "mailto"].contains(url.scheme ?? "") {
109
+ UIApplication.shared.open(url)
110
+ }
111
+ decisionHandler(.cancel)
112
+ }
113
+
114
+ /// `target="_blank"` arrives here rather than as a navigation, and goes the same way out.
115
+ func webView(
116
+ _ webView: WKWebView,
117
+ createWebViewWith configuration: WKWebViewConfiguration,
118
+ for action: WKNavigationAction,
119
+ windowFeatures: WKWindowFeatures
120
+ ) -> WKWebView? {
121
+ if let url = action.request.url, ["http", "https", "mailto"].contains(url.scheme ?? "") {
122
+ UIApplication.shared.open(url)
123
+ }
124
+ return nil
125
+ }
126
+ }
@@ -0,0 +1,57 @@
1
+ # The Xcode project, described rather than committed.
2
+ #
3
+ # The `__DRIFT_*__` placeholders are substituted by `src/ios.ts` when it stages a copy — plain text
4
+ # replacement rather than Xcode build settings, so what XcodeGen reads is a finished spec and there
5
+ # is one place a wrong value could come from.
6
+ #
7
+ # A `.xcodeproj` is a generated artefact with absolute paths and a merge conflict in every pull
8
+ # request; XcodeGen turns this file into one at build time, and `drift-package bootstrap
9
+ # --target=ios` fetches XcodeGen itself. Every value the packager substitutes is a placeholder
10
+ # here, and the defaults are visibly placeholders rather than plausible.
11
+ name: DriftHost
12
+ options:
13
+ bundleIdPrefix: dev.driftengine
14
+ deploymentTarget:
15
+ # 15 is where WKWebView's modern JavaScript and WebGL2 are dependable. WebGPU arrives at 26
16
+ # and the engine's acceptance probe decides whether it is usable, per target.
17
+ iOS: '15.0'
18
+ createIntermediateGroups: true
19
+
20
+ settings:
21
+ base:
22
+ PRODUCT_NAME: __DRIFT_APP_NAME__
23
+ MARKETING_VERSION: __DRIFT_VERSION_NAME__
24
+ CURRENT_PROJECT_VERSION: __DRIFT_VERSION_CODE__
25
+ # Signing is decided by the build, not by this file: an unsigned archive is what a sideload
26
+ # tool re-signs, and a team id turns automatic signing back on.
27
+ CODE_SIGN_STYLE: Automatic
28
+ DEVELOPMENT_TEAM: __DRIFT_TEAM_ID__
29
+ TARGETED_DEVICE_FAMILY: '1,2'
30
+
31
+ targets:
32
+ DriftHost:
33
+ type: application
34
+ platform: iOS
35
+ sources:
36
+ - path: DriftHost
37
+ # Written into the staging copy by the packager: the game, and the bridge shim beside it.
38
+ - path: www
39
+ type: folder
40
+ optional: true
41
+ - path: drift-shim.js
42
+ optional: true
43
+ info:
44
+ path: DriftHost/Info.plist
45
+ properties:
46
+ CFBundleDisplayName: __DRIFT_APP_NAME__
47
+ CFBundleIdentifier: __DRIFT_APP_ID__
48
+ UILaunchScreen: {}
49
+ # A game is landscape or portrait as it likes; the host locks nothing, and
50
+ # `ScreenPresentation.lockOrientation` reports iOS's refusal rather than hiding it.
51
+ UISupportedInterfaceOrientations:
52
+ - UIInterfaceOrientationPortrait
53
+ - UIInterfaceOrientationLandscapeLeft
54
+ - UIInterfaceOrientationLandscapeRight
55
+ UIRequiresFullScreen: true
56
+ UIStatusBarHidden: true
57
+ UIViewControllerBasedStatusBarAppearance: false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftengine/package",
3
- "version": "3.61.0",
3
+ "version": "3.61.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",
@@ -21,6 +21,11 @@
21
21
  "!src/**/*.test.ts",
22
22
  "!src/**/*.test.mjs",
23
23
  "!src/**/__snapshots__",
24
+ "bin",
25
+ "android",
26
+ "ios",
27
+ "assets",
28
+ "scripts",
24
29
  "README.md",
25
30
  "LICENSE",
26
31
  "NOTICE"
@@ -30,7 +35,7 @@
30
35
  "drift-package": "bin/drift-package.mjs"
31
36
  },
32
37
  "peerDependencies": {
33
- "@driftengine/core": "3.61.0",
38
+ "@driftengine/core": "3.61.2",
34
39
  "steamworks.js": ">=0.4.0"
35
40
  },
36
41
  "dependencies": {
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Build the macOS artifacts on a Mac.
4
+ #
5
+ # **This is not the only route any more.** A Mac target cross-builds from Linux, where the packager
6
+ # ad-hoc signs it with rcodesign and archives it with `zip -y`. What that route cannot produce is a
7
+ # .dmg — hdiutil is macOS only — so this script is what makes one, and what signs with a real
8
+ # Developer ID. `drift-package doctor` prints which of the two a machine is on.
9
+ #
10
+ # The signing mode is decided by `planSigning` from the environment and printed by the build. With
11
+ # no certificate this is an **ad-hoc** signature, which is the minimum that runs: Apple Silicon's
12
+ # loader refuses an unsigned binary outright. A .dmg built that way still carries the quarantine
13
+ # flag when downloaded, and the first launch is refused until `xattr -dr com.apple.quarantine`
14
+ # clears it — which is the reason to buy a Developer ID eventually, and not a reason to wait.
15
+ #
16
+ # ./build-macos.sh --project ~/work/game
17
+ # CSC_LINK=~/certs/developer-id.p12 CSC_KEY_PASSWORD=... ./build-macos.sh --project .
18
+ # APPLE_ID=... APPLE_APP_SPECIFIC_PASSWORD=... APPLE_TEAM_ID=... ./build-macos.sh --project .
19
+
20
+ set -euo pipefail
21
+
22
+ PROJECT=""
23
+ TARGET="mac-arm64"
24
+
25
+ while [ $# -gt 0 ]; do
26
+ case "$1" in
27
+ --project) PROJECT="$2"; shift 2 ;;
28
+ --target) TARGET="$2"; shift 2 ;;
29
+ -h|--help) sed -n '2,16p' "$0"; exit 0 ;;
30
+ *) echo "unknown argument: $1" >&2; exit 2 ;;
31
+ esac
32
+ done
33
+
34
+ [ -n "$PROJECT" ] || { echo "--project is required" >&2; exit 2; }
35
+ [ -f "$PROJECT/drift.package.json" ] || {
36
+ echo "no drift.package.json in $PROJECT; that file is what a build is configured from" >&2
37
+ exit 1
38
+ }
39
+
40
+ command -v node >/dev/null || { echo "node is not on PATH" >&2; exit 1; }
41
+ command -v codesign >/dev/null || {
42
+ echo "codesign is missing: install the Xcode command line tools with xcode-select --install" >&2
43
+ exit 1
44
+ }
45
+
46
+ # See the note in build-windows.ps1: an Electron-based terminal exports this, and it turns the
47
+ # artifact into one that cannot find Electron.
48
+ unset ELECTRON_RUN_AS_NODE
49
+
50
+ cd "$PROJECT"
51
+ npm ci
52
+ npx drift-package doctor
53
+ npx drift-package build --target="$TARGET"
54
+
55
+ echo
56
+ echo "Artifacts:"
57
+ find "out/$TARGET" -maxdepth 1 -type f -print
58
+
59
+ # Said again at the end, because the interesting line scrolled past twenty minutes ago.
60
+ if [ -z "${CSC_LINK:-}${CSC_NAME:-}" ]; then
61
+ cat <<'EOF'
62
+
63
+ This build is ad-hoc signed. It runs here. A copy downloaded through a browser will be refused by
64
+ Gatekeeper until the person clears the quarantine flag:
65
+
66
+ xattr -dr com.apple.quarantine "/Applications/<the app>.app"
67
+
68
+ A Developer ID certificate in CSC_LINK removes that, and notarisation credentials remove it for
69
+ everybody rather than for the people willing to run a command.
70
+ EOF
71
+ fi