feat: add mod editing, revision merges and secure game storage

Add immutable revisions, PGVN editor, asset replacement, merge and rebase workflows, authenticated media delivery and reachability GC. Harden the script interpreter, pin unrpyc and remove tracked build metadata. Include current compatibility checks, Naninovel import and regression tests.
This commit is contained in:
mrleo1nid committed 2026-10-25 04:42:48 +03:00
1 parent 06ff19ac4a
commit c8d83b3fe7
65 files changed
+3825 -68

No files matched your search

+19 -3
View File
@@ -2,6 +2,7 @@
// Python values map to JS: None→null, list/tuple→Array, dict→PyDict, str/num/bool as is.
import type { Expr, PyStmt } from "./types";
import { evaluateNani } from "./naninovel";
export class PyDict {
map = new Map<any, any>();
@@ -157,9 +158,16 @@ function compare(op: string, a: any, b: any): boolean {
}
function index(len: number, i: number) {
if (!Number.isInteger(i)) throw new Error("Индекс последовательности должен быть целым числом");
return i < 0 ? len + i : i;
}
function safeProperty(name: any): boolean {
if (typeof name !== "string") return typeof name === "number" && Number.isFinite(name);
return !name.startsWith("__") &&
!["constructor", "prototype", "caller", "callee", "arguments"].includes(name);
}
function methodOf(obj: any, name: string): any {
if (Array.isArray(obj)) {
const m: Record<string, Builtin> = {
@@ -184,7 +192,7 @@ function methodOf(obj: any, name: string): any {
if (i >= 0) obj.splice(i, 1);
},
};
return m[name];
return Object.hasOwn(m, name) ? m[name] : undefined;
}
if (obj instanceof PyDict) {
const m: Record<string, Builtin> = {
@@ -205,7 +213,7 @@ function methodOf(obj: any, name: string): any {
clear: () => obj.map.clear(),
copy: () => new PyDict(obj.map.entries()),
};
return m[name];
return Object.hasOwn(m, name) ? m[name] : undefined;
}
if (typeof obj === "string") {
const m: Record<string, Builtin> = {
@@ -230,7 +238,7 @@ function methodOf(obj: any, name: string): any {
isdigit: () => /^\d+$/.test(obj),
isalpha: () => /^\p{L}+$/u.test(obj),
};
return m[name];
return Object.hasOwn(m, name) ? m[name] : undefined;
}
return undefined;
}
@@ -256,6 +264,7 @@ export class Python {
}
lookup(name: string): any {
if (!safeProperty(name)) return null;
for (let i = this.locals.length - 1; i >= 0; i--)
if (this.locals[i].has(name)) return this.locals[i].get(name);
if (this.scope.has(name)) return this.scope.get(name);
@@ -266,12 +275,15 @@ export class Python {
eval(e: Expr | null | undefined): any {
if (!e) return null;
switch (e.k) {
case "nani":
return evaluateNani(e.ast, (name) => safeProperty(name) && this.scope.has(name) ? this.scope.get(name) : undefined);
case "const":
return e.v;
case "name":
return this.lookup(e.id);
case "attr": {
const o = this.eval(e.o);
if (!safeProperty(e.a) || typeof o === "function") return null;
if (o === null || o === undefined) {
this.warnOnce(`атрибут ${e.a} у None`);
return null;
@@ -285,6 +297,7 @@ export class Python {
const o = this.eval(e.o);
const i = this.eval(e.i);
if (o instanceof PyDict) return o.has(i) ? o.get(i) : null;
if (!safeProperty(i) || typeof o === "function") return null;
if (Array.isArray(o) || typeof o === "string") return o[index(o.length, i)] ?? null;
return o?.[i] ?? null;
}
@@ -362,6 +375,7 @@ export class Python {
bind(t: Expr, value: any, frame?: Map<string, any>) {
switch (t.k) {
case "name":
if (!safeProperty(t.id)) throw new Error("Служебное имя недоступно");
if (frame) frame.set(t.id, value);
else this.scope.set(t.id, value);
return;
@@ -372,6 +386,7 @@ export class Python {
}
case "attr": {
const o = this.eval(t.o);
if (!safeProperty(t.a) || typeof o === "function") throw new Error("Служебный атрибут недоступен");
if (o instanceof PyDict) o.set(t.a, value);
else if (o) o[t.a] = value;
return;
@@ -380,6 +395,7 @@ export class Python {
const o = this.eval(t.o);
const i = this.eval(t.i);
if (o instanceof PyDict) o.set(i, value);
else if (!safeProperty(i) || typeof o === "function") throw new Error("Служебный ключ недоступен");
else if (Array.isArray(o)) o[index(o.length, i)] = value;
else if (o) o[i] = value;
return;