@nodecraft/rendermite 1.1.4 → 1.2.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 +27 -2
- package/assets/builtin/renderchest/models/block/banner_standing.json +225 -0
- package/assets/builtin/renderchest/models/block/banner_wall.json +155 -0
- package/assets/builtin/renderchest/models/block/bed_foot.json +44 -0
- package/assets/builtin/renderchest/models/block/bed_head.json +44 -0
- package/assets/builtin/renderchest/models/block/chest_left.json +182 -0
- package/assets/builtin/renderchest/models/block/chest_right.json +182 -0
- package/assets/builtin/renderchest/models/block/copper_golem_statue.json +709 -0
- package/assets/builtin/renderchest/models/block/fluid.json +83 -0
- package/assets/builtin/renderchest/models/block/fluid_tinted.json +89 -0
- package/assets/builtin/renderchest/models/block/hanging_sign.json +54 -0
- package/assets/builtin/renderchest/models/block/standing_sign.json +32 -0
- package/assets/builtin/renderchest/models/block/wall_sign.json +19 -0
- package/dist/atlas/registry.js +1 -1
- package/dist/atlas/sources.js +1 -1
- package/dist/atlas-pack-CoOih7By.js +130 -0
- package/dist/atlas-pack-DRTrWcSU.d.ts +41 -0
- package/dist/block-entities-xv3bQpEX.js +490 -0
- package/dist/blocks/atlas-pack.d.ts +2 -0
- package/dist/blocks/atlas-pack.js +2 -0
- package/dist/blocks/block-entities.d.ts +93 -0
- package/dist/blocks/block-entities.js +2 -0
- package/dist/blocks/blockstate-eval.d.ts +2 -0
- package/dist/blocks/blockstate-eval.js +2 -0
- package/dist/blocks/cli.d.ts +9 -0
- package/dist/blocks/cli.js +2 -0
- package/dist/blocks/export.d.ts +2 -0
- package/dist/blocks/export.js +2 -0
- package/dist/blocks/obj-mesh.d.ts +2 -0
- package/dist/blocks/obj-mesh.js +2 -0
- package/dist/blocks/tints.d.ts +2 -0
- package/dist/blocks/tints.js +2 -0
- package/dist/{blockstate-CdstnW2-.js → blockstate-DVb8Mhxc.js} +1 -1
- package/dist/blockstate-eval-BRQ3NWY2.js +264 -0
- package/dist/blockstate-eval-CObtPWzJ.d.ts +122 -0
- package/dist/cli-CWUKXFbx.js +161 -0
- package/dist/cli.js +9 -7
- package/dist/{descriptor-DWebtkI1.js → descriptor-BVIoEpPO.js} +3 -3
- package/dist/{discovery-B9aqV_Rz.js → discovery-CsZu08re.js} +1 -1
- package/dist/dynamic/generators.js +1 -1
- package/dist/{entity-fallback-Cu22uEIK.js → entity-fallback-BTkM8j0V.js} +3 -155
- package/dist/entity-textures-DFRIVdF1.js +209 -0
- package/dist/export-BIpqNU3H.d.ts +44 -0
- package/dist/export-DY5sGwt0.js +790 -0
- package/dist/{face-cQ-pVHoe.d.ts → face-BKwkeTps.d.ts} +1 -1
- package/dist/{fluid-bucket-BzoLofB_.js → fluid-bucket-B-I4ZYHl.js} +1 -1
- package/dist/{generators-BlG4KmtP.js → generators-BCZVlWig.js} +1 -1
- package/dist/{geo-w9HHIrb5.js → geo-BguldlU-.js} +5 -5
- package/dist/{geometry-w0yQbzDZ.d.ts → geometry-Bkur58Vi.d.ts} +1 -1
- package/dist/index.d.ts +10 -5
- package/dist/index.js +21 -16
- package/dist/{item-styles-upgEO1yP.js → item-styles-DjYRdyHi.js} +1 -1
- package/dist/items/blockstate.js +1 -1
- package/dist/items/discovery.js +1 -1
- package/dist/{jar-D9z-X7tA.js → jar-5aLDI0nT.js} +2 -2
- package/dist/{legacy-reshape-C05SfAPR.js → legacy-reshape-CcdstKyI.js} +2 -2
- package/dist/{legacy-tints-B09J0Rxh.js → legacy-tints-l-541wgI.js} +1 -1
- package/dist/{loader-CWsTZgrk.js → loader-C-NuJaEi.js} +119 -9
- package/dist/{loader-Lb0ZeILz.js → loader-C18GpurU.js} +1 -1
- package/dist/{loader-DD83d80u.d.ts → loader-C_1_amsq.d.ts} +18 -1
- package/dist/{locator-ycZzh_6z.js → locator-COMEtEn9.js} +23 -1
- package/dist/{manager-CkletR5K.js → manager-CYbAkWuT.js} +2 -2
- package/dist/models/loader.d.ts +1 -1
- package/dist/models/loader.js +1 -1
- package/dist/models/types.d.ts +1 -1
- package/dist/{models-BKxGkPS7.js → models-CQwcTZcg.js} +1 -1
- package/dist/mods/models.js +1 -1
- package/dist/mods/prepare.js +1 -1
- package/dist/mods/synthesize.js +1 -1
- package/dist/mods/tints.js +1 -1
- package/dist/{renderer-DB1Oe7xs.js → obj-bvs3gwex.js} +234 -234
- package/dist/obj-mesh-C6Sp4Ysc.d.ts +41 -0
- package/dist/obj-mesh-rnYbSbpv.js +97 -0
- package/dist/output/item-styles.js +1 -1
- package/dist/{pipeline-Bp6amYz_.d.ts → pipeline-BDvXcw20.d.ts} +1 -1
- package/dist/{pipeline-CgJaVpoL.js → pipeline-DeGRR0CP.js} +14 -14
- package/dist/{prepare-D56Z-9cg.js → prepare-B8oPdGYz.js} +5 -5
- package/dist/{prepare-Cc8QZ3j0.js → prepare-C95vjt_o.js} +5 -5
- package/dist/{registry-DS5PZ57l.js → registry-DlduyjtM.js} +2 -2
- package/dist/render/entity-fallback.d.ts +3 -3
- package/dist/render/entity-fallback.js +1 -1
- package/dist/render/face.d.ts +1 -1
- package/dist/render/face.js +1 -1
- package/dist/render/fluid-bucket.js +1 -1
- package/dist/render/geo.d.ts +2 -2
- package/dist/render/geo.js +1 -1
- package/dist/render/geometry.d.ts +1 -1
- package/dist/render/legacy-tints.d.ts +1 -1
- package/dist/render/legacy-tints.js +1 -1
- package/dist/render/obj.d.ts +10 -3
- package/dist/render/obj.js +2 -2
- package/dist/render/pipeline.d.ts +1 -1
- package/dist/render/pipeline.js +1 -1
- package/dist/render/renderer.d.ts +1 -1
- package/dist/render/renderer.js +1 -1
- package/dist/render/tints.d.ts +1 -1
- package/dist/render/tints.js +1 -1
- package/dist/{renderer-C79EQDSn.d.ts → renderer-U20hnhbi.d.ts} +2 -2
- package/dist/resources/descriptor.js +1 -1
- package/dist/resources/entity-textures.d.ts +51 -0
- package/dist/resources/entity-textures.js +2 -0
- package/dist/resources/jar.js +1 -1
- package/dist/resources/locator.js +1 -1
- package/dist/resources/manager.js +1 -1
- package/dist/resources/synthetic.js +1 -1
- package/dist/{run-uq8_hUx9.d.ts → run-CvIqiZ6T.d.ts} +1 -1
- package/dist/{run-U9pM9wKU.js → run-Diql5y56.js} +7 -7
- package/dist/run.d.ts +1 -1
- package/dist/run.js +1 -1
- package/dist/{sources-Bxk-WOzH.js → sources-BlUriDCs.js} +1 -1
- package/dist/{synthesize-3Q0aaOnK.js → synthesize-jTjVjPAt.js} +2 -2
- package/dist/{synthetic-ZAUjXT7E.js → synthetic-rc9BPT4P.js} +0 -0
- package/dist/textures/loader.js +1 -1
- package/dist/tints-2k3ZhkY3.d.ts +41 -0
- package/dist/{tints-3VI_2SNy.js → tints-B3WQcPtx.js} +1 -1
- package/dist/{tints-Do9Sh3dg.d.ts → tints-DqhgFi8a.d.ts} +1 -1
- package/dist/tints-a9FWZLma.js +176 -0
- package/dist/{tints-CebmvjmP.js → tints-nxtGrqv5.js} +3 -3
- package/dist/{types-DpRSF9Vj.d.ts → types-qlp3I8_-.d.ts} +11 -0
- package/dist/versions/download.js +1 -1
- package/dist/versions/legacy-reshape.js +1 -1
- package/dist/versions/prepare.js +1 -1
- package/dist/workers/render-worker.d.ts +1 -1
- package/dist/workers/render-worker.js +2 -2
- package/package.json +1 -1
- package/dist/{download-D9aARBFp.js → download-1wAfrGKx.js} +1 -1
- package/dist/{face-CMJ3b3Ae.js → face-CKHboPfh.js} +1 -1
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
import { t as packAtlas } from "./atlas-pack-CoOih7By.js";
|
|
2
|
+
import { n as ResourceLocator } from "./locator-COMEtEn9.js";
|
|
3
|
+
import { n as blockEntityGeometry, r as synthesizeVariants, t as INVISIBLE_BLOCKS } from "./block-entities-xv3bQpEX.js";
|
|
4
|
+
import { a as parseBlockState, n as collectModels } from "./blockstate-eval-BRQ3NWY2.js";
|
|
5
|
+
import { a as salvageJson, i as resolveTexture, t as ModelLoader } from "./loader-C-NuJaEi.js";
|
|
6
|
+
import { n as buildObjMesh } from "./obj-mesh-rnYbSbpv.js";
|
|
7
|
+
import { t as buildBlockTints } from "./tints-a9FWZLma.js";
|
|
8
|
+
import { t as loadAtlasResolver } from "./registry-DlduyjtM.js";
|
|
9
|
+
import { n as LayeredResourceManager, t as FolderResourceManager } from "./manager-CYbAkWuT.js";
|
|
10
|
+
import { n as TextureLoader } from "./loader-C18GpurU.js";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
14
|
+
import sharp from "sharp";
|
|
15
|
+
//#region src/blocks/export.ts
|
|
16
|
+
/**
|
|
17
|
+
* Block pack export: block geometry and textures as data, for a renderer that
|
|
18
|
+
* draws blocks itself rather than consuming finished icons.
|
|
19
|
+
*
|
|
20
|
+
* Everything above the rasterizer is reused unchanged — jar layering, parent
|
|
21
|
+
* chain flattening, `#texture` resolution, atlas sources — and the output is
|
|
22
|
+
* flat: a stitched atlas plus JSON that names sprites, baked elements and the
|
|
23
|
+
* blockstate → model mapping with nothing left to resolve at runtime.
|
|
24
|
+
*
|
|
25
|
+
* Scope is Java Edition, 1.13 and up (see docs/block-pack-export.md). Nothing
|
|
26
|
+
* here is version-gated: 1.13+ sits entirely inside the `models` and `modern`
|
|
27
|
+
* eras, so `prepareVersionResources` layers the jar and little else.
|
|
28
|
+
*/
|
|
29
|
+
/** `render_type` values collapsed onto the pack's three layers. */
|
|
30
|
+
const RENDER_TYPES = {
|
|
31
|
+
solid: "solid",
|
|
32
|
+
cutout: "cutout",
|
|
33
|
+
cutout_mipped: "cutout",
|
|
34
|
+
cutout_mipped_all: "cutout",
|
|
35
|
+
tripwire: "cutout",
|
|
36
|
+
translucent: "translucent",
|
|
37
|
+
translucent_moving_block: "translucent"
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* A sprite whose strip is longer than this exports as its first frame only.
|
|
41
|
+
* A broken `.mcmeta` can claim thousands of frames, and packing them would
|
|
42
|
+
* blow the atlas out for one texture.
|
|
43
|
+
*/
|
|
44
|
+
const MAX_FRAMES_PER_SPRITE = 256;
|
|
45
|
+
const FACE_DIRECTIONS = /* @__PURE__ */ new Set([
|
|
46
|
+
"down",
|
|
47
|
+
"up",
|
|
48
|
+
"north",
|
|
49
|
+
"south",
|
|
50
|
+
"west",
|
|
51
|
+
"east"
|
|
52
|
+
]);
|
|
53
|
+
/** Old models spell the vertical faces `bottom`/`top` in `cullface`. */
|
|
54
|
+
const CULLFACE_ALIASES = {
|
|
55
|
+
bottom: "down",
|
|
56
|
+
top: "up"
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Vanilla's own id for the missing-texture sprite. A face whose texture does
|
|
60
|
+
* not resolve is drawn by the game in black and magenta rather than skipped —
|
|
61
|
+
* Blockbench leaves `"texture": "#missing"` on unassigned faces and mods ship
|
|
62
|
+
* models naming textures they forgot to include. Dropping those faces would
|
|
63
|
+
* put holes in geometry the player can see, and would read as our bug rather
|
|
64
|
+
* than the mod's.
|
|
65
|
+
*/
|
|
66
|
+
const MISSING_SPRITE = "minecraft:missingno";
|
|
67
|
+
/**
|
|
68
|
+
* Fluids are blocks in `blockstates.json` but the game meshes them in code, so
|
|
69
|
+
* their models state nothing and their textures would otherwise never reach the
|
|
70
|
+
* atlas — leaving a consumer that has to mesh fluids itself with no pixels to do
|
|
71
|
+
* it from.
|
|
72
|
+
*
|
|
73
|
+
* Which textures those are is read off the files rather than listed: a fluid's
|
|
74
|
+
* two frames are `<name>_still` and `<name>_flow` (or `_flowing`) by a
|
|
75
|
+
* convention every loader follows, and mods put them wherever they like —
|
|
76
|
+
* Create under `fluid/`, Immersive Engineering under `block/fluid/`, Mekanism
|
|
77
|
+
* under `liquid/`. A vanilla-only list would have missed all of them.
|
|
78
|
+
*/
|
|
79
|
+
const FLUID_SUFFIX = /_(?:still|flow|flowing)$/;
|
|
80
|
+
/** Sampled where water meets a non-opaque neighbour; matches no suffix. */
|
|
81
|
+
const EXTRA_FLUID_SPRITES = ["minecraft:block/water_overlay"];
|
|
82
|
+
/** `MissingTextureAtlasSprite`: 16x16, black and magenta in 8px quarters. */
|
|
83
|
+
function missingTexture() {
|
|
84
|
+
const size = 16;
|
|
85
|
+
const data = /* @__PURE__ */ new Uint8ClampedArray(1024);
|
|
86
|
+
for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) {
|
|
87
|
+
const magenta = x < 8 !== y < 8;
|
|
88
|
+
const offset = (y * size + x) * 4;
|
|
89
|
+
data[offset] = magenta ? 248 : 0;
|
|
90
|
+
data[offset + 1] = 0;
|
|
91
|
+
data[offset + 2] = magenta ? 248 : 0;
|
|
92
|
+
data[offset + 3] = 255;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
width: size,
|
|
96
|
+
height: size,
|
|
97
|
+
data
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Vanilla's `BlockElement.uvsByFace`: the UV a face gets when the model states
|
|
102
|
+
* none. Deliberately not `render/geometry.ts`'s auto-UVs — those are in the
|
|
103
|
+
* rasterizer's own unflipped convention, and the pack has to speak the same
|
|
104
|
+
* language a model file's explicit `uv` does.
|
|
105
|
+
*/
|
|
106
|
+
function defaultFaceUv(direction, from, to) {
|
|
107
|
+
switch (direction) {
|
|
108
|
+
case "down": return [
|
|
109
|
+
from[0],
|
|
110
|
+
16 - to[2],
|
|
111
|
+
to[0],
|
|
112
|
+
16 - from[2]
|
|
113
|
+
];
|
|
114
|
+
case "up": return [
|
|
115
|
+
from[0],
|
|
116
|
+
from[2],
|
|
117
|
+
to[0],
|
|
118
|
+
to[2]
|
|
119
|
+
];
|
|
120
|
+
case "north": return [
|
|
121
|
+
16 - to[0],
|
|
122
|
+
16 - to[1],
|
|
123
|
+
16 - from[0],
|
|
124
|
+
16 - from[1]
|
|
125
|
+
];
|
|
126
|
+
case "south": return [
|
|
127
|
+
from[0],
|
|
128
|
+
16 - to[1],
|
|
129
|
+
to[0],
|
|
130
|
+
16 - from[1]
|
|
131
|
+
];
|
|
132
|
+
case "west": return [
|
|
133
|
+
from[2],
|
|
134
|
+
16 - to[1],
|
|
135
|
+
to[2],
|
|
136
|
+
16 - from[1]
|
|
137
|
+
];
|
|
138
|
+
case "east": return [
|
|
139
|
+
16 - to[2],
|
|
140
|
+
16 - to[1],
|
|
141
|
+
16 - from[2],
|
|
142
|
+
16 - from[1]
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Alpha decides the render layer: vanilla assets do not state cutout versus
|
|
148
|
+
* translucent (`render_type` is a loader extension adopted only recently), and
|
|
149
|
+
* inferring it covers mods for free.
|
|
150
|
+
*/
|
|
151
|
+
function inferRenderLayer(frames) {
|
|
152
|
+
let sawHole = false;
|
|
153
|
+
for (const frame of frames) for (let offset = 3; offset < frame.data.length; offset += 4) {
|
|
154
|
+
const alpha = frame.data[offset];
|
|
155
|
+
if (alpha === 0) sawHole = true;
|
|
156
|
+
else if (alpha !== 255) return "translucent";
|
|
157
|
+
}
|
|
158
|
+
return sawHole ? "cutout" : "solid";
|
|
159
|
+
}
|
|
160
|
+
function sortedByKey(entries) {
|
|
161
|
+
return Object.fromEntries(Object.entries(entries).toSorted(([left], [right]) => left.localeCompare(right)));
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Serialize a definition, keeping only the variants whose model was exported.
|
|
165
|
+
* Returns null when that leaves nothing — the consumer must never be handed a
|
|
166
|
+
* model id the pack does not carry.
|
|
167
|
+
*/
|
|
168
|
+
/**
|
|
169
|
+
* Shifts a vendored model's boxes without touching anything else. The array is
|
|
170
|
+
* returned as-is when there is nothing to shift, because models loaded from a
|
|
171
|
+
* shared parent hand out the same element array and callers rely on that.
|
|
172
|
+
*/
|
|
173
|
+
function translateElements(elements, offset) {
|
|
174
|
+
if (offset === void 0) return elements;
|
|
175
|
+
const move = (point) => [
|
|
176
|
+
point[0] + offset[0],
|
|
177
|
+
point[1] + offset[1],
|
|
178
|
+
point[2] + offset[2]
|
|
179
|
+
];
|
|
180
|
+
return elements.map((element) => {
|
|
181
|
+
const rotation = element.rotation;
|
|
182
|
+
return {
|
|
183
|
+
...element,
|
|
184
|
+
from: move(element.from),
|
|
185
|
+
to: move(element.to),
|
|
186
|
+
...rotation && { rotation: {
|
|
187
|
+
...rotation,
|
|
188
|
+
origin: move(rotation.origin)
|
|
189
|
+
} }
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
function serializeBlockState(definition, exported) {
|
|
194
|
+
const cases = [];
|
|
195
|
+
for (const entry of definition.cases) {
|
|
196
|
+
const apply = entry.apply.filter((variant) => exported(variant.model));
|
|
197
|
+
if (apply.length === 0) continue;
|
|
198
|
+
cases.push({
|
|
199
|
+
when: entry.when,
|
|
200
|
+
apply: apply.map((variant) => {
|
|
201
|
+
return {
|
|
202
|
+
model: variant.model,
|
|
203
|
+
...variant.x !== 0 && { x: variant.x },
|
|
204
|
+
...variant.y !== 0 && { y: variant.y },
|
|
205
|
+
...variant.z !== 0 && { z: variant.z },
|
|
206
|
+
...variant.uvlock && { uvlock: true },
|
|
207
|
+
...variant.weight !== 1 && { weight: variant.weight }
|
|
208
|
+
};
|
|
209
|
+
})
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (cases.length === 0) return null;
|
|
213
|
+
return definition.kind === "variants" ? { variants: cases } : { multipart: cases };
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Any texture the model binds, for a model that declares no `particle`. Ordered
|
|
217
|
+
* by the chain's own key order so a run is reproducible, and `particle` itself
|
|
218
|
+
* is not a candidate: it was already tried.
|
|
219
|
+
*/
|
|
220
|
+
function firstBoundTexture(model) {
|
|
221
|
+
for (const name of Object.keys(model.textures)) {
|
|
222
|
+
if (name === "particle") continue;
|
|
223
|
+
const resolved = resolveTexture(model, `#${name}`);
|
|
224
|
+
if (resolved !== null) return resolved;
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The block's own item model, baked, for a block model that draws nothing.
|
|
230
|
+
*
|
|
231
|
+
* A mod that builds a block's shape in Java still has to put something in the
|
|
232
|
+
* inventory, so it ships an item model with real geometry, and that is the
|
|
233
|
+
* closest the assets come to describing the block: Ender IO's conduit is a cube
|
|
234
|
+
* of its own texture, AE2's formed quantum ring is the ring, MoreRed's wire is
|
|
235
|
+
* the cross it draws with nothing connected. Flat icons fall out on their own,
|
|
236
|
+
* since `item/generated` states no elements.
|
|
237
|
+
*
|
|
238
|
+
* Null unless the substitute is whole: a part-magenta stand-in reads as a broken
|
|
239
|
+
* texture, which is worse than the recorded gap it replaces.
|
|
240
|
+
*/
|
|
241
|
+
async function itemModelSubstitute(blockId, resources, models) {
|
|
242
|
+
const block = ResourceLocator.tryParse(blockId);
|
|
243
|
+
if (block === null) return null;
|
|
244
|
+
const item = new ResourceLocator(block.namespace, `item/${block.path}`);
|
|
245
|
+
if (!await resources.hasResource("model", item)) return null;
|
|
246
|
+
let resolved;
|
|
247
|
+
try {
|
|
248
|
+
resolved = await models.load(item.toString());
|
|
249
|
+
} catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
const { emitted, sprites, tintIndices } = bakeModel(item.toString(), resolved, []);
|
|
253
|
+
if (emitted.elements.length === 0) return null;
|
|
254
|
+
if ((await Promise.all([...sprites].map(async (sprite) => sprite !== MISSING_SPRITE && await resources.hasResource("texture", ResourceLocator.parse(sprite))))).includes(false)) return null;
|
|
255
|
+
return {
|
|
256
|
+
id: item.toString(),
|
|
257
|
+
emitted,
|
|
258
|
+
sprites,
|
|
259
|
+
tintIndices
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
/** Bake one flattened model into pack form, reporting every face it loses. */
|
|
263
|
+
function bakeModel(id, model, failures) {
|
|
264
|
+
const sprites = /* @__PURE__ */ new Set();
|
|
265
|
+
const tintIndices = /* @__PURE__ */ new Set();
|
|
266
|
+
const elements = [];
|
|
267
|
+
for (const element of model.elements) {
|
|
268
|
+
const faces = {};
|
|
269
|
+
for (const [name, face] of Object.entries(element.faces)) {
|
|
270
|
+
if (!face || !FACE_DIRECTIONS.has(name)) continue;
|
|
271
|
+
const direction = name;
|
|
272
|
+
const texture = resolveTexture(model, face.texture);
|
|
273
|
+
if (texture === null) failures.push({
|
|
274
|
+
id,
|
|
275
|
+
kind: "texture",
|
|
276
|
+
reason: `unresolved texture ${face.texture} on ${direction}; using the missing-texture sprite`
|
|
277
|
+
});
|
|
278
|
+
const sprite = texture === null ? MISSING_SPRITE : texture.toString();
|
|
279
|
+
sprites.add(sprite);
|
|
280
|
+
const cullRaw = typeof face.cullface === "string" ? face.cullface : null;
|
|
281
|
+
const cullface = cullRaw === null ? void 0 : CULLFACE_ALIASES[cullRaw] ?? (FACE_DIRECTIONS.has(cullRaw) ? cullRaw : void 0);
|
|
282
|
+
if (typeof face.tintindex === "number" && face.tintindex >= 0) tintIndices.add(face.tintindex);
|
|
283
|
+
faces[direction] = {
|
|
284
|
+
sprite,
|
|
285
|
+
uv: face.uv ?? defaultFaceUv(direction, element.from, element.to),
|
|
286
|
+
...face.rotation && { rotation: face.rotation },
|
|
287
|
+
...typeof face.tintindex === "number" && face.tintindex >= 0 && { tintindex: face.tintindex },
|
|
288
|
+
...cullface !== void 0 && { cullface }
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
if (Object.keys(faces).length === 0) continue;
|
|
292
|
+
elements.push({
|
|
293
|
+
from: element.from,
|
|
294
|
+
to: element.to,
|
|
295
|
+
...element.rotation && { rotation: element.rotation },
|
|
296
|
+
...element.shade === false && { shade: false },
|
|
297
|
+
faces
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
const renderType = model.renderType === null ? void 0 : RENDER_TYPES[model.renderType];
|
|
301
|
+
const particle = resolveTexture(model, "#particle") ?? (elements.length === 0 ? firstBoundTexture(model) : null);
|
|
302
|
+
if (particle !== null) sprites.add(particle.toString());
|
|
303
|
+
return {
|
|
304
|
+
emitted: {
|
|
305
|
+
ao: model.ambientOcclusion,
|
|
306
|
+
elements,
|
|
307
|
+
...particle !== null && { particle: particle.toString() },
|
|
308
|
+
...renderType !== void 0 && { renderType },
|
|
309
|
+
...model.splitParts && { multiblock: model.splitParts }
|
|
310
|
+
},
|
|
311
|
+
sprites,
|
|
312
|
+
tintIndices
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
async function exportBlockPack(options) {
|
|
316
|
+
const { output, version } = options;
|
|
317
|
+
const resources = options.builtin ? new LayeredResourceManager([options.resources, new FolderResourceManager(options.builtin)]) : options.resources;
|
|
318
|
+
const namespaces = options.namespaces ?? ["minecraft"];
|
|
319
|
+
const failures = [];
|
|
320
|
+
const report = (stage, done, total) => options.onProgress?.(stage, done, total);
|
|
321
|
+
const textures = new TextureLoader(resources);
|
|
322
|
+
textures.setAtlasResolver(await loadAtlasResolver(resources, (locator) => textures.load(locator)));
|
|
323
|
+
const models = new ModelLoader(resources);
|
|
324
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
325
|
+
const blockModels = /* @__PURE__ */ new Map();
|
|
326
|
+
const modelRefs = /* @__PURE__ */ new Set();
|
|
327
|
+
const blockLocators = [];
|
|
328
|
+
for (const namespace of namespaces) blockLocators.push(...await resources.listResources("blockstate", namespace));
|
|
329
|
+
let done = 0;
|
|
330
|
+
for (const locator of blockLocators) {
|
|
331
|
+
const id = locator.toString();
|
|
332
|
+
report("blockstates", ++done, blockLocators.length);
|
|
333
|
+
const raw = await resources.getResource("blockstate", locator);
|
|
334
|
+
if (raw === null) {
|
|
335
|
+
failures.push({
|
|
336
|
+
id,
|
|
337
|
+
kind: "blockstate",
|
|
338
|
+
reason: "listed but unreadable"
|
|
339
|
+
});
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
const text = raw.toString("utf8");
|
|
344
|
+
let json;
|
|
345
|
+
try {
|
|
346
|
+
json = JSON.parse(text);
|
|
347
|
+
} catch {
|
|
348
|
+
json = salvageJson(text);
|
|
349
|
+
}
|
|
350
|
+
const definition = parseBlockState(locator.namespace, json, id, {
|
|
351
|
+
onCaseError: (reason) => {
|
|
352
|
+
failures.push({
|
|
353
|
+
id,
|
|
354
|
+
kind: "blockstate",
|
|
355
|
+
reason: `${reason}; case dropped`
|
|
356
|
+
});
|
|
357
|
+
},
|
|
358
|
+
onNote: (reason) => {
|
|
359
|
+
failures.push({
|
|
360
|
+
id,
|
|
361
|
+
kind: "blockstate",
|
|
362
|
+
reason
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
const references = collectModels(definition);
|
|
367
|
+
definitions.set(id, definition);
|
|
368
|
+
blockModels.set(id, references);
|
|
369
|
+
for (const model of references) modelRefs.add(model);
|
|
370
|
+
} catch (err) {
|
|
371
|
+
failures.push({
|
|
372
|
+
id,
|
|
373
|
+
kind: "blockstate",
|
|
374
|
+
reason: err.message
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const emittedModels = {};
|
|
379
|
+
const modelTintIndices = /* @__PURE__ */ new Map();
|
|
380
|
+
const spriteIds = /* @__PURE__ */ new Set();
|
|
381
|
+
done = 0;
|
|
382
|
+
for (const reference of [...modelRefs].toSorted((left, right) => left.localeCompare(right))) {
|
|
383
|
+
report("models", ++done, modelRefs.size);
|
|
384
|
+
let resolved;
|
|
385
|
+
try {
|
|
386
|
+
resolved = await models.load(reference);
|
|
387
|
+
} catch (err) {
|
|
388
|
+
failures.push({
|
|
389
|
+
id: reference,
|
|
390
|
+
kind: "model",
|
|
391
|
+
reason: err.message
|
|
392
|
+
});
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
const { emitted, sprites, tintIndices } = bakeModel(reference, resolved, failures);
|
|
396
|
+
if (resolved.splitParts) failures.push({
|
|
397
|
+
id: reference,
|
|
398
|
+
kind: "geometry",
|
|
399
|
+
reason: `split multiblock spanning ${resolved.splitParts.length} block positions; the pack carries the whole mesh, since which slice a position draws lives in the block entity`
|
|
400
|
+
});
|
|
401
|
+
if (resolved.obj) try {
|
|
402
|
+
const mesh = await buildObjMesh(resolved, resources, MISSING_SPRITE);
|
|
403
|
+
if (mesh) {
|
|
404
|
+
emitted.mesh = mesh.groups;
|
|
405
|
+
for (const group of mesh.groups) sprites.add(group.sprite);
|
|
406
|
+
for (const material of mesh.unresolved) failures.push({
|
|
407
|
+
id: reference,
|
|
408
|
+
kind: "texture",
|
|
409
|
+
reason: `OBJ material ${material} resolves to no texture; using the missing-texture sprite`
|
|
410
|
+
});
|
|
411
|
+
if (mesh.conditional && mesh.available.length > 1) failures.push({
|
|
412
|
+
id: reference,
|
|
413
|
+
kind: "geometry",
|
|
414
|
+
reason: `the loader picks between this OBJ's ${mesh.available.length} named objects at runtime (${mesh.available.slice(0, 8).join(", ")}${mesh.available.length > 8 ? ", …" : ""}) and nothing in the assets says which; the pack draws all of them, tagged with their object names`
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
} catch (err) {
|
|
418
|
+
failures.push({
|
|
419
|
+
id: reference,
|
|
420
|
+
kind: "geometry",
|
|
421
|
+
reason: err.message
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
emittedModels[reference] = emitted;
|
|
425
|
+
modelTintIndices.set(reference, tintIndices);
|
|
426
|
+
for (const sprite of sprites) spriteIds.add(sprite);
|
|
427
|
+
}
|
|
428
|
+
for (const [id, references] of blockModels) {
|
|
429
|
+
if (references.some((reference) => {
|
|
430
|
+
const model = emittedModels[reference];
|
|
431
|
+
return model !== void 0 && (model.elements.length > 0 || model.mesh?.length);
|
|
432
|
+
})) continue;
|
|
433
|
+
const invisible = INVISIBLE_BLOCKS[id];
|
|
434
|
+
if (invisible !== void 0) {
|
|
435
|
+
failures.push({
|
|
436
|
+
id,
|
|
437
|
+
kind: "intentional",
|
|
438
|
+
reason: `empty on purpose: ${invisible}`
|
|
439
|
+
});
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
const geometry = await blockEntityGeometry(id, {
|
|
443
|
+
resources,
|
|
444
|
+
textures,
|
|
445
|
+
models
|
|
446
|
+
});
|
|
447
|
+
if (geometry === null) {
|
|
448
|
+
failures.push({
|
|
449
|
+
id,
|
|
450
|
+
kind: "geometry",
|
|
451
|
+
reason: "the game draws this block in code and no geometry is vendored for it"
|
|
452
|
+
});
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
const cases = [];
|
|
456
|
+
const substituted = [];
|
|
457
|
+
for (const shape of geometry.shapes) {
|
|
458
|
+
const textures = {
|
|
459
|
+
...geometry.textures,
|
|
460
|
+
...shape.textures
|
|
461
|
+
};
|
|
462
|
+
if ((await Promise.all(Object.values(textures).map(async (texture) => resources.hasResource("texture", ResourceLocator.parse(texture))))).includes(false)) continue;
|
|
463
|
+
let resolved;
|
|
464
|
+
try {
|
|
465
|
+
resolved = await models.load(shape.model);
|
|
466
|
+
} catch (err) {
|
|
467
|
+
failures.push({
|
|
468
|
+
id,
|
|
469
|
+
kind: "geometry",
|
|
470
|
+
reason: `substitute ${shape.model} did not load: ${err.message}`
|
|
471
|
+
});
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
const bound = {
|
|
475
|
+
...resolved,
|
|
476
|
+
textures: {
|
|
477
|
+
...resolved.textures,
|
|
478
|
+
...textures
|
|
479
|
+
},
|
|
480
|
+
elements: translateElements(resolved.elements, shape.translate)
|
|
481
|
+
};
|
|
482
|
+
const suffix = Object.values(shape.when ?? {}).join("_");
|
|
483
|
+
const substituteId = `rendermite:block/${ResourceLocator.parse(id).path}${suffix && `_${suffix}`}`;
|
|
484
|
+
const { emitted, sprites, tintIndices } = bakeModel(substituteId, bound, failures);
|
|
485
|
+
if (emitted.elements.length === 0) continue;
|
|
486
|
+
if ((await Promise.all([...sprites].map(async (sprite) => sprite !== MISSING_SPRITE && await resources.hasResource("texture", ResourceLocator.parse(sprite))))).includes(false)) {
|
|
487
|
+
failures.push({
|
|
488
|
+
id,
|
|
489
|
+
kind: "geometry",
|
|
490
|
+
reason: `substitute ${shape.model} left textures unresolved, so the block is left empty rather than part magenta`
|
|
491
|
+
});
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
const still = emitted.elements[0].faces.up?.sprite;
|
|
495
|
+
const flow = emitted.elements[0].faces.north?.sprite;
|
|
496
|
+
if (geometry.fluid !== void 0 && still !== void 0 && flow !== void 0) emitted.fluid = {
|
|
497
|
+
still,
|
|
498
|
+
flow,
|
|
499
|
+
...geometry.fluid
|
|
500
|
+
};
|
|
501
|
+
emittedModels[substituteId] = emitted;
|
|
502
|
+
modelTintIndices.set(substituteId, tintIndices);
|
|
503
|
+
substituted.push(substituteId);
|
|
504
|
+
for (const sprite of sprites) spriteIds.add(sprite);
|
|
505
|
+
for (const variant of synthesizeVariants(geometry.orient)) cases.push({
|
|
506
|
+
when: {
|
|
507
|
+
...shape.when,
|
|
508
|
+
...variant.when
|
|
509
|
+
},
|
|
510
|
+
apply: [{
|
|
511
|
+
model: substituteId,
|
|
512
|
+
x: variant.x,
|
|
513
|
+
y: variant.y,
|
|
514
|
+
z: 0,
|
|
515
|
+
uvlock: false,
|
|
516
|
+
weight: 1
|
|
517
|
+
}]
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
if (cases.length === 0) {
|
|
521
|
+
failures.push({
|
|
522
|
+
id,
|
|
523
|
+
kind: "geometry",
|
|
524
|
+
reason: `the game draws this block in code; vendored ${geometry.shapes.map((shape) => shape.model).join(", ")} could not be bound to it`
|
|
525
|
+
});
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
definitions.set(id, {
|
|
529
|
+
kind: "variants",
|
|
530
|
+
cases
|
|
531
|
+
});
|
|
532
|
+
blockModels.set(id, substituted);
|
|
533
|
+
failures.push({
|
|
534
|
+
id,
|
|
535
|
+
kind: "geometry",
|
|
536
|
+
reason: `the game draws this block in code; the pack substitutes vendored geometry from ${geometry.shapes.map((shape) => shape.model).join(", ")} and synthesises the orientation variants the blockstate does not carry`
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
const adoptSubstitute = (substitute) => {
|
|
540
|
+
emittedModels[substitute.id] = substitute.emitted;
|
|
541
|
+
modelTintIndices.set(substitute.id, substitute.tintIndices);
|
|
542
|
+
for (const sprite of substitute.sprites) spriteIds.add(sprite);
|
|
543
|
+
return substitute.id;
|
|
544
|
+
};
|
|
545
|
+
for (const [id, references] of blockModels) {
|
|
546
|
+
if (INVISIBLE_BLOCKS[id] !== void 0) continue;
|
|
547
|
+
const empty = references.filter((reference) => {
|
|
548
|
+
const model = emittedModels[reference];
|
|
549
|
+
return model === void 0 || model.elements.length === 0 && !model.mesh?.length;
|
|
550
|
+
});
|
|
551
|
+
if (empty.length === 0) continue;
|
|
552
|
+
const substitute = await itemModelSubstitute(id, resources, models);
|
|
553
|
+
if (substitute === null) continue;
|
|
554
|
+
const stood = adoptSubstitute(substitute);
|
|
555
|
+
for (const part of definitions.get(id)?.cases ?? []) for (const variant of part.apply) if (empty.includes(variant.model)) variant.model = stood;
|
|
556
|
+
blockModels.set(id, [...references.filter((reference) => !empty.includes(reference)), stood]);
|
|
557
|
+
const drawnInCode = empty.length === references.length ? "this block" : empty.join(", ");
|
|
558
|
+
failures.push({
|
|
559
|
+
id,
|
|
560
|
+
kind: "geometry",
|
|
561
|
+
reason: `the game draws ${drawnInCode} in code; the pack substitutes the block's own item model ${stood}, which carries the shape but not the orientation or connections`
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
const blockStates = {};
|
|
565
|
+
for (const [id, definition] of definitions) {
|
|
566
|
+
const serialized = serializeBlockState(definition, (model) => emittedModels[model] !== void 0);
|
|
567
|
+
if (serialized === null) {
|
|
568
|
+
failures.push({
|
|
569
|
+
id,
|
|
570
|
+
kind: "blockstate",
|
|
571
|
+
reason: "every case named a model that could not be exported"
|
|
572
|
+
});
|
|
573
|
+
blockModels.delete(id);
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
blockStates[id] = serialized;
|
|
577
|
+
}
|
|
578
|
+
for (const locator of blockLocators) {
|
|
579
|
+
const id = locator.toString();
|
|
580
|
+
if (blockStates[id] !== void 0 || INVISIBLE_BLOCKS[id] !== void 0) continue;
|
|
581
|
+
const substitute = await itemModelSubstitute(id, resources, models);
|
|
582
|
+
if (substitute === null) continue;
|
|
583
|
+
const stood = adoptSubstitute(substitute);
|
|
584
|
+
blockStates[id] = { variants: [{
|
|
585
|
+
when: {},
|
|
586
|
+
apply: [{ model: stood }]
|
|
587
|
+
}] };
|
|
588
|
+
blockModels.set(id, [stood]);
|
|
589
|
+
failures.push({
|
|
590
|
+
id,
|
|
591
|
+
kind: "geometry",
|
|
592
|
+
reason: `no case of this blockstate could be read; the pack substitutes the block's own item model ${stood}, which carries the shape but not the orientation`
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
for (const namespace of namespaces) for (const locator of await resources.listResources("texture", namespace)) if (FLUID_SUFFIX.test(locator.path)) spriteIds.add(locator.toString());
|
|
596
|
+
for (const extra of EXTRA_FLUID_SPRITES) if (await resources.hasResource("texture", ResourceLocator.parse(extra))) spriteIds.add(extra);
|
|
597
|
+
const loaded = [];
|
|
598
|
+
const missingSprites = /* @__PURE__ */ new Set();
|
|
599
|
+
done = 0;
|
|
600
|
+
for (const id of [...spriteIds].toSorted((left, right) => left.localeCompare(right))) {
|
|
601
|
+
report("sprites", ++done, spriteIds.size);
|
|
602
|
+
if (id === MISSING_SPRITE) continue;
|
|
603
|
+
let animated;
|
|
604
|
+
try {
|
|
605
|
+
animated = await textures.loadAnimated(ResourceLocator.parse(id));
|
|
606
|
+
} catch (err) {
|
|
607
|
+
failures.push({
|
|
608
|
+
id,
|
|
609
|
+
kind: "sprite",
|
|
610
|
+
reason: err.message
|
|
611
|
+
});
|
|
612
|
+
missingSprites.add(id);
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
if (!animated) {
|
|
616
|
+
failures.push({
|
|
617
|
+
id,
|
|
618
|
+
kind: "sprite",
|
|
619
|
+
reason: "texture not found"
|
|
620
|
+
});
|
|
621
|
+
missingSprites.add(id);
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
const overlong = animated.frameCount > MAX_FRAMES_PER_SPRITE;
|
|
625
|
+
if (overlong) failures.push({
|
|
626
|
+
id,
|
|
627
|
+
kind: "sprite",
|
|
628
|
+
reason: `${animated.frameCount} frames exceeds the ${MAX_FRAMES_PER_SPRITE} cap; exported static`
|
|
629
|
+
});
|
|
630
|
+
const starts = overlong ? [0] : animated.frameStartTicks();
|
|
631
|
+
const total = overlong ? 1 : animated.tickLength;
|
|
632
|
+
const sequence = starts.map((start, index) => {
|
|
633
|
+
return {
|
|
634
|
+
frame: animated.resolveFrame(start).frame,
|
|
635
|
+
time: (index + 1 < starts.length ? starts[index + 1] : total) - start
|
|
636
|
+
};
|
|
637
|
+
});
|
|
638
|
+
const frames = /* @__PURE__ */ new Map();
|
|
639
|
+
for (const entry of sequence) if (!frames.has(entry.frame)) frames.set(entry.frame, animated.frame(entry.frame));
|
|
640
|
+
loaded.push({
|
|
641
|
+
id,
|
|
642
|
+
frames,
|
|
643
|
+
sequence,
|
|
644
|
+
interpolate: animated.interpolated
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
spriteIds.add(MISSING_SPRITE);
|
|
648
|
+
loaded.push({
|
|
649
|
+
id: MISSING_SPRITE,
|
|
650
|
+
frames: /* @__PURE__ */ new Map([[0, missingTexture()]]),
|
|
651
|
+
sequence: [{
|
|
652
|
+
frame: 0,
|
|
653
|
+
time: 1
|
|
654
|
+
}],
|
|
655
|
+
interpolate: false
|
|
656
|
+
});
|
|
657
|
+
const packed = packAtlas(loaded.flatMap((sprite) => [...sprite.frames].map(([index, texture]) => {
|
|
658
|
+
return {
|
|
659
|
+
key: `${sprite.id}#${index}`,
|
|
660
|
+
texture
|
|
661
|
+
};
|
|
662
|
+
})), {
|
|
663
|
+
padding: options.padding,
|
|
664
|
+
maxPageSize: options.maxPageSize
|
|
665
|
+
});
|
|
666
|
+
for (const key of packed.oversized) {
|
|
667
|
+
const id = key.split("#", 1)[0];
|
|
668
|
+
if (!missingSprites.has(id)) {
|
|
669
|
+
failures.push({
|
|
670
|
+
id,
|
|
671
|
+
kind: "sprite",
|
|
672
|
+
reason: "sprite is larger than one atlas page; drawn as the missing texture"
|
|
673
|
+
});
|
|
674
|
+
missingSprites.add(id);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (missingSprites.has(MISSING_SPRITE)) throw new Error("the missing-texture sprite does not fit an atlas page; maxPageSize is too small");
|
|
678
|
+
if (missingSprites.size > 0) for (const model of Object.values(emittedModels)) {
|
|
679
|
+
for (const element of model.elements) for (const face of Object.values(element.faces)) if (missingSprites.has(face.sprite)) face.sprite = MISSING_SPRITE;
|
|
680
|
+
for (const group of model.mesh ?? []) if (missingSprites.has(group.sprite)) group.sprite = MISSING_SPRITE;
|
|
681
|
+
if (model.particle !== void 0 && missingSprites.has(model.particle)) model.particle = MISSING_SPRITE;
|
|
682
|
+
}
|
|
683
|
+
for (const [id, model] of Object.entries(emittedModels)) {
|
|
684
|
+
const sprites = [...model.elements.flatMap((element) => Object.values(element.faces).map((face) => face.sprite)), ...(model.mesh ?? []).map((group) => group.sprite)];
|
|
685
|
+
if (sprites.length > 0 && sprites.every((sprite) => sprite === MISSING_SPRITE)) failures.push({
|
|
686
|
+
id,
|
|
687
|
+
kind: "texture",
|
|
688
|
+
reason: "every face uses the missing-texture sprite; the model binds none of the textures it names"
|
|
689
|
+
});
|
|
690
|
+
if (model.elements.length === 0 && !model.mesh?.length) failures.push({
|
|
691
|
+
id,
|
|
692
|
+
kind: "geometry",
|
|
693
|
+
reason: "model has no drawable elements"
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
const rects = new Map(packed.rects.map((rect) => [rect.key, rect]));
|
|
697
|
+
const emittedSprites = {};
|
|
698
|
+
for (const sprite of loaded) {
|
|
699
|
+
const first = rects.get(`${sprite.id}#${sprite.sequence[0].frame}`);
|
|
700
|
+
if (!first) continue;
|
|
701
|
+
const frames = [];
|
|
702
|
+
for (const entry of sprite.sequence) {
|
|
703
|
+
const rect = rects.get(`${sprite.id}#${entry.frame}`);
|
|
704
|
+
if (rect) frames.push({
|
|
705
|
+
page: rect.page,
|
|
706
|
+
x: rect.x,
|
|
707
|
+
y: rect.y,
|
|
708
|
+
time: entry.time
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
emittedSprites[sprite.id] = {
|
|
712
|
+
page: first.page,
|
|
713
|
+
x: first.x,
|
|
714
|
+
y: first.y,
|
|
715
|
+
w: first.width,
|
|
716
|
+
h: first.height,
|
|
717
|
+
layer: inferRenderLayer(sprite.frames.values().toArray()),
|
|
718
|
+
...frames.length === sprite.sequence.length && frames.length > 1 && { frames },
|
|
719
|
+
...sprite.interpolate && { interpolate: true }
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
const tints = await buildBlockTints(resources, textures);
|
|
723
|
+
for (const [block, references] of blockModels) {
|
|
724
|
+
const used = /* @__PURE__ */ new Set();
|
|
725
|
+
for (const reference of references) for (const index of modelTintIndices.get(reference) ?? []) used.add(index);
|
|
726
|
+
for (const index of used) if (tints.pack.blocks[block]?.[String(index)] === void 0) failures.push({
|
|
727
|
+
id: block,
|
|
728
|
+
kind: "tint",
|
|
729
|
+
reason: `tintindex ${index} is not in the tint table; exported untinted`
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
await mkdir(output, { recursive: true });
|
|
733
|
+
const hashes = {};
|
|
734
|
+
let writtenFiles = 0;
|
|
735
|
+
const emit = async (file, data) => {
|
|
736
|
+
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
737
|
+
const target = path.join(output, file);
|
|
738
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
739
|
+
await writeFile(target, buffer);
|
|
740
|
+
hashes[file] = createHash("sha256").update(buffer).digest("hex");
|
|
741
|
+
writtenFiles++;
|
|
742
|
+
};
|
|
743
|
+
const pageFiles = [];
|
|
744
|
+
for (const [index, page] of packed.pages.entries()) {
|
|
745
|
+
const file = `atlas-${index}.png`;
|
|
746
|
+
const raw = Buffer.from(page.data.buffer, page.data.byteOffset, page.data.byteLength);
|
|
747
|
+
await emit(file, await sharp(raw, { raw: {
|
|
748
|
+
width: page.width,
|
|
749
|
+
height: page.height,
|
|
750
|
+
channels: 4
|
|
751
|
+
} }).png({ compressionLevel: 9 }).toBuffer());
|
|
752
|
+
pageFiles.push({
|
|
753
|
+
file,
|
|
754
|
+
width: page.width,
|
|
755
|
+
height: page.height
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
for (const file of tints.files) await emit(file.path, file.data);
|
|
759
|
+
await emit("atlas.json", JSON.stringify({
|
|
760
|
+
padding: packed.padding,
|
|
761
|
+
pages: pageFiles,
|
|
762
|
+
sprites: sortedByKey(emittedSprites)
|
|
763
|
+
}));
|
|
764
|
+
await emit("models.json", JSON.stringify(sortedByKey(emittedModels)));
|
|
765
|
+
await emit("blockstates.json", JSON.stringify(sortedByKey(blockStates)));
|
|
766
|
+
await emit("tints.json", JSON.stringify(tints.pack));
|
|
767
|
+
failures.sort((left, right) => left.id.localeCompare(right.id) || left.kind.localeCompare(right.kind));
|
|
768
|
+
await emit("failures.json", JSON.stringify(failures, null, " "));
|
|
769
|
+
const counts = {
|
|
770
|
+
blocks: Object.keys(blockStates).length,
|
|
771
|
+
models: Object.keys(emittedModels).length,
|
|
772
|
+
sprites: Object.keys(emittedSprites).length,
|
|
773
|
+
pages: packed.pages.length,
|
|
774
|
+
failures: failures.length
|
|
775
|
+
};
|
|
776
|
+
await emit("meta.json", JSON.stringify({
|
|
777
|
+
generator: "rendermite",
|
|
778
|
+
version,
|
|
779
|
+
namespaces,
|
|
780
|
+
counts,
|
|
781
|
+
hashes: sortedByKey(hashes)
|
|
782
|
+
}, null, " "));
|
|
783
|
+
return {
|
|
784
|
+
...counts,
|
|
785
|
+
writtenFiles,
|
|
786
|
+
failures
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
//#endregion
|
|
790
|
+
export { exportBlockPack as t };
|