@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/dist/scan.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { normalize as y, cdName as R, GameFiles as q, IGNORED_FILE as U } from "@lewismoten/gamemaker-sandbox-engine/core/gameFiles";
|
|
2
|
+
import { listZip as X, crc32 as V, isZip as K } from "./zip.js";
|
|
3
|
+
import { gameTitle as Q, parseGame as Y } from "@lewismoten/gamemaker-sandbox-engine/formats/game";
|
|
4
|
+
import { isFli as tt } from "@lewismoten/gamemaker-sandbox-engine/formats/fli";
|
|
5
|
+
import { isGif as et } from "@lewismoten/gamemaker-sandbox-engine/formats/gif";
|
|
6
|
+
import { isVoc as st } from "@lewismoten/gamemaker-sandbox-engine/formats/voc";
|
|
7
|
+
import { markZipLeftovers as nt } from "./project/importLeftovers.js";
|
|
8
|
+
const at = {
|
|
9
|
+
fli: ["an animation (FLI)", tt],
|
|
10
|
+
gif: ["a picture (GIF)", et],
|
|
11
|
+
voc: ["a sound (VOC)", st]
|
|
12
|
+
};
|
|
13
|
+
async function it(t) {
|
|
14
|
+
const s = [];
|
|
15
|
+
for (const a of t) {
|
|
16
|
+
const i = at[/\.([^./\\]+)$/.exec(a.path)?.[1].toLowerCase() ?? ""];
|
|
17
|
+
if (!i || !a.size) continue;
|
|
18
|
+
const o = await a.read().catch(() => {
|
|
19
|
+
});
|
|
20
|
+
o && !i[1](o) && s.push({ path: a.path, says: i[0] });
|
|
21
|
+
}
|
|
22
|
+
return s;
|
|
23
|
+
}
|
|
24
|
+
const S = (t, s) => /\.exe$/i.test(t) && s === 160598, D = 3, ot = 64 << 20, rt = (t) => /\.exe$/i.test(t), ct = /\.(txt|doc|diz|nfo|me|1st|htm|html|md|wri|rtf|cfg|conf|bat)$/i, H = (t) => `${t} (a self-extracting archive)`;
|
|
25
|
+
function pt(t, s) {
|
|
26
|
+
const a = t.filter((e) => !P(u(e.path))), i = a.map((e) => M(u(e.path)));
|
|
27
|
+
if (!a.length) return "it is empty";
|
|
28
|
+
if (a.length === 1) return `only ${i[0]}${s.has(a[0]) ? ", a self-extracting archive with no game" : ""}`;
|
|
29
|
+
if (a.every((e) => ct.test(e.path))) return `only text files (${i.join(", ")})`;
|
|
30
|
+
const o = i.slice(0, 3).join(", ");
|
|
31
|
+
return `${a.length} files, such as ${o}${a.length > 3 ? "…" : ""}`;
|
|
32
|
+
}
|
|
33
|
+
const u = (t) => t.replace(/\\/g, "/").replace(/^(\.\/|\/)+/, ""), B = (t) => t.includes("/") ? t.slice(0, t.lastIndexOf("/")) : "", M = (t) => t.slice(t.lastIndexOf("/") + 1), P = (t) => /(^|\/)__MACOSX\//i.test(t) || /^\._|^\.ds_store$|^thumbs\.db$/i.test(M(t)), W = (t) => /\.gam$/i.test(t), F = (t) => /\.zip$/i.test(t), lt = (t) => !F(t.path) && (!U.test(M(t.path)) || S(t.path, t.size));
|
|
34
|
+
function ht(t, s = /* @__PURE__ */ new Set()) {
|
|
35
|
+
const a = t.filter((n) => !P(u(n.path)) && !F(n.path)), i = [...new Set(a.filter((n) => W(n.path) || s.has(n)).map((n) => B(u(n.path))))].sort((n, r) => r.length - n.length), o = new Map(i.map((n) => [n, []])), e = [];
|
|
36
|
+
for (const n of a) {
|
|
37
|
+
const r = B(u(n.path)), d = i.length === 1 ? i[0] : i.find((g) => g === "" || r === g || r.startsWith(`${g}/`));
|
|
38
|
+
d === void 0 ? e.push(n) : lt(n) && o.get(d).push(n);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
games: [...o].map(([n, r]) => ({ dir: n, files: r })).sort((n, r) => n.dir.localeCompare(r.dir, void 0, { numeric: !0, sensitivity: "base" })),
|
|
42
|
+
loose: e
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function ft(t) {
|
|
46
|
+
let s = "";
|
|
47
|
+
for (let a = 0; a < 4 && t[153270 + a]; a++) s += String.fromCharCode(t[153270 + a]);
|
|
48
|
+
return s = s.trim().toLowerCase(), s === "." || !s ? "" : s;
|
|
49
|
+
}
|
|
50
|
+
async function mt(t) {
|
|
51
|
+
const s = /* @__PURE__ */ new Map(), a = /* @__PURE__ */ new Set(), i = /* @__PURE__ */ new Map(), o = t.filter((e) => !P(u(e.path)));
|
|
52
|
+
for (const e of o.filter((n) => S(n.path, n.size))) {
|
|
53
|
+
const n = await e.read().catch(() => {
|
|
54
|
+
});
|
|
55
|
+
if (!n) continue;
|
|
56
|
+
i.set(e, V(n));
|
|
57
|
+
const r = ft(n), d = u(e.path), g = o.filter((l) => l !== e && B(u(l.path)) === B(d)), O = y(`${M(d).replace(/\.[^.]*$/, "")}${r}`), I = g.find((l) => y(l.path) === O);
|
|
58
|
+
if (I && (s.set(e, I), a.add(I)), r && r !== ".gam")
|
|
59
|
+
for (const l of g) y(l.path).endsWith(r) && a.add(l);
|
|
60
|
+
}
|
|
61
|
+
return { players: s, games: a, crcs: i };
|
|
62
|
+
}
|
|
63
|
+
function ut(t, s) {
|
|
64
|
+
if (!t) return 0;
|
|
65
|
+
const a = new Set(s.flatMap((i) => [y(i.path), R(i.path)]));
|
|
66
|
+
try {
|
|
67
|
+
return [...new Set(
|
|
68
|
+
Y(t).scenes.flatMap((o) => o.files).filter((o) => o && !/\.(his|sav|rec)$/i.test(o)).map(y)
|
|
69
|
+
)].filter((o) => !a.has(o) && !a.has(R(o))).length;
|
|
70
|
+
} catch {
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function dt(t, s) {
|
|
75
|
+
try {
|
|
76
|
+
if (t) return Q(Y(t)) || s;
|
|
77
|
+
} catch {
|
|
78
|
+
}
|
|
79
|
+
return s;
|
|
80
|
+
}
|
|
81
|
+
const J = (t, s) => [...t, s ? `${s}/` : ""].filter(Boolean).join(" › "), gt = () => new Promise((t) => setTimeout(t, 0));
|
|
82
|
+
async function It(t, { signal: s, onProgress: a, onStep: i, zipLimits: o } = {}) {
|
|
83
|
+
const e = { games: [], zips: 0, emptyZips: [], badZips: [], loose: [] }, n = t.filter((l) => F(l.path));
|
|
84
|
+
let r = 0;
|
|
85
|
+
const d = async (l, $, E, x) => {
|
|
86
|
+
const { players: G, games: C, crcs: Z } = await mt(l), { games: _, loose: L } = ht(l, C);
|
|
87
|
+
(e.misnamed ??= []).push(...await it(l.filter((c) => !F(c.path)))), $.length || e.loose.push(...L);
|
|
88
|
+
let m = 0;
|
|
89
|
+
const v = async (c, p, z, j, A) => {
|
|
90
|
+
const N = [];
|
|
91
|
+
let h = "", f = 0;
|
|
92
|
+
for (const [b, w] of p.entries()) {
|
|
93
|
+
const T = await w.read().catch(() => {
|
|
94
|
+
});
|
|
95
|
+
T && N.push(V(T)), b === 0 && (h = dt(T, j ?? y(w.path)), f = ut(T, z));
|
|
96
|
+
}
|
|
97
|
+
const k = z.map((b) => Z.get(b)).filter((b) => b !== void 0);
|
|
98
|
+
e.games.push({
|
|
99
|
+
name: h,
|
|
100
|
+
from: J($.length ? [$[0].path, ...E].map(u) : [], c.dir),
|
|
101
|
+
gamCrcs: N,
|
|
102
|
+
...k.length ? { playerCrcs: k } : {},
|
|
103
|
+
fileCount: z.length,
|
|
104
|
+
bytes: z.reduce((b, w) => b + w.size, 0),
|
|
105
|
+
zips: $.slice(0, 1),
|
|
106
|
+
inner: E,
|
|
107
|
+
paths: z.map((b) => b.path),
|
|
108
|
+
gam: j,
|
|
109
|
+
...f ? { missing: f } : {},
|
|
110
|
+
...A?.(f) ? { leftover: A(f) } : {}
|
|
111
|
+
}), m++;
|
|
112
|
+
};
|
|
113
|
+
for (const c of _) {
|
|
114
|
+
const p = c.files.filter((h) => W(h.path) || C.has(h)).sort((h, f) => y(h.path).localeCompare(y(f.path)));
|
|
115
|
+
if (p.length < 2) {
|
|
116
|
+
await v(c, p, c.files);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const z = new Map([...G].filter(([h, f]) => c.files.includes(h) && c.files.includes(f)).map(([h, f]) => [f, h])), j = new Set(z.values()), A = c.files.some((h) => S(h.path, h.size)), N = $.length ? "the zip" : "the folder";
|
|
120
|
+
for (const h of [...p].sort((f, k) => Number(z.has(k)) - Number(z.has(f)))) {
|
|
121
|
+
const f = z.get(h), k = c.files.filter((w) => w === h || !p.includes(w) && (!f || !j.has(w) || w === f)), b = y(`${M(u(h.path)).replace(/\.[^.]*$/, "")}.gam`);
|
|
122
|
+
await v(
|
|
123
|
+
c,
|
|
124
|
+
[h],
|
|
125
|
+
k,
|
|
126
|
+
b,
|
|
127
|
+
(w) => w && !f ? `${b.toUpperCase()}: ${w} of its files ${w === 1 ? "isn’t" : "aren’t"} in ${N}${A ? "; no player loads it" : ""}` : void 0
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const c of l.filter((p) => F(p.path) && !P(u(p.path)))) {
|
|
132
|
+
s?.throwIfAborted();
|
|
133
|
+
const p = x === 0;
|
|
134
|
+
p && a?.(r++, n.length, c.path);
|
|
135
|
+
const z = await O(c, p ? [c] : $, p ? [] : [...E, c.path], x);
|
|
136
|
+
m += z ?? 0, p && await gt();
|
|
137
|
+
}
|
|
138
|
+
if (x < D)
|
|
139
|
+
for (const c of L.filter((p) => rt(p.path) && !S(p.path, p.size) && p.size <= ot)) {
|
|
140
|
+
s?.throwIfAborted();
|
|
141
|
+
const p = await O(c, x === 0 ? [c] : $, x === 0 ? [] : [...E, c.path], x, !0);
|
|
142
|
+
p !== void 0 && (g.add(c), m += p);
|
|
143
|
+
}
|
|
144
|
+
return m;
|
|
145
|
+
}, g = /* @__PURE__ */ new Set(), O = async (l, $, E, x, G = !1) => {
|
|
146
|
+
const C = [$[0].path, ...E].map(u).join(" › ");
|
|
147
|
+
let Z;
|
|
148
|
+
try {
|
|
149
|
+
i?.("unpack", C);
|
|
150
|
+
const m = await l.read();
|
|
151
|
+
if (G) {
|
|
152
|
+
if (!m || m[0] !== 77 || m[1] !== 90) return;
|
|
153
|
+
try {
|
|
154
|
+
Z = X(m, o).map((v) => ({ path: v.name, size: v.size, read: v.read }));
|
|
155
|
+
} catch {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
if (!m || !K(m)) throw new Error("It is not a zip file.");
|
|
160
|
+
Z = X(m, o).map((v) => ({ path: v.name, size: v.size, read: v.read }));
|
|
161
|
+
}
|
|
162
|
+
} catch (m) {
|
|
163
|
+
return e.zips++, e.badZips.push({ path: C, error: m.message || "It could not be read." }), 0;
|
|
164
|
+
}
|
|
165
|
+
e.zips++, G && I.add(C);
|
|
166
|
+
const _ = x + 1 >= D ? Z.filter((m) => !F(m.path)) : Z;
|
|
167
|
+
i?.("find", C);
|
|
168
|
+
const L = await d(_, $, E, x + 1);
|
|
169
|
+
return L || e.emptyZips.push({ path: G ? H(C) : C, held: pt(_, g) }), L;
|
|
170
|
+
}, I = /* @__PURE__ */ new Set();
|
|
171
|
+
return await d(t, [], [], 0), e.games = nt(wt(e.games, I)), e.loose = e.loose.filter((l) => !g.has(l)), e.games.sort((l, $) => l.from.localeCompare($.from, void 0, { numeric: !0, sensitivity: "base" })), a?.(n.length, n.length), e;
|
|
172
|
+
}
|
|
173
|
+
function wt(t, s) {
|
|
174
|
+
const a = (e) => {
|
|
175
|
+
const n = [...s].find((r) => e.from === r || e.from.startsWith(`${r} › `));
|
|
176
|
+
return n ? e.from.replace(n, H(n)) : e.from || "the files picked";
|
|
177
|
+
}, i = /* @__PURE__ */ new Map(), o = [];
|
|
178
|
+
for (const e of t) {
|
|
179
|
+
if (!e.gamCrcs.length) {
|
|
180
|
+
o.push(e);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const n = [...e.gamCrcs].sort().join(","), r = i.get(n);
|
|
184
|
+
r ? r.push(e) : i.set(n, [e]);
|
|
185
|
+
}
|
|
186
|
+
for (const e of i.values()) {
|
|
187
|
+
const n = e.reduce((d, g) => g.fileCount > d.fileCount ? g : d), r = e.filter((d) => d !== n);
|
|
188
|
+
o.push(r.length ? { ...n, alsoIn: r.map(a) } : n);
|
|
189
|
+
}
|
|
190
|
+
return o;
|
|
191
|
+
}
|
|
192
|
+
function Zt(t) {
|
|
193
|
+
const s = (o, e, n = `${e}s`) => `${o} ${o === 1 ? e : n}`, a = [`${s(t.games.length, "game")} found`];
|
|
194
|
+
t.emptyZips.length && a.push(`${s(t.emptyZips.length, "zip")} had no game`), t.badZips.length && a.push(`${s(t.badZips.length, "zip")} could not be read`), t.misnamed?.length && a.push(`${s(t.misnamed.length, "file")} not what ${t.misnamed.length === 1 ? "its" : "their"} extension says`);
|
|
195
|
+
const i = t.games.reduce((o, e) => o + (e.alsoIn?.length ?? 0), 0);
|
|
196
|
+
return i && a.push(`${s(i, "copy", "copies")} of the same ${i === 1 ? "game" : "games"} left out`), !t.games.length && t.zips && !t.emptyZips.length && !t.badZips.length && a.push(`in ${s(t.zips, "zip")}`), a.join(", ");
|
|
197
|
+
}
|
|
198
|
+
async function yt(t, s) {
|
|
199
|
+
const a = new q(), i = [];
|
|
200
|
+
for (const [o, e] of t.entries()) {
|
|
201
|
+
await s?.(o, t.length, e.path);
|
|
202
|
+
const n = M(u(e.path));
|
|
203
|
+
if (S(n, e.size)) {
|
|
204
|
+
const r = await e.read();
|
|
205
|
+
r && i.push({ name: n, bytes: r });
|
|
206
|
+
} else if (!U.test(n)) {
|
|
207
|
+
const r = await e.read();
|
|
208
|
+
r && a.set(n, r);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return { files: a, players: i };
|
|
212
|
+
}
|
|
213
|
+
class kt {
|
|
214
|
+
constructor(s, a) {
|
|
215
|
+
this.zipLimits = a, this.loose = new Map(s.map((i) => [i.path, i]));
|
|
216
|
+
}
|
|
217
|
+
zipLimits;
|
|
218
|
+
loose;
|
|
219
|
+
last;
|
|
220
|
+
/** One of a found game's files by its DOS name (its title screen's), without reading the rest. */
|
|
221
|
+
async file(s, a) {
|
|
222
|
+
const i = s.zips.length ? await this.zipEntries(s) : this.loose, o = y(a), e = R(a), n = s.paths.find((r) => y(r) === o) ?? s.paths.find((r) => y(r) === e);
|
|
223
|
+
return n ? i.get(n)?.read() : void 0;
|
|
224
|
+
}
|
|
225
|
+
/** The .gam a found game plays: its own when its folder holds others, else the first. */
|
|
226
|
+
mainGam(s) {
|
|
227
|
+
return s.gam ?? s.paths.filter(W).map(y).sort()[0];
|
|
228
|
+
}
|
|
229
|
+
async read(s, a) {
|
|
230
|
+
const i = s.zips.length ? await this.zipEntries(s) : this.loose, o = await yt(
|
|
231
|
+
s.paths.map((e) => i.get(e)).filter((e) => !!e),
|
|
232
|
+
a
|
|
233
|
+
);
|
|
234
|
+
return s.gam ? { ...o, name: s.name, gam: s.gam } : o;
|
|
235
|
+
}
|
|
236
|
+
async zipEntries(s) {
|
|
237
|
+
const a = [s.zips[0].path, ...s.inner].join(`
|
|
238
|
+
`);
|
|
239
|
+
if (this.last?.key === a) return this.last.entries;
|
|
240
|
+
this.last = void 0;
|
|
241
|
+
let i = await s.zips[0].read();
|
|
242
|
+
for (const e of s.inner) {
|
|
243
|
+
const n = i && X(i, this.zipLimits).find((r) => r.name === e);
|
|
244
|
+
i = n ? await n.read() : void 0;
|
|
245
|
+
}
|
|
246
|
+
if (!i) throw new Error(`${J([s.zips[0].path, ...s.inner].map(u), "")} could not be read again.`);
|
|
247
|
+
const o = new Map(X(i, this.zipLimits).map((e) => [e.name, { path: e.name, size: e.size, read: e.read }]));
|
|
248
|
+
return this.last = { key: a, entries: o }, o;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function Ft(t, s = t.webkitRelativePath || t.name) {
|
|
252
|
+
return { path: s, size: t.size, read: async () => new Uint8Array(await t.arrayBuffer()) };
|
|
253
|
+
}
|
|
254
|
+
export {
|
|
255
|
+
kt as GameReader,
|
|
256
|
+
S as PLAYER_EXE,
|
|
257
|
+
yt as gameFromFiles,
|
|
258
|
+
ht as groupGames,
|
|
259
|
+
mt as playedBy,
|
|
260
|
+
Ft as scanFile,
|
|
261
|
+
It as scanImport,
|
|
262
|
+
Zt as scanSummary
|
|
263
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type ScanFile } from './project/importScan.js';
|
|
2
|
+
import { type ImportedPlayer } from './project/importPlayer.js';
|
|
3
|
+
export { scanImport, GameReader, scanFile, gameFromFiles, scanSummary, groupGames, playedBy, PLAYER_EXE, type ScanFile, type ScanOptions, type ScanReport, type FoundGame, type ReadGame, } from './project/importScan.js';
|
|
4
|
+
export { createZip, readZip, listZip, isZip, crc32, type ZipEntry, type ZipListing, type ZipLimits } from './project/zip.js';
|
|
5
|
+
export interface UnpackOptions {
|
|
6
|
+
/** Optional source name, used for messages and archive detection. */
|
|
7
|
+
name?: string;
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
/** Bounds per archive and on the returned game's file data. Defaults: 8192 files, 128 MiB. */
|
|
10
|
+
maxEntries?: number;
|
|
11
|
+
maxBytes?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface UnpackedGame {
|
|
14
|
+
name: string;
|
|
15
|
+
gam: string;
|
|
16
|
+
/** Portable tuples: pass these to the engine's own GameFiles constructor. */
|
|
17
|
+
entries: [string, Uint8Array][];
|
|
18
|
+
/** The game's player, when one was supplied: its Game-Maker build (`origin`) and, if customized, its settings. */
|
|
19
|
+
player?: ImportedPlayer;
|
|
20
|
+
}
|
|
21
|
+
export type GameInput = Uint8Array | ArrayBuffer | Iterable<ScanFile | File>;
|
|
22
|
+
/** Discover and read games from folders, ZIPs, ZIP-based SFX EXEs, nested archives, or GMPK packs. */
|
|
23
|
+
export declare function unpackGames(input: GameInput, options?: UnpackOptions): Promise<UnpackedGame[]>;
|
|
24
|
+
/** Read exactly one game; collections use unpackGames or scanImport plus GameReader. */
|
|
25
|
+
export declare function unpackGame(input: GameInput, options?: UnpackOptions): Promise<UnpackedGame>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type PreservedParts } from './preservedRegions.js';
|
|
2
|
+
export type { PreservedParts } from './preservedRegions.js';
|
|
3
|
+
import { type PlayerPatches } from '@lewismoten/gamemaker-sandbox-engine/player/patchRules';
|
|
4
|
+
/** Game-Maker 3.00's XFERPLAY.EXE as it came on the CD (cd/gm/xferplay.exe). */
|
|
5
|
+
export declare const STOCK_PLAYER_MD5 = "fcaf47d50ececa0b59cd22b3fae3cda3";
|
|
6
|
+
/**
|
|
7
|
+
* A patched copy of the CD's player, with an imported 3.00 player's preserved parts (`kept`)
|
|
8
|
+
* written back. Throws when `exe` is any other file, or when a patch is unknown or breaks its
|
|
9
|
+
* rules (nothing is written then).
|
|
10
|
+
*/
|
|
11
|
+
export declare function applyPlayerPatches(exe: Uint8Array, patches: PlayerPatches, kept?: PreservedParts): Uint8Array;
|
|
12
|
+
interface ReadPlayer {
|
|
13
|
+
/** The changes that could be read as patches. */
|
|
14
|
+
patches: PlayerPatches;
|
|
15
|
+
/** True when those explain every byte that differs from the CD's player. */
|
|
16
|
+
explained: boolean;
|
|
17
|
+
/** Entries that differ but break their rules (left out of `patches`). */
|
|
18
|
+
unreadable: string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* What a player of XFERPLAY 3.00's size differs from the CD's in, as patches, its renamed file
|
|
22
|
+
* extensions included (`explained` false when other bytes changed too, such as an About page of
|
|
23
|
+
* its own). Undefined for any other size.
|
|
24
|
+
*/
|
|
25
|
+
export declare function readPlayerPatches(exe: Uint8Array): ReadPlayer | undefined;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type BuildData } from './playerBuildData.js';
|
|
2
|
+
type Confidence = 'high' | 'medium' | 'low';
|
|
3
|
+
export interface PlayerIdentity {
|
|
4
|
+
/** '1.0', '1.04', '2.00', '2.02', '2.04', '3.00', '3.00-playgame', or 'unknown'. */
|
|
5
|
+
build: string;
|
|
6
|
+
/** For people: "Game-Maker 2.02", "Game-Maker 3.00 (PLAYGAME.EXE)" or "an unknown Game-Maker build". */
|
|
7
|
+
label: string;
|
|
8
|
+
confidence: Confidence;
|
|
9
|
+
reasons: string[];
|
|
10
|
+
/** True when every byte is the build's stock copy (its owner field aside). */
|
|
11
|
+
stock: boolean;
|
|
12
|
+
/** The About page's version line as this copy has it (an author could edit it). */
|
|
13
|
+
versionLine?: string;
|
|
14
|
+
/** Who replaced RSD's About text, as that text names them. */
|
|
15
|
+
aboutAuthor?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare const buildData: (id: string) => BuildData | undefined;
|
|
18
|
+
/** "Game-Maker 2.02"; the CD's PLAYGAME.EXE is named as such. */
|
|
19
|
+
export declare function buildLabel(id: string): string;
|
|
20
|
+
/** A copy with the build's owner field zeroed (what its MD5 and block CRCs were taken of). */
|
|
21
|
+
export declare function withoutOwner(exe: Uint8Array, b: BuildData): Uint8Array;
|
|
22
|
+
/** The About page's lines, read from the build's string slots. */
|
|
23
|
+
export declare function aboutLines(exe: Uint8Array, b: BuildData): string[];
|
|
24
|
+
/** The indexes of the 128-byte blocks of `exe` (owner field zeroed) that differ from the build's stock copy. */
|
|
25
|
+
export declare function changedBlocks(exe: Uint8Array, b: BuildData): number[];
|
|
26
|
+
/** True when a run of the About page's slots lies in a changed block. */
|
|
27
|
+
export declare function aboutChanged(b: BuildData, blocks: number[]): boolean;
|
|
28
|
+
export declare function identifyPlayer(exe: Uint8Array): PlayerIdentity;
|
|
29
|
+
export interface FolderEstimate {
|
|
30
|
+
/** "3.00", "2.00 or later", "before 3.00", or "unknown". */
|
|
31
|
+
estimate: string;
|
|
32
|
+
confidence: Confidence;
|
|
33
|
+
reasons: string[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* What a folder without a readable player says about the build it was made with. Every
|
|
37
|
+
* pre-3.00 .gam is 6,600 bytes with no version in it, so this is an estimate with reasons.
|
|
38
|
+
*/
|
|
39
|
+
export declare function estimateFromFolder(names: string[], gams: Uint8Array[]): FolderEstimate;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const BLOCK = 128;
|
|
2
|
+
export interface BuildData {
|
|
3
|
+
id: string;
|
|
4
|
+
size: number;
|
|
5
|
+
md5: string;
|
|
6
|
+
owner?: [number, number];
|
|
7
|
+
version: number;
|
|
8
|
+
about: number[];
|
|
9
|
+
text?: Record<string, [number, number] | [number, number, string]>;
|
|
10
|
+
ext?: Record<string, number[]>;
|
|
11
|
+
crc: string;
|
|
12
|
+
}
|
|
13
|
+
export declare const BUILD_DATA: BuildData[];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type PlayerPatches } from '@lewismoten/gamemaker-sandbox-engine/player/patchRules';
|
|
2
|
+
import { type BuildData } from './playerBuildData.js';
|
|
3
|
+
import { type PreservedRegion } from './preservedRegions.js';
|
|
4
|
+
interface PreservedPlayer {
|
|
5
|
+
patches: PlayerPatches;
|
|
6
|
+
regions: PreservedRegion[];
|
|
7
|
+
}
|
|
8
|
+
/** A known build's copy as patches plus the raw blocks they don't explain. */
|
|
9
|
+
export declare function preservePlayer(exe: Uint8Array, b: BuildData): PreservedPlayer;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PlayerPatches } from '@lewismoten/gamemaker-sandbox-engine/player/patchRules';
|
|
2
|
+
/** A run of an author's bytes: where it starts in the player, and its bytes in base64. */
|
|
3
|
+
export interface PreservedRegion {
|
|
4
|
+
offset: number;
|
|
5
|
+
data: string;
|
|
6
|
+
}
|
|
7
|
+
/** What an import kept of a 3.00 player: its raw regions and the patches read from it then. */
|
|
8
|
+
export interface PreservedParts {
|
|
9
|
+
regions: PreservedRegion[];
|
|
10
|
+
patches: PlayerPatches;
|
|
11
|
+
}
|
|
12
|
+
export declare const toBase64: (b: Uint8Array) => string;
|
|
13
|
+
/**
|
|
14
|
+
* Writes the regions into `exe` (a patched copy of the same build), in place, except the bytes of
|
|
15
|
+
* patches whose value differs from the import's (those are the project author's newer choices).
|
|
16
|
+
*/
|
|
17
|
+
export declare function writePreserved(exe: Uint8Array, kept: PreservedParts, patches: PlayerPatches): void;
|
|
18
|
+
/** The printable runs (4+ characters) of a region, for showing it: [offset, text]. */
|
|
19
|
+
export declare function regionText(r: PreservedRegion): [number, string][];
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** `name` with its extension swapped by `map` (lower case, no dots), keeping its case; unchanged when the map has none for it. */
|
|
2
|
+
export declare function swapExtension(name: string, map: Map<string, string>): string;
|
|
3
|
+
/** Rewrites every file name with an extension in `map` inside `bytes` (a .gam, slide list or backdrop), in place, keeping its case. */
|
|
4
|
+
export declare function rewriteNames(bytes: Uint8Array, map: Map<string, string>): Uint8Array;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** The container of these files (not compressed). */
|
|
2
|
+
export declare function encodePack(entries: Iterable<[string, Uint8Array]>): Uint8Array;
|
|
3
|
+
/**
|
|
4
|
+
* Reads a pack as its bytes come in: push() each chunk (already unzipped) and get the files
|
|
5
|
+
* it completed. `done` once the end mark is read.
|
|
6
|
+
*/
|
|
7
|
+
export declare class PackReader {
|
|
8
|
+
private chunks;
|
|
9
|
+
private total;
|
|
10
|
+
/** Bytes the next step needs (it waits until that many are in, so a big file is joined once). */
|
|
11
|
+
private want;
|
|
12
|
+
private started;
|
|
13
|
+
done: boolean;
|
|
14
|
+
push(chunk: Uint8Array): [string, Uint8Array][];
|
|
15
|
+
}
|
|
16
|
+
/** Every file of a whole pack (not compressed). */
|
|
17
|
+
export declare function decodePack(bytes: Uint8Array): [string, Uint8Array][];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type GameFiles } from '@lewismoten/gamemaker-sandbox-engine/core/gameFiles';
|
|
2
|
+
import { type PlayerIdentity } from '../player/playerBuild.js';
|
|
3
|
+
import type { PreservedRegion } from '../player/preservedRegions.js';
|
|
4
|
+
import type { PlayerPatches } from '@lewismoten/gamemaker-sandbox-engine/player/patchRules';
|
|
5
|
+
export { aboutLines, buildData, buildLabel, estimateFromFolder, identifyPlayer, type FolderEstimate, type PlayerIdentity } from '../player/playerBuild.js';
|
|
6
|
+
export { BLOCK, BUILD_DATA, type BuildData } from '../player/playerBuildData.js';
|
|
7
|
+
export { preservePlayer } from '../player/playerPreserve.js';
|
|
8
|
+
export { regionText, type PreservedRegion } from '../player/preservedRegions.js';
|
|
9
|
+
/** Where an imported game's player came from and what its author changed (the companion's record). */
|
|
10
|
+
export interface PlayerOrigin extends PlayerIdentity {
|
|
11
|
+
/** The .EXE it came from. */
|
|
12
|
+
exe: string;
|
|
13
|
+
size: number;
|
|
14
|
+
md5: string;
|
|
15
|
+
/** The About page's lines when its author changed them (read-only: RSD's or the author's attribution). */
|
|
16
|
+
about?: string[];
|
|
17
|
+
/** Changed bytes no DOS player setting explains (an owner field never among them). */
|
|
18
|
+
regions: PreservedRegion[];
|
|
19
|
+
/** The settings read from it at import, so the DOS export can tell the project author's later changes. */
|
|
20
|
+
importPatches: PlayerPatches;
|
|
21
|
+
}
|
|
22
|
+
export interface ImportedPlayer {
|
|
23
|
+
/** The .EXE it came from. */
|
|
24
|
+
exe: string;
|
|
25
|
+
patches: PlayerPatches;
|
|
26
|
+
/** What was found, for a toast ('' when there's nothing to say, as for a stock player). */
|
|
27
|
+
message: string;
|
|
28
|
+
origin: PlayerOrigin;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Reads the player .EXE among an imported folder's files (`players`: the .EXEs the import
|
|
32
|
+
* otherwise leaves out): which Game-Maker build it is, its settings as DOS player patches (an
|
|
33
|
+
* older build's by 3.00's patch ids), and its other changed bytes, kept as they were; renames the
|
|
34
|
+
* game's files back if it renamed their extensions. Undefined when there is no player.
|
|
35
|
+
*/
|
|
36
|
+
export declare function adoptPlayer(files: GameFiles, players: {
|
|
37
|
+
name: string;
|
|
38
|
+
bytes: Uint8Array;
|
|
39
|
+
}[]): ImportedPlayer | undefined;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { GameFiles } from '@lewismoten/gamemaker-sandbox-engine/core/gameFiles';
|
|
2
|
+
import { type ZipLimits } from './zip.js';
|
|
3
|
+
/** A file to look at: picked, dropped or in a zip. Its path is from the top of what was given. */
|
|
4
|
+
export interface ScanFile {
|
|
5
|
+
path: string;
|
|
6
|
+
size: number;
|
|
7
|
+
read: () => Promise<Uint8Array | undefined>;
|
|
8
|
+
}
|
|
9
|
+
/** A game found, without its files' bytes. */
|
|
10
|
+
export interface FoundGame {
|
|
11
|
+
/** Its title (the first scene's), else its .gam file's name. */
|
|
12
|
+
name: string;
|
|
13
|
+
/** Where it is: "games/zark.zip › zark/", or the folder, or "" for the files picked. */
|
|
14
|
+
from: string;
|
|
15
|
+
/** Its .gam files' CRC-32s (a CD shareware game is matched by these). */
|
|
16
|
+
gamCrcs: number[];
|
|
17
|
+
/** Its player .EXEs' CRC-32s, with gamCrcs its fingerprint (imported/fingerprint.ts). */
|
|
18
|
+
playerCrcs?: number[];
|
|
19
|
+
/** The files it brings (the .EXEs DOS alone needs are left out). */
|
|
20
|
+
fileCount: number;
|
|
21
|
+
/** The size of those files together, in bytes. */
|
|
22
|
+
bytes?: number;
|
|
23
|
+
/** The picked zip it is in (none for loose files), and the paths of the zips inside that one it is in, outermost first. */
|
|
24
|
+
zips: ScanFile[];
|
|
25
|
+
inner: string[];
|
|
26
|
+
/** Its files' paths, inside that zip or among the loose files. */
|
|
27
|
+
paths: string[];
|
|
28
|
+
/** When a folder holds several games, each with its own player: the .gam this one plays (named as it is once renamed back), and its title. */
|
|
29
|
+
gam?: string;
|
|
30
|
+
/** Where else the same game is (its .gam files byte for byte): those copies are left out. */
|
|
31
|
+
alsoIn?: string[];
|
|
32
|
+
/** Files its .gam names that are not here. */
|
|
33
|
+
missing?: number;
|
|
34
|
+
/** Why it is left unticked: a leftover .gam beside the real game, lacking files, that no player loads. */
|
|
35
|
+
leftover?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface ScanReport {
|
|
38
|
+
games: FoundGame[];
|
|
39
|
+
/** Zips looked in (inside others too). */
|
|
40
|
+
zips: number;
|
|
41
|
+
/** Zips with no .gam file in them, and what they did hold ("only text files"). */
|
|
42
|
+
emptyZips: {
|
|
43
|
+
path: string;
|
|
44
|
+
held: string;
|
|
45
|
+
}[];
|
|
46
|
+
/** Zips that could not be read, and why. */
|
|
47
|
+
badZips: {
|
|
48
|
+
path: string;
|
|
49
|
+
error: string;
|
|
50
|
+
}[];
|
|
51
|
+
/** Files in no game, zips aside (none of them is imported, unless no game was found at all). */
|
|
52
|
+
loose: ScanFile[];
|
|
53
|
+
/** Files whose contents aren't what their extension says (a renamed text file named .FLI). */
|
|
54
|
+
misnamed?: {
|
|
55
|
+
path: string;
|
|
56
|
+
says: string;
|
|
57
|
+
}[];
|
|
58
|
+
}
|
|
59
|
+
/** A game's own player is an .EXE of XFERPLAY 3.00's size; the import reads those and leaves out the rest. */
|
|
60
|
+
export declare const PLAYER_EXE: (name: string, size: number) => boolean;
|
|
61
|
+
interface Group {
|
|
62
|
+
dir: string;
|
|
63
|
+
files: ScanFile[];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The games among these files (by their folders) and the files in none. Zips are left out.
|
|
67
|
+
* `played`: game files a player .EXE names with an extension of its own (playedBy).
|
|
68
|
+
*/
|
|
69
|
+
export declare function groupGames(files: ScanFile[], played?: ReadonlySet<ScanFile>): {
|
|
70
|
+
games: Group[];
|
|
71
|
+
loose: ScanFile[];
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Each player .EXE here and the game file beside it that it plays: the one named after it, with
|
|
75
|
+
* the .gam extension it asks for (Acord Games' players ask for .z0g; Virtual Mario's for none).
|
|
76
|
+
* `games`: the files with an extension a player beside them asks for in place of .gam (a
|
|
77
|
+
* leftover SPACE.Z0G beside ZAPAK.EXE too).
|
|
78
|
+
*/
|
|
79
|
+
export declare function playedBy(files: ScanFile[]): Promise<{
|
|
80
|
+
players: Map<ScanFile, ScanFile>;
|
|
81
|
+
games: Set<ScanFile>;
|
|
82
|
+
crcs: Map<ScanFile, number>;
|
|
83
|
+
}>;
|
|
84
|
+
export interface ScanOptions {
|
|
85
|
+
signal?: AbortSignal;
|
|
86
|
+
/** Optional bounds for every archive opened, including nested archives. */
|
|
87
|
+
zipLimits?: ZipLimits;
|
|
88
|
+
/** Told before each zip given (not those inside them) is read, and once at the end. */
|
|
89
|
+
onProgress?: (done: number, total: number, path?: string) => void;
|
|
90
|
+
/** Told which step a zip is at: 'unpack' while it is read, 'find' while its games are looked for. */
|
|
91
|
+
onStep?: (step: 'unpack' | 'find', path: string) => void;
|
|
92
|
+
}
|
|
93
|
+
/** Looks through what was given for its games. Throws an AbortError when canceled. */
|
|
94
|
+
export declare function scanImport(given: ScanFile[], { signal, onProgress, onStep, zipLimits }?: ScanOptions): Promise<ScanReport>;
|
|
95
|
+
/** "212 games found, 3 zips had no game" */
|
|
96
|
+
export declare function scanSummary(r: Pick<ScanReport, 'games' | 'emptyZips' | 'badZips' | 'zips'> & {
|
|
97
|
+
misnamed?: ScanReport['misnamed'];
|
|
98
|
+
}): string;
|
|
99
|
+
/** A found game's files, ready to import. */
|
|
100
|
+
export interface ReadGame {
|
|
101
|
+
files: GameFiles;
|
|
102
|
+
/** Its player .EXEs (left out of the files): a hand-edited one becomes DOS player patches. */
|
|
103
|
+
players: {
|
|
104
|
+
name: string;
|
|
105
|
+
bytes: Uint8Array;
|
|
106
|
+
}[];
|
|
107
|
+
/** Its title and the .gam it plays, when its folder holds others (FoundGame.gam); else the project names them. */
|
|
108
|
+
name?: string;
|
|
109
|
+
gam?: string;
|
|
110
|
+
}
|
|
111
|
+
/** Told before each file is read (the number done, the total, its path); it may return a promise to wait for. */
|
|
112
|
+
export type FileProgress = (done: number, total: number, path: string) => void | Promise<void>;
|
|
113
|
+
/** Fills a game's files from these, as an import of the game's own files always has. */
|
|
114
|
+
export declare function gameFromFiles(list: ScanFile[], onFile?: FileProgress): Promise<ReadGame>;
|
|
115
|
+
/**
|
|
116
|
+
* Reads found games' files again, from the files given. The zip read last is kept (read the
|
|
117
|
+
* games in the order found, and a zip with several is read once); none before it is.
|
|
118
|
+
*/
|
|
119
|
+
export declare class GameReader {
|
|
120
|
+
private readonly zipLimits?;
|
|
121
|
+
private loose;
|
|
122
|
+
private last?;
|
|
123
|
+
constructor(given: ScanFile[], zipLimits?: ZipLimits | undefined);
|
|
124
|
+
/** One of a found game's files by its DOS name (its title screen's), without reading the rest. */
|
|
125
|
+
file(game: FoundGame, name: string): Promise<Uint8Array | undefined>;
|
|
126
|
+
/** The .gam a found game plays: its own when its folder holds others, else the first. */
|
|
127
|
+
mainGam(game: FoundGame): string | undefined;
|
|
128
|
+
read(game: FoundGame, onFile?: FileProgress): Promise<ReadGame>;
|
|
129
|
+
private zipEntries;
|
|
130
|
+
}
|
|
131
|
+
/** A picked or dropped file as one to look at (`path` is where it was under the folder given). */
|
|
132
|
+
export declare function scanFile(file: File, path?: string): ScanFile;
|
|
133
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface ZipEntry {
|
|
2
|
+
name: string;
|
|
3
|
+
data: Uint8Array;
|
|
4
|
+
/** Unix file mode (e.g. 0o100755 for a program); stored so unzipping keeps it executable. */
|
|
5
|
+
mode?: number;
|
|
6
|
+
/** Kept uncompressed (stored), as an ePub's first file, `mimetype`, must be. */
|
|
7
|
+
store?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function crc32(data: Uint8Array): number;
|
|
10
|
+
export declare function createZip(entries: ZipEntry[], date?: Date): Promise<Uint8Array>;
|
|
11
|
+
/** Limits for a zip from somewhere untrusted (linkIn/): more entries or bytes than these throw. */
|
|
12
|
+
export interface ZipLimits {
|
|
13
|
+
maxEntries: number;
|
|
14
|
+
/** Unpacked bytes in all, as the directory says and as actually inflated. */
|
|
15
|
+
maxBytes: number;
|
|
16
|
+
}
|
|
17
|
+
/** An entry of a zip's directory, read only when asked (listZip). */
|
|
18
|
+
export interface ZipListing {
|
|
19
|
+
name: string;
|
|
20
|
+
/** Unpacked bytes, as the directory says. */
|
|
21
|
+
size: number;
|
|
22
|
+
/** Inflates it (stored and deflated entries; others give undefined). */
|
|
23
|
+
read: () => Promise<Uint8Array | undefined>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A zip's files from its central directory, without inflating any (a self-extracting .EXE's
|
|
27
|
+
* too, which is never run): an import looks for the
|
|
28
|
+
* .gam files first and reads the rest only for the games picked (project/importScan.ts).
|
|
29
|
+
*/
|
|
30
|
+
export declare function listZip(bytes: Uint8Array, limits?: ZipLimits): ZipListing[];
|
|
31
|
+
export declare function readZip(bytes: Uint8Array, limits?: ZipLimits): Promise<ZipEntry[]>;
|
|
32
|
+
export declare function isZip(bytes: Uint8Array): boolean;
|