@lewismoten/gamemaker-sandbox-archives 0.0.0-stage → 3.2.0
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/CHANGELOG.md +22 -0
- package/LICENSE +24 -0
- package/NOTICE.md +11 -0
- package/README.md +100 -2
- package/dist/applyPatches-yRE1fvi0.js +86 -0
- package/dist/index.js +74 -0
- package/dist/pack.js +70 -0
- package/dist/player/applyPatches.js +10 -0
- package/dist/player/md5.js +25 -0
- package/dist/project/extensionNames.js +20 -0
- package/dist/project/importLeftovers.js +16 -0
- package/dist/project/importPlayer.js +599 -0
- package/dist/scan.js +263 -0
- package/dist/types/index.d.ts +25 -0
- package/dist/types/player/applyPatches.d.ts +25 -0
- package/dist/types/player/md5.d.ts +2 -0
- package/dist/types/player/patchSpans.d.ts +2 -0
- package/dist/types/player/playerBuild.d.ts +40 -0
- package/dist/types/player/playerBuildData.d.ts +13 -0
- package/dist/types/player/playerPreserve.d.ts +10 -0
- package/dist/types/player/preservedRegions.d.ts +19 -0
- package/dist/types/project/extensionNames.d.ts +4 -0
- package/dist/types/project/gamePack.d.ts +17 -0
- package/dist/types/project/importLeftovers.d.ts +3 -0
- package/dist/types/project/importPlayer.d.ts +39 -0
- package/dist/types/project/importScan.d.ts +133 -0
- package/dist/types/project/zip.d.ts +32 -0
- package/dist/zip.js +115 -0
- package/package.json +99 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Changes to `@lewismoten/gamemaker-sandbox-archives`, released as `archives-v*` tags. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
|
+
|
|
6
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
## [3.2.0] - 2026-11-07
|
|
9
|
+
|
|
10
|
+
The first release: game discovery, ZIP and SFX extraction, ZIP creation and a full import scanner, with no Node-only
|
|
11
|
+
code. Imports check what FLI, GIF and VOC files really hold and keep the player's build and author bytes.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Game discovery, ZIP and SFX extraction, and ZIP creation, with no Node-only code.
|
|
16
|
+
- A dedicated entry for the complete import scanner, with documented and verified scan-and-select APIs.
|
|
17
|
+
- Importing checks what FLI, GIF and VOC files really hold, and keeps the player's build and author bytes.
|
|
18
|
+
|
|
19
|
+
Full list: [changes/release/archives/3.2.0.md](https://github.com/lewismoten/GameMaker-SandboxEdition/blob/archives-v3.2.0/changes/release/archives/3.2.0.md)
|
|
20
|
+
|
|
21
|
+
[Unreleased]: https://github.com/lewismoten/GameMaker-SandboxEdition/compare/archives-v3.2.0...HEAD
|
|
22
|
+
[3.2.0]: https://github.com/lewismoten/GameMaker-SandboxEdition/releases/tag/archives-v3.2.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2014 gandrewstone
|
|
4
|
+
Copyright (c) 2026 Lewis Moten (modifications: the web port, the 1994 desk, the
|
|
5
|
+
Sandbox Software and Bard of Bizarre games, and the Mac, Windows and Linux players;
|
|
6
|
+
see NOTICE.md)
|
|
7
|
+
|
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
in the Software without restriction, including without limitation the rights
|
|
11
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
furnished to do so, subject to the following conditions:
|
|
14
|
+
|
|
15
|
+
The above copyright notice and this permission notice shall be included in all
|
|
16
|
+
copies or substantial portions of the Software.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
+
SOFTWARE.
|
package/NOTICE.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Notices
|
|
2
|
+
|
|
3
|
+
This package contains portable archive and game-import code from Game-Maker: Sandbox Edition by Lewis Moten, under the MIT License. See LICENSE for the original copyright and license terms.
|
|
4
|
+
|
|
5
|
+
The ZIP implementation was written for this project from PKWARE's ZIP file format (APPNOTE). Compression and decompression use the platform's CompressionStream and DecompressionStream; no third-party ZIP library is bundled. The ZIP reader handles stored and deflated files and ZIP-based self-extracting archives. It never executes DOS programs.
|
|
6
|
+
|
|
7
|
+
Game discovery and player-file recovery follow RSD Game-Maker 3.0 by Recreational Software Designs: G. Andrew Stone, G. Oliver Stone, Oliver Stone Jr. and Joan Stone. Andy Stone released its source under the MIT License in 2014. No original DOS executable or game content is included.
|
|
8
|
+
|
|
9
|
+
Player identification uses this project's MD5 implementation, written from RFC 1321 (R. Rivest, MIT Laboratory for Computer Science and RSA Data Security, Inc., 1992). It does not include the RFC's reference implementation. The implementation is covered by the project's MIT License.
|
|
10
|
+
|
|
11
|
+
The underlying ZIP and import code was written with Anthropic Claude Opus 5.5 under Lewis Moten's direction. OpenAI Codex (GPT-6/6.1) consolidated the portable package, API, tests, documentation, and consumer integration under his direction.
|
package/README.md
CHANGED
|
@@ -1,3 +1,101 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @lewismoten/gamemaker-sandbox-archives
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Discover and unpack Game-Maker games in browsers or Node.js 22.12+. This runtime package works alongside the engine; it does not depend on the development tools.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install @lewismoten/gamemaker-sandbox-archives
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Install through your configured npm registry; configure authentication according to that registry's instructions.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { unpackGame } from '@lewismoten/gamemaker-sandbox-archives';
|
|
13
|
+
import { GameFiles } from '@lewismoten/gamemaker-sandbox-engine';
|
|
14
|
+
|
|
15
|
+
const response = await fetch('/stripe-chase.zip');
|
|
16
|
+
if (!response.ok) throw new Error('Could not download the game');
|
|
17
|
+
const game = await unpackGame(await response.arrayBuffer());
|
|
18
|
+
const files = new GameFiles(game.entries);
|
|
19
|
+
// Supply files and game.gam to Machine/runGame from the engine package.
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Supported inputs:
|
|
23
|
+
|
|
24
|
+
- File/directory selections (`File[]` preserves `webkitRelativePath`), or portable `ScanFile[]` records with `path`, `size`, and async `read`.
|
|
25
|
+
- ZIPs, ZIPs inside ZIPs, and ZIP-based self-extracting DOS EXEs, including EXEs inside ZIPs. Both ZIP-relative and file-relative SFX offsets are supported.
|
|
26
|
+
- Raw or gzip-compressed GMPK game packs (`.gmpak`).
|
|
27
|
+
|
|
28
|
+
Executables are inspected as data and never run. Ordinary executables are not archive formats. A supplied Game-Maker player EXE is identified by build (1.0, 1.04, 2.00, 2.02, 2.04 or 3.00: size and MD5, then its About text's version line, with a confidence and reasons). A customized one can restore renamed game-file extensions; its settings (read at each build's own offsets) are returned in `player`, and its other changed bytes in `player.origin.regions` (128-byte blocks, a buyer's owner field never kept), which `applyPlayerPatches` writes back into a 3.00 player. Archives with several games use `unpackGames`; `unpackGame` requires exactly one. Low-level discovery (`scanImport`, `GameReader`, `scanFile`) exposes selection, progress, leftovers, and malformed archive reports using the same importer as the main application.
|
|
29
|
+
|
|
30
|
+
`UnpackedGame.entries` is a portable tuple array, avoiding a dependency on the engine's class identity. ZIPs are searched up to the importer's existing three archive levels. `maxEntries` (8192) and `maxBytes` (128 MiB) bound each archive and the returned game; `signal` supports cancellation between reads. ZIP64, encrypted ZIPs, and compression methods other than stored/deflate are not supported. HTML game exports are not archive inputs.
|
|
31
|
+
|
|
32
|
+
## Scan and choose games before reading their files
|
|
33
|
+
|
|
34
|
+
The full import scanner is included, both at the package root and through `/scan`. It
|
|
35
|
+
finds games in folder selections and nested ZIP/SFX archives, reports damaged or empty
|
|
36
|
+
archives, identifies incomplete leftovers, and recognizes customized DOS player files.
|
|
37
|
+
Scanning reads game headers and archive directories; `GameReader` then reads the chosen
|
|
38
|
+
game's files. This lets an application offer a choice when an archive contains several games.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { scanFile, scanImport, scanSummary, GameReader, type FoundGame }
|
|
42
|
+
from '@lewismoten/gamemaker-sandbox-archives/scan';
|
|
43
|
+
import { GameFiles } from '@lewismoten/gamemaker-sandbox-engine';
|
|
44
|
+
|
|
45
|
+
const given = Array.from(input.files ?? [], (file) => scanFile(file));
|
|
46
|
+
const report = await scanImport(given, {
|
|
47
|
+
signal: controller.signal,
|
|
48
|
+
zipLimits: { maxEntries: 8192, maxBytes: 128 << 20 },
|
|
49
|
+
onProgress: (done, total, path) => showProgress(done, total, path),
|
|
50
|
+
});
|
|
51
|
+
showGames(report.games); // Let the user choose a FoundGame.
|
|
52
|
+
showSummary(scanSummary(report));
|
|
53
|
+
|
|
54
|
+
const reader = new GameReader(given, { maxEntries: 8192, maxBytes: 128 << 20 });
|
|
55
|
+
async function openChosenGame(choice: FoundGame) {
|
|
56
|
+
const game = await reader.read(choice);
|
|
57
|
+
return new GameFiles(game.files.entries());
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`report.games` holds the choices; `badZips`, `emptyZips`, and `loose` explain the rest.
|
|
62
|
+
`GameReader.file` reads just one chosen resource and `mainGam` identifies a game's entry
|
|
63
|
+
point. `scanFile` adapts browser files, while `ScanFile` records work with any asynchronous
|
|
64
|
+
file source. `/scan` also exports `groupGames`, `playedBy`, `PLAYER_EXE`, `gameFromFiles`,
|
|
65
|
+
and their report types. The higher-level `unpackGame` and `unpackGames` APIs use this same
|
|
66
|
+
scanner and also perform customized-player recovery.
|
|
67
|
+
|
|
68
|
+
## ZIP reading and writing
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { createZip, readZip, listZip } from '@lewismoten/gamemaker-sandbox-archives/zip';
|
|
72
|
+
|
|
73
|
+
const gameBytes = new Uint8Array([71, 77]); // stand-in for a real .GAM file's bytes
|
|
74
|
+
const bytes = await createZip(
|
|
75
|
+
[
|
|
76
|
+
{ name: 'GAME/DEMO.GAM', data: gameBytes },
|
|
77
|
+
{ name: 'GAME/README.TXT', data: new TextEncoder().encode('How to play') },
|
|
78
|
+
],
|
|
79
|
+
new Date(2000, 0, 1, 12), // a fixed date and stable entry order make repeatable distribution builds
|
|
80
|
+
);
|
|
81
|
+
const entries = await readZip(bytes);
|
|
82
|
+
const listing = listZip(bytes); // lazy entry.read()
|
|
83
|
+
console.log(entries.map((e) => `${e.name} ${e.data.length}`).join(', '), listing.length);
|
|
84
|
+
// prints: GAME/DEMO.GAM 2, GAME/README.TXT 11 2
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
A test (`web/test/readmeExamples.test.ts`) runs this block and the engine's in Node against the built package and compares what they print with the `// prints:` line.
|
|
88
|
+
|
|
89
|
+
ZIP creation uses deflate when available and smaller; otherwise it stores entries. It shares the reader's CRC code and adds no dependency. Consumers that only import unpacking can tree-shake the writer. Entry data is returned in memory; this package writes nothing to the filesystem. For the existing streaming game-pack format, `/pack` exports `encodePack`, `decodePack`, and `PackReader`.
|
|
90
|
+
|
|
91
|
+
## Subpath exports
|
|
92
|
+
|
|
93
|
+
`zip` (above), `pack` and `scan` are the main entry points. `project/zip`, `project/gamePack` and `project/importScan` are the same modules under their source names. The other subpaths are `project/importPlayer`, `project/importLeftovers`, `project/extensionNames`, `player/applyPatches` and `player/md5`.
|
|
94
|
+
|
|
95
|
+
## License and notices
|
|
96
|
+
|
|
97
|
+
The source is MIT licensed. The published package carries `LICENSE` and `NOTICE.md` (credits for third-party code and formats); it includes no game assets.
|
|
98
|
+
|
|
99
|
+
## Developing
|
|
100
|
+
|
|
101
|
+
This package's source lives in [the project's repository](https://github.com/lewismoten/GameMaker-SandboxEdition), in `web/packages/archives`. The folder holds its source in `src/`, build configuration, license and notices. It depends on the engine through its declared npm dependency. Run `npm install`, then `npm run verify` in the package folder. Inside the repository, run `npm ci` at the root and `npm run verify:packages`, which builds isolated source copies and supplies packed workspace dependencies, so unpublished sibling versions can be verified. Use `npm pack` to inspect the distribution. The build is independent of the site's Vite configuration. The distribution includes JavaScript, TypeScript declarations, LICENSE, and NOTICE.md; no game assets. Keep npm tarballs outside source control. The [monorepo guide](https://github.com/lewismoten/GameMaker-SandboxEdition/blob/master/docs/monorepo.md) covers the workspace layout, and the [package release guide](https://github.com/lewismoten/GameMaker-SandboxEdition/blob/master/docs/package-releases.md) covers publishing.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { md5Hex as h } from "./player/md5.js";
|
|
2
|
+
import { patchEntry as d, patchProblem as u, writePatch as w, readPatch as y, isStock as l, writeStock as E } from "@lewismoten/gamemaker-sandbox-engine/player/patchRules";
|
|
3
|
+
import { extKind as m, extValueProblem as S, extensionClashes as P, writeExtensions as x, readExtensions as C, writeStockExtensions as k } from "@lewismoten/gamemaker-sandbox-engine/player/fileExtensions";
|
|
4
|
+
import { PATCHES as v } from "@lewismoten/gamemaker-sandbox-engine/player/patchTable";
|
|
5
|
+
function A(e) {
|
|
6
|
+
const n = m(e);
|
|
7
|
+
if (n) return n.offsets.flatMap((t) => [t + 1, t + 2, t + 3]);
|
|
8
|
+
const r = d(e);
|
|
9
|
+
if (!r) return [];
|
|
10
|
+
const o = (t, f) => Array.from({ length: f }, (s, i) => t + i);
|
|
11
|
+
switch (r.kind) {
|
|
12
|
+
case "text":
|
|
13
|
+
case "format":
|
|
14
|
+
return o(r.offset, r.length);
|
|
15
|
+
case "number":
|
|
16
|
+
case "frame":
|
|
17
|
+
return r.fields.flatMap((t) => o(t.offset, t.size));
|
|
18
|
+
case "rgb":
|
|
19
|
+
return [...r.offsets];
|
|
20
|
+
case "select":
|
|
21
|
+
case "fkey":
|
|
22
|
+
return o(r.offset, 2);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const M = (e) => btoa(String.fromCharCode(...e)), p = (e) => Uint8Array.from(atob(e.data), (n) => n.charCodeAt(0));
|
|
26
|
+
function O(e, n, r) {
|
|
27
|
+
const o = /* @__PURE__ */ new Set();
|
|
28
|
+
for (const t of /* @__PURE__ */ new Set([...Object.keys(r), ...Object.keys(n.patches)]))
|
|
29
|
+
if (JSON.stringify(r[t]) !== JSON.stringify(n.patches[t])) for (const f of A(t)) o.add(f);
|
|
30
|
+
for (const t of n.regions) {
|
|
31
|
+
const f = p(t);
|
|
32
|
+
if (t.offset < 0 || t.offset + f.length > e.length) throw new Error("A preserved part of the player lies outside it.");
|
|
33
|
+
f.forEach((s, i) => {
|
|
34
|
+
o.has(t.offset + i) || (e[t.offset + i] = s);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function K(e) {
|
|
39
|
+
const n = [], r = p(e);
|
|
40
|
+
let o = "", t = 0;
|
|
41
|
+
for (let f = 0; f <= r.length; f++) {
|
|
42
|
+
const s = r[f];
|
|
43
|
+
f < r.length && s >= 32 && s < 127 ? (o || (t = f), o += String.fromCharCode(s)) : (o.trim().length >= 4 && n.push([e.offset + t, o]), o = "");
|
|
44
|
+
}
|
|
45
|
+
return n;
|
|
46
|
+
}
|
|
47
|
+
const b = "fcaf47d50ececa0b59cd22b3fae3cda3", g = 160598;
|
|
48
|
+
function L(e, n, r) {
|
|
49
|
+
if (e.length !== g || h(e) !== b)
|
|
50
|
+
throw new Error("This is not the Game-Maker 3.00 CD’s XFERPLAY.EXE, so it can’t be customized safely.");
|
|
51
|
+
const o = e.slice();
|
|
52
|
+
for (const [t, f] of Object.entries(n)) {
|
|
53
|
+
const s = m(t);
|
|
54
|
+
if (s) {
|
|
55
|
+
const c = S(s.ext, f) ?? P(n).get(s.ext);
|
|
56
|
+
if (c) throw new Error(`${s.label} (.${s.ext}): ${c}`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const i = d(t);
|
|
60
|
+
if (!i) throw new Error(`Unknown player patch ${t}`);
|
|
61
|
+
const a = u(i, f);
|
|
62
|
+
if (a) throw new Error(`${i.label} (${t}): ${a}`);
|
|
63
|
+
w(o, i, f);
|
|
64
|
+
}
|
|
65
|
+
return x(o, n), r && O(o, r, n), o;
|
|
66
|
+
}
|
|
67
|
+
function R(e) {
|
|
68
|
+
if (e.length !== g) return;
|
|
69
|
+
const n = {}, r = [], o = e.slice();
|
|
70
|
+
for (const s of v) {
|
|
71
|
+
const i = y(e, s);
|
|
72
|
+
i === void 0 || u(s, i) ? i !== void 0 && l(s, i) || r.push(s.id) : l(s, i) || (n[s.id] = i), E(o, s);
|
|
73
|
+
}
|
|
74
|
+
const t = C(e);
|
|
75
|
+
Object.assign(n, t.patches), k(o, t.read);
|
|
76
|
+
const f = h(o) === b;
|
|
77
|
+
return { patches: n, explained: f && !r.length, unreadable: r };
|
|
78
|
+
}
|
|
79
|
+
export {
|
|
80
|
+
b as S,
|
|
81
|
+
K as a,
|
|
82
|
+
L as b,
|
|
83
|
+
A as p,
|
|
84
|
+
R as r,
|
|
85
|
+
M as t
|
|
86
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { scanFile as u, scanImport as y, GameReader as A } from "./scan.js";
|
|
2
|
+
import { PLAYER_EXE as U, gameFromFiles as B, groupGames as N, playedBy as R, scanSummary as S } from "./scan.js";
|
|
3
|
+
import { adoptPlayer as b } from "./project/importPlayer.js";
|
|
4
|
+
import { decodePack as E } from "./pack.js";
|
|
5
|
+
import { crc32 as F, createZip as L, isZip as M, listZip as j, readZip as C } from "./zip.js";
|
|
6
|
+
async function G(a, c) {
|
|
7
|
+
const r = new Blob([a]).stream().pipeThrough(new DecompressionStream("gzip")).getReader(), i = [];
|
|
8
|
+
let t = 0;
|
|
9
|
+
for (; ; ) {
|
|
10
|
+
const { done: o, value: f } = await r.read();
|
|
11
|
+
if (o) break;
|
|
12
|
+
if (t += f.length, t > c)
|
|
13
|
+
throw await r.cancel(), new Error("Game archive exceeds the byte limit");
|
|
14
|
+
i.push(f);
|
|
15
|
+
}
|
|
16
|
+
const n = new Uint8Array(t);
|
|
17
|
+
let h = 0;
|
|
18
|
+
for (const o of i)
|
|
19
|
+
n.set(o, h), h += o.length;
|
|
20
|
+
return n;
|
|
21
|
+
}
|
|
22
|
+
async function v(a, c = {}) {
|
|
23
|
+
const { signal: r, maxEntries: i = 8192, maxBytes: t = 128 << 20 } = c;
|
|
24
|
+
if (!Number.isSafeInteger(i) || i < 1 || !Number.isSafeInteger(t) || t < 1)
|
|
25
|
+
throw new RangeError("Archive limits must be positive integers");
|
|
26
|
+
r?.throwIfAborted();
|
|
27
|
+
let n;
|
|
28
|
+
if (a instanceof Uint8Array || a instanceof ArrayBuffer) {
|
|
29
|
+
let e = a instanceof Uint8Array ? a : new Uint8Array(a);
|
|
30
|
+
if (e.length > t) throw new Error("Game archive exceeds the byte limit");
|
|
31
|
+
if (e[0] === 31 && e[1] === 139 && (e = await G(e, t)), e[0] === 71 && e[1] === 77 && e[2] === 80 && e[3] === 75)
|
|
32
|
+
n = E(e).map(([s, m]) => ({ path: s, size: m.length, read: () => Promise.resolve(m) }));
|
|
33
|
+
else {
|
|
34
|
+
const s = e, m = e[0] === 77 && e[1] === 90 ? ".exe" : ".zip", l = c.name ?? "game";
|
|
35
|
+
n = [{ path: l.toLowerCase().endsWith(m) ? l : l + m, size: e.length, read: () => Promise.resolve(s) }];
|
|
36
|
+
}
|
|
37
|
+
} else n = Array.from(a, (e) => "path" in e ? e : u(e));
|
|
38
|
+
if (n.length > i || n.some((e) => e.size > t)) throw new Error("Game input exceeds archive limits");
|
|
39
|
+
const h = { maxEntries: i, maxBytes: t }, o = await y(n, { signal: r, zipLimits: h });
|
|
40
|
+
if (!o.games.length && o.badZips.length) throw new Error(o.badZips.map((e) => `${e.path}: ${e.error}`).join("; "));
|
|
41
|
+
const f = new A(n, h), g = [];
|
|
42
|
+
for (const e of o.games) {
|
|
43
|
+
r?.throwIfAborted();
|
|
44
|
+
const s = await f.read(e), m = b(s.files, s.players), l = s.files.mainGame(e.gam);
|
|
45
|
+
if (!l) throw new Error(`No .GAM file in ${e.name}`);
|
|
46
|
+
const p = s.files.entries();
|
|
47
|
+
if (p.length > i || p.reduce((w, [, d]) => w + d.length, 0) > t) throw new Error("Unpacked game exceeds archive limits");
|
|
48
|
+
g.push({ name: e.name, gam: l, entries: p, ...m ? { player: m } : {} });
|
|
49
|
+
}
|
|
50
|
+
return g;
|
|
51
|
+
}
|
|
52
|
+
async function Z(a, c) {
|
|
53
|
+
const r = await v(a, c);
|
|
54
|
+
if (r.length !== 1)
|
|
55
|
+
throw new Error(r.length ? `Archive contains ${r.length} games; choose one with unpackGames` : "No Game-Maker .GAM file was found");
|
|
56
|
+
return r[0];
|
|
57
|
+
}
|
|
58
|
+
export {
|
|
59
|
+
A as GameReader,
|
|
60
|
+
U as PLAYER_EXE,
|
|
61
|
+
F as crc32,
|
|
62
|
+
L as createZip,
|
|
63
|
+
B as gameFromFiles,
|
|
64
|
+
N as groupGames,
|
|
65
|
+
M as isZip,
|
|
66
|
+
j as listZip,
|
|
67
|
+
R as playedBy,
|
|
68
|
+
C as readZip,
|
|
69
|
+
u as scanFile,
|
|
70
|
+
y as scanImport,
|
|
71
|
+
S as scanSummary,
|
|
72
|
+
Z as unpackGame,
|
|
73
|
+
v as unpackGames
|
|
74
|
+
};
|
package/dist/pack.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const c = [71, 77, 80, 75], l = 1;
|
|
2
|
+
function g(h) {
|
|
3
|
+
const s = [new Uint8Array([...c, l])];
|
|
4
|
+
for (const [t, o] of h) {
|
|
5
|
+
const n = new TextEncoder().encode(t);
|
|
6
|
+
if (!n.length || n.length > 255) throw new Error(`gamePack: bad file name ${JSON.stringify(t)}`);
|
|
7
|
+
const a = new Uint8Array(1 + n.length + 4);
|
|
8
|
+
a[0] = n.length, a.set(n, 1), new DataView(a.buffer).setUint32(1 + n.length, o.length, !0), s.push(a, o);
|
|
9
|
+
}
|
|
10
|
+
s.push(new Uint8Array([0]));
|
|
11
|
+
const r = new Uint8Array(s.reduce((t, o) => t + o.length, 0));
|
|
12
|
+
let e = 0;
|
|
13
|
+
for (const t of s)
|
|
14
|
+
r.set(t, e), e += t.length;
|
|
15
|
+
return r;
|
|
16
|
+
}
|
|
17
|
+
class u {
|
|
18
|
+
chunks = [];
|
|
19
|
+
total = 0;
|
|
20
|
+
/** Bytes the next step needs (it waits until that many are in, so a big file is joined once). */
|
|
21
|
+
want = 5;
|
|
22
|
+
started = !1;
|
|
23
|
+
done = !1;
|
|
24
|
+
push(s) {
|
|
25
|
+
const r = [];
|
|
26
|
+
if (this.done || !s.length || (this.chunks.push(s), this.total += s.length, this.total < this.want)) return r;
|
|
27
|
+
const e = this.chunks.length === 1 ? this.chunks[0] : f(this.chunks, this.total);
|
|
28
|
+
let t = 0;
|
|
29
|
+
if (!this.started) {
|
|
30
|
+
if (c.some((n, a) => e[a] !== n) || e[4] !== l) throw new Error("gamePack: not a game pack");
|
|
31
|
+
this.started = !0, t = 5;
|
|
32
|
+
}
|
|
33
|
+
for (this.want = 1; t < e.length; ) {
|
|
34
|
+
const n = e[t];
|
|
35
|
+
if (n === 0) {
|
|
36
|
+
this.done = !0, t++;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
if (t + 1 + n + 4 > e.length) {
|
|
40
|
+
this.want = 1 + n + 4;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
const a = new DataView(e.buffer, e.byteOffset + t + 1 + n, 4).getUint32(0, !0), i = t + 1 + n + 4;
|
|
44
|
+
if (i + a > e.length) {
|
|
45
|
+
this.want = 1 + n + 4 + a;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
r.push([String.fromCharCode(...e.subarray(t + 1, t + 1 + n)), e.slice(i, i + a)]), t = i + a;
|
|
49
|
+
}
|
|
50
|
+
const o = e.subarray(t);
|
|
51
|
+
return this.chunks = o.length ? [o] : [], this.total = o.length, r;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function f(h, s) {
|
|
55
|
+
const r = new Uint8Array(s);
|
|
56
|
+
let e = 0;
|
|
57
|
+
for (const t of h)
|
|
58
|
+
r.set(t, e), e += t.length;
|
|
59
|
+
return r;
|
|
60
|
+
}
|
|
61
|
+
function w(h) {
|
|
62
|
+
const s = new u(), r = s.push(h);
|
|
63
|
+
if (!s.done) throw new Error("gamePack: the pack is cut short");
|
|
64
|
+
return r;
|
|
65
|
+
}
|
|
66
|
+
export {
|
|
67
|
+
u as PackReader,
|
|
68
|
+
w as decodePack,
|
|
69
|
+
g as encodePack
|
|
70
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import "./md5.js";
|
|
2
|
+
import { S as m, b as s, r as P } from "../applyPatches-yRE1fvi0.js";
|
|
3
|
+
import "@lewismoten/gamemaker-sandbox-engine/player/patchTable";
|
|
4
|
+
import "@lewismoten/gamemaker-sandbox-engine/player/patchRules";
|
|
5
|
+
import "@lewismoten/gamemaker-sandbox-engine/player/fileExtensions";
|
|
6
|
+
export {
|
|
7
|
+
m as STOCK_PLAYER_MD5,
|
|
8
|
+
s as applyPlayerPatches,
|
|
9
|
+
P as readPlayerPatches
|
|
10
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const d = [7, 12, 17, 22, 5, 9, 14, 20, 4, 11, 16, 23, 6, 10, 15, 21], M = Array.from({ length: 64 }, (f, c) => Math.floor(Math.abs(Math.sin(c + 1)) * 2 ** 32) >>> 0);
|
|
2
|
+
function p(f) {
|
|
3
|
+
const c = f.length * 8, l = new Uint8Array(((f.length + 8 >> 6) + 1) * 64);
|
|
4
|
+
l.set(f), l[f.length] = 128;
|
|
5
|
+
const U = new DataView(l.buffer);
|
|
6
|
+
U.setUint32(l.length - 8, c >>> 0, !0), U.setUint32(l.length - 4, Math.floor(c / 2 ** 32), !0);
|
|
7
|
+
let h = 1732584193, u = 4023233417, g = 2562383102, w = 271733878;
|
|
8
|
+
const b = new Uint32Array(16);
|
|
9
|
+
for (let r = 0; r < l.length; r += 64) {
|
|
10
|
+
for (let t = 0; t < 16; t++) b[t] = U.getUint32(r + t * 4, !0);
|
|
11
|
+
let [i, e, o, n] = [h, u, g, w];
|
|
12
|
+
for (let t = 0; t < 64; t++) {
|
|
13
|
+
let s, a;
|
|
14
|
+
t < 16 ? [s, a] = [e & o | ~e & n, t] : t < 32 ? [s, a] = [n & e | ~n & o, (5 * t + 1) % 16] : t < 48 ? [s, a] = [e ^ o ^ n, (3 * t + 5) % 16] : [s, a] = [o ^ (e | ~n), 7 * t % 16];
|
|
15
|
+
const A = i + s + M[t] + b[a] >>> 0, m = d[(t >> 4) * 4 + t % 4];
|
|
16
|
+
[i, n, o] = [n, o, e], e = e + (A << m | A >>> 32 - m) >>> 0;
|
|
17
|
+
}
|
|
18
|
+
h = h + i >>> 0, u = u + e >>> 0, g = g + o >>> 0, w = w + n >>> 0;
|
|
19
|
+
}
|
|
20
|
+
const y = new DataView(new ArrayBuffer(16));
|
|
21
|
+
return [h, u, g, w].forEach((r, i) => y.setUint32(i * 4, r, !0)), Array.from(new Uint8Array(y.buffer), (r) => r.toString(16).padStart(2, "0")).join("");
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
p as md5Hex
|
|
25
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const g = (o) => o.replace(/[\\^$.*+?()[\]{}|-]/g, "\\$&");
|
|
2
|
+
function l(o, n) {
|
|
3
|
+
const t = /\.([^.]*)$/.exec(o), r = t ? n.get(t[1].toLowerCase()) : void 0;
|
|
4
|
+
if (!t || r === void 0) return o;
|
|
5
|
+
const c = /[A-Z]/.test(t[1]) && t[1] === t[1].toUpperCase();
|
|
6
|
+
return o.slice(0, t.index + 1) + (c ? r.toUpperCase() : r);
|
|
7
|
+
}
|
|
8
|
+
function f(o, n) {
|
|
9
|
+
const t = o.slice(), r = Array.from(t, (e) => String.fromCharCode(e)).join("").toLowerCase(), c = [...n.keys()].filter((e) => e && e.length === n.get(e).length);
|
|
10
|
+
if (!c.length) return t;
|
|
11
|
+
for (const e of r.matchAll(new RegExp(`[a-z0-9_~!@#$%^&()-]\\.(${c.map(g).join("|")})(?![a-z0-9])`, "g"))) {
|
|
12
|
+
const i = e.index + 2, p = n.get(e[1]), a = t[i] >= 65 && t[i] <= 90;
|
|
13
|
+
for (let s = 0; s < p.length; s++) t[i + s] = (a ? p[s].toUpperCase() : p[s]).charCodeAt(0);
|
|
14
|
+
}
|
|
15
|
+
return t;
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
f as rewriteNames,
|
|
19
|
+
l as swapExtension
|
|
20
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const s = (t) => t.zips.length ? [t.zips[0].path, ...t.inner].join(`
|
|
2
|
+
`) : void 0;
|
|
3
|
+
function r(t) {
|
|
4
|
+
const i = t.gam ?? t.paths.filter((e) => /\.gam$/i.test(e)).sort()[0];
|
|
5
|
+
return (i ? i.slice(i.replace(/\\/g, "/").lastIndexOf("/") + 1) : t.name).toUpperCase();
|
|
6
|
+
}
|
|
7
|
+
function o(t) {
|
|
8
|
+
const i = new Set(t.filter((e) => !e.missing && !e.leftover).map(s));
|
|
9
|
+
return t.map((e) => {
|
|
10
|
+
const n = s(e);
|
|
11
|
+
return !n || !e.missing || e.leftover || !i.has(n) ? e : { ...e, leftover: `${r(e)}: ${e.missing} of its files ${e.missing === 1 ? "isn’t" : "aren’t"} in the zip` };
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
o as markZipLeftovers
|
|
16
|
+
};
|