@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.
Files changed (83) hide show
  1. package/README.md +2 -0
  2. package/dist/blocks/cli.js +1 -1
  3. package/dist/blocks/entities.d.ts +30 -0
  4. package/dist/blocks/entities.js +2 -0
  5. package/dist/blocks/entity-geo.d.ts +18 -0
  6. package/dist/blocks/entity-geo.js +2 -0
  7. package/dist/blocks/entity-models.d.ts +45 -0
  8. package/dist/blocks/entity-models.js +2 -0
  9. package/dist/blocks/export.d.ts +1 -1
  10. package/dist/blocks/export.js +1 -1
  11. package/dist/blocks/item-sprites.d.ts +1 -1
  12. package/dist/blocks/obj-mesh.js +1 -1
  13. package/dist/blocks/pack-format.d.ts +2 -2
  14. package/dist/blocks/sprite-sheet.d.ts +1 -1
  15. package/dist/classfile-BdLIk4Pe.d.ts +67 -0
  16. package/dist/classfile-Bo7FUrrD.js +287 -0
  17. package/dist/{cli-DMTaIbDI.js → cli-D2KJc_yl.js} +28 -4
  18. package/dist/cli.js +4 -4
  19. package/dist/entities/classfile.d.ts +2 -0
  20. package/dist/entities/classfile.js +2 -0
  21. package/dist/entities/geo-model.d.ts +13 -0
  22. package/dist/entities/geo-model.js +2 -0
  23. package/dist/entities/inherit.d.ts +29 -0
  24. package/dist/entities/inherit.js +2 -0
  25. package/dist/entities/jar-classes.d.ts +2 -0
  26. package/dist/entities/jar-classes.js +2 -0
  27. package/dist/entities/link.d.ts +71 -0
  28. package/dist/entities/link.js +2 -0
  29. package/dist/entities/mappings.d.ts +2 -0
  30. package/dist/entities/mappings.js +2 -0
  31. package/dist/entities/merge.d.ts +18 -0
  32. package/dist/entities/merge.js +2 -0
  33. package/dist/entities/model-bytecode.d.ts +31 -0
  34. package/dist/entities/model-bytecode.js +2 -0
  35. package/dist/entities/model-code.d.ts +2 -0
  36. package/dist/entities/model-code.js +2 -0
  37. package/dist/entities/model.d.ts +2 -0
  38. package/dist/entities/model.js +1 -0
  39. package/dist/entities-DvmA9fEV.js +175 -0
  40. package/dist/{entity-fallback-DutR9Hdz.js → entity-fallback-Bth0fa7m.js} +1 -1
  41. package/dist/entity-geo-B2CkuMWx.js +100 -0
  42. package/dist/entity-models-_EZ3PVtN.js +127 -0
  43. package/dist/{export-wvt0Fryo.d.ts → export-B8tPsf4K.d.ts} +22 -1
  44. package/dist/{export-sadR0pWp.js → export-BfEIwSxY.js} +93 -1
  45. package/dist/geo-CmjrXu0G.d.ts +59 -0
  46. package/dist/{geo-BC8jtG5Q.js → geo-Desq-gRC.js} +6 -5
  47. package/dist/geo-model-Bo1gpEwX.js +163 -0
  48. package/dist/index.d.ts +1 -1
  49. package/dist/index.js +8 -8
  50. package/dist/inherit-ByHjOJX3.js +77 -0
  51. package/dist/jar-classes-CSRrPAUy.d.ts +41 -0
  52. package/dist/jar-classes-DCtELr1i.js +102 -0
  53. package/dist/link-5GmBLTzk.js +355 -0
  54. package/dist/mappings-BEPVaiwE.js +139 -0
  55. package/dist/mappings-DxX4KoZl.d.ts +75 -0
  56. package/dist/merge-NDYxpmL8.js +48 -0
  57. package/dist/model-PnQ4gV0-.d.ts +49 -0
  58. package/dist/model-bytecode-gWy5_uCj.js +742 -0
  59. package/dist/model-code-1Vuk-zj_.js +49 -0
  60. package/dist/model-code-BY44ao27.d.ts +16 -0
  61. package/dist/mods/prepare.js +1 -1
  62. package/dist/{obj-BaGvPUbb.js → obj-CJHEHpki.js} +5 -5
  63. package/dist/{obj-mesh-CazmV8ya.js → obj-mesh-JwPkZEWL.js} +1 -1
  64. package/dist/{pack-format-oRExIeD3.d.ts → pack-format-QcVFxG_b.d.ts} +100 -1
  65. package/dist/pack.d.ts +1 -1
  66. package/dist/{pipeline-AAfohoHU.js → pipeline-BUIMzMzg.js} +4 -4
  67. package/dist/render/entity-fallback.js +1 -1
  68. package/dist/render/face.js +1 -1
  69. package/dist/render/geo.d.ts +2 -54
  70. package/dist/render/geo.js +2 -2
  71. package/dist/render/obj.js +1 -1
  72. package/dist/render/pipeline.js +1 -1
  73. package/dist/render/renderer.js +1 -1
  74. package/dist/resources/descriptor.js +1 -1
  75. package/dist/{run-UKgVb-kW.js → run-BQJkqc5f.js} +2 -2
  76. package/dist/run.js +1 -1
  77. package/dist/versions/prepare.js +1 -1
  78. package/dist/workers/render-worker.js +2 -2
  79. package/package.json +1 -1
  80. package/dist/{descriptor-D7VKgeGT.js → descriptor-JyXhfd-c.js} +1 -1
  81. package/dist/{face-CMJ3b3Ae.js → face-B2JjWT46.js} +1 -1
  82. package/dist/{prepare-8LswFoS-.js → prepare-D49Gu4Lv.js} +1 -1
  83. 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 };