mc-assets 0.2.34 → 0.2.36

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 CHANGED
@@ -28,7 +28,7 @@ For contributing & building instructions see [building](#building) section.
28
28
  > Tested on Node.js 18 and above.
29
29
 
30
30
 
31
- All blockstates + models + all atlas textures for all versions bundled with rsbuild (uncompressed): 5.02 MB.
31
+ All blockstates + models + all atlas textures for all versions bundled with rsbuild (uncompressed): 5.15 MB.
32
32
 
33
33
  This packages includes versions for: 1.7.10, 1.8, 1.8.1, 1.8.2, 1.8.3, 1.8.4, 1.8.5, 1.8.6, 1.8.7, 1.8.8, 1.8.9, 1.9, 1.9.1, 1.9.2, 1.9.3, 1.9.4, 1.10, 1.10.1, 1.10.2, 1.11, 1.11.1, 1.11.2, 1.12, 1.12.1, 1.12.2, 1.13, 1.13.1, 1.13.2, 1.14, 1.14.1, 1.14.2, 1.14.3, 1.14.4, 1.15, 1.15.1, 1.15.2, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4, 1.16.5, 1.17, 1.17.1, 1.18, 1.18.1, 1.18.2, 1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4, 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21, 1.21.1, 1.21.2, 1.21.3, 1.21.4.
34
34
 
@@ -18,6 +18,9 @@ export declare class AssetsParser {
18
18
  parseProperties(properties: string): Record<string, string | boolean>;
19
19
  getElements(queriedBlock: Omit<QueriedBlock, 'stateId'>, fallbackVariant?: boolean): 0 | 1 | BlockElement[];
20
20
  private resolvedModel;
21
+ issues: string[];
22
+ matchedModels: string[];
23
+ matchedConditions: string[];
21
24
  private getModelsByBlock;
22
25
  getResolvedModelsByModel(model: string, debugQueryName?: string, clearModel?: boolean): {
23
26
  resolvedModel: Pick<BlockModel, "textures" | "ao" | "elements"> & {
@@ -1,3 +1,11 @@
1
+ const getNamespace = (name) => {
2
+ const parts = name.split(':');
3
+ if (parts.length === 1)
4
+ return 'minecraft';
5
+ if (parts.length === 2)
6
+ return parts[0];
7
+ return parts.slice(0, -1).join(':');
8
+ };
1
9
  export class AssetsParser {
2
10
  version;
3
11
  blockStatesStore;
@@ -43,7 +51,12 @@ export class AssetsParser {
43
51
  }
44
52
  // looks like workaround
45
53
  resolvedModel = {};
54
+ issues = [];
55
+ matchedModels = [];
56
+ matchedConditions = [];
46
57
  getModelsByBlock(queriedBlock, fallbackVariant, multiOptim) {
58
+ this.matchedModels = [];
59
+ this.matchedConditions = [];
47
60
  const matchProperties = (block, /* to match against */ properties) => {
48
61
  if (!properties) {
49
62
  return true;
@@ -69,9 +82,12 @@ export class AssetsParser {
69
82
  };
70
83
  let applyModels = [];
71
84
  const blockStates = this.blockStatesStore.get(this.version, queriedBlock.name);
72
- if (!blockStates)
85
+ if (!blockStates) {
86
+ this.issues.push(`Block ${queriedBlock.name} not found in all registered blockstates. Place it into assets/${getNamespace(queriedBlock.name)}/blockstates/${queriedBlock.name}.json`);
73
87
  return;
88
+ }
74
89
  const states = blockStates.variants;
90
+ let stateMatched = false;
75
91
  if (states) {
76
92
  let state = states[''] || states['normal'];
77
93
  for (const key in states) {
@@ -82,9 +98,15 @@ export class AssetsParser {
82
98
  break;
83
99
  }
84
100
  }
101
+ if (state) {
102
+ const matchedStateName = Object.entries(states).find(([key]) => state === states[key])?.[0];
103
+ this.matchedConditions.push(`variant:${matchedStateName}`);
104
+ }
85
105
  if (!state) {
86
106
  if (fallbackVariant) {
87
- state = states[Object.keys(states)[0]];
107
+ const firstKey = Object.keys(states)[0];
108
+ state = states[firstKey];
109
+ this.matchedConditions.push(`fallback:${firstKey}`);
88
110
  }
89
111
  else {
90
112
  return;
@@ -92,12 +114,14 @@ export class AssetsParser {
92
114
  }
93
115
  if (state) {
94
116
  applyModels.push(state);
117
+ stateMatched = true;
95
118
  }
96
119
  }
97
120
  if (blockStates.multipart) {
98
121
  for (const { when, apply } of blockStates.multipart) {
99
122
  if (!when || matchProperties(queriedBlock, when)) {
100
123
  applyModels.push(apply);
124
+ this.matchedConditions.push(when ? `multipart:${JSON.stringify(when)}` : 'multipart:always');
101
125
  }
102
126
  }
103
127
  if (!applyModels.length && fallbackVariant) {
@@ -105,11 +129,22 @@ export class AssetsParser {
105
129
  const apply = multipartWithWhen[0]?.apply;
106
130
  if (apply) {
107
131
  applyModels.push(apply);
132
+ this.matchedConditions.push('multipart:fallback');
108
133
  }
109
134
  }
110
135
  }
111
- if (!applyModels.length)
136
+ if (!applyModels.length) {
137
+ if (!stateMatched) {
138
+ const blockstatesCount = Object.keys(states ?? {}).length;
139
+ if (blockstatesCount) {
140
+ this.issues.push(`Block did not match any possible state (${blockstatesCount} possible states)`);
141
+ }
142
+ else {
143
+ this.issues.push(`Blockstates for ${queriedBlock.name} are not defined`);
144
+ }
145
+ }
112
146
  return;
147
+ }
113
148
  const modelsResolved = [];
114
149
  let part = 0;
115
150
  for (const model of applyModels) {
@@ -141,8 +176,11 @@ export class AssetsParser {
141
176
  this.resolvedModel = {};
142
177
  }
143
178
  const modelData = this.blockModelsStore.get(this.version, model);
144
- if (!modelData)
179
+ if (!modelData) {
180
+ this.issues.push(`Model ${model} not found. Ensure it is present in assets/${getNamespace(model)}/models/${model}.json`);
145
181
  return;
182
+ }
183
+ this.matchedModels.push(model);
146
184
  return this.getResolvedModelsByModelData(modelData, debugQueryName, clearModel);
147
185
  }
148
186
  getResolvedModelsByModelData(modelData, debugQueryName, clearModel = true) {
@@ -154,8 +192,11 @@ export class AssetsParser {
154
192
  collectedParentModels.push(model);
155
193
  if (model.parent) {
156
194
  const parent = this.blockModelsStore.get(this.version, model.parent);
157
- if (!parent)
195
+ if (!parent) {
196
+ this.issues.push(`Parent model ${model.parent} not found for ${debugQueryName}`);
158
197
  return;
198
+ }
199
+ this.matchedModels.push(`parent:${model.parent}`);
159
200
  collectModels(parent);
160
201
  }
161
202
  };
@@ -192,14 +233,12 @@ export class AssetsParser {
192
233
  originalTexturePath = originalTexturePath.split('/').at(-1).replace('#', '');
193
234
  this.resolvedModel.textures ??= {};
194
235
  if (chain.includes(originalTexturePath)) {
195
- console.warn(`${debugQueryName}: Circular texture reference detected: ${chain.join(' -> ')}`);
236
+ this.issues.push(`${debugQueryName}: Circular texture reference detected: ${chain.join(' -> ')}`);
196
237
  return;
197
238
  }
198
239
  const existingKey = this.resolvedModel.textures[originalTexturePath];
199
240
  if (!existingKey) {
200
- // todo this also needs to be done at the validation stage
201
- // throw new Error(`Cannot resolve texture ${key} to ${value} because it is not defined`)
202
- console.warn(`${debugQueryName}: Cannot resolve texture ${originalTexturePath} for ${_originalKey} because it is not defined`);
241
+ this.issues.push(`${debugQueryName}: Cannot resolve texture ${originalTexturePath} for ${_originalKey} because it is not defined`);
203
242
  return;
204
243
  }
205
244
  else {
@@ -1,3 +1,4 @@
1
+ export declare const MAX_CANVAS_SIZE = 16384;
1
2
  export declare const getAtlasSize: (numberOfTiles: number, tileSize: number) => {
2
3
  width: number;
3
4
  height: number;
@@ -1,3 +1,5 @@
1
+ import { createAtlas } from 'apl-image-packer';
2
+ export const MAX_CANVAS_SIZE = 16_384;
1
3
  function nextPowerOfTwo(n) {
2
4
  if (n === 0)
3
5
  return 1;
@@ -17,30 +19,8 @@ export const getAtlasSize = (numberOfTiles, tileSize) => {
17
19
  };
18
20
  };
19
21
  export const makeTextureAtlas = ({ input, getLoadedImage, tileSize = 16, getCanvas = (imgSize) => typeof document !== 'undefined' && document.createElement ? document.createElement('canvas') : new globalThis.Canvas(imgSize, imgSize, 'png'), }) => {
20
- const tilesCount = input.length;
21
- const imgSize = getAtlasSize(tilesCount, tileSize).width;
22
- const MAX_CANVAS_SIZE = 16_384;
23
- if (imgSize > MAX_CANVAS_SIZE) {
24
- throw new Error(`Image resolution ${imgSize} is too big, max is ${MAX_CANVAS_SIZE}x${MAX_CANVAS_SIZE}`);
25
- }
26
- const canvas = getCanvas(imgSize);
27
- canvas.width = imgSize;
28
- canvas.height = imgSize;
29
- const g = canvas.getContext('2d');
30
- g.imageSmoothingEnabled = false;
31
- const texturesIndex = {};
32
- let nextX = 0;
33
- let nextY = 0;
34
- let rowMaxY = 0;
35
- const goToNextRow = () => {
36
- nextX = 0;
37
- nextY += Math.ceil(rowMaxY / tileSize) * tileSize;
38
- rowMaxY = 0;
39
- };
40
- const suSv = tileSize / imgSize;
41
- const tilesPerRow = Math.ceil(imgSize / tileSize);
42
- for (const i in input) {
43
- const keyValue = input[i];
22
+ // Pre-calculate all texture dimensions and prepare images
23
+ const texturesWithDimensions = input.map(keyValue => {
44
24
  const inputData = getLoadedImage(keyValue);
45
25
  let img;
46
26
  if (inputData.image) {
@@ -53,57 +33,76 @@ export const makeTextureAtlas = ({ input, getLoadedImage, tileSize = 16, getCanv
53
33
  else {
54
34
  throw new Error('No image or contents');
55
35
  }
56
- let su = suSv;
57
- let sv = suSv;
58
36
  let renderWidth = tileSize * (inputData.tileWidthMult ?? 1);
59
37
  let renderHeight = tileSize;
60
38
  if (inputData.useOriginalSize || inputData.renderWidth || inputData.renderHeight) {
61
39
  const texWidth = inputData.renderWidth ?? img.width;
62
40
  const texHeight = inputData.renderHeight ?? img.height;
63
- // todo check have enough space
64
41
  renderWidth = Math.ceil(texWidth / tileSize) * tileSize;
65
42
  renderHeight = Math.ceil(texHeight / tileSize) * tileSize;
66
- su = texWidth / imgSize;
67
- sv = texHeight / imgSize;
68
- // renderWidth and renderHeight take full tile size so everything is aligned to the grid
69
- if (renderHeight > imgSize || renderWidth > imgSize) {
70
- throw new Error('Texture ' + keyValue + ' is too big');
71
- }
72
- }
73
- if (nextX + renderWidth > imgSize) {
74
- goToNextRow();
75
43
  }
76
- const x = nextX;
77
- const y = nextY;
78
- const yIndex = y / tileSize;
79
- const xIndex = x / tileSize;
44
+ return {
45
+ keyValue,
46
+ img,
47
+ inputData,
48
+ renderWidth,
49
+ renderHeight,
50
+ renderSourceWidth: inputData.useOriginalSize ? img.width : inputData.renderSourceWidth ?? Math.min(img.width, img.height),
51
+ renderSourceHeight: inputData.useOriginalSize ? img.height : inputData.renderSourceHeight ?? Math.min(img.width, img.height),
52
+ renderSourceStartX: inputData.renderSourceStartX ?? 0,
53
+ renderSourceStartY: inputData.renderSourceStartY ?? 0,
54
+ };
55
+ });
56
+ // Use apl-image-packer to calculate optimal positions
57
+ const atlas = createAtlas(texturesWithDimensions.map(tex => ({
58
+ width: tex.renderWidth,
59
+ height: tex.renderHeight,
60
+ data: tex // Store all texture data for later use
61
+ })));
62
+ // Round up atlas size to power of 2
63
+ const imgSize = Math.max(nextPowerOfTwo(atlas.width), nextPowerOfTwo(atlas.height));
64
+ if (imgSize > MAX_CANVAS_SIZE) {
65
+ const sizeGroups = texturesWithDimensions.reduce((acc, t) => {
66
+ const key = `${t.renderWidth}x${t.renderHeight}`;
67
+ acc[key] = (acc[key] || 0) + 1;
68
+ return acc;
69
+ }, {});
70
+ const sizeGroupsStr = Object.entries(sizeGroups)
71
+ .sort(([, a], [, b]) => b - a)
72
+ .map(([size, count]) => `${size}(${count})`)
73
+ .join(', ');
74
+ throw new Error(`Required atlas size ${imgSize} exceeds maximum ${MAX_CANVAS_SIZE}. Texture sizes: ${sizeGroupsStr}`);
75
+ }
76
+ const canvas = getCanvas(imgSize);
77
+ canvas.width = imgSize;
78
+ canvas.height = imgSize;
79
+ const g = canvas.getContext('2d');
80
+ g.imageSmoothingEnabled = false;
81
+ const texturesIndex = {};
82
+ const suSv = tileSize / imgSize;
83
+ const tilesPerRow = Math.ceil(imgSize / tileSize);
84
+ // Draw textures at their calculated positions
85
+ for (const coord of atlas.coords) {
86
+ const tex = coord.img.data;
87
+ const x = coord.x;
88
+ const y = coord.y;
89
+ const yIndex = Math.floor(y / tileSize);
90
+ const xIndex = Math.floor(x / tileSize);
80
91
  const tileIndex = yIndex * tilesPerRow + xIndex;
81
- nextX += renderWidth;
82
- rowMaxY = Math.max(rowMaxY, renderHeight);
83
- if (nextX >= imgSize) {
84
- goToNextRow();
85
- }
86
- const renderSourceDefaultSize = Math.min(img.width, img.height);
87
- const renderSourceWidth = inputData.useOriginalSize ? img.width : inputData.renderSourceWidth ?? renderSourceDefaultSize;
88
- const renderSourceHeight = inputData.useOriginalSize ? img.height : inputData.renderSourceHeight ?? renderSourceDefaultSize;
89
92
  try {
90
- g.drawImage(img, inputData.renderSourceStartX ?? 0, inputData.renderSourceStartY ?? 0, renderSourceWidth, renderSourceHeight, x, y, renderWidth, renderHeight);
93
+ g.drawImage(tex.img, tex.renderSourceStartX, tex.renderSourceStartY, tex.renderSourceWidth, tex.renderSourceHeight, x, y, tex.renderWidth, tex.renderHeight);
91
94
  }
92
95
  catch (err) {
93
- throw new Error(`Error drawing ${keyValue}: ${err}`);
96
+ throw new Error(`Error drawing ${tex.keyValue}: ${err}`);
94
97
  }
95
- // remove the extension eg .png
96
- const cleanName = keyValue.split('.').slice(0, -1).join('.') || keyValue;
98
+ const cleanName = tex.keyValue.split('.').slice(0, -1).join('.') || tex.keyValue;
99
+ const su = tex.renderWidth / imgSize;
100
+ const sv = tex.renderHeight / imgSize;
97
101
  texturesIndex[cleanName] = {
98
102
  u: x / imgSize,
99
103
  v: y / imgSize,
100
104
  tileIndex,
101
- ...su == suSv && sv == suSv ? {} : {
102
- su,
103
- sv,
104
- // width: renderWidth,
105
- // height: renderHeight
106
- }
105
+ ...su == suSv && sv == suSv ? {} : { su, sv }
107
106
  };
108
107
  }
109
108
  return {
@@ -49,5 +49,6 @@ export declare class AtlasParser {
49
49
  readonly newAtlasParser: AtlasParser;
50
50
  readonly image: string;
51
51
  }>;
52
+ createDebugImage(writeNames?: boolean): Promise<string>;
52
53
  }
53
54
  export {};
@@ -1,5 +1,5 @@
1
1
  import { VersionedStore } from './versionedStore';
2
- import { makeTextureAtlas } from './atlasCreator';
2
+ import { makeTextureAtlas, MAX_CANVAS_SIZE } from './atlasCreator';
3
3
  import { getLoadedImage } from './utils';
4
4
  export class AtlasParser {
5
5
  atlasJson;
@@ -122,4 +122,110 @@ export class AtlasParser {
122
122
  }
123
123
  };
124
124
  }
125
+ async createDebugImage(writeNames = false) {
126
+ const atlas = this.atlas.latest;
127
+ if (atlas.width !== atlas.height) {
128
+ throw new Error('Atlas must be square');
129
+ }
130
+ const wantedSize = Math.min(MAX_CANVAS_SIZE, atlas.width * (writeNames ? 6 : 1));
131
+ const scale = wantedSize / atlas.width;
132
+ const height = atlas.height * scale;
133
+ const width = atlas.width * scale;
134
+ const canvas = globalThis.Canvas ? new globalThis.Canvas(width, height) : document.createElement('canvas');
135
+ canvas.width = width;
136
+ canvas.height = height;
137
+ const ctx = canvas.getContext('2d');
138
+ // Disable image smoothing for pixelated rendering
139
+ ctx.imageSmoothingEnabled = false;
140
+ // For older browsers
141
+ //@ts-ignore
142
+ ctx.webkitImageSmoothingEnabled = false;
143
+ //@ts-ignore
144
+ ctx.mozImageSmoothingEnabled = false;
145
+ //@ts-ignore
146
+ ctx.msImageSmoothingEnabled = false;
147
+ // Draw the base atlas image
148
+ const img = await getLoadedImage(this.latestImage);
149
+ ctx.drawImage(img, 0, 0, atlas.width, atlas.height, 0, 0, width, height);
150
+ // Draw debug rectangles for each texture
151
+ ctx.strokeStyle = '#ff0000';
152
+ ctx.lineWidth = 2;
153
+ const textureNames = Object.keys(atlas.textures);
154
+ const totalTextures = textureNames.length;
155
+ let lastProgress = 0;
156
+ textureNames.forEach((textureName, i) => {
157
+ const texture = atlas.textures[textureName];
158
+ // Log progress every 10%
159
+ const progress = Math.floor((i / totalTextures) * 100);
160
+ if (progress >= lastProgress + 10) {
161
+ console.log(`Processing textures: ${progress}% (${i}/${totalTextures})`);
162
+ lastProgress = progress;
163
+ }
164
+ const x = texture.u * atlas.width * scale;
165
+ const y = texture.v * atlas.height * scale;
166
+ const width = (texture.su || atlas.suSv) * atlas.width * scale;
167
+ const height = (texture.sv || atlas.suSv) * atlas.height * scale;
168
+ // Create striped pattern
169
+ const pattern = ctx.createPattern((() => {
170
+ const patternCanvas = globalThis.Canvas ? new globalThis.Canvas(10, 10) : document.createElement('canvas');
171
+ patternCanvas.width = 10;
172
+ patternCanvas.height = 10;
173
+ const patternCtx = patternCanvas.getContext('2d');
174
+ patternCtx.fillStyle = '#ff0000';
175
+ patternCtx.fillRect(0, 0, 5, 10);
176
+ patternCtx.fillStyle = '#ffff00';
177
+ patternCtx.fillRect(5, 0, 5, 10);
178
+ return patternCanvas;
179
+ })(), 'repeat');
180
+ ctx.strokeStyle = pattern;
181
+ ctx.strokeRect(x, y, width, height);
182
+ if (writeNames) {
183
+ // Configure text style
184
+ const text = textureName;
185
+ const padding = 4;
186
+ const maxWidth = width - padding * 2;
187
+ // Start with a relatively large font size and decrease until text fits
188
+ let fontSize = 12;
189
+ do {
190
+ ctx.font = `${fontSize}px monospace`;
191
+ const metrics = ctx.measureText(text);
192
+ if (metrics.width <= maxWidth || fontSize <= 6) {
193
+ break;
194
+ }
195
+ fontSize -= 1;
196
+ } while (fontSize > 6);
197
+ ctx.fillStyle = 'white';
198
+ ctx.strokeStyle = 'black';
199
+ ctx.lineWidth = Math.max(1, fontSize / 6); // Scale outline with font size
200
+ ctx.textBaseline = 'top';
201
+ // Draw text with outline for better visibility
202
+ const textX = x + padding;
203
+ const textY = y + padding;
204
+ // Split text into lines if it's still too wide
205
+ const words = text.split(/(?=[A-Z_/])/g);
206
+ let line = '';
207
+ let lines = [];
208
+ for (const word of words) {
209
+ const testLine = line + word;
210
+ const metrics = ctx.measureText(testLine);
211
+ if (metrics.width > maxWidth && line !== '') {
212
+ lines.push(line);
213
+ line = word;
214
+ }
215
+ else {
216
+ line = testLine;
217
+ }
218
+ }
219
+ lines.push(line);
220
+ // Draw each line
221
+ const lineHeight = fontSize * 1.2;
222
+ lines.forEach((line, i) => {
223
+ ctx.strokeText(line, textX, textY + i * lineHeight);
224
+ ctx.fillText(line, textX, textY + i * lineHeight);
225
+ });
226
+ }
227
+ });
228
+ console.log(`Processing textures: 100% (${totalTextures}/${totalTextures})`);
229
+ return canvas.toDataURL();
230
+ }
125
231
  }
Binary file
Binary file