@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,29 @@
1
+ import { r as ExtractedModel } from "../model-PnQ4gV0-.js";
2
+ import { t as ClassFile } from "../classfile-BdLIk4Pe.js";
3
+ import { t as ModelCode } from "../model-code-BY44ao27.js";
4
+ //#region src/entities/inherit.d.ts
5
+ interface Ancestry {
6
+ /** The class and its ancestors by internal name, parsed. */
7
+ ancestors: Map<string, ClassFile>;
8
+ /** The same classes, most derived first, with the bytes to interpret. */
9
+ chain: Array<{
10
+ internalName: string;
11
+ bytes: Uint8Array;
12
+ }>;
13
+ }
14
+ /** Reads a class and everything it inherits from that the jar still holds. */
15
+ declare function ancestryOf(internalName: string, read: (internalName: string) => Promise<Uint8Array | null>): Promise<Ancestry>;
16
+ /**
17
+ * Geometry for one class, with the code it builds on available and its ancestors' parts filled in.
18
+ *
19
+ * `code` widens what a call may be followed into beyond the ancestors, which some models need: the
20
+ * witch's body is the villager's, and a donkey's chest comes from the saddle model. Without it those
21
+ * models still come out, minus the piece the other class holds.
22
+ *
23
+ * Every ancestor is merged rather than only the first with boxes, because a deep hierarchy splits
24
+ * several ways: 1.16.5's sheep overrides a head in `SheepFurModel` and takes its legs from
25
+ * `QuadrupedModel` two hops up.
26
+ */
27
+ declare function extractInheritedModel(internalName: string, read: (internalName: string) => Promise<Uint8Array | null>, code?: ModelCode): Promise<ExtractedModel | null>;
28
+ //#endregion
29
+ export { Ancestry, ancestryOf, extractInheritedModel };
@@ -0,0 +1,2 @@
1
+ import { n as extractInheritedModel, t as ancestryOf } from "../inherit-ByHjOJX3.js";
2
+ export { ancestryOf, extractInheritedModel };
@@ -0,0 +1,2 @@
1
+ import { n as JarClassReader, r as layeredClasses, t as ClassSource } from "../jar-classes-CSRrPAUy.js";
2
+ export { ClassSource, JarClassReader, layeredClasses };
@@ -0,0 +1,2 @@
1
+ import { n as layeredClasses, t as JarClassReader } from "../jar-classes-DCtELr1i.js";
2
+ export { JarClassReader, layeredClasses };
@@ -0,0 +1,71 @@
1
+ import { t as ClassFile } from "../classfile-BdLIk4Pe.js";
2
+ import { t as Mappings } from "../mappings-DxX4KoZl.js";
3
+ //#region src/entities/link.d.ts
4
+ /**
5
+ * Entity types whose renderer or model is not named after them. Values are class simple names to look
6
+ * for; several may be listed because the game renamed the holder between versions — 1.21.4 folded
7
+ * `ChestedHorseRenderer` into `DonkeyRenderer`, and a pack is exported for both eras.
8
+ */
9
+ declare const RENDERER_ALIASES: Readonly<Record<string, string | readonly string[]>>;
10
+ /**
11
+ * Entity types the game draws without a `ModelPart` hierarchy at all, so having no geometry for them
12
+ * is the right answer rather than a gap.
13
+ *
14
+ * Three kinds: things that draw a *block* or an *item* instead (a dropped stack, a falling block, a
15
+ * thrown egg), things the renderer draws with hand-built quads (an arrow, a painting, the end
16
+ * crystal's beam), and things that are deliberately invisible (a marker, the display entities).
17
+ *
18
+ * Listed rather than inferred so that a version adding a mob cannot slip through as "no model": the
19
+ * coverage test fails on any type that is neither linked nor named here.
20
+ */
21
+ declare const NO_MODEL: ReadonlySet<string>;
22
+ interface LinkContext {
23
+ mappings: Mappings;
24
+ /** Reads a class by its obfuscated name, or `null` when the jar has no such class. */
25
+ read: (obfuscated: string) => Promise<ClassFile | null>;
26
+ /** Whether this class actually yields geometry, used to skip abstract bases. */
27
+ hasGeometry: (obfuscated: string) => Promise<boolean>;
28
+ /**
29
+ * `buildModelNameIndex` over the jar being exported, for a mod whose classes follow none of
30
+ * vanilla's naming. Absent for a vanilla-only link, where the named routes are exact and a fuzzy
31
+ * one would only ever be wrong.
32
+ */
33
+ nameIndex?: Map<string, string[]>;
34
+ }
35
+ interface EntityModelLink {
36
+ /** Deobfuscated class name, for diagnostics and for the pack's model id. */
37
+ className: string;
38
+ /** The class to actually read the bytecode of. */
39
+ obfuscated: string;
40
+ /** How it was found, so a report can say how much rests on the curated table. */
41
+ route: 'model-name' | 'renderer' | 'alias' | 'mod-name' | 'vanilla-name';
42
+ }
43
+ /**
44
+ * A class's simple name reduced to the entity it is about.
45
+ *
46
+ * `Lionfish_Model`, `ModelLionfish`, `LionfishEntityModel` and `lionfish` all come out `lionfish`.
47
+ * Mods put the marker word at either end and sometimes both — `Modelcutter` is the old MCP style,
48
+ * `Lionfish_Model` the modern one — so the strip repeats until nothing more comes off. Separators go
49
+ * too, because a mod's class is `Deepling_Brute_Model` where its entity is `deepling_brute`.
50
+ */
51
+ declare function normaliseModelName(name: string): string;
52
+ /**
53
+ * Normalised name → the classes that could be it, for a jar whose classes are not named the way
54
+ * vanilla's are.
55
+ *
56
+ * Built once per jar because the alternative is scanning three thousand classes per entity. Nested
57
+ * classes are indexed under the part after the `$` as well: occultism files a familiar's head as
58
+ * `HeadlessFamiliarRenderer$CthulhuHeadModel`, and only the tail of that names the mob.
59
+ */
60
+ declare function buildModelNameIndex(internalNames: Iterable<string>): Map<string, string[]>;
61
+ /**
62
+ * The class holding one entity's geometry, or `null` when nothing links.
63
+ *
64
+ * A candidate is only accepted if it *yields geometry*: a renderer chain can land on an abstract base
65
+ * that names no boxes, and returning that would report coverage the pack does not have.
66
+ */
67
+ declare function linkEntityModel(entityId: string, context: LinkContext): Promise<EntityModelLink | null>;
68
+ /** Reads and caches classes out of an extracted jar directory or a jar reader. */
69
+ declare function classReader(load: (obfuscated: string) => Promise<Uint8Array | null>): LinkContext['read'];
70
+ //#endregion
71
+ export { EntityModelLink, LinkContext, NO_MODEL, RENDERER_ALIASES, buildModelNameIndex, classReader, linkEntityModel, normaliseModelName };
@@ -0,0 +1,2 @@
1
+ import { a as linkEntityModel, i as classReader, n as RENDERER_ALIASES, o as normaliseModelName, r as buildModelNameIndex, t as NO_MODEL } from "../link-5GmBLTzk.js";
2
+ export { NO_MODEL, RENDERER_ALIASES, buildModelNameIndex, classReader, linkEntityModel, normaliseModelName };
@@ -0,0 +1,2 @@
1
+ import { a as isObfuscated, c as resolveMappings, i as identityMappings, n as MappingsUnavailableError, o as loadMappings, r as downloadClientMappings, s as parseMappings, t as Mappings } from "../mappings-DxX4KoZl.js";
2
+ export { Mappings, MappingsUnavailableError, downloadClientMappings, identityMappings, isObfuscated, loadMappings, parseMappings, resolveMappings };
@@ -0,0 +1,2 @@
1
+ import { a as loadMappings, i as isObfuscated, n as downloadClientMappings, o as parseMappings, r as identityMappings, s as resolveMappings, t as MappingsUnavailableError } from "../mappings-BEPVaiwE.js";
2
+ export { MappingsUnavailableError, downloadClientMappings, identityMappings, isObfuscated, loadMappings, parseMappings, resolveMappings };
@@ -0,0 +1,18 @@
1
+ import { i as Part, r as ExtractedModel } from "../model-PnQ4gV0-.js";
2
+ //#region src/entities/merge.d.ts
3
+ /**
4
+ * Where a piece's parts sit: at the top, or inside the one anonymous root a `MeshDefinition` makes.
5
+ * Both merge into the same list of named slots and only one of them is nested.
6
+ */
7
+ declare function slotsOf(parts: Part[]): {
8
+ container: Part | null;
9
+ slots: Part[];
10
+ };
11
+ /** True when this piece built its own mesh rather than adding to one it was handed. */
12
+ declare function buildsOwnMesh(model: ExtractedModel): boolean;
13
+ /** `base` plus the parts of `extra` that `base` does not name. */
14
+ declare function mergeModels(base: ExtractedModel, extra: ExtractedModel): ExtractedModel;
15
+ declare function countCubes(part: Part): number;
16
+ declare function cubesOf(model: ExtractedModel): number;
17
+ //#endregion
18
+ export { buildsOwnMesh, countCubes, cubesOf, mergeModels, slotsOf };
@@ -0,0 +1,2 @@
1
+ import { a as slotsOf, i as mergeModels, n as countCubes, r as cubesOf, t as buildsOwnMesh } from "../merge-NDYxpmL8.js";
2
+ export { buildsOwnMesh, countCubes, cubesOf, mergeModels, slotsOf };
@@ -0,0 +1,31 @@
1
+ import { r as ExtractedModel } from "../model-PnQ4gV0-.js";
2
+ import { t as ClassFile } from "../classfile-BdLIk4Pe.js";
3
+ //#region src/entities/model-bytecode.d.ts
4
+ /**
5
+ * Extracts whatever geometry a class's methods build.
6
+ *
7
+ * Every method is tried, because where the model is built differs by era: 1.17+ vanilla uses a static
8
+ * factory, older vanilla and most mods use the constructor. The one that built the fullest *mesh* is
9
+ * the model, and the methods that only add to a mesh handed to them are merged into it:
10
+ * `QuadrupedModel` puts its head and body in `createBodyMesh` and its four legs in `createLegs`, so
11
+ * picking a single winner silently drops whichever it did not pick.
12
+ *
13
+ * Only those helpers merge, never two methods that each built their own mesh. Those are variants of the
14
+ * same model, a boat and a chest boat, and folding them together puts a chest on every boat.
15
+ */
16
+ /** What a call may be followed into, and whether this class is the sort that makes such calls. */
17
+ interface Following {
18
+ /** The class a call names, or null for the builder API and everything else off limits. */
19
+ resolve: (internalName: string) => ClassFile | null;
20
+ /**
21
+ * Whether the class being extracted is model code. A renderer is not: it constructs the models it
22
+ * draws, so following those calls makes it look like it holds their geometry, and a pufferfish
23
+ * renderer that builds all three sizes comes out as one model with three sets of spines.
24
+ *
25
+ * Defaults to true, which is right when the caller has no way to tell the two apart.
26
+ */
27
+ isModelCode?: boolean;
28
+ }
29
+ declare function extractModel(bytes: Uint8Array, following?: Following): ExtractedModel;
30
+ //#endregion
31
+ export { Following, extractModel };
@@ -0,0 +1,2 @@
1
+ import { t as extractModel } from "../model-bytecode-gWy5_uCj.js";
2
+ export { extractModel };
@@ -0,0 +1,2 @@
1
+ import { n as isModelCode, r as loadModelCode, t as ModelCode } from "../model-code-BY44ao27.js";
2
+ export { ModelCode, isModelCode, loadModelCode };
@@ -0,0 +1,2 @@
1
+ import { n as loadModelCode, t as isModelCode } from "../model-code-1Vuk-zj_.js";
2
+ export { isModelCode, loadModelCode };
@@ -0,0 +1,2 @@
1
+ import { i as Part, n as CubeFace, r as ExtractedModel, t as Cube } from "../model-PnQ4gV0-.js";
2
+ export { Cube, CubeFace, ExtractedModel, Part };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,175 @@
1
+ import { n as ResourceLocator } from "./locator-COMEtEn9.js";
2
+ import { l as findMobTexture, u as tokens } from "./entity-textures-DFRIVdF1.js";
3
+ //#region src/blocks/entities.ts
4
+ /**
5
+ * The entity registry, and one colour per entity.
6
+ *
7
+ * A world viewer has no geometry for a mob — nothing in any jar does, at any version, because entity
8
+ * models are Java code — so what it draws is a box the size of the hitbox. Two things make that box
9
+ * worth looking at rather than merely present: the entity's *name*, so a reader knows what it is, and
10
+ * the entity's *colour*, so a pen of cows does not read as the same grey mass as the shoal beside it.
11
+ *
12
+ * Both come out of the assets today, with no geometry decision made:
13
+ *
14
+ * - The registry is the lang file. `entity.<ns>.<name>` is written per registered entity type by
15
+ * datagen, so it is the closest thing a jar carries to the entity registry — the same trick the item
16
+ * sheet uses to tell a real item from a template model.
17
+ * - The colour is the mean of the opaque pixels of the entity's own texture sheet. Crude on purpose:
18
+ * it is a marker's tint, not a render, and a mean is the one summary that cannot be wrong about
19
+ * which mob it came from.
20
+ *
21
+ * `texture` is carried alongside so that whatever eventually draws real entity geometry has the
22
+ * id → sheet mapping already resolved, which is the fiddly half of the problem.
23
+ */
24
+ /** `entity.<ns>.<name>`, and only the plain ones: a deeper key is a subtitle or a variant label. */
25
+ const ENTITY_KEY_SEGMENTS = 3;
26
+ /**
27
+ * Texture names filed under a mob's own directory that are not the mob.
28
+ *
29
+ * A llama's directory holds `spit`, which is the projectile it throws; a horse's holds its armour, a
30
+ * cat's its collar, a happy ghast's its harness. Those score exactly as well on name overlap as the
31
+ * body sheet does — `entity/llama/spit` and `entity/llama/creamy` both name the llama and sit at the
32
+ * same depth — so without this the tie breaks arbitrarily and a llama comes out the colour of spit.
33
+ *
34
+ * Overlays are here for the same reason: a blink or an eye layer is a few opaque pixels whose mean
35
+ * says nothing about the animal.
36
+ */
37
+ const NOT_THE_MOB = /* @__PURE__ */ new Set([
38
+ "spit",
39
+ "armor",
40
+ "saddle",
41
+ "collar",
42
+ "harness",
43
+ "chest",
44
+ "overlay",
45
+ "blink",
46
+ "eyes",
47
+ "markings",
48
+ "glow"
49
+ ]);
50
+ /**
51
+ * Alpha at or below which a pixel does not count toward the mean.
52
+ *
53
+ * Not zero: mob sheets are full of near-transparent antialiasing at the edges of a part, and letting
54
+ * those in drags every colour toward whatever the sheet's background happens to be.
55
+ */
56
+ const OPAQUE_ALPHA = 128;
57
+ /** The mean of a texture's opaque pixels as `#rrggbb`, or `null` when it has none. */
58
+ function meanColor(texture) {
59
+ let red = 0;
60
+ let green = 0;
61
+ let blue = 0;
62
+ let counted = 0;
63
+ for (let offset = 0; offset < texture.data.length; offset += 4) {
64
+ if (texture.data[offset + 3] <= OPAQUE_ALPHA) continue;
65
+ red += texture.data[offset];
66
+ green += texture.data[offset + 1];
67
+ blue += texture.data[offset + 2];
68
+ counted++;
69
+ }
70
+ if (counted === 0) return null;
71
+ return `#${[
72
+ red,
73
+ green,
74
+ blue
75
+ ].map((channel) => Math.round(channel / counted).toString(16).padStart(2, "0")).join("")}`;
76
+ }
77
+ /**
78
+ * Every entity type a namespace registers, by its lang keys.
79
+ *
80
+ * Keys with more segments than `<prefix>.<ns>.<name>` are dropped: those are a variant's own label
81
+ * (`entity.minecraft.villager.farmer`) or a death message, never a type.
82
+ */
83
+ async function entityTypesOf(resources, namespace) {
84
+ const raw = await resources.getResource("lang", new ResourceLocator(namespace, "en_us"));
85
+ if (!raw) return [];
86
+ let lang;
87
+ try {
88
+ lang = JSON.parse(raw.toString("utf8"));
89
+ } catch {
90
+ return [];
91
+ }
92
+ const prefix = `entity.${namespace}.`;
93
+ const types = [];
94
+ for (const [key, value] of Object.entries(lang)) {
95
+ if (!key.startsWith(prefix) || key.split(".").length !== ENTITY_KEY_SEGMENTS || typeof value !== "string") continue;
96
+ types.push({
97
+ id: `${namespace}:${key.slice(prefix.length)}`,
98
+ name: value
99
+ });
100
+ }
101
+ return types;
102
+ }
103
+ /**
104
+ * A mob's sheet where the conventional paths do not have it.
105
+ *
106
+ * `findMobTexture` wants a file named exactly after the entity, which is how most of them are filed.
107
+ * The rest are named after a *variant* — a rabbit is `entity/rabbit/brown`, a horse is
108
+ * `entity/horse/horse_white`, an elder guardian is `entity/guardian/guardian_elder` — so this falls
109
+ * back to the best token overlap under `entity/`, preferring a shallower path so a family's plain
110
+ * sheet beats its armour or blink overlay.
111
+ *
112
+ * Restricted to `entity/` deliberately. Widening it finds a *block* texture with a matching word for
113
+ * anything that has no sheet at all, and a lectern-coloured marker is worse than no colour.
114
+ *
115
+ * The name is also matched with its underscores squashed out, which is a real vanilla convention and
116
+ * not a guess: an armour stand is filed under `entity/armorstand/` and a magma cube is
117
+ * `entity/slime/magmacube`, and neither shares a single token with its own id.
118
+ */
119
+ async function findVariantTexture(resources, namespace, name) {
120
+ const wanted = tokens(name);
121
+ const squashed = name.replaceAll("_", "");
122
+ let best = null;
123
+ let bestScore = 0;
124
+ for (const locator of await resources.listResources("texture", namespace)) {
125
+ if (!locator.path.startsWith("entity/")) continue;
126
+ const leaf = locator.path.split("/").at(-1) ?? "";
127
+ if (tokens(leaf).some((token) => NOT_THE_MOB.has(token))) continue;
128
+ const candidate = tokens(locator.path);
129
+ let score = 0;
130
+ for (const token of wanted) if (candidate.includes(token)) score += 2;
131
+ if (squashed !== name && candidate.includes(squashed)) score += wanted.length * 2;
132
+ if (score === 0) continue;
133
+ const depth = locator.path.split("/").length;
134
+ score = score * 10 - depth - candidate.length;
135
+ if (score > bestScore) {
136
+ best = locator;
137
+ bestScore = score;
138
+ }
139
+ }
140
+ return best;
141
+ }
142
+ async function collectEntities(options) {
143
+ const { resources, textures } = options;
144
+ const entities = {};
145
+ const failures = [];
146
+ const types = [];
147
+ for (const namespace of options.namespaces) types.push(...await entityTypesOf(resources, namespace));
148
+ let done = 0;
149
+ for (const type of types.toSorted((left, right) => left.id.localeCompare(right.id))) {
150
+ options.onProgress?.(++done, types.length);
151
+ const [namespace, name] = [type.id.slice(0, type.id.indexOf(":")), type.id.slice(type.id.indexOf(":") + 1)];
152
+ const entry = { name: type.name };
153
+ const locator = await findMobTexture(resources, namespace, name) ?? await findVariantTexture(resources, namespace, name);
154
+ if (locator) {
155
+ const texture = await textures.load(locator).catch(() => null);
156
+ const colour = texture ? meanColor(texture) : null;
157
+ entry.texture = locator.toString();
158
+ if (colour) entry.color = colour;
159
+ else failures.push({
160
+ id: type.id,
161
+ reason: `texture ${locator} has no opaque pixels; exported without a colour`
162
+ });
163
+ } else failures.push({
164
+ id: type.id,
165
+ reason: "no entity texture found; exported without a colour"
166
+ });
167
+ entities[type.id] = entry;
168
+ }
169
+ return {
170
+ entities,
171
+ failures
172
+ };
173
+ }
174
+ //#endregion
175
+ export { entityTypesOf as n, collectEntities as t };
@@ -1,6 +1,6 @@
1
1
  import { n as ResourceLocator } from "./locator-COMEtEn9.js";
