@nodecraft/rendermite 1.5.0 → 1.6.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.
- package/README.md +2 -0
- package/dist/blocks/cli.js +1 -1
- package/dist/blocks/entities.d.ts +30 -0
- package/dist/blocks/entities.js +2 -0
- package/dist/blocks/entity-geo.d.ts +18 -0
- package/dist/blocks/entity-geo.js +2 -0
- package/dist/blocks/entity-models.d.ts +45 -0
- package/dist/blocks/entity-models.js +2 -0
- package/dist/blocks/export.d.ts +1 -1
- package/dist/blocks/export.js +1 -1
- package/dist/blocks/item-sprites.d.ts +1 -1
- package/dist/blocks/obj-mesh.js +1 -1
- package/dist/blocks/pack-format.d.ts +2 -2
- package/dist/blocks/sprite-sheet.d.ts +1 -1
- package/dist/classfile-BdLIk4Pe.d.ts +67 -0
- package/dist/classfile-Bo7FUrrD.js +287 -0
- package/dist/{cli-DMTaIbDI.js → cli-D2KJc_yl.js} +28 -4
- package/dist/cli.js +4 -4
- package/dist/entities/classfile.d.ts +2 -0
- package/dist/entities/classfile.js +2 -0
- package/dist/entities/geo-model.d.ts +13 -0
- package/dist/entities/geo-model.js +2 -0
- package/dist/entities/inherit.d.ts +29 -0
- package/dist/entities/inherit.js +2 -0
- package/dist/entities/jar-classes.d.ts +2 -0
- package/dist/entities/jar-classes.js +2 -0
- package/dist/entities/link.d.ts +71 -0
- package/dist/entities/link.js +2 -0
- package/dist/entities/mappings.d.ts +2 -0
- package/dist/entities/mappings.js +2 -0
- package/dist/entities/merge.d.ts +18 -0
- package/dist/entities/merge.js +2 -0
- package/dist/entities/model-bytecode.d.ts +31 -0
- package/dist/entities/model-bytecode.js +2 -0
- package/dist/entities/model-code.d.ts +2 -0
- package/dist/entities/model-code.js +2 -0
- package/dist/entities/model.d.ts +2 -0
- package/dist/entities/model.js +1 -0
- package/dist/entities-DvmA9fEV.js +175 -0
- package/dist/{entity-fallback-DutR9Hdz.js → entity-fallback-Bth0fa7m.js} +1 -1
- package/dist/entity-geo-B2CkuMWx.js +100 -0
- package/dist/entity-models-_EZ3PVtN.js +127 -0
- package/dist/{export-wvt0Fryo.d.ts → export-B8tPsf4K.d.ts} +22 -1
- package/dist/{export-sadR0pWp.js → export-BfEIwSxY.js} +93 -1
- package/dist/geo-CmjrXu0G.d.ts +59 -0
- package/dist/{geo-BC8jtG5Q.js → geo-Desq-gRC.js} +6 -5
- package/dist/geo-model-Bo1gpEwX.js +163 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +8 -8
- package/dist/inherit-ByHjOJX3.js +77 -0
- package/dist/jar-classes-CSRrPAUy.d.ts +41 -0
- package/dist/jar-classes-DCtELr1i.js +102 -0
- package/dist/link-5GmBLTzk.js +355 -0
- package/dist/mappings-BEPVaiwE.js +139 -0
- package/dist/mappings-DxX4KoZl.d.ts +75 -0
- package/dist/merge-NDYxpmL8.js +48 -0
- package/dist/model-PnQ4gV0-.d.ts +49 -0
- package/dist/model-bytecode-gWy5_uCj.js +742 -0
- package/dist/model-code-1Vuk-zj_.js +49 -0
- package/dist/model-code-BY44ao27.d.ts +16 -0
- package/dist/mods/prepare.js +1 -1
- package/dist/{obj-BaGvPUbb.js → obj-CJHEHpki.js} +5 -5
- package/dist/{obj-mesh-CazmV8ya.js → obj-mesh-JwPkZEWL.js} +1 -1
- package/dist/{pack-format-oRExIeD3.d.ts → pack-format-QcVFxG_b.d.ts} +100 -1
- package/dist/pack.d.ts +1 -1
- package/dist/{pipeline-AAfohoHU.js → pipeline-BUIMzMzg.js} +4 -4
- package/dist/render/entity-fallback.js +1 -1
- package/dist/render/face.js +1 -1
- package/dist/render/geo.d.ts +2 -54
- package/dist/render/geo.js +2 -2
- package/dist/render/obj.js +1 -1
- package/dist/render/pipeline.js +1 -1
- package/dist/render/renderer.js +1 -1
- package/dist/resources/descriptor.js +1 -1
- package/dist/{run-UKgVb-kW.js → run-BQJkqc5f.js} +2 -2
- package/dist/run.js +1 -1
- package/dist/versions/prepare.js +1 -1
- package/dist/workers/render-worker.js +2 -2
- package/package.json +1 -1
- package/dist/{descriptor-D7VKgeGT.js → descriptor-JyXhfd-c.js} +1 -1
- package/dist/{face-CMJ3b3Ae.js → face-B2JjWT46.js} +1 -1
- package/dist/{prepare-8LswFoS-.js → prepare-D49Gu4Lv.js} +1 -1
- package/dist/{prepare-B_tRy9lB.js → prepare-Myd_ec4v.js} +1 -1
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
import { t as parseClassFile } from "./classfile-Bo7FUrrD.js";
|
|
2
|
+
import { i as mergeModels, r as cubesOf, t as buildsOwnMesh } from "./merge-NDYxpmL8.js";
|
|
3
|
+
//#region src/entities/model-bytecode.ts
|
|
4
|
+
/**
|
|
5
|
+
* Abstract interpretation of a boxy-model builder, enough to recover the geometry.
|
|
6
|
+
*
|
|
7
|
+
* The whole premise of the entity-geometry extractor: entity models are Java code, but the code is a
|
|
8
|
+
* straight line of constant-argument calls into a tiny builder vocabulary. Vanilla 1.17+ builds a
|
|
9
|
+
* `MeshDefinition` of `PartDefinition`s from `CubeListBuilder`s; pre-1.17 vanilla and most mods poke
|
|
10
|
+
* `ModelRenderer`-alikes directly. Both are the same eight operations.
|
|
11
|
+
*
|
|
12
|
+
* Calls are classified by **descriptor shape, not class name**. That is what makes one interpreter
|
|
13
|
+
* cover obfuscated vanilla (`fyp.a(FFFFFF)Lfyp;`) and a mod shipping its own copy of the API
|
|
14
|
+
* (`AdvancedModelBox.addBox(FFFFFFFZ)V`) without a mapping file or a per-mod table.
|
|
15
|
+
*
|
|
16
|
+
* Conditionals are taken and recorded. A model builder's branches are all on a constructor argument —
|
|
17
|
+
* a `CubeDeformation`, a baby-variant flag — so every arm builds a real shape and the fall-through is
|
|
18
|
+
* one of them; the note says so. A *loop* is refused outright, because following it would emit the
|
|
19
|
+
* same boxes twice or spin, and a wrong shape is worse than a recorded gap.
|
|
20
|
+
*/
|
|
21
|
+
/** Argument slot count and shape of a descriptor's parameter list. */
|
|
22
|
+
function parseDescriptor(descriptor) {
|
|
23
|
+
const args = [];
|
|
24
|
+
let at = 1;
|
|
25
|
+
while (descriptor[at] !== ")") {
|
|
26
|
+
const start = at;
|
|
27
|
+
while (descriptor[at] === "[") at++;
|
|
28
|
+
if (descriptor[at] === "L") at = descriptor.indexOf(";", at) + 1;
|
|
29
|
+
else at++;
|
|
30
|
+
args.push(descriptor.slice(start, at));
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
args,
|
|
34
|
+
returns: descriptor.slice(at + 1)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const NUMERIC = /* @__PURE__ */ new Set([
|
|
38
|
+
"F",
|
|
39
|
+
"D",
|
|
40
|
+
"I",
|
|
41
|
+
"J",
|
|
42
|
+
"S",
|
|
43
|
+
"B",
|
|
44
|
+
"C"
|
|
45
|
+
]);
|
|
46
|
+
/** Every entity sheet is a power of two, at least 16 in each direction. A texture offset rarely is. */
|
|
47
|
+
function isSheetSize(width, height) {
|
|
48
|
+
const power = (value) => value >= 16 && (value & value - 1) === 0;
|
|
49
|
+
return power(width) && power(height);
|
|
50
|
+
}
|
|
51
|
+
/** `ldc`, `ldc_w`, `ldc2_w`: a constant, narrow or wide index. */
|
|
52
|
+
const LOAD_CONSTANT = /* @__PURE__ */ new Set([
|
|
53
|
+
18,
|
|
54
|
+
19,
|
|
55
|
+
20
|
|
56
|
+
]);
|
|
57
|
+
/** `lconst_0`/`lconst_1` and `dconst_0`/`dconst_1`, which push 0 and 1. */
|
|
58
|
+
const LONG_OR_DOUBLE_CONSTANT = /* @__PURE__ */ new Set([
|
|
59
|
+
9,
|
|
60
|
+
10,
|
|
61
|
+
14,
|
|
62
|
+
15
|
|
63
|
+
]);
|
|
64
|
+
/** Every `return` that ends a method, with or without a value. */
|
|
65
|
+
const RETURNS = /* @__PURE__ */ new Set([
|
|
66
|
+
172,
|
|
67
|
+
173,
|
|
68
|
+
174,
|
|
69
|
+
175,
|
|
70
|
+
176,
|
|
71
|
+
177
|
|
72
|
+
]);
|
|
73
|
+
/**
|
|
74
|
+
* Every array load: reading back an element of an array the builder filled. All eight, because
|
|
75
|
+
* missing one costs a whole model — `daload` alone was the ender dragon.
|
|
76
|
+
*/
|
|
77
|
+
const ARRAY_LOADS = /* @__PURE__ */ new Set([
|
|
78
|
+
46,
|
|
79
|
+
47,
|
|
80
|
+
48,
|
|
81
|
+
49,
|
|
82
|
+
50,
|
|
83
|
+
51,
|
|
84
|
+
52,
|
|
85
|
+
53
|
|
86
|
+
]);
|
|
87
|
+
/** Every array store, the mirror of `ARRAY_LOADS`: `dastore` alone was the silverfish. */
|
|
88
|
+
const ARRAY_STORES = /* @__PURE__ */ new Set([
|
|
89
|
+
79,
|
|
90
|
+
80,
|
|
91
|
+
81,
|
|
92
|
+
82,
|
|
93
|
+
83,
|
|
94
|
+
84,
|
|
95
|
+
85,
|
|
96
|
+
86
|
|
97
|
+
]);
|
|
98
|
+
/** The four invoke forms. `invokeinterface` carries two extra bytes; `invokestatic` has no receiver. */
|
|
99
|
+
const INVOKES = /* @__PURE__ */ new Set([
|
|
100
|
+
182,
|
|
101
|
+
183,
|
|
102
|
+
184,
|
|
103
|
+
185
|
|
104
|
+
]);
|
|
105
|
+
/** Field names a model writes its texture dimensions to, across the eras and the loader APIs. */
|
|
106
|
+
const TEXTURE_WIDTH_FIELDS = /* @__PURE__ */ new Set(["texWidth", "textureWidth"]);
|
|
107
|
+
const TEXTURE_HEIGHT_FIELDS = /* @__PURE__ */ new Set(["texHeight", "textureHeight"]);
|
|
108
|
+
/** Deep enough for `<init>` → `super()` → `super()` and the static factories that mirror it. */
|
|
109
|
+
const MAX_INLINE_DEPTH = 5;
|
|
110
|
+
/** Frames per method. A model spends a handful; the cap is there so a strange jar cannot spend all day. */
|
|
111
|
+
const INLINE_BUDGET = 96;
|
|
112
|
+
/** How far up a hierarchy to look for an inherited method, which is as deep as a model hierarchy goes. */
|
|
113
|
+
const MAX_METHOD_HOPS = 6;
|
|
114
|
+
function newShared(resolve) {
|
|
115
|
+
return {
|
|
116
|
+
handles: [],
|
|
117
|
+
fields: /* @__PURE__ */ new Map(),
|
|
118
|
+
notes: [],
|
|
119
|
+
textureSize: [64, 32],
|
|
120
|
+
branched: false,
|
|
121
|
+
nextHandle: 1,
|
|
122
|
+
resolve,
|
|
123
|
+
budget: INLINE_BUDGET
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** The method a call names, looked up where the JVM would look: the owner, then up its ancestors. */
|
|
127
|
+
function findMethod(resolve, owner, name, descriptor) {
|
|
128
|
+
let at = owner;
|
|
129
|
+
for (let hops = 0; at && hops < MAX_METHOD_HOPS; hops++) {
|
|
130
|
+
const classFile = resolve(at);
|
|
131
|
+
if (!classFile) return null;
|
|
132
|
+
const method = classFile.methods.find((candidate) => candidate.name === name && candidate.descriptor === descriptor && candidate.code);
|
|
133
|
+
if (method?.code) return {
|
|
134
|
+
code: method.code,
|
|
135
|
+
classFile
|
|
136
|
+
};
|
|
137
|
+
at = classFile.superClass;
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
var Interpreter = class Interpreter {
|
|
142
|
+
stack = [];
|
|
143
|
+
locals = /* @__PURE__ */ new Map();
|
|
144
|
+
shared;
|
|
145
|
+
depth;
|
|
146
|
+
/** What this frame returned, for the caller that inlined it. */
|
|
147
|
+
returned = null;
|
|
148
|
+
constructor(shared, depth = 0) {
|
|
149
|
+
this.shared = shared;
|
|
150
|
+
this.depth = depth;
|
|
151
|
+
}
|
|
152
|
+
handle() {
|
|
153
|
+
const created = {
|
|
154
|
+
id: this.shared.nextHandle++,
|
|
155
|
+
cubes: [],
|
|
156
|
+
children: [],
|
|
157
|
+
pendingUv: [0, 0],
|
|
158
|
+
pendingInflate: 0,
|
|
159
|
+
pendingMirror: false
|
|
160
|
+
};
|
|
161
|
+
this.shared.handles.push(created);
|
|
162
|
+
return created;
|
|
163
|
+
}
|
|
164
|
+
/** Puts the receiver and arguments where the callee's bytecode reads them; longs take two slots. */
|
|
165
|
+
seed(receiver, descriptor, args) {
|
|
166
|
+
let slot = 0;
|
|
167
|
+
if (receiver) this.locals.set(slot++, receiver);
|
|
168
|
+
for (const [index, shape] of parseDescriptor(descriptor).args.entries()) {
|
|
169
|
+
this.locals.set(slot, args[index] ?? { kind: "unknown" });
|
|
170
|
+
slot += shape === "J" || shape === "D" ? 2 : 1;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Interprets a call into the model's own code, with the arguments it was actually given.
|
|
175
|
+
*
|
|
176
|
+
* This is what makes an inherited part come out right. `CowModel` calls `super(12, 0)` and the legs
|
|
177
|
+
* it inherits are `addBox(-2, 0, -2, 4, cubeHeight, 4)`: without the 12 they are boxes of height
|
|
178
|
+
* zero, and a cow with four invisible legs looks exactly like a bug in the mesher.
|
|
179
|
+
*
|
|
180
|
+
* Restricted to model code on purpose. The builder API is in the same jar, and `CubeListBuilder`
|
|
181
|
+
* must keep its abstract treatment rather than being followed into.
|
|
182
|
+
*/
|
|
183
|
+
inline(owner, name, descriptor, receiver, args) {
|
|
184
|
+
if (this.depth >= MAX_INLINE_DEPTH || this.shared.budget <= 0) return null;
|
|
185
|
+
const target = findMethod(this.shared.resolve, owner, name, descriptor);
|
|
186
|
+
if (!target) return null;
|
|
187
|
+
this.shared.budget--;
|
|
188
|
+
const frame = new Interpreter(this.shared, this.depth + 1);
|
|
189
|
+
frame.seed(receiver, descriptor, args);
|
|
190
|
+
return frame.run(target.code, target.classFile) ? frame.returned : null;
|
|
191
|
+
}
|
|
192
|
+
pop() {
|
|
193
|
+
return this.stack.pop() ?? { kind: "unknown" };
|
|
194
|
+
}
|
|
195
|
+
number(value) {
|
|
196
|
+
return value.kind === "number" ? value.value : 0;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Applies one call. Everything is decided by the descriptor: how many arguments to take, whether
|
|
200
|
+
* there is a receiver, and which of the builder operations the shape corresponds to.
|
|
201
|
+
*/
|
|
202
|
+
call(owner, name, descriptor, hasReceiver) {
|
|
203
|
+
const { args, returns } = parseDescriptor(descriptor);
|
|
204
|
+
const taken = [];
|
|
205
|
+
for (let index = args.length - 1; index >= 0; index--) taken.unshift(this.pop());
|
|
206
|
+
const receiver = hasReceiver ? this.pop() : null;
|
|
207
|
+
const target = receiver?.kind === "object" ? receiver.handle : null;
|
|
208
|
+
const numbers = taken.map((value) => this.number(value));
|
|
209
|
+
const floats = args.filter((arg) => NUMERIC.has(arg)).length;
|
|
210
|
+
if (args.length === 2 && args.every((arg) => arg === "I") && target) {
|
|
211
|
+
const known = taken.every((value) => value.kind === "number");
|
|
212
|
+
if (known && target.uvFromConstructor && isSheetSize(numbers[0], numbers[1])) this.shared.textureSize = [numbers[0], numbers[1]];
|
|
213
|
+
else if (known) target.pendingUv = [numbers[0], numbers[1]];
|
|
214
|
+
this.push(returns === "V" ? null : receiver);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const named = args[0] === "Ljava/lang/String;" && floats >= 6 && args.length >= 7 && NUMERIC.has(args[6]);
|
|
218
|
+
const first = named ? 1 : 0;
|
|
219
|
+
if (target && (named || floats >= 6 && args.length >= 6 && NUMERIC.has(args[0]) && NUMERIC.has(args[5]))) {
|
|
220
|
+
const extra = args.slice(first + 6);
|
|
221
|
+
target.cubes.push({
|
|
222
|
+
origin: [
|
|
223
|
+
numbers[first],
|
|
224
|
+
numbers[first + 1],
|
|
225
|
+
numbers[first + 2]
|
|
226
|
+
],
|
|
227
|
+
size: [
|
|
228
|
+
numbers[first + 3],
|
|
229
|
+
numbers[first + 4],
|
|
230
|
+
numbers[first + 5]
|
|
231
|
+
],
|
|
232
|
+
uv: [...target.pendingUv],
|
|
233
|
+
inflate: extra.some((arg) => NUMERIC.has(arg)) ? this.number(taken[first + 6]) : target.pendingInflate,
|
|
234
|
+
mirror: target.pendingMirror || extra.includes("Z") && this.number(taken[args.length - 1]) === 1
|
|
235
|
+
});
|
|
236
|
+
this.push(returns === "V" ? null : receiver);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (args.length === 3 && args.every((arg) => NUMERIC.has(arg))) {
|
|
240
|
+
if (target) {
|
|
241
|
+
target.pivot = [
|
|
242
|
+
numbers[0],
|
|
243
|
+
numbers[1],
|
|
244
|
+
numbers[2]
|
|
245
|
+
];
|
|
246
|
+
this.push(returns === "V" ? null : receiver);
|
|
247
|
+
} else {
|
|
248
|
+
const pose = this.handle();
|
|
249
|
+
pose.pivot = [
|
|
250
|
+
numbers[0],
|
|
251
|
+
numbers[1],
|
|
252
|
+
numbers[2]
|
|
253
|
+
];
|
|
254
|
+
this.push({
|
|
255
|
+
kind: "object",
|
|
256
|
+
handle: pose
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (args.length === 6 && args.every((arg) => NUMERIC.has(arg)) && !target) {
|
|
262
|
+
const pose = this.handle();
|
|
263
|
+
pose.pivot = [
|
|
264
|
+
numbers[0],
|
|
265
|
+
numbers[1],
|
|
266
|
+
numbers[2]
|
|
267
|
+
];
|
|
268
|
+
pose.rotation = [
|
|
269
|
+
numbers[3],
|
|
270
|
+
numbers[4],
|
|
271
|
+
numbers[5]
|
|
272
|
+
];
|
|
273
|
+
this.push({
|
|
274
|
+
kind: "object",
|
|
275
|
+
handle: pose
|
|
276
|
+
});
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (args.length === 3 && args[0] === "Ljava/lang/String;" && taken[1].kind === "object") {
|
|
280
|
+
const child = this.handle();
|
|
281
|
+
child.name = taken[0].kind === "string" ? taken[0].value : `part${child.id}`;
|
|
282
|
+
const builder = taken[1].handle;
|
|
283
|
+
const pose = taken[2].kind === "object" ? taken[2].handle : null;
|
|
284
|
+
child.cubes = [...builder.cubes];
|
|
285
|
+
child.pivot = pose?.pivot ?? [
|
|
286
|
+
0,
|
|
287
|
+
0,
|
|
288
|
+
0
|
|
289
|
+
];
|
|
290
|
+
child.rotation = pose?.rotation ?? [
|
|
291
|
+
0,
|
|
292
|
+
0,
|
|
293
|
+
0
|
|
294
|
+
];
|
|
295
|
+
builder.consumed = true;
|
|
296
|
+
if (pose) pose.consumed = true;
|
|
297
|
+
if (target) {
|
|
298
|
+
const existing = target.children.findIndex((sibling) => sibling.name === child.name);
|
|
299
|
+
if (existing === -1) target.children.push(child);
|
|
300
|
+
else {
|
|
301
|
+
target.children[existing].consumed = true;
|
|
302
|
+
target.children[existing] = child;
|
|
303
|
+
}
|
|
304
|
+
} else child.orphaned = true;
|
|
305
|
+
this.push({
|
|
306
|
+
kind: "object",
|
|
307
|
+
handle: child
|
|
308
|
+
});
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const lambda = taken.find((value) => value.kind === "lambda");
|
|
312
|
+
if (lambda?.kind === "lambda" && receiver?.kind === "object" && taken.length === 1) {
|
|
313
|
+
const applied = this.inline(lambda.owner, lambda.name, lambda.descriptor, null, [receiver]);
|
|
314
|
+
this.push(returns === "V" ? null : applied ?? receiver);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (args.length === 1 && args[0] === "Ljava/lang/String;" && returns.startsWith("L") && target) {
|
|
318
|
+
const name_ = taken[0].kind === "string" ? taken[0].value : "";
|
|
319
|
+
const found = target.children.find((child) => child.name === name_);
|
|
320
|
+
if (found) this.push({
|
|
321
|
+
kind: "object",
|
|
322
|
+
handle: found
|
|
323
|
+
});
|
|
324
|
+
else {
|
|
325
|
+
const child = this.handle();
|
|
326
|
+
child.name = name_ || `part${child.id}`;
|
|
327
|
+
target.children.push(child);
|
|
328
|
+
this.push({
|
|
329
|
+
kind: "object",
|
|
330
|
+
handle: child
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (args.length === 1 && args[0].startsWith("L") && target && taken[0].kind === "object" && (taken[0].handle.cubes.length > 0 || taken[0].handle.children.length > 0)) {
|
|
336
|
+
target.children.push(taken[0].handle);
|
|
337
|
+
this.push(returns === "V" ? null : receiver);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const objects = taken.filter((value) => value.kind === "object");
|
|
341
|
+
if (!target && objects.length === 1 && taken.length - objects.length === args.length - 1 && args.length >= 2 && args.slice(1).every((arg) => NUMERIC.has(arg))) {
|
|
342
|
+
if (args.length === 3) this.shared.textureSize = [numbers[1], numbers[2]];
|
|
343
|
+
this.push(objects[0]);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (args.length === 1 && args[0] === "Z" && target) {
|
|
347
|
+
target.pendingMirror = this.number(taken[0]) === 1;
|
|
348
|
+
this.push(returns === "V" ? null : receiver);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (args.length === 0 && returns.startsWith("L")) {
|
|
352
|
+
if (target) {
|
|
353
|
+
target.isRoot = true;
|
|
354
|
+
this.push(receiver);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const built = this.inline(owner, name, descriptor, null, taken);
|
|
358
|
+
this.push(built ?? {
|
|
359
|
+
kind: "object",
|
|
360
|
+
handle: this.handle()
|
|
361
|
+
});
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const inlined = this.inline(owner, name, descriptor, receiver, taken);
|
|
365
|
+
this.push(returns === "V" ? null : inlined ?? { kind: "unknown" });
|
|
366
|
+
}
|
|
367
|
+
push(value) {
|
|
368
|
+
if (value) this.stack.push(value);
|
|
369
|
+
}
|
|
370
|
+
/** Runs one method's code. Returns false when it hit something it will not guess at. */
|
|
371
|
+
run(code, constants) {
|
|
372
|
+
let at = 0;
|
|
373
|
+
while (at < code.length) {
|
|
374
|
+
const op = code[at++];
|
|
375
|
+
switch (true) {
|
|
376
|
+
case op === 1:
|
|
377
|
+
this.push({ kind: "unknown" });
|
|
378
|
+
break;
|
|
379
|
+
case op >= 2 && op <= 8:
|
|
380
|
+
this.push({
|
|
381
|
+
kind: "number",
|
|
382
|
+
value: op - 3
|
|
383
|
+
});
|
|
384
|
+
break;
|
|
385
|
+
case LONG_OR_DOUBLE_CONSTANT.has(op):
|
|
386
|
+
this.push({
|
|
387
|
+
kind: "number",
|
|
388
|
+
value: op === 10 || op === 15 ? 1 : 0
|
|
389
|
+
});
|
|
390
|
+
break;
|
|
391
|
+
case op >= 11 && op <= 13:
|
|
392
|
+
this.push({
|
|
393
|
+
kind: "number",
|
|
394
|
+
value: op - 11
|
|
395
|
+
});
|
|
396
|
+
break;
|
|
397
|
+
case op === 16:
|
|
398
|
+
this.push({
|
|
399
|
+
kind: "number",
|
|
400
|
+
value: code[at++] << 24 >> 24
|
|
401
|
+
});
|
|
402
|
+
break;
|
|
403
|
+
case op === 17:
|
|
404
|
+
this.push({
|
|
405
|
+
kind: "number",
|
|
406
|
+
value: (code[at] << 8 | code[at + 1]) << 16 >> 16
|
|
407
|
+
});
|
|
408
|
+
at += 2;
|
|
409
|
+
break;
|
|
410
|
+
case LOAD_CONSTANT.has(op): {
|
|
411
|
+
let index;
|
|
412
|
+
if (op === 18) index = code[at++];
|
|
413
|
+
else {
|
|
414
|
+
index = code[at] << 8 | code[at + 1];
|
|
415
|
+
at += 2;
|
|
416
|
+
}
|
|
417
|
+
const value = constants.literal(index);
|
|
418
|
+
if (typeof value === "string") this.push({
|
|
419
|
+
kind: "string",
|
|
420
|
+
value
|
|
421
|
+
});
|
|
422
|
+
else if (typeof value === "number") this.push({
|
|
423
|
+
kind: "number",
|
|
424
|
+
value
|
|
425
|
+
});
|
|
426
|
+
else if (typeof value === "bigint") this.push({
|
|
427
|
+
kind: "number",
|
|
428
|
+
value: Number(value)
|
|
429
|
+
});
|
|
430
|
+
else this.push({ kind: "unknown" });
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
case op >= 21 && op <= 25:
|
|
434
|
+
this.push(this.locals.get(code[at++]) ?? { kind: "unknown" });
|
|
435
|
+
break;
|
|
436
|
+
case op >= 26 && op <= 45:
|
|
437
|
+
this.push(this.locals.get((op - 26) % 4) ?? { kind: "unknown" });
|
|
438
|
+
break;
|
|
439
|
+
case op >= 54 && op <= 58:
|
|
440
|
+
this.locals.set(code[at++], this.pop());
|
|
441
|
+
break;
|
|
442
|
+
case op >= 59 && op <= 78:
|
|
443
|
+
this.locals.set((op - 59) % 4, this.pop());
|
|
444
|
+
break;
|
|
445
|
+
case ARRAY_LOADS.has(op): {
|
|
446
|
+
this.pop();
|
|
447
|
+
const array = this.pop();
|
|
448
|
+
this.push(array.kind === "array" ? array.items[0] ?? { kind: "unknown" } : { kind: "unknown" });
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
case op >= 120 && op <= 131:
|
|
452
|
+
this.pop();
|
|
453
|
+
this.pop();
|
|
454
|
+
this.push({
|
|
455
|
+
kind: "number",
|
|
456
|
+
value: 0
|
|
457
|
+
});
|
|
458
|
+
break;
|
|
459
|
+
case op === 193:
|
|
460
|
+
at += 2;
|
|
461
|
+
this.pop();
|
|
462
|
+
this.push({
|
|
463
|
+
kind: "number",
|
|
464
|
+
value: 0
|
|
465
|
+
});
|
|
466
|
+
break;
|
|
467
|
+
case op === 191: return true;
|
|
468
|
+
case op === 190:
|
|
469
|
+
this.pop();
|
|
470
|
+
this.push({
|
|
471
|
+
kind: "number",
|
|
472
|
+
value: 0
|
|
473
|
+
});
|
|
474
|
+
break;
|
|
475
|
+
case ARRAY_STORES.has(op): {
|
|
476
|
+
const value = this.pop();
|
|
477
|
+
this.pop();
|
|
478
|
+
const array = this.pop();
|
|
479
|
+
if (array.kind === "array") array.items.push(value);
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
case op === 87:
|
|
483
|
+
this.pop();
|
|
484
|
+
break;
|
|
485
|
+
case op === 88:
|
|
486
|
+
this.pop();
|
|
487
|
+
this.pop();
|
|
488
|
+
break;
|
|
489
|
+
case op === 89: {
|
|
490
|
+
const top = this.pop();
|
|
491
|
+
this.push(top);
|
|
492
|
+
this.push(top);
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
case op === 90: {
|
|
496
|
+
const top = this.pop();
|
|
497
|
+
const under = this.pop();
|
|
498
|
+
this.push(top);
|
|
499
|
+
this.push(under);
|
|
500
|
+
this.push(top);
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
case op >= 96 && op <= 111: {
|
|
504
|
+
const right = this.number(this.pop());
|
|
505
|
+
const left = this.number(this.pop());
|
|
506
|
+
const operate = [
|
|
507
|
+
() => left + right,
|
|
508
|
+
() => left - right,
|
|
509
|
+
() => left * right,
|
|
510
|
+
() => left / right
|
|
511
|
+
][Math.floor((op - 96) / 4)];
|
|
512
|
+
this.push({
|
|
513
|
+
kind: "number",
|
|
514
|
+
value: operate()
|
|
515
|
+
});
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
case op >= 112 && op <= 115: {
|
|
519
|
+
const right = this.number(this.pop());
|
|
520
|
+
const left = this.number(this.pop());
|
|
521
|
+
this.push({
|
|
522
|
+
kind: "number",
|
|
523
|
+
value: right === 0 ? 0 : left % right
|
|
524
|
+
});
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
case op >= 116 && op <= 119:
|
|
528
|
+
this.push({
|
|
529
|
+
kind: "number",
|
|
530
|
+
value: -this.number(this.pop())
|
|
531
|
+
});
|
|
532
|
+
break;
|
|
533
|
+
case op >= 133 && op <= 147: break;
|
|
534
|
+
case op === 132:
|
|
535
|
+
at += 2;
|
|
536
|
+
break;
|
|
537
|
+
case RETURNS.has(op):
|
|
538
|
+
if (op !== 177) this.returned = this.pop();
|
|
539
|
+
return true;
|
|
540
|
+
case op === 178:
|
|
541
|
+
at += 2;
|
|
542
|
+
this.push({ kind: "unknown" });
|
|
543
|
+
break;
|
|
544
|
+
case op === 179:
|
|
545
|
+
at += 2;
|
|
546
|
+
this.pop();
|
|
547
|
+
break;
|
|
548
|
+
case op === 180: {
|
|
549
|
+
const index = code[at] << 8 | code[at + 1];
|
|
550
|
+
at += 2;
|
|
551
|
+
this.pop();
|
|
552
|
+
const reference = constants.reference(index);
|
|
553
|
+
this.push(this.shared.fields.get(reference?.name ?? "") ?? { kind: "unknown" });
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
case op === 181: {
|
|
557
|
+
const index = code[at] << 8 | code[at + 1];
|
|
558
|
+
at += 2;
|
|
559
|
+
const value = this.pop();
|
|
560
|
+
this.pop();
|
|
561
|
+
const reference = constants.reference(index);
|
|
562
|
+
if (reference) {
|
|
563
|
+
const previous = this.shared.fields.get(reference.name);
|
|
564
|
+
if (previous?.kind === "object" && value.kind === "object" && previous.handle !== value.handle) previous.handle.consumed = true;
|
|
565
|
+
this.shared.fields.set(reference.name, value);
|
|
566
|
+
if (value.kind === "object" && !value.handle.name) value.handle.name = reference.name;
|
|
567
|
+
if (TEXTURE_WIDTH_FIELDS.has(reference.name)) this.shared.textureSize[0] = this.number(value);
|
|
568
|
+
else if (TEXTURE_HEIGHT_FIELDS.has(reference.name)) this.shared.textureSize[1] = this.number(value);
|
|
569
|
+
}
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
case INVOKES.has(op): {
|
|
573
|
+
const index = code[at] << 8 | code[at + 1];
|
|
574
|
+
at += 2;
|
|
575
|
+
if (op === 185) at += 2;
|
|
576
|
+
const reference = constants.reference(index);
|
|
577
|
+
if (!reference) break;
|
|
578
|
+
if (op === 183 && reference.name === "<init>") {
|
|
579
|
+
const { args } = parseDescriptor(reference.descriptor);
|
|
580
|
+
const taken = [];
|
|
581
|
+
for (let count = 0; count < args.length; count++) taken.unshift(this.pop());
|
|
582
|
+
const objectRef = this.pop();
|
|
583
|
+
if (objectRef.kind === "object" && args.length >= 2 && args.at(-1) === "I" && args.at(-2) === "I") {
|
|
584
|
+
objectRef.handle.pendingUv = [this.number(taken[args.length - 2]), this.number(taken[args.length - 1])];
|
|
585
|
+
objectRef.handle.uvFromConstructor = true;
|
|
586
|
+
}
|
|
587
|
+
this.inline(reference.owner, reference.name, reference.descriptor, objectRef, taken);
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
this.call(reference.owner, reference.name, reference.descriptor, op !== 184);
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
case op === 187:
|
|
594
|
+
at += 2;
|
|
595
|
+
this.push({
|
|
596
|
+
kind: "object",
|
|
597
|
+
handle: this.handle()
|
|
598
|
+
});
|
|
599
|
+
break;
|
|
600
|
+
case op === 188:
|
|
601
|
+
at += 1;
|
|
602
|
+
this.pop();
|
|
603
|
+
this.push({
|
|
604
|
+
kind: "array",
|
|
605
|
+
items: []
|
|
606
|
+
});
|
|
607
|
+
break;
|
|
608
|
+
case op === 189:
|
|
609
|
+
at += 2;
|
|
610
|
+
this.pop();
|
|
611
|
+
this.push({
|
|
612
|
+
kind: "array",
|
|
613
|
+
items: []
|
|
614
|
+
});
|
|
615
|
+
break;
|
|
616
|
+
case op === 192:
|
|
617
|
+
at += 2;
|
|
618
|
+
break;
|
|
619
|
+
case op === 186: {
|
|
620
|
+
const index = code[at] << 8 | code[at + 1];
|
|
621
|
+
at += 4;
|
|
622
|
+
const reference = constants.reference(index);
|
|
623
|
+
const { args, returns } = reference ? parseDescriptor(reference.descriptor) : {
|
|
624
|
+
args: [],
|
|
625
|
+
returns: "V"
|
|
626
|
+
};
|
|
627
|
+
for (let count = 0; count < args.length; count++) this.pop();
|
|
628
|
+
const target = constants.lambdaTarget(index);
|
|
629
|
+
this.push(returns === "V" ? null : target ? {
|
|
630
|
+
kind: "lambda",
|
|
631
|
+
...target
|
|
632
|
+
} : { kind: "unknown" });
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
case op >= 153 && op <= 166: {
|
|
636
|
+
const comparisons = op >= 159 ? 2 : 1;
|
|
637
|
+
for (let count = 0; count < comparisons; count++) this.pop();
|
|
638
|
+
at += 2;
|
|
639
|
+
this.shared.branched = true;
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
case op === 198 || op === 199:
|
|
643
|
+
at += 2;
|
|
644
|
+
this.pop();
|
|
645
|
+
this.shared.branched = true;
|
|
646
|
+
break;
|
|
647
|
+
case op === 167: {
|
|
648
|
+
const offset = (code[at] << 8 | code[at + 1]) << 16 >> 16;
|
|
649
|
+
if (offset <= 0) {
|
|
650
|
+
this.shared.notes.push("backward goto: the builder loops, so its geometry is not recoverable");
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
at += offset - 1;
|
|
654
|
+
this.shared.branched = true;
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
case op >= 148 && op <= 152:
|
|
658
|
+
this.pop();
|
|
659
|
+
this.pop();
|
|
660
|
+
this.push({
|
|
661
|
+
kind: "number",
|
|
662
|
+
value: 0
|
|
663
|
+
});
|
|
664
|
+
break;
|
|
665
|
+
default:
|
|
666
|
+
this.shared.notes.push(`unsupported opcode 0x${op.toString(16)} at ${at - 1}`);
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return true;
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
/** Turns a handle tree into the exported shape. */
|
|
674
|
+
function toPart(handle) {
|
|
675
|
+
return {
|
|
676
|
+
name: handle.name ?? `part${handle.id}`,
|
|
677
|
+
pivot: handle.pivot ?? [
|
|
678
|
+
0,
|
|
679
|
+
0,
|
|
680
|
+
0
|
|
681
|
+
],
|
|
682
|
+
rotation: handle.rotation ?? [
|
|
683
|
+
0,
|
|
684
|
+
0,
|
|
685
|
+
0
|
|
686
|
+
],
|
|
687
|
+
cubes: handle.cubes,
|
|
688
|
+
children: handle.children.map((child) => toPart(child))
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
function extractModel(bytes, following) {
|
|
692
|
+
const classFile = parseClassFile(bytes);
|
|
693
|
+
const notes = [];
|
|
694
|
+
const candidates = [];
|
|
695
|
+
const outward = following?.isModelCode ?? true;
|
|
696
|
+
const own = (internalName) => {
|
|
697
|
+
if (internalName === classFile.thisClass) return classFile;
|
|
698
|
+
return outward ? following?.resolve(internalName) ?? null : null;
|
|
699
|
+
};
|
|
700
|
+
for (const method of classFile.methods) {
|
|
701
|
+
if (!method.code) continue;
|
|
702
|
+
const shared = newShared(own);
|
|
703
|
+
let ok;
|
|
704
|
+
try {
|
|
705
|
+
ok = new Interpreter(shared).run(method.code, classFile);
|
|
706
|
+
} catch (err) {
|
|
707
|
+
notes.push(`${method.name}: ${err.message}`);
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
if (!ok) notes.push(`${method.name}: ${shared.notes.join("; ")}`);
|
|
711
|
+
const adopted = new Set(shared.handles.flatMap((handle) => handle.children));
|
|
712
|
+
const roots = shared.handles.filter((handle) => !adopted.has(handle) && !handle.consumed && (handle.cubes.length > 0 || handle.children.length > 0));
|
|
713
|
+
if (roots.length === 0) continue;
|
|
714
|
+
const orphans = roots.filter((root) => root.orphaned).length;
|
|
715
|
+
const model = {
|
|
716
|
+
textureSize: shared.textureSize,
|
|
717
|
+
parts: roots.map((root) => toPart(root)),
|
|
718
|
+
notes: [
|
|
719
|
+
...shared.notes,
|
|
720
|
+
...orphans > 0 ? [`${orphans} disconnected roots: built for a parent in another class, so each sits at the model origin`] : [],
|
|
721
|
+
...shared.branched ? ["a conditional was taken; the shape is one of the variants this builder makes"] : []
|
|
722
|
+
]
|
|
723
|
+
};
|
|
724
|
+
candidates.push({
|
|
725
|
+
model,
|
|
726
|
+
ownMesh: buildsOwnMesh(model),
|
|
727
|
+
cubes: cubesOf(model)
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
const byCubes = candidates.filter((candidate) => candidate.cubes > 0).toSorted((left, right) => right.cubes - left.cubes);
|
|
731
|
+
const base = byCubes.find((candidate) => candidate.ownMesh) ?? byCubes[0];
|
|
732
|
+
if (!base) return {
|
|
733
|
+
textureSize: [64, 32],
|
|
734
|
+
parts: [],
|
|
735
|
+
notes
|
|
736
|
+
};
|
|
737
|
+
let model = base.model;
|
|
738
|
+
for (const helper of byCubes) if (helper !== base && !helper.ownMesh) model = mergeModels(model, helper.model);
|
|
739
|
+
return model;
|
|
740
|
+
}
|
|
741
|
+
//#endregion
|
|
742
|
+
export { extractModel as t };
|