harden blockstate.js
This commit is contained in:
186
src/blockstate.js
Normal file
186
src/blockstate.js
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
/**
|
||||||
|
* A dictionary representing the exact properties of a block in the world.
|
||||||
|
* All property values must be normalized to strings (e.g., { "facing": "north", "waterlogged": "false" }).
|
||||||
|
* @typedef {Object<string, string>} BlockStateMap
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Minecraft block model application definition.
|
||||||
|
* @typedef {Object} BlockModel
|
||||||
|
* @property {string} model - The namespace ID of the model (e.g., "minecraft:block/stone").
|
||||||
|
* @property {number} [x] - X-axis rotation in degrees (defaults to 0).
|
||||||
|
* @property {number} [y] - Y-axis rotation in degrees (defaults to 0).
|
||||||
|
* @property {boolean} [uvlock] - Whether to lock UV coordinates during rotation (defaults to false).
|
||||||
|
* @property {number} [weight] - Selection weight for randomized model arrays (defaults to 1).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A JIT-compiled function that resolves a block state and a spatial hash into an array of models.
|
||||||
|
* @typedef {function(BlockStateMap, number): BlockModel[]} ResolverFunction
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a standard comma-separated variant key into a boolean JavaScript condition.
|
||||||
|
* @param {string} key - The variant key (e.g., "facing=north,half=top").
|
||||||
|
* @returns {string} The compiled JavaScript condition.
|
||||||
|
*/
|
||||||
|
function renderVariantCondition(key) {
|
||||||
|
return key.split(',')
|
||||||
|
.map(it => it.split("="))
|
||||||
|
.map(([n, v]) => `state[${JSON.stringify(n)}] === ${JSON.stringify(v)}`)
|
||||||
|
.join(" && ");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a multipart 'when' condition (including OR/AND arrays) into a boolean JavaScript condition.
|
||||||
|
* @param {Object} [when] - The 'when' condition object from a multipart definition.
|
||||||
|
* @returns {string} The compiled JavaScript condition.
|
||||||
|
*/
|
||||||
|
function renderWhenCondition(when) {
|
||||||
|
if (!when) return "true";
|
||||||
|
|
||||||
|
if (when["OR"]) {
|
||||||
|
return when["OR"]
|
||||||
|
.map(it => `(${renderWhenCondition(it)})`)
|
||||||
|
.join(" || ");
|
||||||
|
}
|
||||||
|
if (when["AND"]) {
|
||||||
|
return when["AND"]
|
||||||
|
.map(it => `(${renderWhenCondition(it)})`)
|
||||||
|
.join(" && ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.entries(when).length === 0) return 'true';
|
||||||
|
|
||||||
|
return Object.entries(when)
|
||||||
|
.map(([key, vals]) => vals.toString().split('|')
|
||||||
|
.map(val => `state[${JSON.stringify(key)}] === ${JSON.stringify(val)}`)
|
||||||
|
.join(' || '))
|
||||||
|
.map(it => `(${it})`)
|
||||||
|
.join(' && ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a model application definition (single or weighted array) into a JavaScript return string.
|
||||||
|
* @param {BlockModel|BlockModel[]} data - The 'apply' property of a blockstate variant or multipart.
|
||||||
|
* @returns {string} The compiled JavaScript return value.
|
||||||
|
*/
|
||||||
|
function renderApply(data) {
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
const parts = [];
|
||||||
|
|
||||||
|
let total = 0.0;
|
||||||
|
for (const {weight = 1.0} of data) total += weight;
|
||||||
|
|
||||||
|
let accum = 0.0;
|
||||||
|
for (const elem of data) {
|
||||||
|
const {weight = 1.0} = elem;
|
||||||
|
accum += weight;
|
||||||
|
|
||||||
|
if (accum === total) {
|
||||||
|
parts.push(JSON.stringify(elem));
|
||||||
|
} else {
|
||||||
|
parts.push(`h < ${accum} ? ${JSON.stringify(elem)} : `);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `(h => ${parts.join('')})(Math.abs(hash) % ${total})`;
|
||||||
|
} else {
|
||||||
|
return JSON.stringify(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compiles a raw Minecraft blockstate JSON object into the source code for a resolver function.
|
||||||
|
* @param {Object} data - The parsed blockstate JSON (containing either 'variants' or 'multipart').
|
||||||
|
* @returns {string} The raw JavaScript function body.
|
||||||
|
*/
|
||||||
|
function renderBlockstateResolver(data) {
|
||||||
|
const {variants, multipart} = data;
|
||||||
|
|
||||||
|
if (variants) {
|
||||||
|
let cases = [];
|
||||||
|
for (const [key, apply] of Object.entries(variants)) {
|
||||||
|
if (!key || key === "normal") continue;
|
||||||
|
cases.push(`if (${(renderVariantCondition(key))}) return [${(renderApply(apply))}];`);
|
||||||
|
}
|
||||||
|
let fallback = variants[""] ?? variants["normal"] ?? {"model": ":invalid"};
|
||||||
|
cases.push(`return [${renderApply(fallback)}];`);
|
||||||
|
return cases.join('else ');
|
||||||
|
} else if (multipart) {
|
||||||
|
const parts = [];
|
||||||
|
parts.push('const parts = [];');
|
||||||
|
for (const {when, apply} of multipart) {
|
||||||
|
parts.push(`if (${renderWhenCondition(when)}) parts.push(${renderApply(apply)});`);
|
||||||
|
}
|
||||||
|
parts.push("return parts;");
|
||||||
|
return parts.join('');
|
||||||
|
} else {
|
||||||
|
console.warn('invalid blockstate', data);
|
||||||
|
return 'return [{"model": ":invalid"}];';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages the fetching, compilation, and caching of JIT-compiled blockstate resolvers.
|
||||||
|
*/
|
||||||
|
export class BlockstateResolver {
|
||||||
|
constructor() {
|
||||||
|
/** @type {Map<string, ResolverFunction>} */
|
||||||
|
this.resolvers = new Map();
|
||||||
|
|
||||||
|
// Define default fallback resolvers
|
||||||
|
this.resolvers.set(':pending', (state, hash) => [{"model": ":pending"}]);
|
||||||
|
this.resolvers.set(':invalid', (state, hash) => [{"model": ":invalid"}]);
|
||||||
|
|
||||||
|
/** @type {Map<string, Promise<void>>} */
|
||||||
|
this.pending = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback fired when a pending blockstate finishes downloading and compiling.
|
||||||
|
* @type {((id: string, fn: ResolverFunction) => void) | undefined}
|
||||||
|
*/
|
||||||
|
this.onResolve = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the compiled resolver for a given block ID.
|
||||||
|
* Triggers a network fetch and returns a `:pending` fallback if the resolver is not yet cached.
|
||||||
|
* @param {string} id - The block identifier (e.g., "minecraft:stone" or "stone").
|
||||||
|
* @returns {ResolverFunction} The JIT-compiled resolver function.
|
||||||
|
*/
|
||||||
|
getResolver(id) {
|
||||||
|
id = id.includes(':') ? id : `minecraft:${id}`;
|
||||||
|
|
||||||
|
if (this.resolvers.has(id)) {
|
||||||
|
return this.resolvers.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.pending.has(id)) {
|
||||||
|
const [space, name] = id.split(':');
|
||||||
|
|
||||||
|
this.pending.set(id, fetch(`assets/${space}/blockstates/${name}.json`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
const source = renderBlockstateResolver(data);
|
||||||
|
|
||||||
|
/** @type {ResolverFunction} */
|
||||||
|
const resolver = new Function(`return function (state, hash) { ${source} }`)();
|
||||||
|
|
||||||
|
this.resolvers.set(id, resolver);
|
||||||
|
this.pending.delete(id);
|
||||||
|
|
||||||
|
if (this.onResolve) {
|
||||||
|
this.onResolve(id, resolver);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(reason => {
|
||||||
|
this.resolvers.set(id, this.resolvers.get(':invalid'));
|
||||||
|
this.pending.delete(id);
|
||||||
|
console.log(`failed to resolve blockstate ${id}: ${reason}`);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.resolvers.get(':pending');
|
||||||
|
}
|
||||||
|
}
|
||||||
132
src/minerama.js
132
src/minerama.js
@@ -1,3 +1,5 @@
|
|||||||
|
import {BlockstateResolver} from "./blockstate";
|
||||||
|
|
||||||
export class Renderer {
|
export class Renderer {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.canvas = document.createElement('canvas');
|
this.canvas = document.createElement('canvas');
|
||||||
@@ -7,136 +9,6 @@ export class Renderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderVariantCondition(key) {
|
|
||||||
return key.split(',')
|
|
||||||
.map(it => it.split("="))
|
|
||||||
.map(([n, v]) => `state[${JSON.stringify(n)}] === ${JSON.stringify(v)}`)
|
|
||||||
.join(" && ");
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderWhenCondition(when) {
|
|
||||||
if (!when) return "true";
|
|
||||||
|
|
||||||
if (when["OR"]) {
|
|
||||||
return when["OR"]
|
|
||||||
.map(it => `(${renderWhenCondition(it)})`)
|
|
||||||
.join(" || ");
|
|
||||||
}
|
|
||||||
if (when["AND"]) {
|
|
||||||
return when["AND"]
|
|
||||||
.map(it => `(${renderWhenCondition(it)})`)
|
|
||||||
.join(" && ")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.entries(when).length === 0) return 'true';
|
|
||||||
|
|
||||||
return Object.entries(when)
|
|
||||||
.map(([key, vals]) => vals.toString().split('|')
|
|
||||||
.map(val => `state[${JSON.stringify(key)}] === ${JSON.stringify(val)}`)
|
|
||||||
.join(' || '))
|
|
||||||
.map(it => `(${it})`)
|
|
||||||
.join(' && ');
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderApply(data) {
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
const parts = [];
|
|
||||||
|
|
||||||
let total = 0.0;
|
|
||||||
for (const {weight = 1.0} of data) total += weight;
|
|
||||||
|
|
||||||
let accum = 0.0;
|
|
||||||
for (const elem of data) {
|
|
||||||
const {weight = 1.0} = elem;
|
|
||||||
accum += weight;
|
|
||||||
|
|
||||||
if (accum === total) {
|
|
||||||
parts.push(JSON.stringify(elem));
|
|
||||||
} else {
|
|
||||||
parts.push(`h < ${accum} ? ${JSON.stringify(elem)} : `);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return `(h => ${parts.join('')})(Math.abs(hash) % ${total})`;
|
|
||||||
} else {
|
|
||||||
return JSON.stringify(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Object} data
|
|
||||||
* @returns {String}
|
|
||||||
*/
|
|
||||||
function renderBlockstateResolver(data) {
|
|
||||||
const {variants, multipart} = data;
|
|
||||||
|
|
||||||
if (variants) {
|
|
||||||
let cases = [];
|
|
||||||
for (const [key, apply] of Object.entries(variants)) {
|
|
||||||
if (!key || key === "normal") continue;
|
|
||||||
cases.push(`if (${(renderVariantCondition(key))}) return [${(renderApply(apply))}];`);
|
|
||||||
}
|
|
||||||
let fallback = variants[""] ?? variants["normal"] ?? {"model": ":invalid"};
|
|
||||||
cases.push(`return [${renderApply(fallback)}]`)
|
|
||||||
return cases.join('else ');
|
|
||||||
} else if (multipart) {
|
|
||||||
const parts = [];
|
|
||||||
parts.push('const parts = [];')
|
|
||||||
for (const {when, apply} of multipart) {
|
|
||||||
parts.push(`if (${renderWhenCondition(when)}) parts.push(${renderApply(apply)});`);
|
|
||||||
}
|
|
||||||
parts.push("return parts;");
|
|
||||||
return parts.join('');
|
|
||||||
} else {
|
|
||||||
console.warn('invalid blockstate', data);
|
|
||||||
return 'return [{"model": ":invalid"}]';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export class BlockstateResolver {
|
|
||||||
constructor() {
|
|
||||||
this.resolvers = new Map();
|
|
||||||
this.resolvers.set(':pending', (state, hash) => [{"model": ":pending"}]);
|
|
||||||
this.resolvers.set(':invalid', (state, hash) => [{"model": ":invalid"}]);
|
|
||||||
|
|
||||||
this.pending = new Map();
|
|
||||||
|
|
||||||
this.onResolve = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
getResolver(id) {
|
|
||||||
id = id.includes(':') ? id : `minecraft:${id}`;
|
|
||||||
if (this.resolvers.has(id)) {
|
|
||||||
return this.resolvers.get(id);
|
|
||||||
}
|
|
||||||
if (!this.pending.has(id)) {
|
|
||||||
const [space, name] = id.split(':');
|
|
||||||
this.pending.set(id, fetch(`assets/${space}/blockstates/${name}.json`)
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(data => {
|
|
||||||
const source = renderBlockstateResolver(data);
|
|
||||||
const resolver = new Function(`return function (state, hash) { ${source} }`)();
|
|
||||||
this.resolvers.set(id, resolver);
|
|
||||||
this.pending.delete(id);
|
|
||||||
if (this.onResolve) {
|
|
||||||
this.onResolve(id, resolver);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(reason => {
|
|
||||||
this.resolvers.set(id, this.resolvers.get(':invalid'));
|
|
||||||
this.pending.delete(id);
|
|
||||||
console.log(`failed to resolve blockstate ${id}: ${reason}`)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return this.resolvers.get(':pending');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {String} src
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async function parse(src) {
|
async function parse(src) {
|
||||||
const lines = src.replace(/#.*?(?:\n|$)/, '\n').trim().split(/(?:\n[ \t]*)+/);
|
const lines = src.replace(/#.*?(?:\n|$)/, '\n').trim().split(/(?:\n[ \t]*)+/);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user