2
2
  import { a as findChestTexture, i as findBedTexture, l as findMobTexture, n as HEAD_LAYOUTS, o as findEntityTexture, r as coversSheet, t as DYE_COLORS, u as tokens } from "./entity-textures-DFRIVdF1.js";
3
- import { c as renderModel } from "./obj-BaGvPUbb.js";
3
+ import { c as renderModel } from "./obj-CJHEHpki.js";
4
4
  //#region src/render/entity-fallback.ts
5
5
  /**
6
6
  * Heuristic rendering for mod items that parent `builtin/entity` — chests,
@@ -0,0 +1,100 @@
1
+ import { n as ResourceLocator } from "./locator-COMEtEn9.js";
2
+ import { t as geoToModel } from "./geo-model-Bo1gpEwX.js";
3
+ import { i as parseGeo, r as nameTokens } from "./geo-Desq-gRC.js";
4
+ //#region src/blocks/entity-geo.ts
5
+ /**
6
+ * Finding the `.geo.json` a GeckoLib entity is drawn from.
7
+ *
8
+ * The registration is in Java — `GeoModel.getModelResource` returns a resource location — so there is
9
+ * no data to read it out of, and this is a search by name. The same problem `findGeoForItem` solves for
10
+ * items, with the directories a *mob* is filed under instead: `geo/entity/` if the mod is tidy, a flat
11
+ * `geo/` if not (Creeper Overhaul, Ars Nouveau and Aquatic Frontiers all are).
12
+ *
13
+ * The search never leaves a `geo` directory, and every rule below is a name test rather than a
14
+ * similarity score, so the worst case is finding nothing rather than drawing a mob as another one.
15
+ */
16
+ /** Where a mod keeps entity geometry, in the order worth trying. */
17
+ const GEO_DIRECTORIES = [
18
+ "geo/entity",
19
+ "geo",
20
+ "geckolib/geo/entity",
21
+ "geckolib/geo",
22
+ "geo/entities"
23
+ ];
24
+ /** A geo path under some `geo` directory, for the scan that runs when the conventional spots miss. */
25
+ const UNDER_GEO = /(?:^|\/)(?:geo|geckolib\/geo)\//;
26
+ /**
27
+ * The geometry for one entity, or null when the mod does not ship any under that name.
28
+ *
29
+ * `namespace` and `name` come from the entity id, so `occultism:afrit` looks for
30
+ * `assets/occultism/geo/entity/afrit.geo.json` and its neighbours. Nothing is inferred from the
31
+ * *class* here: a GeckoLib mob has no model class to inspect.
32
+ */
33
+ async function findGeoForEntity(resources, namespace, name) {
34
+ const paths = GEO_DIRECTORIES.map((directory) => `${directory}/${name}.geo.json`);
35
+ for (const path of paths) {
36
+ const found = await load(resources, namespace, path);
37
+ if (found) return found;
38
+ }
39
+ const files = (await resources.listResources("raw", namespace)).map((candidate) => candidate.path).filter((path) => path.endsWith(".geo.json") && UNDER_GEO.test(path)).toSorted((left, right) => left.length - right.length);
40
+ for (const path of ranked(files, name)) {
41
+ const found = await load(resources, namespace, path);
42
+ if (found) return found;
43
+ }
44
+ return null;
45
+ }
46
+ const baseName = (path) => path.split("/").pop()?.replace(/\.geo\.json$/, "") ?? "";
47
+ /**
48
+ * The geo files that could be this entity's, best first.
49
+ *
50
+ * A mod's entity id and its geometry's filename agree less often than you would hope, and they
51
+ * disagree in three shapes, each of which is here because a real mod does it:
52
+ *
53
+ * 1. **The same name spelled differently.** Ars Nouveau registers `bookwyrm` and ships
54
+ * `book_wyrm.geo.json`.
55
+ * 2. **A decorated entity, the plain model.** `familiar_drygmy` is drawn by `drygmy`, and
56
+ * `marid_unbound` by `marid`. So a geo whose every token appears in the entity's name qualifies,
57
+ * most specific first — a subset test rather than a similarity score, so an unrelated model can
58
+ * never win.
59
+ * 3. **A plain entity, a decorated model.** Occultism registers `foliot` and ships only its jobs:
60
+ * `foliot_worker`, `foliot_crusher`, `foliot_lumberjack`. Any of them is a foliot, so the shortest
61
+ * wins. Last, because this is the one direction that is a guess.
62
+ */
63
+ function ranked(files, name) {
64
+ const flattened = name.replaceAll("_", "");
65
+ const wanted = new Set(nameTokens(name));
66
+ const exact = files.filter((path) => baseName(path) === name);
67
+ const spelling = files.filter((path) => baseName(path).replaceAll("_", "") === flattened);
68
+ const decorated = files.filter((path) => {
69
+ const tokens = nameTokens(baseName(path));
70
+ return tokens.length > 0 && tokens.every((token) => wanted.has(token));
71
+ }).toSorted((left, right) => nameTokens(baseName(right)).length - nameTokens(baseName(left)).length);
72
+ const variants = files.filter((path) => {
73
+ const tokens = new Set(nameTokens(baseName(path)));
74
+ return [...wanted].every((token) => tokens.has(token));
75
+ });
76
+ return [.../* @__PURE__ */ new Set([
77
+ ...exact,
78
+ ...spelling,
79
+ ...decorated,
80
+ ...variants
81
+ ])];
82
+ }
83
+ async function load(resources, namespace, path) {
84
+ const raw = await resources.getResource("raw", new ResourceLocator(namespace, path));
85
+ if (!raw) return null;
86
+ let model;
87
+ try {
88
+ const geo = parseGeo(JSON.parse(raw.toString("utf8")));
89
+ if (!geo) return null;
90
+ model = geoToModel(geo);
91
+ } catch {
92
+ return null;
93
+ }
94
+ return model.parts.length > 0 ? {
95
+ id: `${namespace}:${path}`,
96
+ model
97
+ } : null;
98
+ }
99
+ //#endregion
100
+ export { findGeoForEntity as t };
@@ -0,0 +1,127 @@
1
+ import { t as findGeoForEntity } from "./entity-geo-B2CkuMWx.js";
2
+ import { n as extractInheritedModel } from "./inherit-ByHjOJX3.js";
3
+ import { a as linkEntityModel, i as classReader, r as buildModelNameIndex } from "./link-5GmBLTzk.js";
4
+ import { s as resolveMappings, t as MappingsUnavailableError } from "./mappings-BEPVaiwE.js";
5
+ import { n as loadModelCode } from "./model-code-1Vuk-zj_.js";
6
+ //#region src/blocks/entity-models.ts
7
+ /**
8
+ * Entity geometry for the pack: link each entity to the class that builds it, read that class's
9
+ * bytecode, and stitch the sheets those models are drawn with.
10
+ *
11
+ * The pieces all live in `src/entities/` — this is the part that knows what a *pack* wants. Which is
12
+ * mostly two things the extractor does not care about: several entities share one model (every zombie
13
+ * in the game is a `HumanoidModel`), so models are keyed by class and pointed at rather than copied;
14
+ * and the `uv` values in a model address the entity's own sheet, so those sheets have to be in an
15
+ * atlas the consumer can sample.
16
+ *
17
+ * Needs the *code*, not just the assets. An `--assets` folder has none, and a pack exported from one
18
+ * carries no entity geometry — which is the honest answer rather than a silent gap.
19
+ */
20
+ function countCubes(part) {
21
+ return part.cubes.length + part.children.reduce((sum, child) => sum + countCubes(child), 0);
22
+ }
23
+ /** Drops the internal shape into the pack's, omitting what is at its default. */
24
+ function toPackPart(part) {
25
+ return {
26
+ name: part.name,
27
+ pivot: [...part.pivot],
28
+ rotation: [...part.rotation],
29
+ cubes: part.cubes.map((cube) => {
30
+ return {
31
+ origin: [...cube.origin],
32
+ size: [...cube.size],
33
+ uv: [...cube.uv],
34
+ ...cube.inflate !== 0 && { inflate: cube.inflate },
35
+ ...cube.mirror && { mirror: true },
36
+ ...cube.faces && { faces: cube.faces }
37
+ };
38
+ }),
39
+ children: part.children.map((child) => toPackPart(child))
40
+ };
41
+ }
42
+ /**
43
+ * Recovers geometry for as many of `entityIds` as the code describes.
44
+ *
45
+ * Mappings are resolved once: downloaded for an obfuscated jar, and the identity for one that is not
46
+ * (from the 26 line the client ships in the clear, and a mod jar never was obfuscated). A version that
47
+ * is obfuscated *and* unmapped — 1.13.x — yields nothing, with the reason recorded once rather than
48
+ * per entity.
49
+ */
50
+ async function collectEntityGeometry(options) {
51
+ const { classes, entityIds } = options;
52
+ const models = {};
53
+ const modelOf = /* @__PURE__ */ new Map();
54
+ const spriteIds = /* @__PURE__ */ new Set();
55
+ const failures = [];
56
+ const names = await classes.list();
57
+ let mappings;
58
+ try {
59
+ ({mappings} = await resolveMappings(options.version, options.cacheDir, async () => names));
60
+ } catch (err) {
61
+ failures.push({
62
+ id: options.version,
63
+ reason: err instanceof MappingsUnavailableError ? `no entity geometry: ${err.message}` : `no entity geometry: mappings could not be loaded (${err.message})`
64
+ });
65
+ return {
66
+ models,
67
+ modelOf,
68
+ spriteIds,
69
+ failures
70
+ };
71
+ }
72
+ const code = await loadModelCode(names, async (internal) => await classes.read(internal), (internal) => mappings.originalOf.get(internal) ?? internal);
73
+ const extracted = /* @__PURE__ */ new Map();
74
+ const geometryOf = async (internal) => {
75
+ if (!extracted.has(internal)) extracted.set(internal, await extractInheritedModel(internal, async (name) => await classes.read(name), code));
76
+ return extracted.get(internal) ?? null;
77
+ };
78
+ const context = {
79
+ mappings,
80
+ read: classReader(async (internal) => await classes.read(internal)),
81
+ hasGeometry: async (internal) => {
82
+ const model = await geometryOf(internal);
83
+ return model !== null && model.parts.reduce((sum, part) => sum + countCubes(part), 0) > 0;
84
+ },
85
+ nameIndex: buildModelNameIndex(names)
86
+ };
87
+ const geoFor = async (entityId) => {
88
+ if (!options.resources) return null;
89
+ const [namespace, name] = entityId.includes(":") ? entityId.split(":") : ["minecraft", entityId];
90
+ return await findGeoForEntity(options.resources, namespace, name);
91
+ };
92
+ let done = 0;
93
+ for (const entityId of [...entityIds].toSorted((left, right) => left.localeCompare(right))) {
94
+ options.onProgress?.(++done, entityIds.length);
95
+ const link = await linkEntityModel(entityId, context);
96
+ const guessed = link?.route === "vanilla-name";
97
+ const found = link && !guessed ? {
98
+ id: link.className,
99
+ model: await geometryOf(link.obfuscated)
100
+ } : await geoFor(entityId) ?? (link ? {
101
+ id: link.className,
102
+ model: await geometryOf(link.obfuscated)
103
+ } : null);
104
+ if (!found?.model) continue;
105
+ const { id: modelId, model } = found;
106
+ if (!models[modelId]) models[modelId] = {
107
+ textureSize: [...model.textureSize],
108
+ parts: model.parts.map((part) => toPackPart(part)),
109
+ ...model.notes.length > 0 && { notes: [...model.notes] }
110
+ };
111
+ modelOf.set(entityId, modelId);
112
+ const texture = options.textureOf.get(entityId);
113
+ if (texture) spriteIds.add(texture);
114
+ else failures.push({
115
+ id: entityId,
116
+ reason: `geometry recovered from ${modelId} but no entity texture resolved`
117
+ });
118
+ }
119
+ return {
120
+ models,
121
+ modelOf,
122
+ spriteIds,
123
+ failures
124
+ };
125
+ }
126
+ //#endregion
127
+ export { collectEntityGeometry as t };
@@ -1,4 +1,5 @@
1
1
  import { a as ResourceManager } from "./manager-exOIcdyx.js";
