@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,355 @@
|
|
|
1
|
+
import { t as parseClassFile } from "./classfile-Bo7FUrrD.js";
|
|
2
|
+
//#region src/entities/link.ts
|
|
3
|
+
/**
|
|
4
|
+
* Which class holds a given entity's geometry.
|
|
5
|
+
*
|
|
6
|
+
* Three routes, cheapest first, because the game itself only states this in code:
|
|
7
|
+
*
|
|
8
|
+
* 1. `<Type>Model` — 67 of vanilla 1.21.1's types, and the reason it works is that Mojang names the
|
|
9
|
+
* class after the mob.
|
|
10
|
+
* 2. `<Type>Renderer`, following its **superclass chain**. A renderer that subclasses another never
|
|
11
|
+
* mentions the model at all — `CaveSpiderRenderer` hands a layer to `SpiderRenderer`, which is
|
|
12
|
+
* where `new SpiderModel` lives — so stopping at the class itself loses every shared model in the
|
|
13
|
+
* game: cave spider, husk, stray, wither skeleton, magma cube, zoglin, all of the illagers.
|
|
14
|
+
* 3. `RENDERER_ALIASES` — the mobs whose renderer is simply not named after them. A mooshroom is a
|
|
15
|
+
* `MushroomCowRenderer`, a wither is a `WitherBossRenderer`, a skeleton horse is an
|
|
16
|
+
* `UndeadHorseRenderer`. There is nothing in the assets or the class names to derive that from, so
|
|
17
|
+
* it is written down, and `NO_MODEL` below is what stops the list quietly rotting.
|
|
18
|
+
*
|
|
19
|
+
* The authoritative source would be `EntityRenderers.createEntityRenderers`, which registers every
|
|
20
|
+
* type against its renderer — but it does so through `::new` method references, so reading it means
|
|
21
|
+
* interpreting `invokedynamic` bootstrap arguments. A fifteen-line table that a test can prove
|
|
22
|
+
* complete is the better trade.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Entity types whose renderer or model is not named after them. Values are class simple names to look
|
|
26
|
+
* for; several may be listed because the game renamed the holder between versions — 1.21.4 folded
|
|
27
|
+
* `ChestedHorseRenderer` into `DonkeyRenderer`, and a pack is exported for both eras.
|
|
28
|
+
*/
|
|
29
|
+
const RENDERER_ALIASES = {
|
|
30
|
+
mooshroom: "MushroomCowRenderer",
|
|
31
|
+
skeleton_horse: "UndeadHorseRenderer",
|
|
32
|
+
zombie_horse: "UndeadHorseRenderer",
|
|
33
|
+
donkey: ["DonkeyRenderer", "ChestedHorseRenderer"],
|
|
34
|
+
mule: ["DonkeyRenderer", "ChestedHorseRenderer"],
|
|
35
|
+
trader_llama: "LlamaRenderer",
|
|
36
|
+
zombified_piglin: "PiglinRenderer",
|
|
37
|
+
piglin_brute: "PiglinRenderer",
|
|
38
|
+
wither: "WitherBossRenderer",
|
|
39
|
+
ender_dragon: "EnderDragonRenderer",
|
|
40
|
+
giant: "GiantMobRenderer",
|
|
41
|
+
killer_bunny: "RabbitRenderer",
|
|
42
|
+
glow_squid: "SquidModel",
|
|
43
|
+
magma_cube: ["LavaSlimeRenderer", "LavaSlimeModel"],
|
|
44
|
+
zombie_pigman: "PigZombieRenderer",
|
|
45
|
+
mannequin: "PlayerModel",
|
|
46
|
+
chest_minecart: "MinecartRenderer",
|
|
47
|
+
furnace_minecart: "MinecartRenderer",
|
|
48
|
+
hopper_minecart: "MinecartRenderer",
|
|
49
|
+
spawner_minecart: "MinecartRenderer",
|
|
50
|
+
command_block_minecart: "MinecartRenderer",
|
|
51
|
+
tnt_minecart: "MinecartRenderer"
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Families whose members all draw one model, matched by suffix.
|
|
55
|
+
*
|
|
56
|
+
* 1.21.2 split every boat into one entity type per wood — `acacia_boat`, `birch_chest_boat`, twenty of
|
|
57
|
+
* them — all still drawn by the one `BoatModel` with a different texture. A pattern is the honest way
|
|
58
|
+
* to say that; twenty alias entries would say the same thing twenty times and still miss the next wood
|
|
59
|
+
* Mojang adds.
|
|
60
|
+
*/
|
|
61
|
+
const FAMILY_PATTERNS = [
|
|
62
|
+
{
|
|
63
|
+
suffix: "_chest_raft",
|
|
64
|
+
classes: ["ChestRaftModel", "RaftModel"]
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
suffix: "_raft",
|
|
68
|
+
classes: ["RaftModel"]
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
suffix: "_chest_boat",
|
|
72
|
+
classes: ["ChestBoatModel", "BoatModel"]
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
suffix: "_boat",
|
|
76
|
+
classes: ["BoatModel"]
|
|
77
|
+
}
|
|
78
|
+
];
|
|
79
|
+
/**
|
|
80
|
+
* Entity types the game draws without a `ModelPart` hierarchy at all, so having no geometry for them
|
|
81
|
+
* is the right answer rather than a gap.
|
|
82
|
+
*
|
|
83
|
+
* Three kinds: things that draw a *block* or an *item* instead (a dropped stack, a falling block, a
|
|
84
|
+
* thrown egg), things the renderer draws with hand-built quads (an arrow, a painting, the end
|
|
85
|
+
* crystal's beam), and things that are deliberately invisible (a marker, the display entities).
|
|
86
|
+
*
|
|
87
|
+
* Listed rather than inferred so that a version adding a mob cannot slip through as "no model": the
|
|
88
|
+
* coverage test fails on any type that is neither linked nor named here.
|
|
89
|
+
*/
|
|
90
|
+
const NO_MODEL = /* @__PURE__ */ new Set([
|
|
91
|
+
"item",
|
|
92
|
+
"falling_block",
|
|
93
|
+
"falling_block_type",
|
|
94
|
+
"tnt",
|
|
95
|
+
"item_frame",
|
|
96
|
+
"glow_item_frame",
|
|
97
|
+
"ominous_item_spawner",
|
|
98
|
+
"egg",
|
|
99
|
+
"splash_potion",
|
|
100
|
+
"lingering_potion",
|
|
101
|
+
"wind_charge",
|
|
102
|
+
"breeze_wind_charge",
|
|
103
|
+
"arrow",
|
|
104
|
+
"spectral_arrow",
|
|
105
|
+
"trident",
|
|
106
|
+
"lightning_bolt",
|
|
107
|
+
"end_crystal",
|
|
108
|
+
"wither_skull",
|
|
109
|
+
"shulker_bullet",
|
|
110
|
+
"leash_knot",
|
|
111
|
+
"llama_spit",
|
|
112
|
+
"snowball",
|
|
113
|
+
"potion",
|
|
114
|
+
"ender_pearl",
|
|
115
|
+
"experience_bottle",
|
|
116
|
+
"eye_of_ender",
|
|
117
|
+
"firework_rocket",
|
|
118
|
+
"painting",
|
|
119
|
+
"fishing_bobber",
|
|
120
|
+
"experience_orb",
|
|
121
|
+
"small_fireball",
|
|
122
|
+
"fireball",
|
|
123
|
+
"dragon_fireball",
|
|
124
|
+
"area_effect_cloud",
|
|
125
|
+
"creaking_transient",
|
|
126
|
+
"marker",
|
|
127
|
+
"interaction",
|
|
128
|
+
"block_display",
|
|
129
|
+
"item_display",
|
|
130
|
+
"text_display"
|
|
131
|
+
]);
|
|
132
|
+
/**
|
|
133
|
+
* Bases that define no boxes, so landing on one from a renderer chain means finding nothing.
|
|
134
|
+
*
|
|
135
|
+
* Deliberately *not* `HumanoidModel` or `QuadrupedModel`, which look abstract and are anything but:
|
|
136
|
+
* they are where the shared body is actually built, and every zombie, skeleton and villager in the
|
|
137
|
+
* game inherits its geometry from the first. Excluding them cost six mobs.
|
|
138
|
+
*/
|
|
139
|
+
const ABSTRACT_MODELS = /* @__PURE__ */ new Set([
|
|
140
|
+
"EntityModel",
|
|
141
|
+
"HierarchicalModel",
|
|
142
|
+
"ListModel",
|
|
143
|
+
"AgeableListModel",
|
|
144
|
+
"AgeableHierarchicalModel",
|
|
145
|
+
"ColorableHierarchicalModel",
|
|
146
|
+
"ColorableAgeableListModel",
|
|
147
|
+
"ArmedModel",
|
|
148
|
+
"HeadedModel",
|
|
149
|
+
"SkullModelBase",
|
|
150
|
+
"VillagerHeadModel"
|
|
151
|
+
]);
|
|
152
|
+
/**
|
|
153
|
+
* Mapped classes called `<Name>Model`, wherever they live.
|
|
154
|
+
*
|
|
155
|
+
* Not a fixed package, because 1.21.11 reorganised every model into a sub-package — `CowModel` moved
|
|
156
|
+
* to `client.model.animal.cow` and `TridentModel` to `client.model.object.projectile`. Matching on the
|
|
157
|
+
* simple name under any `client.model` package survives that, and would have survived it in advance.
|
|
158
|
+
*/
|
|
159
|
+
function modelClassesNamed(simple, mappings) {
|
|
160
|
+
return mappings.obfuscatedOf.keys().filter((className) => className.endsWith(`.${simple}Model`) && className.includes(".client.model")).toArray();
|
|
161
|
+
}
|
|
162
|
+
function pascal(name) {
|
|
163
|
+
return name.split("_").filter(Boolean).map((word) => word[0].toUpperCase() + word.slice(1)).join("");
|
|
164
|
+
}
|
|
165
|
+
/** Every `*Model` class a class's constant pool names, deobfuscated. */
|
|
166
|
+
function modelsNamedBy(file, mappings) {
|
|
167
|
+
const found = /* @__PURE__ */ new Set();
|
|
168
|
+
for (const [index, constant] of file.constants.entries()) {
|
|
169
|
+
if (constant?.tag !== "class") continue;
|
|
170
|
+
const original = mappings.originalOf.get(file.utf8(index).replaceAll("/", "."));
|
|
171
|
+
if (original && /Model[A-Z]?$/.test(original) && /\.client\.(?:model|renderer)[.$]/.test(original)) {
|
|
172
|
+
const simple = original.split(/[.$]/).pop() ?? "";
|
|
173
|
+
if (!ABSTRACT_MODELS.has(simple)) found.add(original);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return [...found];
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Walks a model class's own superclass chain for the first ancestor that defines boxes.
|
|
180
|
+
*
|
|
181
|
+
* `ZombieModel` extends `AbstractZombieModel` extends `HumanoidModel`, and only the last of those
|
|
182
|
+
* builds anything — so a zombie's geometry is two hops above the class named after it.
|
|
183
|
+
*/
|
|
184
|
+
async function inheritedModel(className, context, depth = 0) {
|
|
185
|
+
if (depth > 4) return null;
|
|
186
|
+
const obfuscated = context.mappings.obfuscatedOf.get(className);
|
|
187
|
+
if (!obfuscated) return null;
|
|
188
|
+
const parent = (await context.read(obfuscated))?.superClass?.replaceAll("/", ".");
|
|
189
|
+
if (!parent || parent === "java.lang.Object") return null;
|
|
190
|
+
const original = context.mappings.originalOf.get(parent) ?? parent;
|
|
191
|
+
const simple = original.split(/[.$]/).pop() ?? "";
|
|
192
|
+
if (!ABSTRACT_MODELS.has(simple)) {
|
|
193
|
+
const parentObfuscated = context.mappings.obfuscatedOf.get(original);
|
|
194
|
+
if (parentObfuscated && await context.hasGeometry(parentObfuscated)) return {
|
|
195
|
+
className: original,
|
|
196
|
+
obfuscated: parentObfuscated,
|
|
197
|
+
route: "model-name"
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return await inheritedModel(original, context, depth + 1);
|
|
201
|
+
}
|
|
202
|
+
/** Follows a renderer's superclass chain until something names a model. */
|
|
203
|
+
async function modelsForRenderer(obfuscated, context, depth = 0) {
|
|
204
|
+
if (depth > 4) return [];
|
|
205
|
+
const file = await context.read(obfuscated);
|
|
206
|
+
if (!file) return [];
|
|
207
|
+
const named = modelsNamedBy(file, context.mappings);
|
|
208
|
+
if (named.length > 0) return named;
|
|
209
|
+
const parent = file.superClass?.replaceAll("/", ".");
|
|
210
|
+
return parent && parent !== "java.lang.Object" ? await modelsForRenderer(parent, context, depth + 1) : [];
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* A class's simple name reduced to the entity it is about.
|
|
214
|
+
*
|
|
215
|
+
* `Lionfish_Model`, `ModelLionfish`, `LionfishEntityModel` and `lionfish` all come out `lionfish`.
|
|
216
|
+
* Mods put the marker word at either end and sometimes both — `Modelcutter` is the old MCP style,
|
|
217
|
+
* `Lionfish_Model` the modern one — so the strip repeats until nothing more comes off. Separators go
|
|
218
|
+
* too, because a mod's class is `Deepling_Brute_Model` where its entity is `deepling_brute`.
|
|
219
|
+
*/
|
|
220
|
+
function normaliseModelName(name) {
|
|
221
|
+
let value = name.replaceAll(/[^a-z\d]/gi, "").toLowerCase();
|
|
222
|
+
let previous = "";
|
|
223
|
+
while (value !== previous) {
|
|
224
|
+
previous = value;
|
|
225
|
+
value = value.replace(/^(?:model|entity|render(?:er)?)/, "").replace(/(?:model|entity|render(?:er)?)$/, "");
|
|
226
|
+
}
|
|
227
|
+
return value;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Normalised name → the classes that could be it, for a jar whose classes are not named the way
|
|
231
|
+
* vanilla's are.
|
|
232
|
+
*
|
|
233
|
+
* Built once per jar because the alternative is scanning three thousand classes per entity. Nested
|
|
234
|
+
* classes are indexed under the part after the `$` as well: occultism files a familiar's head as
|
|
235
|
+
* `HeadlessFamiliarRenderer$CthulhuHeadModel`, and only the tail of that names the mob.
|
|
236
|
+
*/
|
|
237
|
+
function buildModelNameIndex(internalNames) {
|
|
238
|
+
const index = /* @__PURE__ */ new Map();
|
|
239
|
+
for (const internal of internalNames) {
|
|
240
|
+
const simple = internal.split("/").pop() ?? "";
|
|
241
|
+
if (!/model/i.test(simple)) continue;
|
|
242
|
+
for (const candidate of [simple, ...simple.split("$").slice(1)]) {
|
|
243
|
+
const key = normaliseModelName(candidate);
|
|
244
|
+
if (key.length < 3) continue;
|
|
245
|
+
const held = index.get(key);
|
|
246
|
+
if (held) held.push(internal);
|
|
247
|
+
else index.set(key, [internal]);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return index;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* The class holding one entity's geometry, or `null` when nothing links.
|
|
254
|
+
*
|
|
255
|
+
* A candidate is only accepted if it *yields geometry*: a renderer chain can land on an abstract base
|
|
256
|
+
* that names no boxes, and returning that would report coverage the pack does not have.
|
|
257
|
+
*/
|
|
258
|
+
async function linkEntityModel(entityId, context) {
|
|
259
|
+
const name = entityId.includes(":") ? entityId.slice(entityId.indexOf(":") + 1) : entityId;
|
|
260
|
+
const modded = entityId.includes(":") && !entityId.startsWith("minecraft:");
|
|
261
|
+
const { mappings } = context;
|
|
262
|
+
const accept = async (className, route) => {
|
|
263
|
+
const obfuscated = mappings.obfuscatedOf.get(className);
|
|
264
|
+
if (!obfuscated || !await context.hasGeometry(obfuscated)) return null;
|
|
265
|
+
return {
|
|
266
|
+
className,
|
|
267
|
+
obfuscated,
|
|
268
|
+
route
|
|
269
|
+
};
|
|
270
|
+
};
|
|
271
|
+
for (const className of modelClassesNamed(pascal(name), mappings)) {
|
|
272
|
+
const direct = await accept(className, "model-name");
|
|
273
|
+
if (direct) return direct;
|
|
274
|
+
const inherited = await inheritedModel(className, context);
|
|
275
|
+
if (inherited) return inherited;
|
|
276
|
+
}
|
|
277
|
+
const family = FAMILY_PATTERNS.find((candidate) => name.endsWith(candidate.suffix))?.classes ?? [];
|
|
278
|
+
const aliased = RENDERER_ALIASES[name];
|
|
279
|
+
const attempts = [
|
|
280
|
+
...(typeof aliased === "string" ? [aliased] : aliased ?? []).map((simple) => [simple, "alias"]),
|
|
281
|
+
...family.map((simple) => [simple, "alias"]),
|
|
282
|
+
[`${pascal(name)}Renderer`, "renderer"]
|
|
283
|
+
];
|
|
284
|
+
for (const [simple, route] of attempts) {
|
|
285
|
+
if (!simple) continue;
|
|
286
|
+
const candidates = mappings.obfuscatedOf.keys().filter((className) => className.endsWith(`.${simple}`) && /\.client\.(?:model|renderer)[.$]/.test(className)).toArray();
|
|
287
|
+
candidates.sort((left, right) => right.length - left.length);
|
|
288
|
+
for (const className of candidates) {
|
|
289
|
+
const direct = await accept(className, route) ?? await inheritedModel(className, context);
|
|
290
|
+
if (direct) return {
|
|
291
|
+
...direct,
|
|
292
|
+
route
|
|
293
|
+
};
|
|
294
|
+
const obfuscated = mappings.obfuscatedOf.get(className);
|
|
295
|
+
if (!obfuscated) continue;
|
|
296
|
+
for (const model of await modelsForRenderer(obfuscated, context)) {
|
|
297
|
+
const accepted = await accept(model, route) ?? await inheritedModel(model, context);
|
|
298
|
+
if (accepted) return {
|
|
299
|
+
...accepted,
|
|
300
|
+
route
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
for (const internal of context.nameIndex?.get(normaliseModelName(name)) ?? []) {
|
|
306
|
+
const dotted = internal.replaceAll("/", ".");
|
|
307
|
+
if (await context.hasGeometry(dotted)) return {
|
|
308
|
+
className: dotted,
|
|
309
|
+
obfuscated: dotted,
|
|
310
|
+
route: "mod-name"
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
if (modded) for (const shorter of withTokensDropped(name)) {
|
|
314
|
+
const found = await linkEntityModel(shorter, context);
|
|
315
|
+
if (found) return {
|
|
316
|
+
...found,
|
|
317
|
+
route: "vanilla-name"
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* The name with some of its tokens dropped, longest first: `summon_horse` gives `summon` and `horse`.
|
|
324
|
+
*
|
|
325
|
+
* Capped at five tokens, because the subsets double with each one and no id is longer than that for a
|
|
326
|
+
* reason this would help with.
|
|
327
|
+
*/
|
|
328
|
+
function withTokensDropped(name) {
|
|
329
|
+
const tokens = name.split("_").filter((token) => token.length > 0);
|
|
330
|
+
if (tokens.length < 2 || tokens.length > 5) return [];
|
|
331
|
+
const found = [];
|
|
332
|
+
for (let size = tokens.length - 1; size >= 1; size--) for (let mask = 0; mask < 2 ** tokens.length; mask++) {
|
|
333
|
+
const kept = tokens.filter((token, index) => (mask & 1 << index) !== 0);
|
|
334
|
+
if (kept.length === size) found.push(kept.join("_"));
|
|
335
|
+
}
|
|
336
|
+
return [...new Set(found)];
|
|
337
|
+
}
|
|
338
|
+
/** Reads and caches classes out of an extracted jar directory or a jar reader. */
|
|
339
|
+
function classReader(load) {
|
|
340
|
+
const cache = /* @__PURE__ */ new Map();
|
|
341
|
+
return async (obfuscated) => {
|
|
342
|
+
if (cache.has(obfuscated)) return cache.get(obfuscated) ?? null;
|
|
343
|
+
let parsed = null;
|
|
344
|
+
const bytes = await load(obfuscated);
|
|
345
|
+
if (bytes) try {
|
|
346
|
+
parsed = parseClassFile(bytes);
|
|
347
|
+
} catch {
|
|
348
|
+
parsed = null;
|
|
349
|
+
}
|
|
350
|
+
cache.set(obfuscated, parsed);
|
|
351
|
+
return parsed;
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
//#endregion
|
|
355
|
+
export { linkEntityModel as a, classReader as i, RENDERER_ALIASES as n, normaliseModelName as o, buildModelNameIndex as r, NO_MODEL as t };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
//#region src/entities/mappings.ts
|
|
5
|
+
/**
|
|
6
|
+
* Mojang's official obfuscation mappings, which are what turn a jar full of `fvd.class` into named
|
|
7
|
+
* entity models.
|
|
8
|
+
*
|
|
9
|
+
* Published per version in the same manifest the client jar comes from — `downloads.client_mappings`,
|
|
10
|
+
* about 9 MB — so this is a second download from a place the pipeline already goes. Deliberately not
|
|
11
|
+
* the mcversions index `download.ts` uses for jars: that index carries jars only.
|
|
12
|
+
*
|
|
13
|
+
* ProGuard format, which is two kinds of line:
|
|
14
|
+
*
|
|
15
|
+
* net.minecraft.client.model.CowModel -> fvd:
|
|
16
|
+
* net.minecraft...LayerDefinition createBodyLayer() -> a
|
|
17
|
+
*
|
|
18
|
+
* Unindented lines map a class, indented ones its members. Only classes are read here; the geometry
|
|
19
|
+
* extractor works on descriptor shape rather than member names, so member mappings buy nothing.
|
|
20
|
+
*
|
|
21
|
+
* Published from 1.14.4 through 1.21.11, and then not again — because from the 26 line Mojang stopped
|
|
22
|
+
* *obfuscating* the client. A 26.2 jar ships `net/minecraft/client/model/ambient/BatModel.class` under
|
|
23
|
+
* its real name, so the mapping it needs is the identity one, which `identityMappings` builds from the
|
|
24
|
+
* jar's own class list.
|
|
25
|
+
*
|
|
26
|
+
* That leaves 1.13.x as the only gap: obfuscated, and predating published mappings. Reported rather
|
|
27
|
+
* than worked around.
|
|
28
|
+
*/
|
|
29
|
+
const VERSION_MANIFEST = "https://launchermeta.mojang.com/mc/game/version_manifest_v2.json";
|
|
30
|
+
var MappingsUnavailableError = class extends Error {
|
|
31
|
+
constructor(message, options) {
|
|
32
|
+
super(message, options);
|
|
33
|
+
this.name = "MappingsUnavailableError";
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Downloads a version's client mappings, caching by version beside the jars.
|
|
38
|
+
*
|
|
39
|
+
* Throws rather than returning null: a caller that asked for mappings cannot do its job without them,
|
|
40
|
+
* and the difference between "this version publishes none" and "the network failed" is worth keeping.
|
|
41
|
+
*/
|
|
42
|
+
async function downloadClientMappings(version, cacheDir) {
|
|
43
|
+
const target = path.join(cacheDir, `${version}-client-mappings.txt`);
|
|
44
|
+
if (existsSync(target)) return target;
|
|
45
|
+
await mkdir(cacheDir, { recursive: true });
|
|
46
|
+
const manifestResponse = await fetch(VERSION_MANIFEST);
|
|
47
|
+
if (!manifestResponse.ok) throw new MappingsUnavailableError(`Version manifest fetch failed: ${manifestResponse.status}`);
|
|
48
|
+
const entry = (await manifestResponse.json()).versions?.find((candidate) => candidate.id === version);
|
|
49
|
+
if (!entry) throw new MappingsUnavailableError(`${version} is not in Mojang's version manifest`);
|
|
50
|
+
const metaResponse = await fetch(entry.url);
|
|
51
|
+
if (!metaResponse.ok) throw new MappingsUnavailableError(`Version metadata fetch failed for ${version}: ${metaResponse.status}`);
|
|
52
|
+
const mappings = (await metaResponse.json()).downloads?.client_mappings;
|
|
53
|
+
if (!mappings) throw new MappingsUnavailableError(`${version} publishes no client mappings; they start at 1.14.4`);
|
|
54
|
+
const download = await fetch(mappings.url);
|
|
55
|
+
if (!download.ok) throw new MappingsUnavailableError(`Mappings download failed for ${version}: ${download.status}`);
|
|
56
|
+
const partial = `${target}.partial`;
|
|
57
|
+
await writeFile(partial, Buffer.from(await download.arrayBuffer()));
|
|
58
|
+
await rename(partial, target);
|
|
59
|
+
return target;
|
|
60
|
+
}
|
|
61
|
+
/** Parses the class lines of a ProGuard mapping file. */
|
|
62
|
+
function parseMappings(text) {
|
|
63
|
+
const obfuscatedOf = /* @__PURE__ */ new Map();
|
|
64
|
+
const originalOf = /* @__PURE__ */ new Map();
|
|
65
|
+
for (const line of text.split("\n")) {
|
|
66
|
+
if (line.length === 0 || line.startsWith(" ") || line.startsWith(" ") || line.startsWith("#")) continue;
|
|
67
|
+
const divider = line.indexOf(" -> ");
|
|
68
|
+
if (divider === -1) continue;
|
|
69
|
+
const original = line.slice(0, divider).trim();
|
|
70
|
+
const obfuscated = line.slice(divider + 4).trim().replace(/:$/, "");
|
|
71
|
+
if (original.length === 0 || obfuscated.length === 0) continue;
|
|
72
|
+
obfuscatedOf.set(original, obfuscated);
|
|
73
|
+
originalOf.set(obfuscated, original);
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
obfuscatedOf,
|
|
77
|
+
originalOf
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async function loadMappings(version, cacheDir) {
|
|
81
|
+
return parseMappings(await readFile(await downloadClientMappings(version, cacheDir), "utf8"));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Mappings for a jar that was never obfuscated: every class maps to itself.
|
|
85
|
+
*
|
|
86
|
+
* `internalNames` are as the archive spells them (`net/minecraft/client/model/animal/cow/CowModel`);
|
|
87
|
+
* the maps use dots, because that is what a mapping file and every lookup here use.
|
|
88
|
+
*/
|
|
89
|
+
function identityMappings(internalNames) {
|
|
90
|
+
const obfuscatedOf = /* @__PURE__ */ new Map();
|
|
91
|
+
const originalOf = /* @__PURE__ */ new Map();
|
|
92
|
+
for (const internal of internalNames) {
|
|
93
|
+
const dotted = internal.replaceAll("/", ".");
|
|
94
|
+
obfuscatedOf.set(dotted, dotted);
|
|
95
|
+
originalOf.set(dotted, dotted);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
obfuscatedOf,
|
|
99
|
+
originalOf
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Whether a jar still needs a mapping at all.
|
|
104
|
+
*
|
|
105
|
+
* Decided on the **models package**, not on `net/minecraft/` generally: an obfuscated jar keeps a
|
|
106
|
+
* couple of dozen classes in the clear — the launcher entrypoint and the brand retriever are never
|
|
107
|
+
* obfuscated — so 1.21.1 has 26 `net/minecraft/` entries and zero under `client/model/`, where 26.2
|
|
108
|
+
* has 358. Which is also the precise question the extractor is asking.
|
|
109
|
+
*/
|
|
110
|
+
function isObfuscated(internalNames) {
|
|
111
|
+
for (const name of internalNames) if (name.startsWith("net/minecraft/client/model/")) return false;
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The mappings for a version, however it ships them.
|
|
116
|
+
*
|
|
117
|
+
* **The jar decides, not the manifest.** From the 26 line the client is not obfuscated, so its own
|
|
118
|
+
* class names are the real ones and no mapping is needed — asking Mojang first would mean two HTTP
|
|
119
|
+
* requests per export to learn something the archive already states, and would make an offline export
|
|
120
|
+
* of a current version fail for no reason.
|
|
121
|
+
*/
|
|
122
|
+
async function resolveMappings(version, cacheDir, listClasses) {
|
|
123
|
+
const names = await listClasses();
|
|
124
|
+
if (!isObfuscated(names)) return {
|
|
125
|
+
mappings: identityMappings(names),
|
|
126
|
+
obfuscated: false
|
|
127
|
+
};
|
|
128
|
+
try {
|
|
129
|
+
return {
|
|
130
|
+
mappings: await loadMappings(version, cacheDir),
|
|
131
|
+
obfuscated: true
|
|
132
|
+
};
|
|
133
|
+
} catch (err) {
|
|
134
|
+
if (!(err instanceof MappingsUnavailableError)) throw err;
|
|
135
|
+
throw new MappingsUnavailableError(`${version} publishes no mappings and its jar is obfuscated; entity geometry needs 1.14.4 or newer`, { cause: err });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
//#endregion
|
|
139
|
+
export { loadMappings as a, isObfuscated as i, downloadClientMappings as n, parseMappings as o, identityMappings as r, resolveMappings as s, MappingsUnavailableError as t };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
//#region src/entities/mappings.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Mojang's official obfuscation mappings, which are what turn a jar full of `fvd.class` into named
|
|
4
|
+
* entity models.
|
|
5
|
+
*
|
|
6
|
+
* Published per version in the same manifest the client jar comes from — `downloads.client_mappings`,
|
|
7
|
+
* about 9 MB — so this is a second download from a place the pipeline already goes. Deliberately not
|
|
8
|
+
* the mcversions index `download.ts` uses for jars: that index carries jars only.
|
|
9
|
+
*
|
|
10
|
+
* ProGuard format, which is two kinds of line:
|
|
11
|
+
*
|
|
12
|
+
* net.minecraft.client.model.CowModel -> fvd:
|
|
13
|
+
* net.minecraft...LayerDefinition createBodyLayer() -> a
|
|
14
|
+
*
|
|
15
|
+
* Unindented lines map a class, indented ones its members. Only classes are read here; the geometry
|
|
16
|
+
* extractor works on descriptor shape rather than member names, so member mappings buy nothing.
|
|
17
|
+
*
|
|
18
|
+
* Published from 1.14.4 through 1.21.11, and then not again — because from the 26 line Mojang stopped
|
|
19
|
+
* *obfuscating* the client. A 26.2 jar ships `net/minecraft/client/model/ambient/BatModel.class` under
|
|
20
|
+
* its real name, so the mapping it needs is the identity one, which `identityMappings` builds from the
|
|
21
|
+
* jar's own class list.
|
|
22
|
+
*
|
|
23
|
+
* That leaves 1.13.x as the only gap: obfuscated, and predating published mappings. Reported rather
|
|
24
|
+
* than worked around.
|
|
25
|
+
*/
|
|
26
|
+
/** Class names both ways round, which is what every lookup here needs. */
|
|
27
|
+
interface Mappings {
|
|
28
|
+
/** `net.minecraft.client.model.CowModel` → `fvd`. */
|
|
29
|
+
obfuscatedOf: Map<string, string>;
|
|
30
|
+
/** `fvd` → `net.minecraft.client.model.CowModel`. */
|
|
31
|
+
originalOf: Map<string, string>;
|
|
32
|
+
}
|
|
33
|
+
declare class MappingsUnavailableError extends Error {
|
|
34
|
+
constructor(message: string, options?: ErrorOptions);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Downloads a version's client mappings, caching by version beside the jars.
|
|
38
|
+
*
|
|
39
|
+
* Throws rather than returning null: a caller that asked for mappings cannot do its job without them,
|
|
40
|
+
* and the difference between "this version publishes none" and "the network failed" is worth keeping.
|
|
41
|
+
*/
|
|
42
|
+
declare function downloadClientMappings(version: string, cacheDir: string): Promise<string>;
|
|
43
|
+
/** Parses the class lines of a ProGuard mapping file. */
|
|
44
|
+
declare function parseMappings(text: string): Mappings;
|
|
45
|
+
declare function loadMappings(version: string, cacheDir: string): Promise<Mappings>;
|
|
46
|
+
/**
|
|
47
|
+
* Mappings for a jar that was never obfuscated: every class maps to itself.
|
|
48
|
+
*
|
|
49
|
+
* `internalNames` are as the archive spells them (`net/minecraft/client/model/animal/cow/CowModel`);
|
|
50
|
+
* the maps use dots, because that is what a mapping file and every lookup here use.
|
|
51
|
+
*/
|
|
52
|
+
declare function identityMappings(internalNames: Iterable<string>): Mappings;
|
|
53
|
+
/**
|
|
54
|
+
* Whether a jar still needs a mapping at all.
|
|
55
|
+
*
|
|
56
|
+
* Decided on the **models package**, not on `net/minecraft/` generally: an obfuscated jar keeps a
|
|
57
|
+
* couple of dozen classes in the clear — the launcher entrypoint and the brand retriever are never
|
|
58
|
+
* obfuscated — so 1.21.1 has 26 `net/minecraft/` entries and zero under `client/model/`, where 26.2
|
|
59
|
+
* has 358. Which is also the precise question the extractor is asking.
|
|
60
|
+
*/
|
|
61
|
+
declare function isObfuscated(internalNames: Iterable<string>): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* The mappings for a version, however it ships them.
|
|
64
|
+
*
|
|
65
|
+
* **The jar decides, not the manifest.** From the 26 line the client is not obfuscated, so its own
|
|
66
|
+
* class names are the real ones and no mapping is needed — asking Mojang first would mean two HTTP
|
|
67
|
+
* requests per export to learn something the archive already states, and would make an offline export
|
|
68
|
+
* of a current version fail for no reason.
|
|
69
|
+
*/
|
|
70
|
+
declare function resolveMappings(version: string, cacheDir: string, listClasses: () => Promise<string[]>): Promise<{
|
|
71
|
+
mappings: Mappings;
|
|
72
|
+
obfuscated: boolean;
|
|
73
|
+
}>;
|
|
74
|
+
//#endregion
|
|
75
|
+
export { isObfuscated as a, resolveMappings as c, identityMappings as i, MappingsUnavailableError as n, loadMappings as o, downloadClientMappings as r, parseMappings as s, Mappings as t };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region src/entities/merge.ts
|
|
2
|
+
/** A root the interpreter had to invent, because a `MeshDefinition`'s root part is anonymous. */
|
|
3
|
+
const SYNTHETIC_ROOT = /^part\d+$/;
|
|
4
|
+
/**
|
|
5
|
+
* Where a piece's parts sit: at the top, or inside the one anonymous root a `MeshDefinition` makes.
|
|
6
|
+
* Both merge into the same list of named slots and only one of them is nested.
|
|
7
|
+
*/
|
|
8
|
+
function slotsOf(parts) {
|
|
9
|
+
const [only] = parts;
|
|
10
|
+
if (parts.length === 1 && only.cubes.length === 0 && SYNTHETIC_ROOT.test(only.name)) return {
|
|
11
|
+
container: only,
|
|
12
|
+
slots: only.children
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
container: null,
|
|
16
|
+
slots: parts
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** True when this piece built its own mesh rather than adding to one it was handed. */
|
|
20
|
+
function buildsOwnMesh(model) {
|
|
21
|
+
return slotsOf(model.parts).container !== null;
|
|
22
|
+
}
|
|
23
|
+
/** `base` plus the parts of `extra` that `base` does not name. */
|
|
24
|
+
function mergeModels(base, extra) {
|
|
25
|
+
const target = slotsOf(base.parts);
|
|
26
|
+
const source = slotsOf(extra.parts);
|
|
27
|
+
const named = new Set(target.slots.map((part) => part.name));
|
|
28
|
+
const missing = source.slots.filter((part) => !named.has(part.name));
|
|
29
|
+
if (missing.length === 0) return base;
|
|
30
|
+
const parts = target.container ? [{
|
|
31
|
+
...target.container,
|
|
32
|
+
children: [...target.container.children, ...missing]
|
|
33
|
+
}] : [...target.slots, ...missing];
|
|
34
|
+
const notes = [.../* @__PURE__ */ new Set([...base.notes, ...extra.notes])].filter((note) => parts.length > 1 || !note.includes("disconnected roots"));
|
|
35
|
+
return {
|
|
36
|
+
textureSize: base.textureSize[0] === 64 && base.textureSize[1] === 32 ? extra.textureSize : base.textureSize,
|
|
37
|
+
parts,
|
|
38
|
+
notes
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function countCubes(part) {
|
|
42
|
+
return part.cubes.length + part.children.reduce((total, child) => total + countCubes(child), 0);
|
|
43
|
+
}
|
|
44
|
+
function cubesOf(model) {
|
|
45
|
+
return model.parts.reduce((total, part) => total + countCubes(part), 0);
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { slotsOf as a, mergeModels as i, countCubes as n, cubesOf as r, buildsOwnMesh as t };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//#region src/entities/model.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The shape an entity model comes out as, whichever source produced it.
|
|
4
|
+
*
|
|
5
|
+
* Two do: `model-bytecode.ts` interprets the Java that vanilla and most mods build their models in,
|
|
6
|
+
* and `geo-model.ts` converts the bedrock `.geo.json` a GeckoLib mod ships instead. Both land here, in
|
|
7
|
+
* **Java entity model space** — pixels, y measured *downwards* from 1.5 blocks above the mob's feet, a
|
|
8
|
+
* part's pivot relative to its parent's — because that is the space a consumer has to draw in anyway.
|
|
9
|
+
*/
|
|
10
|
+
/** The six faces a box can carry its own texture rect on. */
|
|
11
|
+
type CubeFace = 'north' | 'south' | 'east' | 'west' | 'up' | 'down';
|
|
12
|
+
/** One box of a part, in the model's own space. */
|
|
13
|
+
interface Cube {
|
|
14
|
+
origin: [number, number, number];
|
|
15
|
+
size: [number, number, number];
|
|
16
|
+
/** Texture offset in pixels, as `texOffs`/`setTextureOffset` set it. */
|
|
17
|
+
uv: [number, number];
|
|
18
|
+
/** `CubeDeformation`/`delta`: grows the box on every side. */
|
|
19
|
+
inflate: number;
|
|
20
|
+
mirror: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Per-face texture rects, which replace the box unwrap `uv` implies when present.
|
|
23
|
+
*
|
|
24
|
+
* Java has no such thing: `addBox` always unwraps the same way. Bedrock geometry does, and uses it
|
|
25
|
+
* heavily — 56% of the cubes in a corpus of GeckoLib mods — so a consumer that ignores this draws
|
|
26
|
+
* over half of every modded mob with the wrong part of its sheet.
|
|
27
|
+
*/
|
|
28
|
+
faces?: Partial<Record<CubeFace, {
|
|
29
|
+
uv: [number, number];
|
|
30
|
+
size: [number, number];
|
|
31
|
+
}>>;
|
|
32
|
+
}
|
|
33
|
+
interface Part {
|
|
34
|
+
name: string;
|
|
35
|
+
/** Where the part pivots, relative to its parent. */
|
|
36
|
+
pivot: [number, number, number];
|
|
37
|
+
/** Radians, as `PartPose.offsetAndRotation` states them. */
|
|
38
|
+
rotation: [number, number, number];
|
|
39
|
+
cubes: Cube[];
|
|
40
|
+
children: Part[];
|
|
41
|
+
}
|
|
42
|
+
interface ExtractedModel {
|
|
43
|
+
textureSize: [number, number];
|
|
44
|
+
parts: Part[];
|
|
45
|
+
/** Why nothing came out, or what was skipped. */
|
|
46
|
+
notes: string[];
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { Part as i, CubeFace as n, ExtractedModel as r, Cube as t };
|