2
+ import { t as ClassSource } from "./jar-classes-CSRrPAUy.js";
2
3
  //#region src/blocks/export.d.ts
3
4
  interface ExportBlockPackOptions {
4
5
  resources: ResourceManager;
@@ -25,6 +26,22 @@ interface ExportBlockPackOptions {
25
26
  * or a handful of charcoal from. On by default.
26
27
  */
27
28
  items?: boolean;
29
+ /**
30
+ * Export `entities.json`: every entity type the namespaces register, with a display name and the
31
+ * mean colour of its sheet. On by default. No geometry — no jar has any — but it is what lets a
32
+ * consumer name and tint the stand-in box it draws for a mob.
33
+ */
34
+ entities?: boolean;
35
+ /**
36
+ * Reads class files, for recovering entity geometry from the client's own code.
37
+ *
38
+ * Entity models are Java at every version, so without this a pack carries entity names and marker
39
+ * colours but no shapes. Absent for an `--assets` run, which has no code to read — the honest
40
+ * answer there is no geometry rather than a silent gap.
41
+ */
42
+ classes?: ClassSource;
43
+ /** Where mappings cache; the jar cache directory. */
44
+ cacheDir?: string;
28
45
  onProgress?: (stage: string, done: number, total: number) => void;
29
46
  }
30
47
  interface BlockPackFailure {
@@ -33,7 +50,7 @@ interface BlockPackFailure {
33
50
  * `intentional` is not a failure: it marks a block that is empty because the
34
51
  * game draws nothing for it. Filter it out to get the real gaps.
35
52
  */
36
- kind: 'blockstate' | 'model' | 'texture' | 'sprite' | 'geometry' | 'tint' | 'biome' | 'item' | 'intentional';
53
+ kind: 'blockstate' | 'model' | 'texture' | 'sprite' | 'geometry' | 'tint' | 'biome' | 'item' | 'entity' | 'intentional';
37
54
  reason: string;
38
55
  }
39
56
  interface BlockPackResult {
@@ -45,6 +62,10 @@ interface BlockPackResult {
45
62
  biomes: number;
46
63
  /** Flat items described in `items.json`; `0` means the file was not written. */
47
64
  items: number;
65
+ /** Entity types described in `entities.json`; `0` means the file was not written. */
66
+ entities: number;
67
+ /** Distinct entity models in `entity-models.json`; `0` means the file was not written. */
68
+ entityModels: number;
48
69
  /** Files actually written — the only trustworthy success signal. */
49
70
  writtenFiles: number;
50
71
  failures: BlockPackFailure[];