diff --git a/plugins.json b/plugins.json index 971cae2e..532578a4 100644 --- a/plugins.json +++ b/plugins.json @@ -1569,5 +1569,23 @@ "repository": "https://github.com/hrmcngs/sb-worn-display-blockbench", "bug_tracker": "https://github.com/hrmcngs/sb-worn-display-blockbench/issues", "creation_date": "2026-05-31" - } + }, + "easy_model_entities": { + "title": "Easy Model Entities", + "author": "Markus Bordihn", + "icon": "icon.png", + "description": "Export Blockbench models for the Easy Model Entities mod (Minecraft: Java Edition) as ready-to-install Data Pack and Resource Pack files.", + "tags": ["Minecraft: Java Edition", "Format", "Entity"], + "version": "1.0.0", + "min_version": "4.9.0", + "variant": "desktop", + "await_loading": true, + "has_changelog": true, + "contributes": { + "formats": ["eme_entity"] + }, + "website": "https://www.curseforge.com/minecraft/mc-mods/easy-model-entities", + "repository": "https://github.com/MarkusBordihn/BOs-Easy-Model-Entities", + "bug_tracker": "https://github.com/MarkusBordihn/BOs-Easy-Model-Entities/issues" + } } diff --git a/plugins/easy_model_entities/LICENSE.MD b/plugins/easy_model_entities/LICENSE.MD new file mode 100644 index 00000000..889c2571 --- /dev/null +++ b/plugins/easy_model_entities/LICENSE.MD @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) 2026 Markus Bordihn + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES +OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/plugins/easy_model_entities/about.md b/plugins/easy_model_entities/about.md new file mode 100644 index 00000000..3b3dcbc9 --- /dev/null +++ b/plugins/easy_model_entities/about.md @@ -0,0 +1,83 @@ +# Easy Model Entities + +> **Note:** This is an early release to gather feedback. Some settings, file formats and the export +> output may still change before the final version. Please report problems and ideas on the +> [issue tracker](https://github.com/MarkusBordihn/BOs-Easy-Model-Entities/issues). + +Turn your Blockbench model into a Minecraft entity you can spawn in game — without writing a mod. +The plugin exports a Data Pack and a Resource Pack for the **Easy Model Entities** mod on +**Minecraft: Java Edition**. + +This plugin only generates the pack files. The **Easy Model Entities mod must be installed** in +Minecraft for them to work, and on the server as well in multiplayer. Without the mod the generated +files do nothing. + +- [CurseForge](https://www.curseforge.com/minecraft/mc-mods/easy-model-entities) +- [Modrinth](https://modrinth.com/mod/easy-model-entities) + +## What you get, and what you do not + +Your model becomes a spawnable entity or a block entity, with a spawn item in the creative +inventory. Size, movement and behavior come from a **fixed set of presets** built into the mod +(wandering, swimming, flying, standing still and so on) — you pick one, you do not script your own. + +Animations are played from your Blockbench clips. These names are played automatically: + +| Clip | When it plays | +|----------|----------------------| +| `idle` | standing still | +| `walk` | moving on the ground | +| `swim` | moving in water | +| `fly` | flying | +| `attack` | attacking | +| `sit` | sitting | + +`hurt` and `death` clips are loaded too, but the mod does not trigger them on its own. Clips with +any other name are kept as custom clips and are played by name via the +`/easy_model_entities animation` command or by another mod. A model without a matching clip simply +renders without animation. + +## How to use + +1. Open or create your model in Blockbench using the **Easy Model Entity** format (File > New > Easy + Model Entity). The built-in **Modded Entity** format also works. Block and item formats are not + supported. +2. Run **File > Export > Export Easy Model Entity**. +3. Choose the **Export Type**: + +- **Complete: Data Pack + Resource Pack (ZIP)** — one archive with both packs and a README that + explains how to install them. Use this the first time and for sharing. +- **Update: Resource Pack only (ZIP)** — after changing the model, texture or animations. +- **Update: Data Pack only (ZIP)** — after changing entity data such as size or movement. +- **Standalone: write into mod project** — writes all files into a selected `src/main/resources` + directory. +- **Model only: mod integration (no data pack)** — writes only the render profile, model and + texture. Use this when your mod ships its own entity classes and needs the visual side only. + +4. Choose the **Type** (not shown for *Model only*, which is always an entity): + +- **Entity** — a living entity controlled by the mod's built-in AI presets. +- **Block Entity** — a static block that can play animations. + +5. Pick a **Preset** that matches your model. The plugin auto-detects a fitting one from your bone + names, so most models need nothing else. Tick **Customize settings** (requires the *Show advanced + customization* Blockbench setting) only to fine-tune dimensions, movement, attributes, rendering + or animation. +6. Set the **Namespace (mod id)** and **Profile ID**, and choose the Minecraft target version. +7. If the project uses textures from another namespace, **Include external textures** decides + whether they are copied into the resource pack (default) or only referenced. Referenced textures + need the mod or pack that provides them; vanilla `minecraft:` textures are never copied. + +The two update exports save you from re-installing both packs for a small change. They keep the +pairing of your last full export, so the updated pack still matches the one already installed. If +you changed the model **and** the entity data, export the complete bundle instead. + +Exported settings are saved inside the `.bbmodel` project and restored on the next export. + +## Blockbench settings + +Two optional settings under **File > Preferences > Settings > Export**: + +- **Show advanced customization** — unlocks the *Customize settings* checkbox in the export dialog. +- **Show experimental presets** — reveals presets that the mod loads but whose specialized movement + or behavior is still in development. Off by default. diff --git a/plugins/easy_model_entities/changelog.json b/plugins/easy_model_entities/changelog.json new file mode 100644 index 00000000..accdd2f1 --- /dev/null +++ b/plugins/easy_model_entities/changelog.json @@ -0,0 +1,22 @@ +{ + "1.0.0": { + "title": "Early release for testing and feedback", + "author": "Markus Bordihn", + "date": "2026-06-13", + "categories": [ + { + "title": "Features", + "list": [ + "Export the current project as Easy Model Entities Data Pack and Resource Pack files.", + "Preset auto-detection from bone names (humanoid, quadruped, aquatic, winged, arthropod, cuboid, floating, static).", + "Entity and Block Entity support.", + "Update exports for a single pack: re-export only the resource pack after model, texture or animation changes, or only the data pack after entity data changes.", + "Export into a mod project, or model-only for mod integrations.", + "Minecraft target versions 1.20.1, 1.21.1, 1.21.11, 26.1.2 and 26.2.", + "Optional advanced customization of dimensions, movement, behavior, attributes, rendering and animation.", + "Validation with blocking errors and performance budget warnings, including unusable texture locations and missing textures." + ] + } + ] + } +} diff --git a/plugins/easy_model_entities/easy_model_entities.js b/plugins/easy_model_entities/easy_model_entities.js new file mode 100644 index 00000000..12d5b1e3 --- /dev/null +++ b/plugins/easy_model_entities/easy_model_entities.js @@ -0,0 +1,4271 @@ +/******/ (() => { // webpackBootstrap +/******/ var __webpack_modules__ = ({ + +/***/ 833 +(module, __unused_webpack_exports, __webpack_require__) { + +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {Validator} = __webpack_require__(229); +const {parseExternalLocation} = __webpack_require__(834); + +class BlockbenchAdapter { + static PROJECT_PROPERTY = 'eme_export'; + + static #base64ToBytes(base64) { + const clean = String(base64).replace(/^data:[^,]*,/, ''); + const binary = + typeof atob === 'function' ? atob(clean) : Buffer.from(clean, + 'base64').toString('binary'); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.codePointAt(i) & 0xff; + } + + return bytes; + } + + static #firstTexture() { + if (typeof Texture === 'undefined') { + return null; + } + + return Texture.getDefault?.() || Texture.all?.[0] || null; + } + + static collectTextures() { + if (typeof Texture === 'undefined' || !Texture.all) { + return []; + } + + return Texture.all.map((texture, position) => ({ + index: position, + name: texture.name || '', + namespace: texture.namespace || '', + folder: texture.folder || '', + path: texture.path || '', + bytes: BlockbenchAdapter.#base64ToBytes(texture.getBase64()) + })); + } + + static #externalTextureLocations() { + if (typeof Texture === 'undefined' || !Texture.all) { + return []; + } + + return Texture.all + .map((texture) => parseExternalLocation({ + namespace: texture.namespace, + folder: texture.folder, + name: texture.name, + path: texture.path + })) + .filter((location) => location && !location.startsWith('minecraft:')); + } + + static #isNumericValue(value) { + if (typeof value === 'number') { + return Number.isFinite(value); + } + + return typeof value === 'string' && value.trim() !== '' + && Number.isFinite(Number(value)); + } + + static #summarizeAnimation(animation) { + const channels = new Set(); + let hasExpression = false; + + Object.values(animation.animators || {}).forEach((animator) => { + (animator.keyframes || []).forEach((keyframe) => { + if (keyframe.channel) { + channels.add(keyframe.channel); + } + (keyframe.data_points || []).forEach((point) => { + ['x', 'y', 'z'].forEach((axis) => { + const value = point?.[axis]; + if (value !== undefined && value !== null + && !BlockbenchAdapter.#isNumericValue(value)) { + hasExpression = true; + } + }); + }); + }); + }); + + return { + name: animation.name || '', + channels: [...channels], + hasExpression: hasExpression + }; + } + + static getModelStats() { + const cubes = typeof Cube !== 'undefined' && Cube.all ? Cube.all : []; + const groups = typeof Group !== 'undefined' && Group.all ? Group.all : []; + const animations = + typeof Animation !== 'undefined' && Animation.all ? Animation.all : []; + const texture = BlockbenchAdapter.#firstTexture(); + + let maxDepth = 0; + groups.forEach((group) => { + let depth = 1; + let parent = group.parent; + while (parent && parent !== 'root' && typeof parent === 'object') { + depth += 1; + parent = parent.parent; + } + if (depth > maxDepth) { + maxDepth = depth; + } + }); + + return { + hasModel: cubes.length > 0 || groups.length > 0, + hasTexture: !!texture, + cubeCount: cubes.length, + boneCount: groups.length, + animationCount: animations.length, + animations: animations.map( + (animation) => BlockbenchAdapter.#summarizeAnimation(animation)), + hierarchyDepth: maxDepth, + boneNames: groups.map((group) => group.name), + externalTextures: BlockbenchAdapter.#externalTextureLocations(), + textureWidth: texture ? texture.width : undefined, + textureHeight: texture ? texture.height : undefined + }; + } + + static getModelBytes() { + return Codecs.project.compile({raw: false}); + } + + static getModelBounds() { + const cubes = typeof Cube !== 'undefined' && Cube.all ? Cube.all : []; + if (cubes.length === 0) { + return null; + } + + const bounds = { + minX: Infinity, + minY: Infinity, + minZ: Infinity, + maxX: -Infinity, + maxY: -Infinity, + maxZ: -Infinity + }; + cubes.forEach((cube) => { + const from = cube.from || [0, 0, 0]; + const to = cube.to || [0, 0, 0]; + bounds.minX = Math.min(bounds.minX, from[0], to[0]); + bounds.minY = Math.min(bounds.minY, from[1], to[1]); + bounds.minZ = Math.min(bounds.minZ, from[2], to[2]); + bounds.maxX = Math.max(bounds.maxX, from[0], to[0]); + bounds.maxY = Math.max(bounds.maxY, from[1], to[1]); + bounds.maxZ = Math.max(bounds.maxZ, from[2], to[2]); + }); + + return bounds; + } + + static getModelCubes() { + const cubes = typeof Cube !== 'undefined' && Cube.all ? Cube.all : []; + + return cubes.map((cube) => ({ + from: (cube.from || [0, 0, 0]).slice(), + to: (cube.to || [0, 0, 0]).slice() + })); + } + + static getProjectName() { + if (typeof Project === 'undefined' || !Project) { + return ''; + } + + if (Project.geometry_name) { + return Project.geometry_name; + } + + if (Project.name) { + return Project.name; + } + + if (typeof Project.getDisplayName === 'function') { + return Project.getDisplayName(false); + } + return ''; + } + + static loadSettings() { + if (typeof Project === 'undefined' || !Project) { + return null; + } + + const stored = Project[BlockbenchAdapter.PROJECT_PROPERTY]; + if (stored && typeof stored === 'object' && Object.keys(stored).length + > 0) { + return structuredClone(stored); + } + + return null; + } + + static saveSettings(settings) { + if (typeof Project !== 'undefined' && Project) { + Project[BlockbenchAdapter.PROJECT_PROPERTY] = structuredClone( + settings); + if (typeof Blockbench !== 'undefined' && Blockbench.dispatchEvent) { + Blockbench.dispatchEvent('eme_settings_saved', {settings}); + } + } + } + + static #zipTo(files, outputType) { + const zip = new JSZip(); + files.forEach((file) => { + zip.file(file.path, file.content); + }); + + return zip.generateAsync({type: outputType}); + } + + static #bundleBlob(bundle) { + if (!bundle.datapack) { + return BlockbenchAdapter.#zipTo(bundle.resourcepack, 'blob'); + } + + if (!bundle.resourcepack) { + return BlockbenchAdapter.#zipTo(bundle.datapack, 'blob'); + } + + return Promise.all([ + BlockbenchAdapter.#zipTo(bundle.datapack, 'uint8array'), + BlockbenchAdapter.#zipTo(bundle.resourcepack, 'uint8array') + ]).then(([datapackZip, resourcepackZip]) => { + const outer = new JSZip(); + outer.file(bundle.readme.path, bundle.readme.content); + outer.file(bundle.datapackFileName || 'datapack.zip', datapackZip); + outer.file(bundle.resourcepackFileName || 'resourcepack.zip', + resourcepackZip); + return outer.generateAsync({type: 'blob'}); + }); + } + + static exportPackBundle(bundle, name) { + return BlockbenchAdapter.#bundleBlob(bundle).then((content) => { + return new Promise((resolve) => { + Blockbench.export( + { + type: 'Zip Archive', + extensions: ['zip'], + name: name, + content: content, + savetype: 'zip' + }, + (path) => resolve(path) + ); + }); + }); + } + + static listExistingFiles(rootDir, files) { + const fs = __webpack_require__(896); + const path = __webpack_require__(928); + + return files + .map((file) => path.join(rootDir, file.path)) + .filter((fullPath) => fs.existsSync(fullPath)); + } + + static writeToDirectory(rootDir, files) { + const fs = __webpack_require__(896); + const path = __webpack_require__(928); + files.forEach((file) => { + const result = Validator.validateOutputPath(file.path); + if (!result.valid) { + throw new Error(`${result.code}: ${result.message} (${file.path})`); + } + const fullPath = path.join(rootDir, file.path); + fs.mkdirSync(path.dirname(fullPath), {recursive: true}); + const data = + file.binary && file.content instanceof Uint8Array ? Buffer.from( + file.content) : file.content; + fs.writeFileSync(fullPath, data); + }); + } + + static pickDirectory(title) { + return Blockbench.pickDirectory( + {title: title, resource_id: 'eme_mod_project'}); + } +} + +module.exports = {BlockbenchAdapter}; + + +/***/ }, + +/***/ 869 +(module, __unused_webpack_exports, __webpack_require__) { + +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {buildServerProfile} = __webpack_require__(790); +const {buildRenderProfile} = __webpack_require__(255); +const {buildDataPackMcmeta, buildResourcePackMcmeta} = __webpack_require__(600); +const {buildReadme} = __webpack_require__(250); +const {hashString} = __webpack_require__(803); +const {MODEL_TYPE_ENTITY} = __webpack_require__(151); +const { + EXPORT_TYPE_PACKS, + includesDataPack, + includesResourcePack, + isSinglePackExport +} = __webpack_require__(984); + +function toJson(value) { + return JSON.stringify(value, null, 2) + '\n'; +} + +function pairingVersion(settings, serverProfile) { + if (isSinglePackExport(settings.exportType) && settings.lastExportedVersion) { + return settings.lastExportedVersion; + } + + return hashString(toJson(serverProfile || buildServerProfile(settings))); +} + +function stampVersion(settings, serverProfile, renderProfile) { + if (!serverProfile) { + return; + } + + const version = pairingVersion(settings, serverProfile); + serverProfile.version = version; + renderProfile.version = version; +} + +function buildProfiles(settings, textureResolution) { + const serverProfile = buildServerProfile(settings); + const renderProfile = buildRenderProfile(settings, textureResolution); + stampVersion(settings, serverProfile, renderProfile); + + return {serverProfile, renderProfile}; +} + +function dataPaths(settings) { + const namespace = settings.namespace; + const id = settings.profileId; + const modelType = settings.modelType || MODEL_TYPE_ENTITY; + + return { + profile: `data/${namespace}/easy_model_entities/profiles/${modelType}/${id}.json`, + renderProfile: + `assets/${namespace}/easy_model_entities/render_profiles/${modelType}/${id}.json`, + model: `assets/${namespace}/easy_model_entities/models/${id}.bbmodel` + }; +} + +function textureFiles(textureResolution) { + const packed = (textureResolution && textureResolution.packed) || []; + + return packed.map((entry) => ({ + path: entry.path, + content: entry.bytes, + binary: true + })); +} + +function datapackFiles(settings, serverProfile) { + return [ + { + path: 'pack.mcmeta', content: toJson(buildDataPackMcmeta(settings)), + binary: false + }, + { + path: dataPaths(settings).profile, content: toJson(serverProfile), + binary: false + } + ]; +} + +function resourcepackFiles(settings, renderProfile, options) { + const paths = dataPaths(settings); + return [ + { + path: 'pack.mcmeta', content: toJson(buildResourcePackMcmeta(settings)), + binary: false + }, + {path: paths.renderProfile, content: toJson(renderProfile), binary: false}, + {path: paths.model, content: options.modelBytes, binary: true}, + ...textureFiles(options.textureResolution) + ]; +} + +function packFileNames(settings) { + return { + datapack: `${settings.profileId}_datapack.zip`, + resourcepack: `${settings.profileId}_resourcepack.zip` + }; +} + +function buildPackBundle(settings, options) { + const opts = options || {}; + const {serverProfile, renderProfile} = buildProfiles(settings, + opts.textureResolution); + const exportType = settings.exportType || EXPORT_TYPE_PACKS; + const fileNames = packFileNames(settings); + const withDataPack = includesDataPack(exportType); + const withResourcePack = includesResourcePack(exportType); + + return { + readme: withDataPack && withResourcePack ? { + path: 'README.md', content: buildReadme(settings, fileNames), + binary: false + } : null, + datapack: withDataPack ? datapackFiles(settings, serverProfile) : null, + resourcepack: withResourcePack + ? resourcepackFiles(settings, renderProfile, opts) : null, + datapackFileName: fileNames.datapack, + resourcepackFileName: fileNames.resourcepack, + serverProfile, + renderProfile + }; +} + +function buildModProjectFiles(settings, options) { + const opts = options || {}; + const renderProfile = buildRenderProfile(settings, opts.textureResolution); + const paths = dataPaths(settings); + + const files = []; + const serverProfile = settings.modelOnly ? null : buildServerProfile( + settings); + stampVersion(settings, serverProfile, renderProfile); + if (serverProfile) { + files.push({ + path: paths.profile, content: toJson(serverProfile), + binary: false + }); + } + + files.push( + { + path: paths.renderProfile, content: toJson(renderProfile), + binary: false + }, + {path: paths.model, content: opts.modelBytes, binary: true}, + ...textureFiles(opts.textureResolution)); + + return {files, serverProfile, renderProfile}; +} + +module.exports = { + buildPackBundle, + buildModProjectFiles, + pairingVersion +}; + + +/***/ }, + +/***/ 600 +(module, __unused_webpack_exports, __webpack_require__) { + +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {getPackFormats} = __webpack_require__(954); + +function buildPack(descriptor, description) { + const pack = {pack_format: descriptor.packFormat}; + if (descriptor.minFormat) { + pack.min_format = descriptor.minFormat; + } + if (descriptor.maxFormat) { + pack.max_format = descriptor.maxFormat; + } + pack.description = description; + + return {pack}; +} + +function buildMcmeta(settings, kind, description) { + const formats = getPackFormats(settings.targetVersion); + if (!formats) { + throw new Error( + `Unsupported or disabled target version: ${settings.targetVersion}`); + } + return buildPack(formats[kind], description); +} + +function buildDataPackMcmeta(settings) { + return buildMcmeta(settings, 'data', + 'Easy Model Entities server profiles'); +} + +function buildResourcePackMcmeta(settings) { + return buildMcmeta(settings, 'resource', + 'Easy Model Entities render assets'); +} + +module.exports = {buildDataPackMcmeta, buildResourcePackMcmeta}; + + +/***/ }, + +/***/ 640 +(module) { + +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +function valuesDiffer(a, b) { + if (Array.isArray(a) || Array.isArray(b)) { + return JSON.stringify(a) !== JSON.stringify(b); + } + + return a !== b; +} + +function diffFlat(values, defaults) { + const result = {}; + Object.keys(values).forEach((key) => { + if (valuesDiffer(values[key], defaults[key])) { + result[key] = values[key]; + } + }); + + return result; +} + +function isEmpty(object) { + return !object || Object.keys(object).length === 0; +} + +function assignIfPresent(target, key, section) { + if (!isEmpty(section)) { + target[key] = section; + } +} + +module.exports = {diffFlat, assignIfPresent}; + + +/***/ }, + +/***/ 250 +(module, __unused_webpack_exports, __webpack_require__) { + +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {ResourceLocation} = __webpack_require__(20); +const TEMPLATE = __webpack_require__(377); + +function buildReadme(settings, fileNames) { + const id = ResourceLocation.buildResourceLocation(settings.namespace, + settings.profileId); + const modelType = settings.modelType || 'entity'; + const serverProfileId = ResourceLocation.buildResourceLocation( + settings.namespace, `${modelType}/${settings.profileId}`); + return TEMPLATE + .replaceAll('{{id}}', id) + .replaceAll('{{serverProfileId}}', serverProfileId) + .replaceAll('{{mcVersion}}', + `Minecraft: Java Edition ${settings.targetVersion}`) + .replaceAll('{{datapackFile}}', fileNames.datapack) + .replaceAll('{{resourcepackFile}}', fileNames.resourcepack); +} + +module.exports = {buildReadme}; + + +/***/ }, + +/***/ 255 +(module, __unused_webpack_exports, __webpack_require__) { + +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const { + bodyType, + animationMode, + defaultRenderingSettings, + defaultAnimationSettings, + isCustom, + MODEL_TYPE_BLOCK_ENTITY +} = __webpack_require__(151); +const {diffFlat, assignIfPresent} = __webpack_require__(640); + +function renderPresetType(settings) { + return settings.modelType === MODEL_TYPE_BLOCK_ENTITY + ? 'static' : settings.presetType; +} + +function toRenderingJson(values) { + return { + scale: values.scale, + shadow_radius: values.shadowRadius, + visible_bounds_width: values.visibleBoundsWidth ?? 0, + visible_bounds_height: values.visibleBoundsHeight ?? 0, + visible_bounds_offset: values.visibleBoundsOffset || [0, 0, 0] + }; +} + +function buildRendering(settings) { + return diffFlat(toRenderingJson(settings.rendering), + toRenderingJson(defaultRenderingSettings(renderPresetType(settings)))); +} + +function toAnimationJson(values) { + return { + mode: values.mode, + swing_speed: values.swingSpeed, + walk_speed_multiplier: values.walkSpeedMultiplier, + idle_strength: values.idleStrength ?? 1, + gait: values.gait || 'natural' + }; +} + +function buildAnimation(settings) { + const defaults = defaultAnimationSettings( + animationMode(renderPresetType(settings))); + const animation = diffFlat(toAnimationJson(settings.animation), + toAnimationJson(defaults)); + + if (settings.animation.mode === 'none') { + delete animation.swing_speed; + delete animation.walk_speed_multiplier; + delete animation.idle_strength; + delete animation.gait; + } + + return animation; +} + +function buildRenderProfile(settings, textureResolution) { + const renderPreset = renderPresetType(settings); + const custom = settings.modelType !== MODEL_TYPE_BLOCK_ENTITY + && isCustom(settings.presetType); + const profile = { + schema_version: settings.schemaVersion, + preset_type: renderPreset + }; + + if (custom || settings.host.bodyType !== bodyType(renderPreset)) { + profile.body_type = settings.host.bodyType; + } + + profile.model = + `${settings.namespace}:easy_model_entities/models/${settings.profileId}`; + + if (textureResolution && textureResolution.texture) { + profile.texture = textureResolution.texture; + } + assignIfPresent(profile, 'textures', + textureResolution && textureResolution.textures); + + assignIfPresent(profile, 'rendering', buildRendering(settings)); + assignIfPresent(profile, 'animation', buildAnimation(settings)); + + return profile; +} + +module.exports = { + buildRenderProfile +}; + + +/***/ }, + +/***/ 790 +(module, __unused_webpack_exports, __webpack_require__) { + +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const { + presetDefaults, + presetDimensions, + movementDefaults, + behaviorModeFor, + wandersByMovement, + isCustom, + DEFAULT_MAX_HEALTH, + DEFAULT_FOLLOW_RANGE, + MODEL_TYPE_BLOCK_ENTITY, + MODEL_TYPE_ENTITY +} = __webpack_require__(151); +const {diffFlat, assignIfPresent} = __webpack_require__(640); + +function effectiveDefaults(settings) { + const preset = settings.presetType; + const movementType = settings.host.movementType; + const movement = movementDefaults(preset, movementType); + const mode = behaviorModeFor(preset, movementType); + + return { + movement: { + speed: movement.speed, + step_height: movement.stepHeight, + gravity: movement.gravity + }, + behavior: { + mode: mode, + look_at_players: mode === 'idle_only' || mode === 'ambient', + random_stroll: wandersByMovement(movementType) && mode === 'ambient' + }, + attributes: { + max_health: DEFAULT_MAX_HEALTH, + movement_speed: settings.movement.speed, + follow_range: DEFAULT_FOLLOW_RANGE + }, + dimensions: settings.modelType === MODEL_TYPE_BLOCK_ENTITY + ? presetDefaults(preset, MODEL_TYPE_BLOCK_ENTITY).dimensions + : presetDimensions(preset) + }; +} + +function buildEntity(settings, custom) { + if (!custom) { + return {}; + } + + const host = settings.host; + + return { + type: host.entityType, + movement_type: host.movementType, + body_type: host.bodyType + }; +} + +function buildDimensions(settings, defaults, custom) { + const values = { + width: settings.dimensions.width, + height: settings.dimensions.height, + eye_height: settings.dimensions.eyeHeight + }; + if (custom) { + return values; + } + + return diffFlat(values, { + width: defaults.dimensions.width, + height: defaults.dimensions.height, + eye_height: defaults.dimensions.eyeHeight + }); +} + +function buildServerProfile(settings) { + const modelType = settings.modelType || MODEL_TYPE_ENTITY; + const blockEntity = modelType === MODEL_TYPE_BLOCK_ENTITY; + const custom = !blockEntity && isCustom(settings.presetType); + const defaults = effectiveDefaults(settings); + + const profile = { + schema_version: settings.schemaVersion, + model_type: modelType, + preset_type: settings.presetType + }; + + if (!blockEntity) { + assignIfPresent(profile, 'entity', buildEntity(settings, custom)); + } + + assignIfPresent(profile, 'dimensions', + buildDimensions(settings, defaults, custom)); + + if (!blockEntity) { + assignIfPresent(profile, 'movement', diffFlat({ + speed: settings.movement.speed, + step_height: settings.movement.stepHeight, + gravity: settings.movement.gravity + }, defaults.movement)); + + assignIfPresent(profile, 'behavior', diffFlat({ + mode: settings.behavior.mode, + look_at_players: settings.behavior.lookAtPlayers, + random_stroll: settings.behavior.randomStroll + }, defaults.behavior)); + + assignIfPresent(profile, 'attributes', diffFlat({ + max_health: settings.attributes.maxHealth, + movement_speed: settings.attributes.movementSpeed, + follow_range: settings.attributes.followRange + }, defaults.attributes)); + } + + return profile; +} + +module.exports = {buildServerProfile}; + + +/***/ }, + +/***/ 579 +(module, __unused_webpack_exports, __webpack_require__) { + +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {BlockbenchAdapter} = __webpack_require__(833); +const { + buildPackBundle, + buildModProjectFiles, + pairingVersion +} = __webpack_require__(869); +const { + EXPORT_TYPE_RESOURCE_PACK, + EXPORT_TYPE_DATA_PACK, + EXPORT_TYPE_MOD_PROJECT +} = __webpack_require__(984); + +function zipExportName(settings) { + const base = `${settings.namespace}_${settings.profileId}`; + if (settings.exportType === EXPORT_TYPE_RESOURCE_PACK) { + return `${base}_resourcepack`; + } + + if (settings.exportType === EXPORT_TYPE_DATA_PACK) { + return `${base}_datapack`; + } + + return `${base}_eme`; +} + +function exportToZip(settings, options) { + return BlockbenchAdapter.exportPackBundle( + buildPackBundle(settings, options), zipExportName(settings)) + .then(() => { + Blockbench.showQuickMessage('Easy Model Entities packs exported', 1500); + }) + .catch((error) => { + Blockbench.showMessageBox({title: 'Export failed', message: String(error)}); + }); +} + +function exportToModProject(settings, options) { + const rootDir = BlockbenchAdapter.pickDirectory( + 'Select src/main/resources directory'); + if (!rootDir) { + return; + } + + const {files} = buildModProjectFiles(settings, options); + const existing = BlockbenchAdapter.listExistingFiles(rootDir, files); + + const write = () => { + try { + BlockbenchAdapter.writeToDirectory(rootDir, files); + Blockbench.showQuickMessage( + 'Easy Model Entities files written to mod project', 1500); + } catch (error) { + Blockbench.showMessageBox( + {title: 'Export failed', message: String(error)}); + } + }; + + if (existing.length > 0) { + Blockbench.showMessageBox( + { + title: 'Overwrite existing files?', + message: `${existing.length} file(s) already exist and will be overwritten:\n\n` + + existing.join('\n'), + buttons: ['Overwrite', 'Cancel'], + confirm: 0, + cancel: 1 + }, + (button) => { + if (button === 0) { + write(); + } + } + ); + } else { + write(); + } +} + +function performExport(settings, target, textureResolution) { + BlockbenchAdapter.saveSettings({ + ...settings, + lastExportedVersion: pairingVersion(settings) + }); + + const options = { + modelBytes: BlockbenchAdapter.getModelBytes(), + textureResolution + }; + + if (target === EXPORT_TYPE_MOD_PROJECT) { + return exportToModProject(settings, options); + } + + return exportToZip(settings, options); +} + +module.exports = {performExport, zipExportName}; + + +/***/ }, + +/***/ 670 +(module, __unused_webpack_exports, __webpack_require__) { + +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {BlockbenchAdapter} = __webpack_require__(833); +const {pickModelSettings} = __webpack_require__(668); +const {FORMAT_ID} = __webpack_require__(995); + +const EME_SETTINGS_KEY = 'emeSettings'; + +function handleCompile(event) { + if (Format?.id !== FORMAT_ID) { + return; + } + + const settings = BlockbenchAdapter.loadSettings(); + if (!settings) { + return; + } + + const picked = pickModelSettings(settings); + if (Object.keys(picked).length > 0) { + event.model[EME_SETTINGS_KEY] = picked; + } +} + +function handleParse(event) { + const formatId = event.model?.meta?.model_format || event.model?.model_format; + if (formatId !== FORMAT_ID) { + return; + } + + const settings = event.model[EME_SETTINGS_KEY]; + if (settings && typeof settings === 'object') { + BlockbenchAdapter.saveSettings(settings); + } +} + +function registerEmeCodecHooks() { + Codecs.project.on('compile', handleCompile); + Codecs.project.on('parse', handleParse); +} + +function unregisterEmeCodecHooks() { + Codecs.project.events?.compile?.remove?.(handleCompile); + Codecs.project.events?.parse?.remove?.(handleParse); +} + +module.exports = {registerEmeCodecHooks, unregisterEmeCodecHooks}; + + +/***/ }, + +/***/ 995 +(module) { + +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const FORMAT_ID = 'eme_entity'; + +let emeFormat = null; + +function toModelIdentifier(name) { + return name.trim().toLowerCase() + .replace(/\s+/g, '_') + .replace(/[^a-z0-9_]/g, '') + .replace(/_+/g, '_') + .replace(/^_|_$/g, ''); +} + +function registerEmeFormat(icon) { + if (emeFormat) { + return; + } + + emeFormat = new ModelFormat(FORMAT_ID, { + name: 'Easy Model Entity', + category: 'minecraft', + description: 'Entity model for the Easy Model Entities mod (Minecraft: Java Edition).', + icon: icon || 'pets', + rotate_cubes: true, + box_uv: true, + optional_box_uv: true, + single_texture: false, + texture_folder: true, + bone_rig: true, + centered_grid: true, + animated_textures: false, + animation_mode: true, + animation_files: false, + java_edition_mapping: false, + model_identifier: true, + locators: true, + codec: Codecs.project, + onActivation() { + if (Project && !Project.model_identifier && Project.name) { + Project.model_identifier = toModelIdentifier(Project.name); + } + } + }); +} + +function unregisterEmeFormat() { + if (emeFormat) { + emeFormat.delete(); + emeFormat = null; + } +} + +module.exports = {FORMAT_ID, registerEmeFormat, unregisterEmeFormat}; + + +/***/ }, + +/***/ 16 +(module) { + +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const EN = { + 'eme.dialog.title': 'Easy Model Entities Export', + 'eme.action.export': 'Export Easy Model Entity', + 'eme.action.exportDescription': 'Export the current project for Easy Model Entities.', + 'eme.field.preset': 'Preset', + 'eme.field.namespace': 'Namespace (mod id)', + 'eme.field.profileId': 'Profile ID', + 'eme.field.targetVersion': 'Minecraft Version', + 'eme.field.exportType': 'Export Type', + 'eme.field.modelType': 'Type', + 'eme.field.customize': 'Customize settings', + 'eme.field.includeExternalTextures': 'Include external textures', + 'eme.field.includeExternalTexturesDescription': + 'Copy textures from other namespaces into the resource pack instead of ' + + 'only referencing them. Vanilla minecraft: textures are never copied.', + 'eme.field.hostEntityType': 'Host Entity Type', + 'eme.field.movementType': 'Movement Type', + 'eme.field.bodyType': 'Body Type', + 'eme.field.width': 'Width', + 'eme.field.height': 'Height', + 'eme.field.eyeHeight': 'Eye Height', + 'eme.field.speed': 'Speed', + 'eme.field.stepHeight': 'Step Height', + 'eme.field.gravity': 'Gravity', + 'eme.field.behaviorMode': 'Behavior Mode', + 'eme.field.maxHealth': 'Max Health', + 'eme.field.movementSpeed': 'Movement Speed', + 'eme.field.followRange': 'Follow Range', + 'eme.field.scale': 'Scale', + 'eme.field.shadowRadius': 'Shadow Radius', + 'eme.field.visibleBoundsWidth': 'Visible Bounds Width (0 = auto)', + 'eme.field.visibleBoundsHeight': 'Visible Bounds Height (0 = auto)', + 'eme.field.visibleBoundsOffsetX': 'Visible Bounds Offset X', + 'eme.field.visibleBoundsOffsetY': 'Visible Bounds Offset Y', + 'eme.field.visibleBoundsOffsetZ': 'Visible Bounds Offset Z', + 'eme.field.animationMode': 'Animation Mode', + 'eme.field.swingSpeed': 'Swing Speed', + 'eme.field.walkSpeedMultiplier': 'Walk Speed Multiplier', + 'eme.field.idleStrength': 'Idle Strength', + 'eme.field.gait': 'Gait', + 'eme.section.host': 'Host', + 'eme.section.dimensions': 'Dimensions', + 'eme.section.movement': 'Movement', + 'eme.section.behavior': 'Behavior', + 'eme.section.attributes': 'Attributes', + 'eme.section.rendering': 'Rendering', + 'eme.section.animation': 'Animation', + 'eme.setting.enable_customization': 'Show advanced customization (Easy Model Entities)', + 'eme.setting.enable_experimental': 'Show experimental presets (Easy Model Entities)', + 'eme.preset.custom': 'Custom (manual settings)', + 'eme.preset.static': 'Static (no animation, no movement)', + 'eme.preset.statue': 'Statue (display, no movement)', + 'eme.preset.ticking': 'Ticking (server + client tick)', + 'eme.preset.animated': 'Animated (continuous animation)', + 'eme.preset.animated_randomly': 'Animated randomly (random idle bursts)', + 'eme.preset.humanoid_still': 'Humanoid (2 legs, standing)', + 'eme.preset.humanoid_wandering': 'Humanoid (2 legs, wandering)', + 'eme.preset.quadruped_still': 'Quadruped (4 legs, ground, standing)', + 'eme.preset.quadruped_wandering': 'Quadruped (4 legs, ground, wandering)', + 'eme.preset.aquatic_still': 'Aquatic (fish, still)', + 'eme.preset.aquatic_swimming': 'Aquatic (fish, swimming)', + 'eme.preset.amphibious_still': 'Amphibious (land/water, still)', + 'eme.preset.amphibious_wandering': 'Amphibious (land/water, wandering)', + 'eme.preset.winged_still': 'Winged (bird, perched)', + 'eme.preset.winged_wandering': 'Winged (bird, flying)', + 'eme.preset.winged_humanoid_still': 'Winged humanoid (standing)', + 'eme.preset.winged_humanoid_wandering': 'Winged humanoid (wandering)', + 'eme.preset.arthropod_still': 'Arthropod (insect/spider, still)', + 'eme.preset.arthropod_wandering': 'Arthropod (insect/spider, wandering)', + 'eme.preset.cuboid_still': 'Cuboid (block shape, still)', + 'eme.preset.cuboid_hopping': 'Cuboid (block shape, hopping)', + 'eme.preset.floating_still': 'Floating (hovering, still)', + 'eme.body.static': 'Static', + 'eme.body.biped': 'Biped (2 legs)', + 'eme.body.quadruped': 'Quadruped (4 legs)', + 'eme.body.aquatic': 'Aquatic', + 'eme.body.amphibious': 'Amphibious', + 'eme.body.winged': 'Winged', + 'eme.body.winged_humanoid': 'Winged humanoid', + 'eme.body.arthropod': 'Arthropod', + 'eme.body.cuboid': 'Cuboid', + 'eme.body.floating': 'Floating', + 'eme.movement.ground': 'Ground', + 'eme.movement.water': 'Water', + 'eme.movement.amphibious': 'Amphibious', + 'eme.movement.static': 'Static', + 'eme.gait.natural': 'Natural', + 'eme.gait.feline': 'Feline (light, quick)', + 'eme.gait.ungulate': 'Ungulate (heavy, slow)', + 'eme.behavior.idle_only': 'Idle only', + 'eme.behavior.ambient': 'Ambient (wanders)', + 'eme.behavior.static': 'Static', + 'eme.behavior.external_owner': 'External owner', + 'eme.animation.automatic': 'Automatic', + 'eme.animation.random_idle': 'Random idle', + 'eme.animation.none': 'None', + 'eme.entity.ground_entity': 'Ground entity', + 'eme.entity.static_entity': 'Static entity', + 'eme.entity.aquatic_entity': 'Aquatic entity', + 'eme.entity.amphibious_entity': 'Amphibious entity', + 'eme.modelType.entity': 'Entity', + 'eme.modelType.block_entity': 'Block Entity', + 'eme.exportType.packs': 'Complete: Data Pack + Resource Pack (ZIP)', + 'eme.exportType.resource_pack': 'Update: Resource Pack only (ZIP)', + 'eme.exportType.data_pack': 'Update: Data Pack only (ZIP)', + 'eme.exportType.mod_project': 'Standalone: write into mod project', + 'eme.exportType.model_only': 'Model only: mod integration (no data pack)' +}; + +const DE = { + 'eme.dialog.title': 'Easy Model Entities Export', + 'eme.action.export': 'Easy Model Entity exportieren', + 'eme.action.exportDescription': 'Exportiert das aktuelle Projekt für Easy Model Entities.', + 'eme.field.preset': 'Vorlage', + 'eme.field.namespace': 'Namespace (Mod-ID)', + 'eme.field.profileId': 'Profil-ID', + 'eme.field.targetVersion': 'Minecraft-Version', + 'eme.field.exportType': 'Export-Typ', + 'eme.field.modelType': 'Typ', + 'eme.field.customize': 'Einstellungen anpassen', + 'eme.field.includeExternalTextures': 'Externe Texturen mitliefern', + 'eme.field.includeExternalTexturesDescription': + 'Texturen aus anderen Namespaces ins Resource Pack kopieren statt sie nur ' + + 'zu referenzieren. Vanilla-Texturen (minecraft:) werden nie kopiert.', + 'eme.field.hostEntityType': 'Host-Entität', + 'eme.field.movementType': 'Bewegungsart', + 'eme.field.bodyType': 'Körpertyp', + 'eme.field.width': 'Breite', + 'eme.field.height': 'Höhe', + 'eme.field.eyeHeight': 'Augenhöhe', + 'eme.field.speed': 'Geschwindigkeit', + 'eme.field.stepHeight': 'Schritthöhe', + 'eme.field.gravity': 'Schwerkraft', + 'eme.field.behaviorMode': 'Verhaltensmodus', + 'eme.field.maxHealth': 'Maximale Lebenspunkte', + 'eme.field.movementSpeed': 'Bewegungsgeschwindigkeit', + 'eme.field.followRange': 'Folgereichweite', + 'eme.field.scale': 'Skalierung', + 'eme.field.shadowRadius': 'Schattenradius', + 'eme.field.visibleBoundsWidth': 'Sichtbarkeitsbox-Breite (0 = auto)', + 'eme.field.visibleBoundsHeight': 'Sichtbarkeitsbox-Höhe (0 = auto)', + 'eme.field.visibleBoundsOffsetX': 'Sichtbarkeitsbox-Versatz X', + 'eme.field.visibleBoundsOffsetY': 'Sichtbarkeitsbox-Versatz Y', + 'eme.field.visibleBoundsOffsetZ': 'Sichtbarkeitsbox-Versatz Z', + 'eme.field.animationMode': 'Animationsmodus', + 'eme.field.swingSpeed': 'Schwunggeschwindigkeit', + 'eme.field.walkSpeedMultiplier': 'Geh-Geschwindigkeitsfaktor', + 'eme.field.idleStrength': 'Leerlauf-Intensität', + 'eme.field.gait': 'Gangart', + 'eme.section.host': 'Host', + 'eme.section.dimensions': 'Abmessungen', + 'eme.section.movement': 'Bewegung', + 'eme.section.behavior': 'Verhalten', + 'eme.section.attributes': 'Attribute', + 'eme.section.rendering': 'Darstellung', + 'eme.section.animation': 'Animation', + 'eme.setting.enable_customization': 'Erweiterte Anpassung anzeigen (Easy Model Entities)', + 'eme.setting.enable_experimental': 'Experimentelle Presets anzeigen (Easy Model Entities)', + 'eme.preset.custom': 'Benutzerdefiniert (manuelle Einstellungen)', + 'eme.preset.static': 'Statisch (keine Animation, keine Bewegung)', + 'eme.preset.statue': 'Statue (Anzeige, keine Bewegung)', + 'eme.preset.ticking': 'Tickend (Server- + Client-Tick)', + 'eme.preset.animated': 'Animiert (durchgehende Animation)', + 'eme.preset.animated_randomly': 'Zufällig animiert (zufällige Leerlauf-Schübe)', + 'eme.preset.humanoid_still': 'Humanoid (2 Beine, stehend)', + 'eme.preset.humanoid_wandering': 'Humanoid (2 Beine, umherlaufend)', + 'eme.preset.quadruped_still': 'Vierbeiner (4 Beine, Boden, stehend)', + 'eme.preset.quadruped_wandering': 'Vierbeiner (4 Beine, Boden, umherlaufend)', + 'eme.preset.aquatic_still': 'Wassertier (Fisch, ruhend)', + 'eme.preset.aquatic_swimming': 'Wassertier (Fisch, schwimmend)', + 'eme.preset.amphibious_still': 'Amphibie (Land/Wasser, ruhend)', + 'eme.preset.amphibious_wandering': 'Amphibie (Land/Wasser, umherlaufend)', + 'eme.preset.winged_still': 'Geflügelt (Vogel, sitzend)', + 'eme.preset.winged_wandering': 'Geflügelt (Vogel, fliegend)', + 'eme.preset.winged_humanoid_still': 'Geflügelter Humanoid (stehend)', + 'eme.preset.winged_humanoid_wandering': 'Geflügelter Humanoid (umherlaufend)', + 'eme.preset.arthropod_still': 'Gliederfüßer (Insekt/Spinne, ruhend)', + 'eme.preset.arthropod_wandering': 'Gliederfüßer (Insekt/Spinne, umherlaufend)', + 'eme.preset.cuboid_still': 'Quaderförmig (blockartig, ruhend)', + 'eme.preset.cuboid_hopping': 'Quaderförmig (blockartig, hüpfend)', + 'eme.preset.floating_still': 'Schwebend (ruhend)', + 'eme.body.static': 'Statisch', + 'eme.body.biped': 'Zweibeiner (2 Beine)', + 'eme.body.quadruped': 'Vierbeiner (4 Beine)', + 'eme.body.aquatic': 'Wassertier', + 'eme.body.amphibious': 'Amphibie', + 'eme.body.winged': 'Geflügelt', + 'eme.body.winged_humanoid': 'Geflügelter Humanoid', + 'eme.body.arthropod': 'Gliederfüßer', + 'eme.body.cuboid': 'Quaderförmig', + 'eme.body.floating': 'Schwebend', + 'eme.movement.ground': 'Boden', + 'eme.movement.water': 'Wasser', + 'eme.movement.amphibious': 'Amphibisch', + 'eme.movement.static': 'Statisch', + 'eme.gait.natural': 'Natürlich', + 'eme.gait.feline': 'Katzenartig (leicht, schnell)', + 'eme.gait.ungulate': 'Huftier (schwer, langsam)', + 'eme.behavior.idle_only': 'Nur Leerlauf', + 'eme.behavior.ambient': 'Umgebung (läuft umher)', + 'eme.behavior.static': 'Statisch', + 'eme.behavior.external_owner': 'Externer Besitzer', + 'eme.animation.automatic': 'Automatisch', + 'eme.animation.random_idle': 'Zufälliger Leerlauf', + 'eme.animation.none': 'Keine', + 'eme.entity.ground_entity': 'Boden-Entität', + 'eme.entity.static_entity': 'Statische Entität', + 'eme.entity.aquatic_entity': 'Wasser-Entität', + 'eme.entity.amphibious_entity': 'Amphibien-Entität', + 'eme.modelType.entity': 'Entität', + 'eme.modelType.block_entity': 'Block-Entität', + 'eme.exportType.packs': 'Komplett: Data Pack + Resource Pack (ZIP)', + 'eme.exportType.resource_pack': 'Update: Nur Resource Pack (ZIP)', + 'eme.exportType.data_pack': 'Update: Nur Data Pack (ZIP)', + 'eme.exportType.mod_project': 'Standalone: in Mod-Projekt schreiben', + 'eme.exportType.model_only': 'Nur Modell: Mod-Integration (kein Data Pack)' +}; + +function registerTranslations() { + if (typeof Language !== 'undefined' && typeof Language.addTranslations + === 'function') { + Language.addTranslations('en', EN); + Language.addTranslations('de', DE); + } +} + +function t(key) { + if (typeof tl === 'function') { + const translated = tl(key); + if (translated && translated !== key) { + return translated; + } + } + + return EN[key] || key; +} + +module.exports = {EN, DE, registerTranslations, t}; + + +/***/ }, + +/***/ 763 +(module) { + +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +class ModelDimensions { + static PIXELS_PER_BLOCK = 16; + static FALLBACK = { + width: 0.6, + height: 0.8, + eyeHeight: 0.5 + }; + + static #EYE_HEIGHT_FACTOR = {biped: 0.9, quadruped: 0.8, static: 0.85}; + + static #round(value) { + return Math.round(value * 1000) / 1000; + } + + static #isUsableBounds(bounds) { + if (!bounds) { + return false; + } + + return ['minX', 'minY', 'minZ', 'maxX', 'maxY', 'maxZ'].every( + (key) => Number.isFinite(bounds[key])); + } + + static deriveDimensions(bounds, bodyType) { + if (!ModelDimensions.#isUsableBounds(bounds)) { + return {...ModelDimensions.FALLBACK}; + } + + const width = Math.max( + Math.max(0, bounds.maxX - bounds.minX), + Math.max(0, bounds.maxZ - bounds.minZ) + ) / ModelDimensions.PIXELS_PER_BLOCK; + const height = Math.max(0, bounds.maxY - bounds.minY) + / ModelDimensions.PIXELS_PER_BLOCK; + + if (width <= 0 || height <= 0) { + return {...ModelDimensions.FALLBACK}; + } + + const eyeHeightFactor = ModelDimensions.#EYE_HEIGHT_FACTOR[bodyType] + ?? ModelDimensions.#EYE_HEIGHT_FACTOR.static; + + return { + width: ModelDimensions.#round(width), + height: ModelDimensions.#round(height), + eyeHeight: ModelDimensions.#round(height * eyeHeightFactor) + }; + } + + static applyModelDimensions(settings, modelDimensions) { + if (!modelDimensions) { + return settings; + } + + settings.dimensions.width = modelDimensions.width; + settings.dimensions.height = modelDimensions.height; + settings.dimensions.eyeHeight = modelDimensions.eyeHeight; + return settings; + } +} + +module.exports = {ModelDimensions}; + + +/***/ }, + +/***/ 858 +(module, __unused_webpack_exports, __webpack_require__) { + +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {standardClipNames} = __webpack_require__(151); + +class PresetDetector { + static #CUBE_RATIO = 1.4; + static #ANIMATED_FALLBACK_PRESET = 'floating_still'; + + static #names(stats) { + const boneNames = (stats && stats.boneNames) || []; + return boneNames.map((name) => String(name).toLowerCase()); + } + + static #any(names, ...needles) { + return names.some( + (name) => needles.some((needle) => name.includes(needle))); + } + + static #count(names, needle) { + return names.filter((name) => name.includes(needle)).length; + } + + static #extents(bounds) { + if (!bounds) { + return null; + } + + if (!['minX', 'minY', 'minZ', 'maxX', 'maxY', 'maxZ'].every( + (key) => Number.isFinite(bounds[key]))) { + return null; + } + + return { + x: Math.max(0, bounds.maxX - bounds.minX), + y: Math.max(0, bounds.maxY - bounds.minY), + z: Math.max(0, bounds.maxZ - bounds.minZ) + }; + } + + static detect(stats, bounds) { + const names = PresetDetector.#names(stats); + const hasWings = PresetDetector.#any(names, 'wing'); + const hasArms = PresetDetector.#any(names, 'arm'); + const legCount = PresetDetector.#count(names, 'leg'); + const hasFrontBackLegs = names.some( + (name) => name.includes('leg') + && (name.includes('front') || name.includes('back'))); + + if (hasWings) { + return hasArms + ? {presetType: 'winged_humanoid_still', reason: 'wings + arms'} + : {presetType: 'winged_still', reason: 'wing bones'}; + } + + if (PresetDetector.#any(names, 'fin', 'fluke', 'fish')) { + return {presetType: 'aquatic_still', reason: 'fin/fish bones'}; + } + + if (legCount >= 6 || PresetDetector.#any(names, 'spider', 'insect', + 'mandible')) { + return {presetType: 'arthropod_still', reason: 'six or more legs'}; + } + + if (hasFrontBackLegs || legCount === 4) { + return {presetType: 'quadruped_still', reason: 'four legs (front/back)'}; + } + + if (hasArms) { + return {presetType: 'humanoid_still', reason: 'arm bones'}; + } + + const extents = PresetDetector.#extents(bounds); + if (extents && extents.x > 0 && extents.y > 0 && extents.z > 0) { + if (Math.max(extents.x, extents.y, extents.z) / Math.min(extents.x, + extents.y, extents.z) <= PresetDetector.#CUBE_RATIO) { + return {presetType: 'cuboid_still', reason: 'cube-like proportions'}; + } + if (extents.y < Math.max(extents.x, extents.z) * 0.5) { + return {presetType: 'aquatic_still', reason: 'flat proportions'}; + } + } + + if (standardClipNames(stats && stats.animations).length > 0) { + return { + presetType: PresetDetector.#ANIMATED_FALLBACK_PRESET, + reason: 'animation clips without distinguishing limbs' + }; + } + + return {presetType: 'statue', reason: 'no distinguishing limbs'}; + } + + static detectPresetType(stats, bounds) { + return PresetDetector.detect(stats, bounds).presetType; + } +} + +module.exports = {PresetDetector}; + + +/***/ }, + +/***/ 229 +(module, __unused_webpack_exports, __webpack_require__) { + +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {ResourceLocation} = __webpack_require__(20); +const { + PRESET_TYPES, + BLOCK_ENTITY_PRESET_TYPES, + ANIMATION_CLIPS, + MODEL_TYPE_BLOCK_ENTITY, + standardClipNames +} = __webpack_require__(151); + +const SUPPORTED_ANIMATION_CHANNELS = new Set(['rotation', 'position']); + +class Validator { + static BUDGETS = { + maxTextureSize: 2048, + softTextureSize: 128, + maxModelFileSizeBytes: 2 * 1024 * 1024, + softModelFileSizeBytes: 1024 * 1024, + maxCubeCount: 512, + softCubeCount: 384, + maxBoneCount: 128, + softBoneCount: 96, + maxHierarchyDepth: 32, + softHierarchyDepth: 24, + maxAnimationCount: 16 + }; + + static LIMITS = { + minDimension: 0.01, + maxDimension: 8, + maxMovementSpeed: 2, + maxStepHeight: 2 + }; + + static REQUIRED_BODY_PARTS = { + quadruped: ['root', 'body', 'head', 'front_left_leg', 'front_right_leg', + 'back_left_leg', 'back_right_leg'], + biped: ['root', 'head', 'body', 'left_arm', 'right_arm', 'left_leg', + 'right_leg'], + static: [] + }; + + static #isFiniteNumber(value) { + return typeof value === 'number' && Number.isFinite(value); + } + + static #validateNumeric(errors, label, value) { + if (!Validator.#isFiniteNumber(value)) { + errors.push({ + code: 'INVALID_NUMERIC', + message: `${label} must be a finite number` + }); + return false; + } + + return true; + } + + static #validateRange(errors, code, label, value, minimum, maximum) { + if (!Validator.#validateNumeric(errors, label, value) + || (value >= minimum && value <= maximum)) { + return; + } + + errors.push({ + code: code, + message: `${label} must be between ${minimum} and ${maximum}` + }); + } + + static #validateNonNegative(errors, code, label, value) { + Validator.#validateRange(errors, code, label, value, 0, + Number.MAX_VALUE); + } + + static #validatePositive(errors, code, label, value) { + if (!Validator.#validateNumeric(errors, label, value) || value > 0) { + return; + } + + errors.push({code: code, message: `${label} must be greater than 0`}); + } + + static #warnIfOverBudget(warnings, value, soft, max, softWarning, + maxWarning) { + if (!Validator.#isFiniteNumber(value)) { + return; + } + if (value > max) { + warnings.push(maxWarning); + } else if (value > soft) { + warnings.push(softWarning); + } + } + + static validateSettings(settings, context) { + const errors = []; + const warnings = []; + const ctx = context || {}; + + if (!ResourceLocation.isValidNamespace(settings.namespace)) { + errors.push({ + code: 'INVALID_NAMESPACE', + message: `Invalid namespace: ${settings.namespace}, use for example ` + + `${ResourceLocation.sanitizeNamespace(settings.namespace)}` + }); + } + if (!ResourceLocation.isValidPath(settings.profileId)) { + errors.push({ + code: 'INVALID_PROFILE_ID', + message: `Invalid profile ID: ${settings.profileId}, use for example ` + + `${ResourceLocation.sanitizeProfileId(settings.profileId)}` + }); + } + + const id = `${settings.namespace}:${settings.profileId}`; + if ( + ResourceLocation.isValidNamespace(settings.namespace) && + ResourceLocation.isValidPath(settings.profileId) && + !ResourceLocation.isValidResourceLocation(id) + ) { + errors.push({ + code: 'INVALID_RESOURCE_LOCATION', + message: `Invalid resource location: ${id}` + }); + } + const blockEntity = settings.modelType === MODEL_TYPE_BLOCK_ENTITY; + if (!blockEntity + && !ResourceLocation.isValidResourceLocation( + settings.host.entityType)) { + errors.push({ + code: 'INVALID_RESOURCE_LOCATION', + message: `Invalid host entity type: ${settings.host.entityType}` + }); + } + const validPresets = blockEntity ? BLOCK_ENTITY_PRESET_TYPES : PRESET_TYPES; + if (!validPresets.includes(settings.presetType)) { + errors.push({ + code: 'INVALID_PRESET_TYPE', + message: `Invalid preset type: ${settings.presetType}` + }); + } + + const limits = Validator.LIMITS; + const {width, height, eyeHeight} = settings.dimensions; + Validator.#validateRange(errors, 'INVALID_DIMENSIONS', 'dimensions.width', + width, limits.minDimension, limits.maxDimension); + Validator.#validateRange(errors, 'INVALID_DIMENSIONS', 'dimensions.height', + height, limits.minDimension, limits.maxDimension); + Validator.#validateRange(errors, 'INVALID_DIMENSIONS', + 'dimensions.eye_height', eyeHeight, 0, + Number.isFinite(height) ? height : limits.maxDimension); + + Validator.#validateRange(errors, 'INVALID_MOVEMENT', 'movement.speed', + settings.movement.speed, 0, limits.maxMovementSpeed); + Validator.#validateRange(errors, 'INVALID_MOVEMENT', + 'movement.step_height', settings.movement.stepHeight, 0, + limits.maxStepHeight); + + Validator.#validateNonNegative(errors, 'INVALID_ATTRIBUTES', + 'attributes.max_health', settings.attributes.maxHealth); + Validator.#validateNonNegative(errors, 'INVALID_ATTRIBUTES', + 'attributes.movement_speed', settings.attributes.movementSpeed); + Validator.#validateNonNegative(errors, 'INVALID_ATTRIBUTES', + 'attributes.follow_range', settings.attributes.followRange); + + const rendering = settings.rendering; + Validator.#validatePositive(errors, 'INVALID_RENDER_SETTINGS', + 'rendering.scale', rendering.scale); + Validator.#validateNonNegative(errors, 'INVALID_RENDER_SETTINGS', + 'rendering.shadow_radius', rendering.shadowRadius); + Validator.#validateNonNegative(errors, 'INVALID_RENDER_SETTINGS', + 'rendering.visible_bounds_width', rendering.visibleBoundsWidth ?? 0); + Validator.#validateNonNegative(errors, 'INVALID_RENDER_SETTINGS', + 'rendering.visible_bounds_height', rendering.visibleBoundsHeight ?? 0); + + const animation = settings.animation; + Validator.#validateNonNegative(errors, 'INVALID_ANIMATION_SETTINGS', + 'animation.swing_speed', animation.swingSpeed); + Validator.#validateNonNegative(errors, 'INVALID_ANIMATION_SETTINGS', + 'animation.walk_speed_multiplier', animation.walkSpeedMultiplier); + Validator.#validateNonNegative(errors, 'INVALID_ANIMATION_SETTINGS', + 'animation.idle_strength', animation.idleStrength ?? 1); + + if (ctx.hasModel === false) { + errors.push( + {code: 'MISSING_MODEL', message: 'No model present in project'}); + } + if (ctx.hasTexture === false) { + errors.push( + {code: 'MISSING_TEXTURE', message: 'No texture present in project'}); + } + (ctx.textureIssues || []).forEach((issue) => errors.push(issue)); + + const referencedTextures = ctx.referencedTextures || []; + if (referencedTextures.length > 0) { + warnings.push({ + code: 'EXTERNAL_TEXTURE_REFERENCE', + message: `${referencedTextures.join( + ', ')} stays a reference and is not ` + + 'part of the export; the mod or pack providing it has to be ' + + 'installed, or enable "Include external textures"' + }); + } + + if (Validator.#isFiniteNumber(ctx.textureWidth) + && Validator.#isFiniteNumber(ctx.textureHeight)) { + const textureSize = Math.max(ctx.textureWidth, ctx.textureHeight); + Validator.#warnIfOverBudget(warnings, textureSize, + Validator.BUDGETS.softTextureSize, Validator.BUDGETS.maxTextureSize, + { + code: 'SOFT_TEXTURE_SIZE', + message: `Texture larger than ${Validator.BUDGETS.softTextureSize}x${Validator.BUDGETS.softTextureSize}` + }, + { + code: 'LARGE_TEXTURE', + message: `Texture larger than ${Validator.BUDGETS.maxTextureSize}x${Validator.BUDGETS.maxTextureSize}` + }); + } + Validator.#warnIfOverBudget(warnings, ctx.modelFileSize, + Validator.BUDGETS.softModelFileSizeBytes, + Validator.BUDGETS.maxModelFileSizeBytes, + { + code: 'SOFT_MODEL_SIZE', + message: 'Model file larger than 1 MB' + }, + {code: 'LARGE_MODEL', message: 'Model file larger than 2 MB'}); + Validator.#warnIfOverBudget(warnings, ctx.cubeCount, + Validator.BUDGETS.softCubeCount, Validator.BUDGETS.maxCubeCount, + { + code: 'SOFT_CUBE_COUNT', + message: `More than ${Validator.BUDGETS.softCubeCount} cubes` + }, + { + code: 'HIGH_CUBE_COUNT', + message: `More than ${Validator.BUDGETS.maxCubeCount} cubes` + }); + Validator.#warnIfOverBudget(warnings, ctx.boneCount, + Validator.BUDGETS.softBoneCount, Validator.BUDGETS.maxBoneCount, + { + code: 'SOFT_BONE_COUNT', + message: `More than ${Validator.BUDGETS.softBoneCount} bones` + }, + { + code: 'HIGH_BONE_COUNT', + message: `More than ${Validator.BUDGETS.maxBoneCount} bones` + }); + Validator.#warnIfOverBudget(warnings, ctx.hierarchyDepth, + Validator.BUDGETS.softHierarchyDepth, + Validator.BUDGETS.maxHierarchyDepth, + { + code: 'SOFT_HIERARCHY_DEPTH', + message: `Hierarchy deeper than ${Validator.BUDGETS.softHierarchyDepth}` + }, + { + code: 'DEEP_HIERARCHY', + message: `Hierarchy deeper than ${Validator.BUDGETS.maxHierarchyDepth}` + }); + if (Validator.#isFiniteNumber(ctx.animationCount) + && ctx.animationCount > Validator.BUDGETS.maxAnimationCount) { + errors.push({ + code: 'HIGH_ANIMATION_COUNT', + message: `More than ${Validator.BUDGETS.maxAnimationCount} animations; the mod refuses to load the model` + }); + } + + (ctx.animations || []).forEach((animation) => { + const name = String(animation.name || '').toLowerCase(); + if (!ANIMATION_CLIPS.includes(name)) { + warnings.push({ + code: 'CUSTOM_ANIMATION_CLIP', + message: `Animation "${animation.name}" is not one of ${ANIMATION_CLIPS.join( + ', ')} and is never played automatically; it can only be played by name via command or API` + }); + } + if (animation.hasExpression) { + warnings.push({ + code: 'UNSUPPORTED_ANIMATION_EXPRESSION', + message: `Animation "${animation.name}" uses expressions; the mod only supports numeric keyframes and drops the clip` + }); + } + (animation.channels || []).forEach((channel) => { + if (!SUPPORTED_ANIMATION_CHANNELS.has(channel)) { + warnings.push({ + code: 'UNSUPPORTED_ANIMATION_CHANNEL', + message: `Animation "${animation.name}" uses the "${channel}" channel which is ignored by the mod` + }); + } + }); + }); + + const clipNames = standardClipNames(ctx.animations); + if (!blockEntity && clipNames.length > 0 + && (settings.animation.mode === 'none' + || settings.host.bodyType === 'static')) { + warnings.push({ + code: 'ANIMATIONS_NEVER_PLAYED', + message: `The preset ${settings.presetType} renders without animation, ` + + `so the mod never plays ${clipNames.join( + ', ')}; pick an animated ` + + 'preset instead' + }); + } + + Validator.getMissingBodyParts(settings.host.bodyType, + ctx.boneNames || []).forEach((part) => { + warnings.push({ + code: 'MISSING_BODY_PART', + message: `Missing recommended body part: ${part}` + }); + }); + + return {errors, warnings, valid: errors.length === 0}; + } + + static getMissingBodyParts(bodyType, boneNames) { + const required = Validator.REQUIRED_BODY_PARTS[bodyType] || []; + const present = new Set( + (boneNames || []).map((name) => String(name).toLowerCase())); + + return required.filter((part) => !present.has(part)); + } + + static validateOutputPath(relativePath) { + if (typeof relativePath !== 'string' || relativePath.length === 0) { + return { + valid: false, + code: 'INVALID_OUTPUT_PATH', + message: 'Empty output path' + }; + } + + const normalized = relativePath.replaceAll('\\', '/'); + if (normalized.includes('..')) { + return { + valid: false, + code: 'PATH_TRAVERSAL', + message: 'Path traversal detected' + }; + } + + if (/^([a-zA-Z]:\/|\/)/.test(normalized)) { + return { + valid: false, + code: 'ABSOLUTE_PATH', + message: 'Absolute paths are not allowed' + }; + } + + return {valid: true, path: normalized}; + } +} + +module.exports = {Validator}; + + +/***/ }, + +/***/ 508 +(module) { + +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +class VisibleBounds { + static PIXELS_PER_BLOCK = 16; + + static #empty() { + return {width: 0, height: 0, offset: [0, 0, 0]}; + } + + static #round(value) { + return Math.round(value * 1000) / 1000; + } + + static #usableCubes(cubes) { + if (!Array.isArray(cubes)) { + return []; + } + + return cubes.filter((cube) => + cube && Array.isArray(cube.from) && Array.isArray(cube.to) + && cube.from.length === 3 && cube.to.length === 3 + && [...cube.from, ...cube.to].every((value) => Number.isFinite(value))); + } + + static #bounds(cubes) { + const bounds = { + min: [Infinity, Infinity, Infinity], + max: [-Infinity, -Infinity, -Infinity] + }; + cubes.forEach((cube) => { + for (let axis = 0; axis < 3; axis++) { + bounds.min[axis] = Math.min(bounds.min[axis], cube.from[axis], + cube.to[axis]); + bounds.max[axis] = Math.max(bounds.max[axis], cube.from[axis], + cube.to[axis]); + } + }); + + return bounds; + } + + static derive(cubes) { + const usable = VisibleBounds.#usableCubes(cubes); + if (usable.length === 0) { + return VisibleBounds.#empty(); + } + + const ppb = VisibleBounds.PIXELS_PER_BLOCK; + const bounds = VisibleBounds.#bounds(usable); + const xExtent = bounds.max[0] - bounds.min[0]; + const yExtent = bounds.max[1] - bounds.min[1]; + const zExtent = bounds.max[2] - bounds.min[2]; + const width = Math.max(xExtent, zExtent) / ppb; + const height = yExtent / ppb; + + if (width <= 0 || height <= 0) { + return VisibleBounds.#empty(); + } + + return { + width: VisibleBounds.#round(width), + height: VisibleBounds.#round(height), + offset: [ + VisibleBounds.#round(((bounds.min[0] + bounds.max[0]) / 2) / ppb), + VisibleBounds.#round(bounds.min[1] / ppb), + VisibleBounds.#round(((bounds.min[2] + bounds.max[2]) / 2) / ppb) + ] + }; + } + + static hasVisibleBounds(settings) { + const rendering = settings && settings.rendering; + return !!rendering && rendering.visibleBoundsWidth > 0 + && rendering.visibleBoundsHeight > 0; + } + + static applyVisibleBounds(settings, derived, options) { + if (!derived || !settings || !settings.rendering) { + return settings; + } + if (options?.preserveExisting && VisibleBounds.hasVisibleBounds(settings)) { + return settings; + } + + settings.rendering.visibleBoundsWidth = derived.width; + settings.rendering.visibleBoundsHeight = derived.height; + settings.rendering.visibleBoundsOffset = derived.offset.slice(); + return settings; + } +} + +module.exports = {VisibleBounds}; + + +/***/ }, + +/***/ 984 +(module) { + +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const EXPORT_TYPE_PACKS = 'packs'; +const EXPORT_TYPE_RESOURCE_PACK = 'resource_pack'; +const EXPORT_TYPE_DATA_PACK = 'data_pack'; +const EXPORT_TYPE_MOD_PROJECT = 'mod_project'; +const EXPORT_TYPE_MODEL_ONLY = 'model_only'; + +const ZIP_EXPORT_TYPES = [EXPORT_TYPE_PACKS, EXPORT_TYPE_RESOURCE_PACK, + EXPORT_TYPE_DATA_PACK]; + +function isZipExport(exportType) { + return ZIP_EXPORT_TYPES.includes(exportType); +} + +function includesDataPack(exportType) { + return exportType !== EXPORT_TYPE_RESOURCE_PACK; +} + +function includesResourcePack(exportType) { + return exportType !== EXPORT_TYPE_DATA_PACK; +} + +function isSinglePackExport(exportType) { + return exportType === EXPORT_TYPE_RESOURCE_PACK + || exportType === EXPORT_TYPE_DATA_PACK; +} + +module.exports = { + EXPORT_TYPE_PACKS, + EXPORT_TYPE_RESOURCE_PACK, + EXPORT_TYPE_DATA_PACK, + EXPORT_TYPE_MOD_PROJECT, + EXPORT_TYPE_MODEL_ONLY, + isZipExport, + includesDataPack, + includesResourcePack, + isSinglePackExport +}; + + +/***/ }, + +/***/ 151 +(module) { + +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const SCHEMA_VERSION = '0.2.0'; + +const MODEL_TYPE_ENTITY = 'entity'; +const MODEL_TYPE_BLOCK_ENTITY = 'block_entity'; +const MODEL_TYPES = [MODEL_TYPE_ENTITY, MODEL_TYPE_BLOCK_ENTITY]; + +const BODY_TYPES = [ + 'static', + 'biped', + 'quadruped', + 'aquatic', + 'amphibious', + 'winged', + 'winged_humanoid', + 'arthropod', + 'cuboid', + 'floating' +]; + +const MOVEMENT_TYPES = ['ground', 'water', 'amphibious', 'static']; +const BEHAVIOR_MODES = ['idle_only', 'ambient', 'static', 'external_owner']; +const ANIMATION_MODES = ['automatic', 'random_idle', 'none']; +const ANIMATION_CLIPS = ['idle', 'walk', 'swim', 'fly', 'hurt', 'death', + 'attack', 'sit']; +const GAIT_TYPES = ['natural', 'feline', 'ungulate']; + +const GROUND_ENTITY = 'easy_model_entities:ground_entity'; +const STATIC_ENTITY = 'easy_model_entities:static_entity'; +const AQUATIC_ENTITY = 'easy_model_entities:aquatic_entity'; +const AMPHIBIOUS_ENTITY = 'easy_model_entities:amphibious_entity'; + +const FALLBACK_DIMENSIONS = {width: 0.6, height: 1.8, eyeHeight: 1.62}; +const BLOCK_ENTITY_DIMENSIONS = {width: 1.0, height: 1.0, eyeHeight: 0.5}; + +const DEFAULT_MAX_HEALTH = 10; +const DEFAULT_FOLLOW_RANGE = 16; + +const PRESET_TYPES = [ + 'custom', + 'static', + 'statue', + 'humanoid_still', + 'humanoid_wandering', + 'quadruped_still', + 'quadruped_wandering', + 'aquatic_still', + 'aquatic_swimming', + 'amphibious_still', + 'amphibious_wandering', + 'winged_still', + 'winged_wandering', + 'winged_humanoid_still', + 'winged_humanoid_wandering', + 'arthropod_still', + 'arthropod_wandering', + 'cuboid_still', + 'cuboid_hopping', + 'floating_still' +]; + +const BLOCK_ENTITY_PRESET_TYPES = [ + 'static', + 'ticking', + 'animated', + 'animated_randomly' +]; + +const STABLE_PRESET_TYPES = new Set([ + 'static', + 'statue', + 'humanoid_still', + 'humanoid_wandering', + 'quadruped_still', + 'quadruped_wandering', + 'aquatic_still', + 'aquatic_swimming', + 'amphibious_still', + 'amphibious_wandering', + 'winged_still', + 'winged_wandering', + 'winged_humanoid_still', + 'winged_humanoid_wandering', + 'arthropod_still', + 'arthropod_wandering', + 'cuboid_still', + 'cuboid_hopping', + 'floating_still', + // Block entity stable presets. + 'animated', + 'animated_randomly' +]); + +const SELECTABLE_PRESET_TYPES = PRESET_TYPES.filter((id) => id !== 'custom'); + +function standardClipNames(animations) { + return (animations || []) + .map((animation) => String(animation.name || '').toLowerCase()) + .filter((name) => ANIMATION_CLIPS.includes(name)); +} + +function isStablePreset(presetType) { + return STABLE_PRESET_TYPES.has(presetType); +} + +function isCustom(presetType) { + return presetType === 'custom'; +} + +function isStill(presetType) { + return String(presetType).endsWith('_still'); +} + +function isMoving(presetType) { + return String(presetType).endsWith('_wandering') + || presetType === 'aquatic_swimming' + || presetType === 'cuboid_hopping'; +} + +function bodyType(presetType) { + switch (presetType) { + case 'humanoid_still': + case 'humanoid_wandering': + return 'biped'; + case 'quadruped_still': + case 'quadruped_wandering': + return 'quadruped'; + case 'aquatic_still': + case 'aquatic_swimming': + return 'aquatic'; + case 'amphibious_still': + case 'amphibious_wandering': + return 'amphibious'; + case 'winged_still': + case 'winged_wandering': + return 'winged'; + case 'winged_humanoid_still': + case 'winged_humanoid_wandering': + return 'winged_humanoid'; + case 'arthropod_still': + case 'arthropod_wandering': + return 'arthropod'; + case 'cuboid_still': + case 'cuboid_hopping': + return 'cuboid'; + case 'floating_still': + return 'floating'; + default: + return 'static'; + } +} + +function isAquatic(presetType) { + return presetType === 'aquatic_still' || presetType === 'aquatic_swimming'; +} + +function isAmphibious(presetType) { + return presetType === 'amphibious_still' + || presetType === 'amphibious_wandering'; +} + +function movementType(presetType) { + if (isAquatic(presetType)) { + return 'water'; + } + if (isAmphibious(presetType)) { + return 'amphibious'; + } + + return isMoving(presetType) ? 'ground' : 'static'; +} + +function entityType(presetType) { + if (isAquatic(presetType)) { + return AQUATIC_ENTITY; + } + if (isAmphibious(presetType)) { + return AMPHIBIOUS_ENTITY; + } + + return isMoving(presetType) || presetType === 'static' ? GROUND_ENTITY + : STATIC_ENTITY; +} + +function presetDimensions(presetType) { + switch (presetType) { + case 'quadruped_still': + case 'quadruped_wandering': + return {width: 0.9, height: 0.9, eyeHeight: 0.6}; + case 'aquatic_still': + case 'aquatic_swimming': + return {width: 0.7, height: 0.4, eyeHeight: 0.25}; + case 'amphibious_still': + case 'amphibious_wandering': + return {width: 0.9, height: 0.6, eyeHeight: 0.4}; + case 'winged_still': + case 'winged_wandering': + return {width: 0.6, height: 0.9, eyeHeight: 0.6}; + case 'winged_humanoid_still': + case 'winged_humanoid_wandering': + return {width: 0.6, height: 0.8, eyeHeight: 0.6}; + case 'arthropod_still': + case 'arthropod_wandering': + return {width: 1.4, height: 0.9, eyeHeight: 0.45}; + case 'cuboid_still': + case 'cuboid_hopping': + case 'floating_still': + return {width: 1.0, height: 1.0, eyeHeight: 0.5}; + default: + return {...FALLBACK_DIMENSIONS}; + } +} + +function presetShadowRadius(presetType) { + switch (presetType) { + case 'quadruped_still': + case 'quadruped_wandering': + case 'amphibious_still': + case 'amphibious_wandering': + return 0.45; + case 'aquatic_still': + case 'aquatic_swimming': + case 'winged_still': + case 'winged_wandering': + case 'winged_humanoid_still': + case 'winged_humanoid_wandering': + return 0.25; + case 'arthropod_still': + case 'arthropod_wandering': + return 0.7; + case 'cuboid_still': + case 'cuboid_hopping': + case 'floating_still': + return 0.5; + default: + return 0.3; + } +} + +function behaviorModeFor(presetType, move) { + if (isMoving(presetType)) { + return 'ambient'; + } + + if (isStill(presetType)) { + return 'idle_only'; + } + + if (presetType === 'custom') { + return move === 'ground' || move === 'amphibious' ? 'idle_only' : 'static'; + } + + return 'static'; +} + +function behaviorMode(presetType) { + return behaviorModeFor(presetType, movementType(presetType)); +} + +function wandersByMovement(move) { + return move === 'ground' || move === 'amphibious' || move === 'water'; +} + +function defaultSpeed(move) { + switch (move) { + case 'ground': + return 0.22; + case 'amphibious': + return 0.15; + case 'water': + return 0.1; + default: + return 0; + } +} + +function movementDefaults(presetType, move) { + return { + speed: defaultSpeed(move), + stepHeight: move === 'ground' || move === 'amphibious' ? 0.6 : 0, + gravity: presetType === 'static' || move !== 'static' + }; +} + +function animationMode(presetType) { + return presetType === 'custom' || presetType === 'static' + || presetType === 'statue' ? 'none' : 'automatic'; +} + +function blockEntityAnimationMode(presetType) { + switch (presetType) { + case 'animated': + return 'automatic'; + case 'animated_randomly': + return 'random_idle'; + default: + return 'none'; + } +} + +function defaultRenderingSettings(presetType) { + return { + scale: 1, + shadowRadius: presetShadowRadius(presetType), + visibleBoundsWidth: 0, + visibleBoundsHeight: 0, + visibleBoundsOffset: [0, 0, 0] + }; +} + +function defaultAnimationSettings(mode) { + return { + mode: mode, + swingSpeed: 1, + walkSpeedMultiplier: 1, + idleStrength: 1, + gait: 'natural' + }; +} + +function blockEntityPresetDefaults(presetType) { + return { + schemaVersion: SCHEMA_VERSION, + modelType: MODEL_TYPE_BLOCK_ENTITY, + presetType: presetType, + host: {entityType: '', movementType: 'static', bodyType: 'static'}, + dimensions: {...BLOCK_ENTITY_DIMENSIONS}, + movement: {speed: 0, stepHeight: 0, gravity: false}, + behavior: {mode: 'static', lookAtPlayers: false, randomStroll: false}, + attributes: { + maxHealth: DEFAULT_MAX_HEALTH, + movementSpeed: 0, + followRange: DEFAULT_FOLLOW_RANGE + }, + rendering: defaultRenderingSettings('cuboid_still'), + animation: defaultAnimationSettings(blockEntityAnimationMode(presetType)) + }; +} + +function presetDefaults(presetType, modelType) { + if (modelType === MODEL_TYPE_BLOCK_ENTITY) { + return blockEntityPresetDefaults(presetType); + } + + const move = movementType(presetType); + const movement = movementDefaults(presetType, move); + const mode = behaviorMode(presetType); + + return { + schemaVersion: SCHEMA_VERSION, + modelType: MODEL_TYPE_ENTITY, + presetType: presetType, + host: { + entityType: entityType(presetType), + movementType: move, + bodyType: bodyType(presetType) + }, + dimensions: presetDimensions(presetType), + movement: { + speed: movement.speed, + stepHeight: movement.stepHeight, + gravity: movement.gravity + }, + behavior: { + mode: mode, + lookAtPlayers: mode === 'idle_only' || mode === 'ambient', + randomStroll: wandersByMovement(move) && mode === 'ambient' + }, + attributes: { + maxHealth: DEFAULT_MAX_HEALTH, + movementSpeed: movement.speed, + followRange: DEFAULT_FOLLOW_RANGE + }, + rendering: defaultRenderingSettings(presetType), + animation: defaultAnimationSettings(animationMode(presetType)) + }; +} + +module.exports = { + SCHEMA_VERSION, + MODEL_TYPE_ENTITY, + MODEL_TYPE_BLOCK_ENTITY, + MODEL_TYPES, + BODY_TYPES, + MOVEMENT_TYPES, + DEFAULT_MAX_HEALTH, + DEFAULT_FOLLOW_RANGE, + BEHAVIOR_MODES, + PRESET_TYPES, + BLOCK_ENTITY_PRESET_TYPES, + ANIMATION_MODES, + ANIMATION_CLIPS, + GAIT_TYPES, + SELECTABLE_PRESET_TYPES, + standardClipNames, + isStablePreset, + isCustom, + bodyType, + behaviorModeFor, + movementDefaults, + wandersByMovement, + animationMode, + presetDimensions, + presetDefaults, + defaultRenderingSettings, + defaultAnimationSettings +}; + + +/***/ }, + +/***/ 668 +(module, __unused_webpack_exports, __webpack_require__) { + +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {MODEL_TYPE_ENTITY, presetDefaults} = __webpack_require__(151); +const {getDefaultVersionId} = __webpack_require__(954); + +const DEFAULT_PRESET = 'statue'; + +const MODEL_SETTING_KEYS = [ + 'schemaVersion', + 'modelType', + 'presetType', + 'namespace', + 'profileId', + 'targetVersion', + 'customize', + 'includeExternalTextures', + 'host', + 'dimensions', + 'movement', + 'behavior', + 'attributes', + 'rendering', + 'animation', + 'lastExportedVersion' +]; + +function pickModelSettings(settings) { + if (!settings || typeof settings !== 'object') { + return settings; + } + + const picked = {}; + MODEL_SETTING_KEYS.forEach((key) => { + if (settings[key] !== undefined) { + picked[key] = settings[key]; + } + }); + + return picked; +} + +function deepMerge(target, source) { + const result = Array.isArray(target) ? target.slice() : ({ + ...target + }); + Object.keys(source).forEach((key) => { + const value = source[key]; + if (value && typeof value === 'object' && !Array.isArray(value)) { + result[key] = deepMerge(result[key] || {}, value); + } else if (Array.isArray(value)) { + result[key] = value.slice(); + } else { + result[key] = value; + } + }); + + return result; +} + +function getDefaults() { + return deepMerge(presetDefaults(DEFAULT_PRESET), { + namespace: 'example_org', + profileId: 'entity', + targetVersion: getDefaultVersionId() + }); +} + +function applyTemplate(presetType, modelType) { + return deepMerge(getDefaults(), + presetDefaults(presetType, modelType || MODEL_TYPE_ENTITY)); +} + +module.exports = { + DEFAULT_PRESET, + getDefaults, + applyTemplate, + deepMerge, + pickModelSettings +}; + + +/***/ }, + +/***/ 954 +(module) { + +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const DEFAULT_VERSION_ID = '1.20.1'; + +const VERSIONS = [ + { + id: '1.20.1', + label: 'Minecraft 1.20.1', + data: {packFormat: 15}, + resource: {packFormat: 15}, + enabled: true + }, + { + id: '1.21.1', + label: 'Minecraft 1.21.1', + data: {packFormat: 48}, + resource: {packFormat: 34}, + enabled: true + }, + { + id: '1.21.11', + label: 'Minecraft 1.21.11', + data: {packFormat: 94, minFormat: [94, 1], maxFormat: [94, 1]}, + resource: {packFormat: 75, minFormat: [75, 0], maxFormat: [75, 0]}, + enabled: true + }, + { + id: '26.1.2', + label: 'Minecraft 26.1.2', + data: {packFormat: 101, minFormat: [101, 1], maxFormat: [101, 1]}, + resource: {packFormat: 84, minFormat: [84, 0], maxFormat: [84, 0]}, + enabled: true + }, + { + id: '26.2', + label: 'Minecraft 26.2', + data: {packFormat: 101, minFormat: [101, 1], maxFormat: [101, 1]}, + resource: {packFormat: 84, minFormat: [84, 0], maxFormat: [84, 0]}, + enabled: true + } +]; + +function getVersions() { + return VERSIONS.map((version) => ({...version})); +} + +function getVersion(id) { + return VERSIONS.find((version) => version.id === id) || null; +} + +function getDefaultVersionId() { + return DEFAULT_VERSION_ID; +} + +function getPackFormats(id) { + const version = getVersion(id); + if (!version?.enabled || !version.data || !version.resource) { + return null; + } + return { + data: version.data, + resource: version.resource + }; +} + +module.exports = { + getVersions, + getDefaultVersionId, + getPackFormats +}; + + +/***/ }, + +/***/ 924 +(module, __unused_webpack_exports, __webpack_require__) { + +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const { + MODEL_TYPE_ENTITY, + MODEL_TYPE_BLOCK_ENTITY +} = __webpack_require__(151); +const {EXPORT_TYPE_DATA_PACK} = __webpack_require__(984); +const {Validator} = __webpack_require__(229); +const {t} = __webpack_require__(16); +const { + presetOptions, + modelTypeOptions, + exportTypeOptions, + hostEntityTypeOptions, + movementTypeOptions, + bodyTypeOptions, + gaitOptions, + behaviorModeOptions, + animationModeOptions, + versionOptions +} = __webpack_require__(664); +const { + settingsToForm, + activeModelType, + activePreset, + presetFormValues, + resolveExportSettings +} = __webpack_require__(807); + +function advancedField(field, showCustomize) { + return Object.assign({condition: showCustomize}, field); +} + +function buildFormConfig(settings, ui) { + const limits = Validator.LIMITS; + const values = settingsToForm(settings); + const state = ui || {}; + const allowCustomize = !!state.showCustomization; + + const standalone = (form) => form.exportType !== 'model_only'; + const blockEntitySelected = (form) => + standalone(form) && form.modelType === MODEL_TYPE_BLOCK_ENTITY; + const customizeOn = (form) => allowCustomize && !!form.customize; + const showServer = (form) => customizeOn(form) && standalone(form); + const showEntity = (form) => showServer(form) && !blockEntitySelected(form); + const showRender = customizeOn; + + const experimental = !!state.experimental; + const entityValue = state.preset || 'statue'; + const blockValue = state.blockPreset || 'static'; + + const config = { + exportType: { + label: t('eme.field.exportType'), + type: 'select', + options: exportTypeOptions(), + value: state.exportType || 'packs' + }, + modelType: { + label: t('eme.field.modelType'), + type: 'select', + options: modelTypeOptions(), + value: state.modelType || MODEL_TYPE_ENTITY, + condition: standalone + }, + preset: { + label: t('eme.field.preset'), + type: 'select', + options: presetOptions(MODEL_TYPE_ENTITY, experimental, entityValue), + value: entityValue, + condition: (form) => !blockEntitySelected(form) + }, + blockPreset: { + label: t('eme.field.preset'), + type: 'select', + options: presetOptions(MODEL_TYPE_BLOCK_ENTITY, experimental, blockValue), + value: blockValue, + condition: blockEntitySelected + }, + namespace: { + label: t('eme.field.namespace'), + type: 'text', + value: values.namespace, + placeholder: 'your_mod_id' + }, + profileId: { + label: t('eme.field.profileId'), type: 'text', + value: values.profileId + }, + targetVersion: { + label: t('eme.field.targetVersion'), + type: 'select', + options: versionOptions(), + value: values.targetVersion + } + }; + + if (state.hasExternalTextures) { + config.includeExternalTextures = { + label: t('eme.field.includeExternalTextures'), + description: t('eme.field.includeExternalTexturesDescription'), + type: 'checkbox', + value: settings.includeExternalTextures !== false, + condition: (form) => form.exportType !== EXPORT_TYPE_DATA_PACK + }; + } + + if (allowCustomize) { + config.customize = { + label: t('eme.field.customize'), + type: 'checkbox', + value: !!state.customize + }; + } + + Object.assign(config, { + host_header: advancedField( + {type: 'info', text: `### ${t('eme.section.host')}`}, showEntity), + hostEntityType: advancedField({ + label: t('eme.field.hostEntityType'), + type: 'select', + options: hostEntityTypeOptions(), + value: values.hostEntityType + }, showEntity), + movementType: advancedField({ + label: t('eme.field.movementType'), + type: 'select', + options: movementTypeOptions(), + value: values.movementType + }, showEntity), + bodyType: advancedField({ + label: t('eme.field.bodyType'), + type: 'select', + options: bodyTypeOptions(), + value: values.bodyType + }, showEntity), + + dimensions_header: advancedField( + {type: 'info', text: `### ${t('eme.section.dimensions')}`}, + showServer), + width: advancedField( + { + label: t('eme.field.width'), type: 'number', value: values.width, + step: 0.1, min: limits.minDimension, max: limits.maxDimension + }, showServer), + height: advancedField( + { + label: t('eme.field.height'), type: 'number', value: values.height, + step: 0.1, min: limits.minDimension, max: limits.maxDimension + }, showServer), + eyeHeight: advancedField( + { + label: t('eme.field.eyeHeight'), type: 'number', + value: values.eyeHeight, step: 0.1, min: 0, + max: limits.maxDimension + }, showServer), + + movement_header: advancedField( + {type: 'info', text: `### ${t('eme.section.movement')}`}, showEntity), + speed: advancedField( + { + label: t('eme.field.speed'), type: 'number', value: values.speed, + step: 0.01, min: 0, max: limits.maxMovementSpeed + }, showEntity), + stepHeight: advancedField( + { + label: t('eme.field.stepHeight'), type: 'number', + value: values.stepHeight, step: 0.1, min: 0, + max: limits.maxStepHeight + }, showEntity), + gravity: advancedField( + { + label: t('eme.field.gravity'), type: 'checkbox', + value: values.gravity + }, showEntity), + + behavior_header: advancedField( + {type: 'info', text: `### ${t('eme.section.behavior')}`}, showEntity), + behaviorMode: advancedField({ + label: t('eme.field.behaviorMode'), + type: 'select', + options: behaviorModeOptions(), + value: values.behaviorMode + }, showEntity), + + attributes_header: advancedField( + {type: 'info', text: `### ${t('eme.section.attributes')}`}, + showEntity), + maxHealth: advancedField( + { + label: t('eme.field.maxHealth'), type: 'number', + value: values.maxHealth, step: 0.5, min: 0 + }, showEntity), + movementSpeed: advancedField( + { + label: t('eme.field.movementSpeed'), type: 'number', + value: values.movementSpeed, step: 0.01, min: 0 + }, showEntity), + followRange: advancedField( + { + label: t('eme.field.followRange'), type: 'number', + value: values.followRange, step: 1, min: 0 + }, showEntity), + + rendering_header: advancedField( + {type: 'info', text: `### ${t('eme.section.rendering')}`}, + showRender), + scale: advancedField( + { + label: t('eme.field.scale'), type: 'number', value: values.scale, + step: 0.1, min: limits.minDimension + }, showRender), + shadowRadius: advancedField( + { + label: t('eme.field.shadowRadius'), type: 'number', + value: values.shadowRadius, step: 0.1, min: 0 + }, showRender), + visibleBoundsWidth: advancedField( + { + label: t('eme.field.visibleBoundsWidth'), type: 'number', + value: values.visibleBoundsWidth, step: 0.1, min: 0 + }, showRender), + visibleBoundsHeight: advancedField( + { + label: t('eme.field.visibleBoundsHeight'), type: 'number', + value: values.visibleBoundsHeight, step: 0.1, min: 0 + }, showRender), + visibleBoundsOffsetX: advancedField( + { + label: t('eme.field.visibleBoundsOffsetX'), type: 'number', + value: values.visibleBoundsOffsetX, step: 0.1 + }, showRender), + visibleBoundsOffsetY: advancedField( + { + label: t('eme.field.visibleBoundsOffsetY'), type: 'number', + value: values.visibleBoundsOffsetY, step: 0.1 + }, showRender), + visibleBoundsOffsetZ: advancedField( + { + label: t('eme.field.visibleBoundsOffsetZ'), type: 'number', + value: values.visibleBoundsOffsetZ, step: 0.1 + }, showRender), + + animation_header: advancedField( + {type: 'info', text: `### ${t('eme.section.animation')}`}, + showRender), + animationMode: advancedField({ + label: t('eme.field.animationMode'), + type: 'select', + options: animationModeOptions(), + value: values.animationMode + }, showRender), + swingSpeed: advancedField( + { + label: t('eme.field.swingSpeed'), type: 'number', + value: values.swingSpeed, step: 0.1, min: 0 + }, showRender), + walkSpeedMultiplier: advancedField( + { + label: t('eme.field.walkSpeedMultiplier'), type: 'number', + value: values.walkSpeedMultiplier, step: 0.1, min: 0 + }, showRender), + idleStrength: advancedField( + { + label: t('eme.field.idleStrength'), type: 'number', + value: values.idleStrength, step: 0.1, min: 0 + }, showRender), + gait: advancedField({ + label: t('eme.field.gait'), + type: 'select', + options: gaitOptions(), + value: values.gait + }, showRender) + }); + + return config; +} + +function openExportDialog(options) { + const settings = options.settings; + const modelDimensions = options.modelDimensions; + const visibleBounds = options.visibleBounds; + let lastKey = `${activeModelType(options)}|${activePreset(options)}`; + + const dialog = new Dialog({ + id: 'eme_export_dialog', + title: t('eme.dialog.title'), + width: 560, + form: buildFormConfig(settings, { + preset: options.preset, + blockPreset: options.blockPreset, + modelType: options.modelType, + exportType: options.exportType, + customize: options.customize, + showCustomization: options.showCustomization, + experimental: options.experimental, + hasExternalTextures: options.hasExternalTextures + }), + onFormChange(form) { + const modelType = activeModelType(form); + const preset = activePreset(form); + const key = `${modelType}|${preset}`; + if (key !== lastKey) { + lastKey = key; + if (modelType !== MODEL_TYPE_ENTITY || preset !== 'custom') { + dialog.setFormValues( + presetFormValues(preset, modelType, modelDimensions, + visibleBounds), false); + } + } + }, + onConfirm(form) { + const finalSettings = resolveExportSettings(form, settings, + modelDimensions, visibleBounds); + options.onExport(finalSettings, finalSettings.exportTarget); + } + }); + dialog.show(); + + return dialog; +} + +module.exports = { + openExportDialog +}; + + +/***/ }, + +/***/ 664 +(module, __unused_webpack_exports, __webpack_require__) { + +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {getVersions} = __webpack_require__(954); +const { + SELECTABLE_PRESET_TYPES, + BLOCK_ENTITY_PRESET_TYPES, + MOVEMENT_TYPES, + BODY_TYPES, + GAIT_TYPES, + BEHAVIOR_MODES, + ANIMATION_MODES, + isStablePreset, + MODEL_TYPE_ENTITY, + MODEL_TYPE_BLOCK_ENTITY +} = __webpack_require__(151); +const { + EXPORT_TYPE_PACKS, + EXPORT_TYPE_RESOURCE_PACK, + EXPORT_TYPE_DATA_PACK, + EXPORT_TYPE_MOD_PROJECT, + EXPORT_TYPE_MODEL_ONLY +} = __webpack_require__(984); +const {t} = __webpack_require__(16); + +function optionsFromEnum(values, prefix) { + const options = {}; + values.forEach((id) => { + options[id] = t(`${prefix}.${id}`); + }); + + return options; +} + +function presetLabel(id) { + return id === 'custom' ? t('eme.preset.custom') : t(`eme.preset.${id}`); +} + +function entityPresetIds(experimental) { + return experimental + ? ['custom', ...SELECTABLE_PRESET_TYPES] + : SELECTABLE_PRESET_TYPES.filter(isStablePreset); +} + +function blockPresetIds(experimental) { + return experimental + ? [...BLOCK_ENTITY_PRESET_TYPES] + : BLOCK_ENTITY_PRESET_TYPES.filter(isStablePreset); +} + +function presetOptions(modelType, experimental, ensure) { + const ids = modelType === MODEL_TYPE_BLOCK_ENTITY + ? blockPresetIds(experimental) : entityPresetIds(experimental); + if (ensure && !ids.includes(ensure)) { + ids.push(ensure); + } + const options = {}; + ids.forEach((id) => { + options[id] = presetLabel(id); + }); + + return options; +} + +function modelTypeOptions() { + return { + [MODEL_TYPE_ENTITY]: t('eme.modelType.entity'), + [MODEL_TYPE_BLOCK_ENTITY]: t('eme.modelType.block_entity') + }; +} + +function exportTypeOptions() { + return { + [EXPORT_TYPE_PACKS]: t('eme.exportType.packs'), + [EXPORT_TYPE_RESOURCE_PACK]: t('eme.exportType.resource_pack'), + [EXPORT_TYPE_DATA_PACK]: t('eme.exportType.data_pack'), + [EXPORT_TYPE_MOD_PROJECT]: t('eme.exportType.mod_project'), + [EXPORT_TYPE_MODEL_ONLY]: t('eme.exportType.model_only') + }; +} + +function hostEntityTypeOptions() { + return { + 'easy_model_entities:ground_entity': t('eme.entity.ground_entity'), + 'easy_model_entities:static_entity': t('eme.entity.static_entity'), + 'easy_model_entities:aquatic_entity': t('eme.entity.aquatic_entity'), + 'easy_model_entities:amphibious_entity': t('eme.entity.amphibious_entity') + }; +} + +function movementTypeOptions() { + return optionsFromEnum(MOVEMENT_TYPES, 'eme.movement'); +} + +function bodyTypeOptions() { + return optionsFromEnum(BODY_TYPES, 'eme.body'); +} + +function gaitOptions() { + return optionsFromEnum(GAIT_TYPES, 'eme.gait'); +} + +function behaviorModeOptions() { + return optionsFromEnum(BEHAVIOR_MODES, 'eme.behavior'); +} + +function animationModeOptions() { + return optionsFromEnum(ANIMATION_MODES, 'eme.animation'); +} + +function versionOptions() { + const options = {}; + getVersions().forEach((version) => { + options[version.id] = version.enabled ? version.label + : `${version.label} (coming soon)`; + }); + + return options; +} + +module.exports = { + presetOptions, + modelTypeOptions, + exportTypeOptions, + hostEntityTypeOptions, + movementTypeOptions, + bodyTypeOptions, + gaitOptions, + behaviorModeOptions, + animationModeOptions, + versionOptions +}; + + +/***/ }, + +/***/ 807 +(module, __unused_webpack_exports, __webpack_require__) { + +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {getDefaults, applyTemplate, deepMerge} = __webpack_require__(668); +const { + MODEL_TYPE_ENTITY, + MODEL_TYPE_BLOCK_ENTITY +} = __webpack_require__(151); +const { + EXPORT_TYPE_PACKS, + EXPORT_TYPE_MODEL_ONLY, + EXPORT_TYPE_MOD_PROJECT, + isZipExport +} = __webpack_require__(984); +const {ModelDimensions} = __webpack_require__(763); +const {VisibleBounds} = __webpack_require__(508); + +function settingsToForm(settings) { + const offset = settings.rendering.visibleBoundsOffset || [0, 0, 0]; + + return { + namespace: settings.namespace, + profileId: settings.profileId, + targetVersion: settings.targetVersion, + hostEntityType: settings.host.entityType, + movementType: settings.host.movementType, + bodyType: settings.host.bodyType, + width: settings.dimensions.width, + height: settings.dimensions.height, + eyeHeight: settings.dimensions.eyeHeight, + speed: settings.movement.speed, + stepHeight: settings.movement.stepHeight, + gravity: settings.movement.gravity, + behaviorMode: settings.behavior.mode, + maxHealth: settings.attributes.maxHealth, + movementSpeed: settings.attributes.movementSpeed, + followRange: settings.attributes.followRange, + scale: settings.rendering.scale, + shadowRadius: settings.rendering.shadowRadius, + visibleBoundsWidth: settings.rendering.visibleBoundsWidth ?? 0, + visibleBoundsHeight: settings.rendering.visibleBoundsHeight ?? 0, + visibleBoundsOffsetX: offset[0], + visibleBoundsOffsetY: offset[1], + visibleBoundsOffsetZ: offset[2], + animationMode: settings.animation.mode, + swingSpeed: settings.animation.swingSpeed, + walkSpeedMultiplier: settings.animation.walkSpeedMultiplier, + idleStrength: settings.animation.idleStrength ?? 1, + gait: settings.animation.gait || 'natural' + }; +} + +function formToSettings(form, base) { + return { + schemaVersion: base.schemaVersion, + modelType: base.modelType, + presetType: base.presetType, + namespace: String(form.namespace || '').trim(), + profileId: String(form.profileId || '').trim(), + targetVersion: base.targetVersion, + host: { + entityType: form.hostEntityType, + movementType: form.movementType, + bodyType: form.bodyType + }, + dimensions: { + width: Number(form.width), + height: Number(form.height), + eyeHeight: Number(form.eyeHeight) + }, + movement: { + speed: Number(form.speed), + stepHeight: Number(form.stepHeight), + gravity: !!form.gravity + }, + behavior: { + mode: form.behaviorMode, + lookAtPlayers: base.behavior ? base.behavior.lookAtPlayers : true, + randomStroll: base.behavior ? base.behavior.randomStroll : false + }, + attributes: { + maxHealth: Number(form.maxHealth), + movementSpeed: Number(form.movementSpeed), + followRange: Number(form.followRange) + }, + rendering: { + scale: Number(form.scale), + shadowRadius: Number(form.shadowRadius), + visibleBoundsWidth: Number(form.visibleBoundsWidth), + visibleBoundsHeight: Number(form.visibleBoundsHeight), + visibleBoundsOffset: [ + Number(form.visibleBoundsOffsetX), + Number(form.visibleBoundsOffsetY), + Number(form.visibleBoundsOffsetZ) + ] + }, + animation: { + mode: form.animationMode, + swingSpeed: Number(form.swingSpeed), + walkSpeedMultiplier: Number(form.walkSpeedMultiplier), + idleStrength: Number(form.idleStrength), + gait: form.gait + } + }; +} + +function activeModelType(form) { + return form.exportType === EXPORT_TYPE_MODEL_ONLY + ? MODEL_TYPE_ENTITY : (form.modelType || MODEL_TYPE_ENTITY); +} + +function activePreset(form) { + return activeModelType(form) === MODEL_TYPE_BLOCK_ENTITY + ? (form.blockPreset || 'static') : (form.preset || 'custom'); +} + +function presetFormValues(presetType, modelType, modelDimensions, + visibleBounds) { + const settings = ModelDimensions.applyModelDimensions( + applyTemplate(presetType, modelType), modelDimensions); + VisibleBounds.applyVisibleBounds(settings, visibleBounds); + const form = settingsToForm(settings); + delete form.namespace; + delete form.profileId; + delete form.targetVersion; + + return form; +} + +function resolveExportSettings(form, base, modelDimensions, visibleBounds) { + const exportType = form.exportType || EXPORT_TYPE_PACKS; + const modelType = activeModelType(form); + const preset = activePreset(form); + + let settings; + if (modelType !== MODEL_TYPE_BLOCK_ENTITY && preset === 'custom') { + settings = deepMerge(getDefaults(), base); + } else { + settings = ModelDimensions.applyModelDimensions( + applyTemplate(preset, modelType), modelDimensions); + VisibleBounds.applyVisibleBounds(settings, visibleBounds); + } + + settings.schemaVersion = base.schemaVersion || settings.schemaVersion; + settings.modelType = modelType; + settings.presetType = preset; + settings.namespace = String(form.namespace || '').trim(); + settings.profileId = String(form.profileId || '').trim(); + settings.targetVersion = form.targetVersion; + + if (form.customize) { + settings = formToSettings(form, settings); + } + settings.customize = !!form.customize; + settings.includeExternalTextures = form.includeExternalTextures !== false; + settings.lastExportedVersion = base.lastExportedVersion; + settings.exportType = exportType; + settings.exportTarget = isZipExport(exportType) + ? EXPORT_TYPE_PACKS : EXPORT_TYPE_MOD_PROJECT; + settings.modelOnly = exportType === EXPORT_TYPE_MODEL_ONLY; + + return settings; +} + +module.exports = { + settingsToForm, + formToSettings, + activeModelType, + activePreset, + presetFormValues, + resolveExportSettings +}; + + +/***/ }, + +/***/ 317 +(module, __unused_webpack_exports, __webpack_require__) { + +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {describeTextureSource} = __webpack_require__(834); + +let patchedVue = null; +let originalGetDescription = null; + +function texturesPanelVue() { + const panel = (typeof Panels !== 'undefined' && Panels.textures) + || (typeof Interface !== 'undefined' && Interface.Panels + && Interface.Panels.textures); + + return (panel && panel.inside_vue) || null; +} + +function patchTexturePanel(formatId) { + const vue = texturesPanelVue(); + if (!vue || typeof vue.getDescription !== 'function' + || originalGetDescription) { + return; + } + + const original = vue.getDescription.bind(vue); + patchedVue = vue; + originalGetDescription = original; + vue.getDescription = function (texture) { + const base = original(texture); + try { + if (typeof Format !== 'undefined' && Format && Format.id === formatId) { + const {label} = describeTextureSource({ + namespace: texture.namespace, + folder: texture.folder, + name: texture.name, + path: texture.path + }); + + return `${base} - ${label}`; + } + } catch (error) { + } + + return base; + }; +} + +function unpatchTexturePanel() { + if (patchedVue && originalGetDescription) { + patchedVue.getDescription = originalGetDescription; + } + patchedVue = null; + originalGetDescription = null; +} + +module.exports = {patchTexturePanel, unpatchTexturePanel}; + + +/***/ }, + +/***/ 20 +(module, __unused_webpack_exports, __webpack_require__) { + +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {hashString} = __webpack_require__(803); + +class ResourceLocation { + static #NAMESPACE_PATTERN = /^[a-z0-9_.-]+$/; + static #PATH_PATTERN = /^[a-z0-9_./-]+$/; + static #MODEL_EXTENSION_PATTERN = /\.(bbmodel|json|gltf|glb|obj|geo)$/i; + static #COMBINING_MARK_PATTERN = /[\u0300-\u036f]/g; + static #TRANSLITERATIONS = new Map(Object.entries({ + 'ä': 'ae', 'ö': 'oe', 'ü': 'ue', 'ß': 'ss', 'å': 'aa', 'æ': 'ae', + 'ø': 'oe', 'œ': 'oe', 'þ': 'th', 'ð': 'dh', 'đ': 'd', 'ł': 'l', + 'ı': 'i', 'ij': 'ij', 'ŋ': 'ng', '&': '_and_', '+': '_plus_' + })); + + static isValidNamespace(namespace) { + return typeof namespace === 'string' + && ResourceLocation.#NAMESPACE_PATTERN.test(namespace); + } + + static isValidPath(path) { + return typeof path === 'string' && ResourceLocation.#PATH_PATTERN.test(path) + && !path.includes('..'); + } + + static isValidResourceLocation(value) { + if (typeof value !== 'string' || !value.includes(':')) { + return false; + } + const parts = value.split(':'); + if (parts.length !== 2) { + return false; + } + return ResourceLocation.isValidNamespace(parts[0]) + && ResourceLocation.isValidPath(parts[1]); + } + + static buildResourceLocation(namespace, path) { + return `${namespace}:${path}`; + } + + static transliterate(value) { + let transliterated = ''; + for (const character of String(value ?? '').toLowerCase()) { + const replacement = ResourceLocation.#TRANSLITERATIONS.get(character); + transliterated += replacement ?? character; + } + + return transliterated.normalize('NFKD') + .replace(ResourceLocation.#COMBINING_MARK_PATTERN, ''); + } + + static sanitizeProfileId(name) { + const rawName = String(name ?? '').trim(); + const base = ResourceLocation.transliterate( + rawName.replace(ResourceLocation.#MODEL_EXTENSION_PATTERN, '')) + .replace(/[^a-z0-9_./-]+/g, '_') + .replace(/_{2,}/g, '_') + .split('/') + .filter((segment) => segment !== '' && segment !== '.' && segment !== '..') + .join('/') + .replace(/^[_./-]+|[_./-]+$/g, ''); + if (base.length > 0) { + return base; + } + + return rawName.length > 0 ? `model_${hashString(rawName)}` : 'entity'; + } + + static sanitizeNamespace(namespace) { + const base = ResourceLocation.transliterate(namespace) + .replace(/[^a-z0-9_.-]+/g, '_') + .replace(/_{2,}/g, '_') + .replace(/^[_.-]+|[_.-]+$/g, ''); + return base.length > 0 ? base : 'example_org'; + } +} + +module.exports = {ResourceLocation}; + + +/***/ }, + +/***/ 834 +(module, __unused_webpack_exports, __webpack_require__) { + +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {ResourceLocation} = __webpack_require__(20); + +const ASSETS_PATTERN = /assets\/([a-z0-9_.-]+)\/textures\/(.+?)(?:\.png)?$/i; + +function stripPng(value) { + return value.replace(/\.png$/i, ''); +} + +function trimSlashes(value) { + return value.replace(/^\/+|\/+$/g, ''); +} + +function customFileName(settings, index) { + return index === 0 + ? `${settings.profileId}.png` + : `${settings.profileId}_${index}.png`; +} + +function customLocation(settings, index) { + return `${settings.namespace}:textures/entity/${customFileName(settings, + index)}`; +} + +function parseExternalLocation(descriptor) { + const namespace = String(descriptor.namespace || '').trim().toLowerCase(); + if (namespace) { + const folder = trimSlashes(String(descriptor.folder || '').trim()); + const name = stripPng(String(descriptor.name || '').trim()).toLowerCase(); + if (!name) { + return null; + } + const rest = folder ? `${folder.toLowerCase()}/${name}` : name; + + return `${namespace}:textures/${rest}.png`; + } + + const path = String(descriptor.path || '').replace(/\\/g, '/'); + const match = path.match(ASSETS_PATTERN); + if (match && match[1].toLowerCase() === 'minecraft') { + return `minecraft:textures/${match[2].toLowerCase()}.png`; + } + + return null; +} + +function packPath(location) { + const [namespace, resourcePath] = location.split(':'); + + return `assets/${namespace}/${resourcePath}`; +} + +function describeTextureSource(descriptor) { + const location = parseExternalLocation(descriptor); + return location + ? {label: location.replace(':textures/', ':').replace(/\.png$/i, '')} + : {label: 'Custom Texture'}; +} + +function resolveTextures(descriptors, settings) { + const textures = {}; + const packed = []; + const issues = []; + const referencedOnly = []; + const resolvedIndices = new Set(); + const includeExternal = settings.includeExternalTextures !== false; + let texture = null; + + (descriptors || []).forEach((descriptor, position) => { + const index = Number.isInteger(descriptor.index) ? descriptor.index + : position; + if (resolvedIndices.has(index)) { + issues.push({ + code: 'DUPLICATE_TEXTURE_INDEX', + message: `Texture slot ${index} is assigned more than once` + }); + return; + } + + const externalLocation = parseExternalLocation(descriptor); + if (externalLocation + && !ResourceLocation.isValidResourceLocation(externalLocation)) { + issues.push({ + code: 'INVALID_TEXTURE_LOCATION', + message: `Texture slot ${index} points to the invalid location ` + + `${externalLocation}, check its namespace and folder` + }); + return; + } + + resolvedIndices.add(index); + const location = externalLocation || customLocation(settings, index); + const vanilla = location.startsWith('minecraft:'); + if (!externalLocation || (includeExternal && !vanilla)) { + packed.push({path: packPath(location), bytes: descriptor.bytes}); + } else if (!vanilla) { + referencedOnly.push(location); + } + + if (index === 0) { + texture = location; + } else { + textures[index] = location; + } + }); + + if (!texture && resolvedIndices.size > 0) { + issues.push({ + code: 'MISSING_TEXTURE_INDEX', + message: 'Texture slot 0 is missing, the model has no primary texture' + }); + } + + return {texture, textures, packed, referencedOnly, issues}; +} + +module.exports = { + resolveTextures, + parseExternalLocation, + describeTextureSource +}; + + +/***/ }, + +/***/ 803 +(module) { + +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +function hashString(value) { + let hash = 0x811c9dc5; + const text = String(value); + for (let i = 0; i < text.length; i++) { + hash ^= text.codePointAt(i); + hash = Math.imul(hash, 0x01000193); + } + + return (hash >>> 0).toString(16).padStart(8, '0'); +} + +module.exports = {hashString}; + + +/***/ }, + +/***/ 734 +(module) { + +"use strict"; +module.exports = ""; + +/***/ }, + +/***/ 448 +(module) { + +"use strict"; +module.exports = ""; + +/***/ }, + +/***/ 377 +(module) { + +"use strict"; +module.exports = "# {{id}} - Easy Model Entities pack\n\nA custom Blockbench model that appears in Minecraft as an entity you can spawn — no coding and no\nextra mod needed for this model. Made with the **Easy Model Entities Exporter** Blockbench plugin\nfor **{{mcVersion}}**.\n\n> **Note:** This is an early release. The pack format may still change, so you might need to\n> re-export this pack after updating the plugin or the mod.\n\nProfile ID: `{{serverProfileId}}`\n\n## What you need\n\nThe **Easy Model Entities** mod for **{{mcVersion}}** (Forge, NeoForge or Fabric, depending on the\nMinecraft version). Without the mod these files do nothing at all.\n\n- [CurseForge](https://www.curseforge.com/minecraft/mc-mods/easy-model-entities)\n- [Modrinth](https://modrinth.com/mod/easy-model-entities)\n\nInstall it like any other mod first. In multiplayer it has to be installed on the server too.\n\n## Installation\n\nTwo files belong together — do not unpack them, just move them into the right folder:\n\n- `{{datapackFile}}` — the entity itself (size, movement, behavior)\n- `{{resourcepackFile}}` — how it looks (model, texture, animations)\n\n### 1. Resource Pack\n\nMove `{{resourcepackFile}}` into your resourcepacks folder:\n\n- Windows: `%appdata%\\.minecraft\\resourcepacks\\`\n- Linux: `~/.minecraft/resourcepacks/`\n- macOS: `~/Library/Application Support/minecraft/resourcepacks/`\n\nStart Minecraft and enable it under **Options > Resource Packs**.\n\n### 2. Data Pack\n\nMove `{{datapackFile}}` into your world's datapacks folder:\n\n- Single player: `.minecraft/saves//datapacks/`\n- Dedicated server: `/world/datapacks/`\n\nLoad the world, or run `/reload` if it is already open.\n\n> **Install both.** With only the resource pack the model shows up but cannot be spawned. With only\n> the data pack the entity exists but renders as a pink placeholder box.\n\n## Using it in game\n\nLook for the spawn item in the creative inventory: entities are in the **Easy Model Entities** tab,\nblock entities in **Easy Model Block Entities**. Place it like a spawn egg.\n\n## What this model can do\n\nMovement and behavior come from a fixed set of presets built into the mod — wandering, swimming,\nflying, standing still and so on. The preset was chosen at export time.\n\nAnimations are played from the clips in the Blockbench project. These names play automatically:\n\n| Clip | When it plays |\n|----------|----------------------|\n| `idle` | standing still |\n| `walk` | moving on the ground |\n| `swim` | moving in water |\n| `fly` | flying |\n| `attack` | attacking |\n| `sit` | sitting |\n\nClips named `hurt` and `death` are loaded as well, but the mod does not trigger them on its own.\nClips with any other name are kept as custom clips and are played by name, for example with\n`/easy_model_entities animation play entity `.\n\nThere is no scripting or custom AI. A model without any matching clip simply renders without\nanimation.\n\n## Updating this pack\n\nChanged only the model, texture or animations? Re-export just the resource pack and replace that\nfile. Changed entity data such as size or movement? Re-export just the data pack. Either way the\nupdated pack still matches the one you already installed. If both changed, export the complete\nbundle again and replace both files.\n"; + +/***/ }, + +/***/ 896 +(module) { + +"use strict"; +module.exports = require("fs"); + +/***/ }, + +/***/ 928 +(module) { + +"use strict"; +module.exports = require("path"); + +/***/ } + +/******/ }); +/************************************************************************/ +/******/ // The module cache +/******/ var __webpack_module_cache__ = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ // Check if module is in cache +/******/ var cachedModule = __webpack_module_cache__[moduleId]; +/******/ if (cachedModule !== undefined) { +/******/ return cachedModule.exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = __webpack_module_cache__[moduleId] = { +/******/ // no module.id needed +/******/ // no module.loaded needed +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/************************************************************************/ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const { + getDefaults, + deepMerge, + applyTemplate, + DEFAULT_PRESET +} = __webpack_require__(668); +const { + MODEL_TYPE_ENTITY, + MODEL_TYPE_BLOCK_ENTITY +} = __webpack_require__(151); +const {ModelDimensions} = __webpack_require__(763); +const {VisibleBounds} = __webpack_require__(508); +const {PresetDetector} = __webpack_require__(858); +const {ResourceLocation} = __webpack_require__(20); +const {resolveTextures} = __webpack_require__(834); +const {patchTexturePanel, unpatchTexturePanel} = __webpack_require__(317); +const {Validator} = __webpack_require__(229); +const { + EXPORT_TYPE_PACKS, + isSinglePackExport +} = __webpack_require__(984); +const {performExport} = __webpack_require__(579); +const {BlockbenchAdapter} = __webpack_require__(833); +const {openExportDialog} = __webpack_require__(924); +const {registerTranslations, t} = __webpack_require__(16); +const exportIconSvg = __webpack_require__(448); +const createIconSvg = __webpack_require__(734); +const {FORMAT_ID, registerEmeFormat, unregisterEmeFormat} = __webpack_require__( + 995); +const {registerEmeCodecHooks, unregisterEmeCodecHooks} = __webpack_require__( + 670); + +const MODDED_ENTITY_FORMAT_ID = 'modded_entity'; +const ACTION_ID = 'eme_export'; +const SETTING_ID = 'eme_enable_customization'; +const EXPERIMENTAL_SETTING_ID = 'eme_enable_experimental'; +const EXPORT_ICON = 'data:image/svg+xml,' + encodeURIComponent(exportIconSvg); +const CREATE_ICON = 'data:image/svg+xml,' + encodeURIComponent(createIconSvg); +let exportAction; +let projectProperty; +let customizationSetting; +let experimentalSetting; + +function settingEnabled(setting) { + return !!setting && typeof setting.value === 'boolean' && setting.value; +} + +function resolveDialogState() { + const storedSettings = BlockbenchAdapter.loadSettings(); + const stats = BlockbenchAdapter.getModelStats(); + const bounds = BlockbenchAdapter.getModelBounds(); + const visibleBounds = VisibleBounds.derive(BlockbenchAdapter.getModelCubes()); + + let settings; + let preset; + let blockPreset; + let modelType; + let customize; + let exportType; + + if (storedSettings) { + settings = deepMerge(getDefaults(), storedSettings); + modelType = settings.modelType || MODEL_TYPE_ENTITY; + customize = !!storedSettings.customize; + exportType = storedSettings.exportType || EXPORT_TYPE_PACKS; + if (modelType === MODEL_TYPE_BLOCK_ENTITY) { + preset = 'custom'; + blockPreset = settings.presetType || 'static'; + } else { + preset = settings.presetType || DEFAULT_PRESET; + blockPreset = 'static'; + } + } else { + preset = PresetDetector.detectPresetType(stats, bounds); + blockPreset = 'static'; + modelType = MODEL_TYPE_ENTITY; + settings = applyTemplate(preset); + customize = false; + exportType = EXPORT_TYPE_PACKS; + } + + const modelDimensions = ModelDimensions.deriveDimensions(bounds, + settings.host.bodyType); + if (!storedSettings) { + ModelDimensions.applyModelDimensions(settings, modelDimensions); + } + + const projectName = BlockbenchAdapter.getProjectName(); + if (projectName + && (!settings.profileId || settings.profileId + === getDefaults().profileId)) { + settings.profileId = ResourceLocation.sanitizeProfileId(projectName); + } + + VisibleBounds.applyVisibleBounds(settings, visibleBounds, { + preserveExisting: !!storedSettings + }); + + return { + settings, preset, blockPreset, modelType, customize, exportType, + visibleBounds, modelDimensions, + hasExternalTextures: (stats.externalTextures || []).length > 0 + }; +} + +function formatIssues(issues) { + return issues.map((issue) => `• ${issue.message}`).join('\n'); +} + +function pairingWarnings(settings) { + if (!isSinglePackExport(settings.exportType) + || settings.lastExportedVersion) { + return []; + } + + return [{ + code: 'NO_PAIRED_EXPORT', + message: 'This project has no earlier export to pair with, so the single ' + + 'pack may not match the installed one. Export both packs instead' + }]; +} + +function runExport(settings, target) { + const textureResolution = resolveTextures( + BlockbenchAdapter.collectTextures(), settings); + const result = Validator.validateSettings(settings, { + ...BlockbenchAdapter.getModelStats(), + textureIssues: textureResolution.issues, + referencedTextures: textureResolution.referencedOnly + }); + const warnings = [...result.warnings, ...pairingWarnings(settings)]; + + if (!result.valid) { + Blockbench.showMessageBox({ + title: t('eme.dialog.title'), + message: 'Export blocked. Please fix the following issues:\n\n' + + formatIssues(result.errors) + }); + return; + } + + const doExport = () => performExport(settings, target, textureResolution); + + if (warnings.length > 0) { + Blockbench.showMessageBox( + { + title: t('eme.dialog.title'), + message: 'The following warnings were found:\n\n' + formatIssues( + warnings) + '\n\nExport anyway?', + buttons: ['Export', 'Cancel'], + confirm: 0, + cancel: 1 + }, + (button) => { + if (button === 0) { + doExport(); + } + } + ); + } else { + doExport(); + } +} + +function openDialog() { + if (!Project) { + Blockbench.showQuickMessage('Open a project first', 1500); + return; + } + + const state = resolveDialogState(); + openExportDialog({ + settings: state.settings, + preset: state.preset, + blockPreset: state.blockPreset, + modelType: state.modelType, + customize: state.customize, + exportType: state.exportType, + showCustomization: settingEnabled(customizationSetting), + experimental: settingEnabled(experimentalSetting), + modelDimensions: state.modelDimensions, + visibleBounds: state.visibleBounds, + hasExternalTextures: state.hasExternalTextures, + onExport: runExport + }); +} + +BBPlugin.register('easy_model_entities', { + title: 'Easy Model Entities', + icon: 'icon.png', + author: 'Markus Bordihn', + description: 'Export Blockbench models for the Easy Model Entities mod (Minecraft: Java Edition) as ready-to-install Data Pack and Resource Pack files.', + tags: ['Minecraft: Java Edition', 'Format', 'Entity'], + version: '1.0.0', + min_version: '4.9.0', + variant: 'desktop', + await_loading: true, + has_changelog: true, + contributes: { + formats: ['eme_entity'] + }, + website: 'https://www.curseforge.com/minecraft/mc-mods/easy-model-entities', + repository: 'https://github.com/MarkusBordihn/BOs-Easy-Model-Entities', + bug_tracker: 'https://github.com/MarkusBordihn/BOs-Easy-Model-Entities/issues', + onload() { + registerTranslations(); + registerEmeFormat(CREATE_ICON); + registerEmeCodecHooks(); + + projectProperty = new Property(ModelProject, 'object', + BlockbenchAdapter.PROJECT_PROPERTY, { + label: 'Easy Model Entities Settings', + exposed: false, + default: undefined + }); + + customizationSetting = new Setting(SETTING_ID, { + name: t('eme.setting.enable_customization'), + category: 'export', + type: 'toggle', + value: false + }); + + experimentalSetting = new Setting(EXPERIMENTAL_SETTING_ID, { + name: t('eme.setting.enable_experimental'), + category: 'export', + type: 'toggle', + value: false + }); + + exportAction = new Action(ACTION_ID, { + name: t('eme.action.export'), + description: t('eme.action.exportDescription'), + icon: EXPORT_ICON, + condition: () => typeof Format !== 'undefined' && Format + && (Format.id === FORMAT_ID || Format.id === MODDED_ENTITY_FORMAT_ID), + click() { + openDialog(); + } + }); + + MenuBar.addAction(exportAction, 'file.export'); + + patchTexturePanel(FORMAT_ID); + }, + onunload() { + unpatchTexturePanel(); + unregisterEmeCodecHooks(); + unregisterEmeFormat(); + if (exportAction) { + exportAction.delete(); + } + if (customizationSetting) { + customizationSetting.delete(); + } + if (experimentalSetting) { + experimentalSetting.delete(); + } + if (projectProperty) { + projectProperty.delete(); + } + } +}); + +/******/ })() +; \ No newline at end of file diff --git a/plugins/easy_model_entities/icon.png b/plugins/easy_model_entities/icon.png new file mode 100644 index 00000000..7c6f764e Binary files /dev/null and b/plugins/easy_model_entities/icon.png differ diff --git a/plugins/easy_model_entities/members.yml b/plugins/easy_model_entities/members.yml new file mode 100644 index 00000000..bb72ac2f --- /dev/null +++ b/plugins/easy_model_entities/members.yml @@ -0,0 +1,2 @@ +maintainers: + - MarkusBordihn diff --git a/plugins/easy_model_entities/src/BlockbenchAdapter.js b/plugins/easy_model_entities/src/BlockbenchAdapter.js new file mode 100644 index 00000000..70d4efef --- /dev/null +++ b/plugins/easy_model_entities/src/BlockbenchAdapter.js @@ -0,0 +1,314 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {Validator} = require('./model/Validator'); +const {parseExternalLocation} = require('./utils/TextureResolver'); + +class BlockbenchAdapter { + static PROJECT_PROPERTY = 'eme_export'; + + static #base64ToBytes(base64) { + const clean = String(base64).replace(/^data:[^,]*,/, ''); + const binary = + typeof atob === 'function' ? atob(clean) : Buffer.from(clean, + 'base64').toString('binary'); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.codePointAt(i) & 0xff; + } + + return bytes; + } + + static #firstTexture() { + if (typeof Texture === 'undefined') { + return null; + } + + return Texture.getDefault?.() || Texture.all?.[0] || null; + } + + static collectTextures() { + if (typeof Texture === 'undefined' || !Texture.all) { + return []; + } + + return Texture.all.map((texture, position) => ({ + index: position, + name: texture.name || '', + namespace: texture.namespace || '', + folder: texture.folder || '', + path: texture.path || '', + bytes: BlockbenchAdapter.#base64ToBytes(texture.getBase64()) + })); + } + + static #externalTextureLocations() { + if (typeof Texture === 'undefined' || !Texture.all) { + return []; + } + + return Texture.all + .map((texture) => parseExternalLocation({ + namespace: texture.namespace, + folder: texture.folder, + name: texture.name, + path: texture.path + })) + .filter((location) => location && !location.startsWith('minecraft:')); + } + + static #isNumericValue(value) { + if (typeof value === 'number') { + return Number.isFinite(value); + } + + return typeof value === 'string' && value.trim() !== '' + && Number.isFinite(Number(value)); + } + + static #summarizeAnimation(animation) { + const channels = new Set(); + let hasExpression = false; + + Object.values(animation.animators || {}).forEach((animator) => { + (animator.keyframes || []).forEach((keyframe) => { + if (keyframe.channel) { + channels.add(keyframe.channel); + } + (keyframe.data_points || []).forEach((point) => { + ['x', 'y', 'z'].forEach((axis) => { + const value = point?.[axis]; + if (value !== undefined && value !== null + && !BlockbenchAdapter.#isNumericValue(value)) { + hasExpression = true; + } + }); + }); + }); + }); + + return { + name: animation.name || '', + channels: [...channels], + hasExpression: hasExpression + }; + } + + static getModelStats() { + const cubes = typeof Cube !== 'undefined' && Cube.all ? Cube.all : []; + const groups = typeof Group !== 'undefined' && Group.all ? Group.all : []; + const animations = + typeof Animation !== 'undefined' && Animation.all ? Animation.all : []; + const texture = BlockbenchAdapter.#firstTexture(); + + let maxDepth = 0; + groups.forEach((group) => { + let depth = 1; + let parent = group.parent; + while (parent && parent !== 'root' && typeof parent === 'object') { + depth += 1; + parent = parent.parent; + } + if (depth > maxDepth) { + maxDepth = depth; + } + }); + + return { + hasModel: cubes.length > 0 || groups.length > 0, + hasTexture: !!texture, + cubeCount: cubes.length, + boneCount: groups.length, + animationCount: animations.length, + animations: animations.map( + (animation) => BlockbenchAdapter.#summarizeAnimation(animation)), + hierarchyDepth: maxDepth, + boneNames: groups.map((group) => group.name), + externalTextures: BlockbenchAdapter.#externalTextureLocations(), + textureWidth: texture ? texture.width : undefined, + textureHeight: texture ? texture.height : undefined + }; + } + + static getModelBytes() { + return Codecs.project.compile({raw: false}); + } + + static getModelBounds() { + const cubes = typeof Cube !== 'undefined' && Cube.all ? Cube.all : []; + if (cubes.length === 0) { + return null; + } + + const bounds = { + minX: Infinity, + minY: Infinity, + minZ: Infinity, + maxX: -Infinity, + maxY: -Infinity, + maxZ: -Infinity + }; + cubes.forEach((cube) => { + const from = cube.from || [0, 0, 0]; + const to = cube.to || [0, 0, 0]; + bounds.minX = Math.min(bounds.minX, from[0], to[0]); + bounds.minY = Math.min(bounds.minY, from[1], to[1]); + bounds.minZ = Math.min(bounds.minZ, from[2], to[2]); + bounds.maxX = Math.max(bounds.maxX, from[0], to[0]); + bounds.maxY = Math.max(bounds.maxY, from[1], to[1]); + bounds.maxZ = Math.max(bounds.maxZ, from[2], to[2]); + }); + + return bounds; + } + + static getModelCubes() { + const cubes = typeof Cube !== 'undefined' && Cube.all ? Cube.all : []; + + return cubes.map((cube) => ({ + from: (cube.from || [0, 0, 0]).slice(), + to: (cube.to || [0, 0, 0]).slice() + })); + } + + static getProjectName() { + if (typeof Project === 'undefined' || !Project) { + return ''; + } + + if (Project.geometry_name) { + return Project.geometry_name; + } + + if (Project.name) { + return Project.name; + } + + if (typeof Project.getDisplayName === 'function') { + return Project.getDisplayName(false); + } + return ''; + } + + static loadSettings() { + if (typeof Project === 'undefined' || !Project) { + return null; + } + + const stored = Project[BlockbenchAdapter.PROJECT_PROPERTY]; + if (stored && typeof stored === 'object' && Object.keys(stored).length + > 0) { + return structuredClone(stored); + } + + return null; + } + + static saveSettings(settings) { + if (typeof Project !== 'undefined' && Project) { + Project[BlockbenchAdapter.PROJECT_PROPERTY] = structuredClone( + settings); + if (typeof Blockbench !== 'undefined' && Blockbench.dispatchEvent) { + Blockbench.dispatchEvent('eme_settings_saved', {settings}); + } + } + } + + static #zipTo(files, outputType) { + const zip = new JSZip(); + files.forEach((file) => { + zip.file(file.path, file.content); + }); + + return zip.generateAsync({type: outputType}); + } + + static #bundleBlob(bundle) { + if (!bundle.datapack) { + return BlockbenchAdapter.#zipTo(bundle.resourcepack, 'blob'); + } + + if (!bundle.resourcepack) { + return BlockbenchAdapter.#zipTo(bundle.datapack, 'blob'); + } + + return Promise.all([ + BlockbenchAdapter.#zipTo(bundle.datapack, 'uint8array'), + BlockbenchAdapter.#zipTo(bundle.resourcepack, 'uint8array') + ]).then(([datapackZip, resourcepackZip]) => { + const outer = new JSZip(); + outer.file(bundle.readme.path, bundle.readme.content); + outer.file(bundle.datapackFileName || 'datapack.zip', datapackZip); + outer.file(bundle.resourcepackFileName || 'resourcepack.zip', + resourcepackZip); + return outer.generateAsync({type: 'blob'}); + }); + } + + static exportPackBundle(bundle, name) { + return BlockbenchAdapter.#bundleBlob(bundle).then((content) => { + return new Promise((resolve) => { + Blockbench.export( + { + type: 'Zip Archive', + extensions: ['zip'], + name: name, + content: content, + savetype: 'zip' + }, + (path) => resolve(path) + ); + }); + }); + } + + static listExistingFiles(rootDir, files) { + const fs = require('fs'); + const path = require('path'); + + return files + .map((file) => path.join(rootDir, file.path)) + .filter((fullPath) => fs.existsSync(fullPath)); + } + + static writeToDirectory(rootDir, files) { + const fs = require('fs'); + const path = require('path'); + files.forEach((file) => { + const result = Validator.validateOutputPath(file.path); + if (!result.valid) { + throw new Error(`${result.code}: ${result.message} (${file.path})`); + } + const fullPath = path.join(rootDir, file.path); + fs.mkdirSync(path.dirname(fullPath), {recursive: true}); + const data = + file.binary && file.content instanceof Uint8Array ? Buffer.from( + file.content) : file.content; + fs.writeFileSync(fullPath, data); + }); + } + + static pickDirectory(title) { + return Blockbench.pickDirectory( + {title: title, resource_id: 'eme_mod_project'}); + } +} + +module.exports = {BlockbenchAdapter}; diff --git a/plugins/easy_model_entities/src/LICENSE.MD b/plugins/easy_model_entities/src/LICENSE.MD new file mode 100644 index 00000000..889c2571 --- /dev/null +++ b/plugins/easy_model_entities/src/LICENSE.MD @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) 2026 Markus Bordihn + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES +OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/plugins/easy_model_entities/src/builders/exporter.js b/plugins/easy_model_entities/src/builders/exporter.js new file mode 100644 index 00000000..f2b3e33d --- /dev/null +++ b/plugins/easy_model_entities/src/builders/exporter.js @@ -0,0 +1,174 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {buildServerProfile} = require('./serverProfile'); +const {buildRenderProfile} = require('./renderProfile'); +const {buildDataPackMcmeta, buildResourcePackMcmeta} = require('./packMeta'); +const {buildReadme} = require('./readme'); +const {hashString} = require('../utils/hash'); +const {MODEL_TYPE_ENTITY} = require('../model/presetTypes'); +const { + EXPORT_TYPE_PACKS, + includesDataPack, + includesResourcePack, + isSinglePackExport +} = require('../model/exportTypes'); + +function toJson(value) { + return JSON.stringify(value, null, 2) + '\n'; +} + +function pairingVersion(settings, serverProfile) { + if (isSinglePackExport(settings.exportType) && settings.lastExportedVersion) { + return settings.lastExportedVersion; + } + + return hashString(toJson(serverProfile || buildServerProfile(settings))); +} + +function stampVersion(settings, serverProfile, renderProfile) { + if (!serverProfile) { + return; + } + + const version = pairingVersion(settings, serverProfile); + serverProfile.version = version; + renderProfile.version = version; +} + +function buildProfiles(settings, textureResolution) { + const serverProfile = buildServerProfile(settings); + const renderProfile = buildRenderProfile(settings, textureResolution); + stampVersion(settings, serverProfile, renderProfile); + + return {serverProfile, renderProfile}; +} + +function dataPaths(settings) { + const namespace = settings.namespace; + const id = settings.profileId; + const modelType = settings.modelType || MODEL_TYPE_ENTITY; + + return { + profile: `data/${namespace}/easy_model_entities/profiles/${modelType}/${id}.json`, + renderProfile: + `assets/${namespace}/easy_model_entities/render_profiles/${modelType}/${id}.json`, + model: `assets/${namespace}/easy_model_entities/models/${id}.bbmodel` + }; +} + +function textureFiles(textureResolution) { + const packed = (textureResolution && textureResolution.packed) || []; + + return packed.map((entry) => ({ + path: entry.path, + content: entry.bytes, + binary: true + })); +} + +function datapackFiles(settings, serverProfile) { + return [ + { + path: 'pack.mcmeta', content: toJson(buildDataPackMcmeta(settings)), + binary: false + }, + { + path: dataPaths(settings).profile, content: toJson(serverProfile), + binary: false + } + ]; +} + +function resourcepackFiles(settings, renderProfile, options) { + const paths = dataPaths(settings); + return [ + { + path: 'pack.mcmeta', content: toJson(buildResourcePackMcmeta(settings)), + binary: false + }, + {path: paths.renderProfile, content: toJson(renderProfile), binary: false}, + {path: paths.model, content: options.modelBytes, binary: true}, + ...textureFiles(options.textureResolution) + ]; +} + +function packFileNames(settings) { + return { + datapack: `${settings.profileId}_datapack.zip`, + resourcepack: `${settings.profileId}_resourcepack.zip` + }; +} + +function buildPackBundle(settings, options) { + const opts = options || {}; + const {serverProfile, renderProfile} = buildProfiles(settings, + opts.textureResolution); + const exportType = settings.exportType || EXPORT_TYPE_PACKS; + const fileNames = packFileNames(settings); + const withDataPack = includesDataPack(exportType); + const withResourcePack = includesResourcePack(exportType); + + return { + readme: withDataPack && withResourcePack ? { + path: 'README.md', content: buildReadme(settings, fileNames), + binary: false + } : null, + datapack: withDataPack ? datapackFiles(settings, serverProfile) : null, + resourcepack: withResourcePack + ? resourcepackFiles(settings, renderProfile, opts) : null, + datapackFileName: fileNames.datapack, + resourcepackFileName: fileNames.resourcepack, + serverProfile, + renderProfile + }; +} + +function buildModProjectFiles(settings, options) { + const opts = options || {}; + const renderProfile = buildRenderProfile(settings, opts.textureResolution); + const paths = dataPaths(settings); + + const files = []; + const serverProfile = settings.modelOnly ? null : buildServerProfile( + settings); + stampVersion(settings, serverProfile, renderProfile); + if (serverProfile) { + files.push({ + path: paths.profile, content: toJson(serverProfile), + binary: false + }); + } + + files.push( + { + path: paths.renderProfile, content: toJson(renderProfile), + binary: false + }, + {path: paths.model, content: opts.modelBytes, binary: true}, + ...textureFiles(opts.textureResolution)); + + return {files, serverProfile, renderProfile}; +} + +module.exports = { + buildPackBundle, + buildModProjectFiles, + pairingVersion +}; diff --git a/plugins/easy_model_entities/src/builders/packMeta.js b/plugins/easy_model_entities/src/builders/packMeta.js new file mode 100644 index 00000000..593eb914 --- /dev/null +++ b/plugins/easy_model_entities/src/builders/packMeta.js @@ -0,0 +1,54 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {getPackFormats} = require('../model/versionMatrix'); + +function buildPack(descriptor, description) { + const pack = {pack_format: descriptor.packFormat}; + if (descriptor.minFormat) { + pack.min_format = descriptor.minFormat; + } + if (descriptor.maxFormat) { + pack.max_format = descriptor.maxFormat; + } + pack.description = description; + + return {pack}; +} + +function buildMcmeta(settings, kind, description) { + const formats = getPackFormats(settings.targetVersion); + if (!formats) { + throw new Error( + `Unsupported or disabled target version: ${settings.targetVersion}`); + } + return buildPack(formats[kind], description); +} + +function buildDataPackMcmeta(settings) { + return buildMcmeta(settings, 'data', + 'Easy Model Entities server profiles'); +} + +function buildResourcePackMcmeta(settings) { + return buildMcmeta(settings, 'resource', + 'Easy Model Entities render assets'); +} + +module.exports = {buildDataPackMcmeta, buildResourcePackMcmeta}; diff --git a/plugins/easy_model_entities/src/builders/profileDiff.js b/plugins/easy_model_entities/src/builders/profileDiff.js new file mode 100644 index 00000000..ce635917 --- /dev/null +++ b/plugins/easy_model_entities/src/builders/profileDiff.js @@ -0,0 +1,49 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +function valuesDiffer(a, b) { + if (Array.isArray(a) || Array.isArray(b)) { + return JSON.stringify(a) !== JSON.stringify(b); + } + + return a !== b; +} + +function diffFlat(values, defaults) { + const result = {}; + Object.keys(values).forEach((key) => { + if (valuesDiffer(values[key], defaults[key])) { + result[key] = values[key]; + } + }); + + return result; +} + +function isEmpty(object) { + return !object || Object.keys(object).length === 0; +} + +function assignIfPresent(target, key, section) { + if (!isEmpty(section)) { + target[key] = section; + } +} + +module.exports = {diffFlat, assignIfPresent}; diff --git a/plugins/easy_model_entities/src/builders/readme.js b/plugins/easy_model_entities/src/builders/readme.js new file mode 100644 index 00000000..e206d346 --- /dev/null +++ b/plugins/easy_model_entities/src/builders/readme.js @@ -0,0 +1,38 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {ResourceLocation} = require('../utils/ResourceLocation'); +const TEMPLATE = require('../resources/readme.md'); + +function buildReadme(settings, fileNames) { + const id = ResourceLocation.buildResourceLocation(settings.namespace, + settings.profileId); + const modelType = settings.modelType || 'entity'; + const serverProfileId = ResourceLocation.buildResourceLocation( + settings.namespace, `${modelType}/${settings.profileId}`); + return TEMPLATE + .replaceAll('{{id}}', id) + .replaceAll('{{serverProfileId}}', serverProfileId) + .replaceAll('{{mcVersion}}', + `Minecraft: Java Edition ${settings.targetVersion}`) + .replaceAll('{{datapackFile}}', fileNames.datapack) + .replaceAll('{{resourcepackFile}}', fileNames.resourcepack); +} + +module.exports = {buildReadme}; diff --git a/plugins/easy_model_entities/src/builders/renderProfile.js b/plugins/easy_model_entities/src/builders/renderProfile.js new file mode 100644 index 00000000..b1c108c5 --- /dev/null +++ b/plugins/easy_model_entities/src/builders/renderProfile.js @@ -0,0 +1,106 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const { + bodyType, + animationMode, + defaultRenderingSettings, + defaultAnimationSettings, + isCustom, + MODEL_TYPE_BLOCK_ENTITY +} = require('../model/presetTypes'); +const {diffFlat, assignIfPresent} = require('./profileDiff'); + +function renderPresetType(settings) { + return settings.modelType === MODEL_TYPE_BLOCK_ENTITY + ? 'static' : settings.presetType; +} + +function toRenderingJson(values) { + return { + scale: values.scale, + shadow_radius: values.shadowRadius, + visible_bounds_width: values.visibleBoundsWidth ?? 0, + visible_bounds_height: values.visibleBoundsHeight ?? 0, + visible_bounds_offset: values.visibleBoundsOffset || [0, 0, 0] + }; +} + +function buildRendering(settings) { + return diffFlat(toRenderingJson(settings.rendering), + toRenderingJson(defaultRenderingSettings(renderPresetType(settings)))); +} + +function toAnimationJson(values) { + return { + mode: values.mode, + swing_speed: values.swingSpeed, + walk_speed_multiplier: values.walkSpeedMultiplier, + idle_strength: values.idleStrength ?? 1, + gait: values.gait || 'natural' + }; +} + +function buildAnimation(settings) { + const defaults = defaultAnimationSettings( + animationMode(renderPresetType(settings))); + const animation = diffFlat(toAnimationJson(settings.animation), + toAnimationJson(defaults)); + + if (settings.animation.mode === 'none') { + delete animation.swing_speed; + delete animation.walk_speed_multiplier; + delete animation.idle_strength; + delete animation.gait; + } + + return animation; +} + +function buildRenderProfile(settings, textureResolution) { + const renderPreset = renderPresetType(settings); + const custom = settings.modelType !== MODEL_TYPE_BLOCK_ENTITY + && isCustom(settings.presetType); + const profile = { + schema_version: settings.schemaVersion, + preset_type: renderPreset + }; + + if (custom || settings.host.bodyType !== bodyType(renderPreset)) { + profile.body_type = settings.host.bodyType; + } + + profile.model = + `${settings.namespace}:easy_model_entities/models/${settings.profileId}`; + + if (textureResolution && textureResolution.texture) { + profile.texture = textureResolution.texture; + } + assignIfPresent(profile, 'textures', + textureResolution && textureResolution.textures); + + assignIfPresent(profile, 'rendering', buildRendering(settings)); + assignIfPresent(profile, 'animation', buildAnimation(settings)); + + return profile; +} + +module.exports = { + buildRenderProfile +}; diff --git a/plugins/easy_model_entities/src/builders/serverProfile.js b/plugins/easy_model_entities/src/builders/serverProfile.js new file mode 100644 index 00000000..f0c7202b --- /dev/null +++ b/plugins/easy_model_entities/src/builders/serverProfile.js @@ -0,0 +1,135 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const { + presetDefaults, + presetDimensions, + movementDefaults, + behaviorModeFor, + wandersByMovement, + isCustom, + DEFAULT_MAX_HEALTH, + DEFAULT_FOLLOW_RANGE, + MODEL_TYPE_BLOCK_ENTITY, + MODEL_TYPE_ENTITY +} = require('../model/presetTypes'); +const {diffFlat, assignIfPresent} = require('./profileDiff'); + +function effectiveDefaults(settings) { + const preset = settings.presetType; + const movementType = settings.host.movementType; + const movement = movementDefaults(preset, movementType); + const mode = behaviorModeFor(preset, movementType); + + return { + movement: { + speed: movement.speed, + step_height: movement.stepHeight, + gravity: movement.gravity + }, + behavior: { + mode: mode, + look_at_players: mode === 'idle_only' || mode === 'ambient', + random_stroll: wandersByMovement(movementType) && mode === 'ambient' + }, + attributes: { + max_health: DEFAULT_MAX_HEALTH, + movement_speed: settings.movement.speed, + follow_range: DEFAULT_FOLLOW_RANGE + }, + dimensions: settings.modelType === MODEL_TYPE_BLOCK_ENTITY + ? presetDefaults(preset, MODEL_TYPE_BLOCK_ENTITY).dimensions + : presetDimensions(preset) + }; +} + +function buildEntity(settings, custom) { + if (!custom) { + return {}; + } + + const host = settings.host; + + return { + type: host.entityType, + movement_type: host.movementType, + body_type: host.bodyType + }; +} + +function buildDimensions(settings, defaults, custom) { + const values = { + width: settings.dimensions.width, + height: settings.dimensions.height, + eye_height: settings.dimensions.eyeHeight + }; + if (custom) { + return values; + } + + return diffFlat(values, { + width: defaults.dimensions.width, + height: defaults.dimensions.height, + eye_height: defaults.dimensions.eyeHeight + }); +} + +function buildServerProfile(settings) { + const modelType = settings.modelType || MODEL_TYPE_ENTITY; + const blockEntity = modelType === MODEL_TYPE_BLOCK_ENTITY; + const custom = !blockEntity && isCustom(settings.presetType); + const defaults = effectiveDefaults(settings); + + const profile = { + schema_version: settings.schemaVersion, + model_type: modelType, + preset_type: settings.presetType + }; + + if (!blockEntity) { + assignIfPresent(profile, 'entity', buildEntity(settings, custom)); + } + + assignIfPresent(profile, 'dimensions', + buildDimensions(settings, defaults, custom)); + + if (!blockEntity) { + assignIfPresent(profile, 'movement', diffFlat({ + speed: settings.movement.speed, + step_height: settings.movement.stepHeight, + gravity: settings.movement.gravity + }, defaults.movement)); + + assignIfPresent(profile, 'behavior', diffFlat({ + mode: settings.behavior.mode, + look_at_players: settings.behavior.lookAtPlayers, + random_stroll: settings.behavior.randomStroll + }, defaults.behavior)); + + assignIfPresent(profile, 'attributes', diffFlat({ + max_health: settings.attributes.maxHealth, + movement_speed: settings.attributes.movementSpeed, + follow_range: settings.attributes.followRange + }, defaults.attributes)); + } + + return profile; +} + +module.exports = {buildServerProfile}; diff --git a/plugins/easy_model_entities/src/exportFlow.js b/plugins/easy_model_entities/src/exportFlow.js new file mode 100644 index 00000000..5ed10e23 --- /dev/null +++ b/plugins/easy_model_entities/src/exportFlow.js @@ -0,0 +1,116 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {BlockbenchAdapter} = require('./BlockbenchAdapter'); +const { + buildPackBundle, + buildModProjectFiles, + pairingVersion +} = require('./builders/exporter'); +const { + EXPORT_TYPE_RESOURCE_PACK, + EXPORT_TYPE_DATA_PACK, + EXPORT_TYPE_MOD_PROJECT +} = require('./model/exportTypes'); + +function zipExportName(settings) { + const base = `${settings.namespace}_${settings.profileId}`; + if (settings.exportType === EXPORT_TYPE_RESOURCE_PACK) { + return `${base}_resourcepack`; + } + + if (settings.exportType === EXPORT_TYPE_DATA_PACK) { + return `${base}_datapack`; + } + + return `${base}_eme`; +} + +function exportToZip(settings, options) { + return BlockbenchAdapter.exportPackBundle( + buildPackBundle(settings, options), zipExportName(settings)) + .then(() => { + Blockbench.showQuickMessage('Easy Model Entities packs exported', 1500); + }) + .catch((error) => { + Blockbench.showMessageBox({title: 'Export failed', message: String(error)}); + }); +} + +function exportToModProject(settings, options) { + const rootDir = BlockbenchAdapter.pickDirectory( + 'Select src/main/resources directory'); + if (!rootDir) { + return; + } + + const {files} = buildModProjectFiles(settings, options); + const existing = BlockbenchAdapter.listExistingFiles(rootDir, files); + + const write = () => { + try { + BlockbenchAdapter.writeToDirectory(rootDir, files); + Blockbench.showQuickMessage( + 'Easy Model Entities files written to mod project', 1500); + } catch (error) { + Blockbench.showMessageBox( + {title: 'Export failed', message: String(error)}); + } + }; + + if (existing.length > 0) { + Blockbench.showMessageBox( + { + title: 'Overwrite existing files?', + message: `${existing.length} file(s) already exist and will be overwritten:\n\n` + + existing.join('\n'), + buttons: ['Overwrite', 'Cancel'], + confirm: 0, + cancel: 1 + }, + (button) => { + if (button === 0) { + write(); + } + } + ); + } else { + write(); + } +} + +function performExport(settings, target, textureResolution) { + BlockbenchAdapter.saveSettings({ + ...settings, + lastExportedVersion: pairingVersion(settings) + }); + + const options = { + modelBytes: BlockbenchAdapter.getModelBytes(), + textureResolution + }; + + if (target === EXPORT_TYPE_MOD_PROJECT) { + return exportToModProject(settings, options); + } + + return exportToZip(settings, options); +} + +module.exports = {performExport, zipExportName}; diff --git a/plugins/easy_model_entities/src/format/EmeCodecHooks.js b/plugins/easy_model_entities/src/format/EmeCodecHooks.js new file mode 100644 index 00000000..0f09b8dd --- /dev/null +++ b/plugins/easy_model_entities/src/format/EmeCodecHooks.js @@ -0,0 +1,64 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {BlockbenchAdapter} = require('../BlockbenchAdapter'); +const {pickModelSettings} = require('../model/templates'); +const {FORMAT_ID} = require('./EmeFormat'); + +const EME_SETTINGS_KEY = 'emeSettings'; + +function handleCompile(event) { + if (Format?.id !== FORMAT_ID) { + return; + } + + const settings = BlockbenchAdapter.loadSettings(); + if (!settings) { + return; + } + + const picked = pickModelSettings(settings); + if (Object.keys(picked).length > 0) { + event.model[EME_SETTINGS_KEY] = picked; + } +} + +function handleParse(event) { + const formatId = event.model?.meta?.model_format || event.model?.model_format; + if (formatId !== FORMAT_ID) { + return; + } + + const settings = event.model[EME_SETTINGS_KEY]; + if (settings && typeof settings === 'object') { + BlockbenchAdapter.saveSettings(settings); + } +} + +function registerEmeCodecHooks() { + Codecs.project.on('compile', handleCompile); + Codecs.project.on('parse', handleParse); +} + +function unregisterEmeCodecHooks() { + Codecs.project.events?.compile?.remove?.(handleCompile); + Codecs.project.events?.parse?.remove?.(handleParse); +} + +module.exports = {registerEmeCodecHooks, unregisterEmeCodecHooks}; diff --git a/plugins/easy_model_entities/src/format/EmeFormat.js b/plugins/easy_model_entities/src/format/EmeFormat.js new file mode 100644 index 00000000..ca7d2f1d --- /dev/null +++ b/plugins/easy_model_entities/src/format/EmeFormat.js @@ -0,0 +1,71 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const FORMAT_ID = 'eme_entity'; + +let emeFormat = null; + +function toModelIdentifier(name) { + return name.trim().toLowerCase() + .replace(/\s+/g, '_') + .replace(/[^a-z0-9_]/g, '') + .replace(/_+/g, '_') + .replace(/^_|_$/g, ''); +} + +function registerEmeFormat(icon) { + if (emeFormat) { + return; + } + + emeFormat = new ModelFormat(FORMAT_ID, { + name: 'Easy Model Entity', + category: 'minecraft', + description: 'Entity model for the Easy Model Entities mod (Minecraft: Java Edition).', + icon: icon || 'pets', + rotate_cubes: true, + box_uv: true, + optional_box_uv: true, + single_texture: false, + texture_folder: true, + bone_rig: true, + centered_grid: true, + animated_textures: false, + animation_mode: true, + animation_files: false, + java_edition_mapping: false, + model_identifier: true, + locators: true, + codec: Codecs.project, + onActivation() { + if (Project && !Project.model_identifier && Project.name) { + Project.model_identifier = toModelIdentifier(Project.name); + } + } + }); +} + +function unregisterEmeFormat() { + if (emeFormat) { + emeFormat.delete(); + emeFormat = null; + } +} + +module.exports = {FORMAT_ID, registerEmeFormat, unregisterEmeFormat}; diff --git a/plugins/easy_model_entities/src/i18n/translations.js b/plugins/easy_model_entities/src/i18n/translations.js new file mode 100644 index 00000000..9c00a1d2 --- /dev/null +++ b/plugins/easy_model_entities/src/i18n/translations.js @@ -0,0 +1,257 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const EN = { + 'eme.dialog.title': 'Easy Model Entities Export', + 'eme.action.export': 'Export Easy Model Entity', + 'eme.action.exportDescription': 'Export the current project for Easy Model Entities.', + 'eme.field.preset': 'Preset', + 'eme.field.namespace': 'Namespace (mod id)', + 'eme.field.profileId': 'Profile ID', + 'eme.field.targetVersion': 'Minecraft Version', + 'eme.field.exportType': 'Export Type', + 'eme.field.modelType': 'Type', + 'eme.field.customize': 'Customize settings', + 'eme.field.includeExternalTextures': 'Include external textures', + 'eme.field.includeExternalTexturesDescription': + 'Copy textures from other namespaces into the resource pack instead of ' + + 'only referencing them. Vanilla minecraft: textures are never copied.', + 'eme.field.hostEntityType': 'Host Entity Type', + 'eme.field.movementType': 'Movement Type', + 'eme.field.bodyType': 'Body Type', + 'eme.field.width': 'Width', + 'eme.field.height': 'Height', + 'eme.field.eyeHeight': 'Eye Height', + 'eme.field.speed': 'Speed', + 'eme.field.stepHeight': 'Step Height', + 'eme.field.gravity': 'Gravity', + 'eme.field.behaviorMode': 'Behavior Mode', + 'eme.field.maxHealth': 'Max Health', + 'eme.field.movementSpeed': 'Movement Speed', + 'eme.field.followRange': 'Follow Range', + 'eme.field.scale': 'Scale', + 'eme.field.shadowRadius': 'Shadow Radius', + 'eme.field.visibleBoundsWidth': 'Visible Bounds Width (0 = auto)', + 'eme.field.visibleBoundsHeight': 'Visible Bounds Height (0 = auto)', + 'eme.field.visibleBoundsOffsetX': 'Visible Bounds Offset X', + 'eme.field.visibleBoundsOffsetY': 'Visible Bounds Offset Y', + 'eme.field.visibleBoundsOffsetZ': 'Visible Bounds Offset Z', + 'eme.field.animationMode': 'Animation Mode', + 'eme.field.swingSpeed': 'Swing Speed', + 'eme.field.walkSpeedMultiplier': 'Walk Speed Multiplier', + 'eme.field.idleStrength': 'Idle Strength', + 'eme.field.gait': 'Gait', + 'eme.section.host': 'Host', + 'eme.section.dimensions': 'Dimensions', + 'eme.section.movement': 'Movement', + 'eme.section.behavior': 'Behavior', + 'eme.section.attributes': 'Attributes', + 'eme.section.rendering': 'Rendering', + 'eme.section.animation': 'Animation', + 'eme.setting.enable_customization': 'Show advanced customization (Easy Model Entities)', + 'eme.setting.enable_experimental': 'Show experimental presets (Easy Model Entities)', + 'eme.preset.custom': 'Custom (manual settings)', + 'eme.preset.static': 'Static (no animation, no movement)', + 'eme.preset.statue': 'Statue (display, no movement)', + 'eme.preset.ticking': 'Ticking (server + client tick)', + 'eme.preset.animated': 'Animated (continuous animation)', + 'eme.preset.animated_randomly': 'Animated randomly (random idle bursts)', + 'eme.preset.humanoid_still': 'Humanoid (2 legs, standing)', + 'eme.preset.humanoid_wandering': 'Humanoid (2 legs, wandering)', + 'eme.preset.quadruped_still': 'Quadruped (4 legs, ground, standing)', + 'eme.preset.quadruped_wandering': 'Quadruped (4 legs, ground, wandering)', + 'eme.preset.aquatic_still': 'Aquatic (fish, still)', + 'eme.preset.aquatic_swimming': 'Aquatic (fish, swimming)', + 'eme.preset.amphibious_still': 'Amphibious (land/water, still)', + 'eme.preset.amphibious_wandering': 'Amphibious (land/water, wandering)', + 'eme.preset.winged_still': 'Winged (bird, perched)', + 'eme.preset.winged_wandering': 'Winged (bird, flying)', + 'eme.preset.winged_humanoid_still': 'Winged humanoid (standing)', + 'eme.preset.winged_humanoid_wandering': 'Winged humanoid (wandering)', + 'eme.preset.arthropod_still': 'Arthropod (insect/spider, still)', + 'eme.preset.arthropod_wandering': 'Arthropod (insect/spider, wandering)', + 'eme.preset.cuboid_still': 'Cuboid (block shape, still)', + 'eme.preset.cuboid_hopping': 'Cuboid (block shape, hopping)', + 'eme.preset.floating_still': 'Floating (hovering, still)', + 'eme.body.static': 'Static', + 'eme.body.biped': 'Biped (2 legs)', + 'eme.body.quadruped': 'Quadruped (4 legs)', + 'eme.body.aquatic': 'Aquatic', + 'eme.body.amphibious': 'Amphibious', + 'eme.body.winged': 'Winged', + 'eme.body.winged_humanoid': 'Winged humanoid', + 'eme.body.arthropod': 'Arthropod', + 'eme.body.cuboid': 'Cuboid', + 'eme.body.floating': 'Floating', + 'eme.movement.ground': 'Ground', + 'eme.movement.water': 'Water', + 'eme.movement.amphibious': 'Amphibious', + 'eme.movement.static': 'Static', + 'eme.gait.natural': 'Natural', + 'eme.gait.feline': 'Feline (light, quick)', + 'eme.gait.ungulate': 'Ungulate (heavy, slow)', + 'eme.behavior.idle_only': 'Idle only', + 'eme.behavior.ambient': 'Ambient (wanders)', + 'eme.behavior.static': 'Static', + 'eme.behavior.external_owner': 'External owner', + 'eme.animation.automatic': 'Automatic', + 'eme.animation.random_idle': 'Random idle', + 'eme.animation.none': 'None', + 'eme.entity.ground_entity': 'Ground entity', + 'eme.entity.static_entity': 'Static entity', + 'eme.entity.aquatic_entity': 'Aquatic entity', + 'eme.entity.amphibious_entity': 'Amphibious entity', + 'eme.modelType.entity': 'Entity', + 'eme.modelType.block_entity': 'Block Entity', + 'eme.exportType.packs': 'Complete: Data Pack + Resource Pack (ZIP)', + 'eme.exportType.resource_pack': 'Update: Resource Pack only (ZIP)', + 'eme.exportType.data_pack': 'Update: Data Pack only (ZIP)', + 'eme.exportType.mod_project': 'Standalone: write into mod project', + 'eme.exportType.model_only': 'Model only: mod integration (no data pack)' +}; + +const DE = { + 'eme.dialog.title': 'Easy Model Entities Export', + 'eme.action.export': 'Easy Model Entity exportieren', + 'eme.action.exportDescription': 'Exportiert das aktuelle Projekt für Easy Model Entities.', + 'eme.field.preset': 'Vorlage', + 'eme.field.namespace': 'Namespace (Mod-ID)', + 'eme.field.profileId': 'Profil-ID', + 'eme.field.targetVersion': 'Minecraft-Version', + 'eme.field.exportType': 'Export-Typ', + 'eme.field.modelType': 'Typ', + 'eme.field.customize': 'Einstellungen anpassen', + 'eme.field.includeExternalTextures': 'Externe Texturen mitliefern', + 'eme.field.includeExternalTexturesDescription': + 'Texturen aus anderen Namespaces ins Resource Pack kopieren statt sie nur ' + + 'zu referenzieren. Vanilla-Texturen (minecraft:) werden nie kopiert.', + 'eme.field.hostEntityType': 'Host-Entität', + 'eme.field.movementType': 'Bewegungsart', + 'eme.field.bodyType': 'Körpertyp', + 'eme.field.width': 'Breite', + 'eme.field.height': 'Höhe', + 'eme.field.eyeHeight': 'Augenhöhe', + 'eme.field.speed': 'Geschwindigkeit', + 'eme.field.stepHeight': 'Schritthöhe', + 'eme.field.gravity': 'Schwerkraft', + 'eme.field.behaviorMode': 'Verhaltensmodus', + 'eme.field.maxHealth': 'Maximale Lebenspunkte', + 'eme.field.movementSpeed': 'Bewegungsgeschwindigkeit', + 'eme.field.followRange': 'Folgereichweite', + 'eme.field.scale': 'Skalierung', + 'eme.field.shadowRadius': 'Schattenradius', + 'eme.field.visibleBoundsWidth': 'Sichtbarkeitsbox-Breite (0 = auto)', + 'eme.field.visibleBoundsHeight': 'Sichtbarkeitsbox-Höhe (0 = auto)', + 'eme.field.visibleBoundsOffsetX': 'Sichtbarkeitsbox-Versatz X', + 'eme.field.visibleBoundsOffsetY': 'Sichtbarkeitsbox-Versatz Y', + 'eme.field.visibleBoundsOffsetZ': 'Sichtbarkeitsbox-Versatz Z', + 'eme.field.animationMode': 'Animationsmodus', + 'eme.field.swingSpeed': 'Schwunggeschwindigkeit', + 'eme.field.walkSpeedMultiplier': 'Geh-Geschwindigkeitsfaktor', + 'eme.field.idleStrength': 'Leerlauf-Intensität', + 'eme.field.gait': 'Gangart', + 'eme.section.host': 'Host', + 'eme.section.dimensions': 'Abmessungen', + 'eme.section.movement': 'Bewegung', + 'eme.section.behavior': 'Verhalten', + 'eme.section.attributes': 'Attribute', + 'eme.section.rendering': 'Darstellung', + 'eme.section.animation': 'Animation', + 'eme.setting.enable_customization': 'Erweiterte Anpassung anzeigen (Easy Model Entities)', + 'eme.setting.enable_experimental': 'Experimentelle Presets anzeigen (Easy Model Entities)', + 'eme.preset.custom': 'Benutzerdefiniert (manuelle Einstellungen)', + 'eme.preset.static': 'Statisch (keine Animation, keine Bewegung)', + 'eme.preset.statue': 'Statue (Anzeige, keine Bewegung)', + 'eme.preset.ticking': 'Tickend (Server- + Client-Tick)', + 'eme.preset.animated': 'Animiert (durchgehende Animation)', + 'eme.preset.animated_randomly': 'Zufällig animiert (zufällige Leerlauf-Schübe)', + 'eme.preset.humanoid_still': 'Humanoid (2 Beine, stehend)', + 'eme.preset.humanoid_wandering': 'Humanoid (2 Beine, umherlaufend)', + 'eme.preset.quadruped_still': 'Vierbeiner (4 Beine, Boden, stehend)', + 'eme.preset.quadruped_wandering': 'Vierbeiner (4 Beine, Boden, umherlaufend)', + 'eme.preset.aquatic_still': 'Wassertier (Fisch, ruhend)', + 'eme.preset.aquatic_swimming': 'Wassertier (Fisch, schwimmend)', + 'eme.preset.amphibious_still': 'Amphibie (Land/Wasser, ruhend)', + 'eme.preset.amphibious_wandering': 'Amphibie (Land/Wasser, umherlaufend)', + 'eme.preset.winged_still': 'Geflügelt (Vogel, sitzend)', + 'eme.preset.winged_wandering': 'Geflügelt (Vogel, fliegend)', + 'eme.preset.winged_humanoid_still': 'Geflügelter Humanoid (stehend)', + 'eme.preset.winged_humanoid_wandering': 'Geflügelter Humanoid (umherlaufend)', + 'eme.preset.arthropod_still': 'Gliederfüßer (Insekt/Spinne, ruhend)', + 'eme.preset.arthropod_wandering': 'Gliederfüßer (Insekt/Spinne, umherlaufend)', + 'eme.preset.cuboid_still': 'Quaderförmig (blockartig, ruhend)', + 'eme.preset.cuboid_hopping': 'Quaderförmig (blockartig, hüpfend)', + 'eme.preset.floating_still': 'Schwebend (ruhend)', + 'eme.body.static': 'Statisch', + 'eme.body.biped': 'Zweibeiner (2 Beine)', + 'eme.body.quadruped': 'Vierbeiner (4 Beine)', + 'eme.body.aquatic': 'Wassertier', + 'eme.body.amphibious': 'Amphibie', + 'eme.body.winged': 'Geflügelt', + 'eme.body.winged_humanoid': 'Geflügelter Humanoid', + 'eme.body.arthropod': 'Gliederfüßer', + 'eme.body.cuboid': 'Quaderförmig', + 'eme.body.floating': 'Schwebend', + 'eme.movement.ground': 'Boden', + 'eme.movement.water': 'Wasser', + 'eme.movement.amphibious': 'Amphibisch', + 'eme.movement.static': 'Statisch', + 'eme.gait.natural': 'Natürlich', + 'eme.gait.feline': 'Katzenartig (leicht, schnell)', + 'eme.gait.ungulate': 'Huftier (schwer, langsam)', + 'eme.behavior.idle_only': 'Nur Leerlauf', + 'eme.behavior.ambient': 'Umgebung (läuft umher)', + 'eme.behavior.static': 'Statisch', + 'eme.behavior.external_owner': 'Externer Besitzer', + 'eme.animation.automatic': 'Automatisch', + 'eme.animation.random_idle': 'Zufälliger Leerlauf', + 'eme.animation.none': 'Keine', + 'eme.entity.ground_entity': 'Boden-Entität', + 'eme.entity.static_entity': 'Statische Entität', + 'eme.entity.aquatic_entity': 'Wasser-Entität', + 'eme.entity.amphibious_entity': 'Amphibien-Entität', + 'eme.modelType.entity': 'Entität', + 'eme.modelType.block_entity': 'Block-Entität', + 'eme.exportType.packs': 'Komplett: Data Pack + Resource Pack (ZIP)', + 'eme.exportType.resource_pack': 'Update: Nur Resource Pack (ZIP)', + 'eme.exportType.data_pack': 'Update: Nur Data Pack (ZIP)', + 'eme.exportType.mod_project': 'Standalone: in Mod-Projekt schreiben', + 'eme.exportType.model_only': 'Nur Modell: Mod-Integration (kein Data Pack)' +}; + +function registerTranslations() { + if (typeof Language !== 'undefined' && typeof Language.addTranslations + === 'function') { + Language.addTranslations('en', EN); + Language.addTranslations('de', DE); + } +} + +function t(key) { + if (typeof tl === 'function') { + const translated = tl(key); + if (translated && translated !== key) { + return translated; + } + } + + return EN[key] || key; +} + +module.exports = {EN, DE, registerTranslations, t}; diff --git a/plugins/easy_model_entities/src/index.js b/plugins/easy_model_entities/src/index.js new file mode 100644 index 00000000..8003a391 --- /dev/null +++ b/plugins/easy_model_entities/src/index.js @@ -0,0 +1,282 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const { + getDefaults, + deepMerge, + applyTemplate, + DEFAULT_PRESET +} = require('./model/templates'); +const { + MODEL_TYPE_ENTITY, + MODEL_TYPE_BLOCK_ENTITY +} = require('./model/presetTypes'); +const {ModelDimensions} = require('./model/ModelDimensions'); +const {VisibleBounds} = require('./model/VisibleBounds'); +const {PresetDetector} = require('./model/PresetDetector'); +const {ResourceLocation} = require('./utils/ResourceLocation'); +const {resolveTextures} = require('./utils/TextureResolver'); +const {patchTexturePanel, unpatchTexturePanel} = require('./ui/texturePanel'); +const {Validator} = require('./model/Validator'); +const { + EXPORT_TYPE_PACKS, + isSinglePackExport +} = require('./model/exportTypes'); +const {performExport} = require('./exportFlow'); +const {BlockbenchAdapter} = require('./BlockbenchAdapter'); +const {openExportDialog} = require('./ui/exportDialog'); +const {registerTranslations, t} = require('./i18n/translations'); +const exportIconSvg = require('./resources/export_icon.svg'); +const createIconSvg = require('./resources/create_icon.svg'); +const {FORMAT_ID, registerEmeFormat, unregisterEmeFormat} = require( + './format/EmeFormat'); +const {registerEmeCodecHooks, unregisterEmeCodecHooks} = require( + './format/EmeCodecHooks'); + +const MODDED_ENTITY_FORMAT_ID = 'modded_entity'; +const ACTION_ID = 'eme_export'; +const SETTING_ID = 'eme_enable_customization'; +const EXPERIMENTAL_SETTING_ID = 'eme_enable_experimental'; +const EXPORT_ICON = 'data:image/svg+xml,' + encodeURIComponent(exportIconSvg); +const CREATE_ICON = 'data:image/svg+xml,' + encodeURIComponent(createIconSvg); +let exportAction; +let projectProperty; +let customizationSetting; +let experimentalSetting; + +function settingEnabled(setting) { + return !!setting && typeof setting.value === 'boolean' && setting.value; +} + +function resolveDialogState() { + const storedSettings = BlockbenchAdapter.loadSettings(); + const stats = BlockbenchAdapter.getModelStats(); + const bounds = BlockbenchAdapter.getModelBounds(); + const visibleBounds = VisibleBounds.derive(BlockbenchAdapter.getModelCubes()); + + let settings; + let preset; + let blockPreset; + let modelType; + let customize; + let exportType; + + if (storedSettings) { + settings = deepMerge(getDefaults(), storedSettings); + modelType = settings.modelType || MODEL_TYPE_ENTITY; + customize = !!storedSettings.customize; + exportType = storedSettings.exportType || EXPORT_TYPE_PACKS; + if (modelType === MODEL_TYPE_BLOCK_ENTITY) { + preset = 'custom'; + blockPreset = settings.presetType || 'static'; + } else { + preset = settings.presetType || DEFAULT_PRESET; + blockPreset = 'static'; + } + } else { + preset = PresetDetector.detectPresetType(stats, bounds); + blockPreset = 'static'; + modelType = MODEL_TYPE_ENTITY; + settings = applyTemplate(preset); + customize = false; + exportType = EXPORT_TYPE_PACKS; + } + + const modelDimensions = ModelDimensions.deriveDimensions(bounds, + settings.host.bodyType); + if (!storedSettings) { + ModelDimensions.applyModelDimensions(settings, modelDimensions); + } + + const projectName = BlockbenchAdapter.getProjectName(); + if (projectName + && (!settings.profileId || settings.profileId + === getDefaults().profileId)) { + settings.profileId = ResourceLocation.sanitizeProfileId(projectName); + } + + VisibleBounds.applyVisibleBounds(settings, visibleBounds, { + preserveExisting: !!storedSettings + }); + + return { + settings, preset, blockPreset, modelType, customize, exportType, + visibleBounds, modelDimensions, + hasExternalTextures: (stats.externalTextures || []).length > 0 + }; +} + +function formatIssues(issues) { + return issues.map((issue) => `• ${issue.message}`).join('\n'); +} + +function pairingWarnings(settings) { + if (!isSinglePackExport(settings.exportType) + || settings.lastExportedVersion) { + return []; + } + + return [{ + code: 'NO_PAIRED_EXPORT', + message: 'This project has no earlier export to pair with, so the single ' + + 'pack may not match the installed one. Export both packs instead' + }]; +} + +function runExport(settings, target) { + const textureResolution = resolveTextures( + BlockbenchAdapter.collectTextures(), settings); + const result = Validator.validateSettings(settings, { + ...BlockbenchAdapter.getModelStats(), + textureIssues: textureResolution.issues, + referencedTextures: textureResolution.referencedOnly + }); + const warnings = [...result.warnings, ...pairingWarnings(settings)]; + + if (!result.valid) { + Blockbench.showMessageBox({ + title: t('eme.dialog.title'), + message: 'Export blocked. Please fix the following issues:\n\n' + + formatIssues(result.errors) + }); + return; + } + + const doExport = () => performExport(settings, target, textureResolution); + + if (warnings.length > 0) { + Blockbench.showMessageBox( + { + title: t('eme.dialog.title'), + message: 'The following warnings were found:\n\n' + formatIssues( + warnings) + '\n\nExport anyway?', + buttons: ['Export', 'Cancel'], + confirm: 0, + cancel: 1 + }, + (button) => { + if (button === 0) { + doExport(); + } + } + ); + } else { + doExport(); + } +} + +function openDialog() { + if (!Project) { + Blockbench.showQuickMessage('Open a project first', 1500); + return; + } + + const state = resolveDialogState(); + openExportDialog({ + settings: state.settings, + preset: state.preset, + blockPreset: state.blockPreset, + modelType: state.modelType, + customize: state.customize, + exportType: state.exportType, + showCustomization: settingEnabled(customizationSetting), + experimental: settingEnabled(experimentalSetting), + modelDimensions: state.modelDimensions, + visibleBounds: state.visibleBounds, + hasExternalTextures: state.hasExternalTextures, + onExport: runExport + }); +} + +BBPlugin.register('easy_model_entities', { + title: 'Easy Model Entities', + icon: 'icon.png', + author: 'Markus Bordihn', + description: 'Export Blockbench models for the Easy Model Entities mod (Minecraft: Java Edition) as ready-to-install Data Pack and Resource Pack files.', + tags: ['Minecraft: Java Edition', 'Format', 'Entity'], + version: '1.0.0', + min_version: '4.9.0', + variant: 'desktop', + await_loading: true, + has_changelog: true, + contributes: { + formats: ['eme_entity'] + }, + website: 'https://www.curseforge.com/minecraft/mc-mods/easy-model-entities', + repository: 'https://github.com/MarkusBordihn/BOs-Easy-Model-Entities', + bug_tracker: 'https://github.com/MarkusBordihn/BOs-Easy-Model-Entities/issues', + onload() { + registerTranslations(); + registerEmeFormat(CREATE_ICON); + registerEmeCodecHooks(); + + projectProperty = new Property(ModelProject, 'object', + BlockbenchAdapter.PROJECT_PROPERTY, { + label: 'Easy Model Entities Settings', + exposed: false, + default: undefined + }); + + customizationSetting = new Setting(SETTING_ID, { + name: t('eme.setting.enable_customization'), + category: 'export', + type: 'toggle', + value: false + }); + + experimentalSetting = new Setting(EXPERIMENTAL_SETTING_ID, { + name: t('eme.setting.enable_experimental'), + category: 'export', + type: 'toggle', + value: false + }); + + exportAction = new Action(ACTION_ID, { + name: t('eme.action.export'), + description: t('eme.action.exportDescription'), + icon: EXPORT_ICON, + condition: () => typeof Format !== 'undefined' && Format + && (Format.id === FORMAT_ID || Format.id === MODDED_ENTITY_FORMAT_ID), + click() { + openDialog(); + } + }); + + MenuBar.addAction(exportAction, 'file.export'); + + patchTexturePanel(FORMAT_ID); + }, + onunload() { + unpatchTexturePanel(); + unregisterEmeCodecHooks(); + unregisterEmeFormat(); + if (exportAction) { + exportAction.delete(); + } + if (customizationSetting) { + customizationSetting.delete(); + } + if (experimentalSetting) { + experimentalSetting.delete(); + } + if (projectProperty) { + projectProperty.delete(); + } + } +}); diff --git a/plugins/easy_model_entities/src/model/ModelDimensions.js b/plugins/easy_model_entities/src/model/ModelDimensions.js new file mode 100644 index 00000000..d896c6f6 --- /dev/null +++ b/plugins/easy_model_entities/src/model/ModelDimensions.js @@ -0,0 +1,81 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +class ModelDimensions { + static PIXELS_PER_BLOCK = 16; + static FALLBACK = { + width: 0.6, + height: 0.8, + eyeHeight: 0.5 + }; + + static #EYE_HEIGHT_FACTOR = {biped: 0.9, quadruped: 0.8, static: 0.85}; + + static #round(value) { + return Math.round(value * 1000) / 1000; + } + + static #isUsableBounds(bounds) { + if (!bounds) { + return false; + } + + return ['minX', 'minY', 'minZ', 'maxX', 'maxY', 'maxZ'].every( + (key) => Number.isFinite(bounds[key])); + } + + static deriveDimensions(bounds, bodyType) { + if (!ModelDimensions.#isUsableBounds(bounds)) { + return {...ModelDimensions.FALLBACK}; + } + + const width = Math.max( + Math.max(0, bounds.maxX - bounds.minX), + Math.max(0, bounds.maxZ - bounds.minZ) + ) / ModelDimensions.PIXELS_PER_BLOCK; + const height = Math.max(0, bounds.maxY - bounds.minY) + / ModelDimensions.PIXELS_PER_BLOCK; + + if (width <= 0 || height <= 0) { + return {...ModelDimensions.FALLBACK}; + } + + const eyeHeightFactor = ModelDimensions.#EYE_HEIGHT_FACTOR[bodyType] + ?? ModelDimensions.#EYE_HEIGHT_FACTOR.static; + + return { + width: ModelDimensions.#round(width), + height: ModelDimensions.#round(height), + eyeHeight: ModelDimensions.#round(height * eyeHeightFactor) + }; + } + + static applyModelDimensions(settings, modelDimensions) { + if (!modelDimensions) { + return settings; + } + + settings.dimensions.width = modelDimensions.width; + settings.dimensions.height = modelDimensions.height; + settings.dimensions.eyeHeight = modelDimensions.eyeHeight; + return settings; + } +} + +module.exports = {ModelDimensions}; diff --git a/plugins/easy_model_entities/src/model/PresetDetector.js b/plugins/easy_model_entities/src/model/PresetDetector.js new file mode 100644 index 00000000..d1b8224a --- /dev/null +++ b/plugins/easy_model_entities/src/model/PresetDetector.js @@ -0,0 +1,115 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {standardClipNames} = require('./presetTypes'); + +class PresetDetector { + static #CUBE_RATIO = 1.4; + static #ANIMATED_FALLBACK_PRESET = 'floating_still'; + + static #names(stats) { + const boneNames = (stats && stats.boneNames) || []; + return boneNames.map((name) => String(name).toLowerCase()); + } + + static #any(names, ...needles) { + return names.some( + (name) => needles.some((needle) => name.includes(needle))); + } + + static #count(names, needle) { + return names.filter((name) => name.includes(needle)).length; + } + + static #extents(bounds) { + if (!bounds) { + return null; + } + + if (!['minX', 'minY', 'minZ', 'maxX', 'maxY', 'maxZ'].every( + (key) => Number.isFinite(bounds[key]))) { + return null; + } + + return { + x: Math.max(0, bounds.maxX - bounds.minX), + y: Math.max(0, bounds.maxY - bounds.minY), + z: Math.max(0, bounds.maxZ - bounds.minZ) + }; + } + + static detect(stats, bounds) { + const names = PresetDetector.#names(stats); + const hasWings = PresetDetector.#any(names, 'wing'); + const hasArms = PresetDetector.#any(names, 'arm'); + const legCount = PresetDetector.#count(names, 'leg'); + const hasFrontBackLegs = names.some( + (name) => name.includes('leg') + && (name.includes('front') || name.includes('back'))); + + if (hasWings) { + return hasArms + ? {presetType: 'winged_humanoid_still', reason: 'wings + arms'} + : {presetType: 'winged_still', reason: 'wing bones'}; + } + + if (PresetDetector.#any(names, 'fin', 'fluke', 'fish')) { + return {presetType: 'aquatic_still', reason: 'fin/fish bones'}; + } + + if (legCount >= 6 || PresetDetector.#any(names, 'spider', 'insect', + 'mandible')) { + return {presetType: 'arthropod_still', reason: 'six or more legs'}; + } + + if (hasFrontBackLegs || legCount === 4) { + return {presetType: 'quadruped_still', reason: 'four legs (front/back)'}; + } + + if (hasArms) { + return {presetType: 'humanoid_still', reason: 'arm bones'}; + } + + const extents = PresetDetector.#extents(bounds); + if (extents && extents.x > 0 && extents.y > 0 && extents.z > 0) { + if (Math.max(extents.x, extents.y, extents.z) / Math.min(extents.x, + extents.y, extents.z) <= PresetDetector.#CUBE_RATIO) { + return {presetType: 'cuboid_still', reason: 'cube-like proportions'}; + } + if (extents.y < Math.max(extents.x, extents.z) * 0.5) { + return {presetType: 'aquatic_still', reason: 'flat proportions'}; + } + } + + if (standardClipNames(stats && stats.animations).length > 0) { + return { + presetType: PresetDetector.#ANIMATED_FALLBACK_PRESET, + reason: 'animation clips without distinguishing limbs' + }; + } + + return {presetType: 'statue', reason: 'no distinguishing limbs'}; + } + + static detectPresetType(stats, bounds) { + return PresetDetector.detect(stats, bounds).presetType; + } +} + +module.exports = {PresetDetector}; diff --git a/plugins/easy_model_entities/src/model/Validator.js b/plugins/easy_model_entities/src/model/Validator.js new file mode 100644 index 00000000..546110fc --- /dev/null +++ b/plugins/easy_model_entities/src/model/Validator.js @@ -0,0 +1,372 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {ResourceLocation} = require('../utils/ResourceLocation'); +const { + PRESET_TYPES, + BLOCK_ENTITY_PRESET_TYPES, + ANIMATION_CLIPS, + MODEL_TYPE_BLOCK_ENTITY, + standardClipNames +} = require('./presetTypes'); + +const SUPPORTED_ANIMATION_CHANNELS = new Set(['rotation', 'position']); + +class Validator { + static BUDGETS = { + maxTextureSize: 2048, + softTextureSize: 128, + maxModelFileSizeBytes: 2 * 1024 * 1024, + softModelFileSizeBytes: 1024 * 1024, + maxCubeCount: 512, + softCubeCount: 384, + maxBoneCount: 128, + softBoneCount: 96, + maxHierarchyDepth: 32, + softHierarchyDepth: 24, + maxAnimationCount: 16 + }; + + static LIMITS = { + minDimension: 0.01, + maxDimension: 8, + maxMovementSpeed: 2, + maxStepHeight: 2 + }; + + static REQUIRED_BODY_PARTS = { + quadruped: ['root', 'body', 'head', 'front_left_leg', 'front_right_leg', + 'back_left_leg', 'back_right_leg'], + biped: ['root', 'head', 'body', 'left_arm', 'right_arm', 'left_leg', + 'right_leg'], + static: [] + }; + + static #isFiniteNumber(value) { + return typeof value === 'number' && Number.isFinite(value); + } + + static #validateNumeric(errors, label, value) { + if (!Validator.#isFiniteNumber(value)) { + errors.push({ + code: 'INVALID_NUMERIC', + message: `${label} must be a finite number` + }); + return false; + } + + return true; + } + + static #validateRange(errors, code, label, value, minimum, maximum) { + if (!Validator.#validateNumeric(errors, label, value) + || (value >= minimum && value <= maximum)) { + return; + } + + errors.push({ + code: code, + message: `${label} must be between ${minimum} and ${maximum}` + }); + } + + static #validateNonNegative(errors, code, label, value) { + Validator.#validateRange(errors, code, label, value, 0, + Number.MAX_VALUE); + } + + static #validatePositive(errors, code, label, value) { + if (!Validator.#validateNumeric(errors, label, value) || value > 0) { + return; + } + + errors.push({code: code, message: `${label} must be greater than 0`}); + } + + static #warnIfOverBudget(warnings, value, soft, max, softWarning, + maxWarning) { + if (!Validator.#isFiniteNumber(value)) { + return; + } + if (value > max) { + warnings.push(maxWarning); + } else if (value > soft) { + warnings.push(softWarning); + } + } + + static validateSettings(settings, context) { + const errors = []; + const warnings = []; + const ctx = context || {}; + + if (!ResourceLocation.isValidNamespace(settings.namespace)) { + errors.push({ + code: 'INVALID_NAMESPACE', + message: `Invalid namespace: ${settings.namespace}, use for example ` + + `${ResourceLocation.sanitizeNamespace(settings.namespace)}` + }); + } + if (!ResourceLocation.isValidPath(settings.profileId)) { + errors.push({ + code: 'INVALID_PROFILE_ID', + message: `Invalid profile ID: ${settings.profileId}, use for example ` + + `${ResourceLocation.sanitizeProfileId(settings.profileId)}` + }); + } + + const id = `${settings.namespace}:${settings.profileId}`; + if ( + ResourceLocation.isValidNamespace(settings.namespace) && + ResourceLocation.isValidPath(settings.profileId) && + !ResourceLocation.isValidResourceLocation(id) + ) { + errors.push({ + code: 'INVALID_RESOURCE_LOCATION', + message: `Invalid resource location: ${id}` + }); + } + const blockEntity = settings.modelType === MODEL_TYPE_BLOCK_ENTITY; + if (!blockEntity + && !ResourceLocation.isValidResourceLocation( + settings.host.entityType)) { + errors.push({ + code: 'INVALID_RESOURCE_LOCATION', + message: `Invalid host entity type: ${settings.host.entityType}` + }); + } + const validPresets = blockEntity ? BLOCK_ENTITY_PRESET_TYPES : PRESET_TYPES; + if (!validPresets.includes(settings.presetType)) { + errors.push({ + code: 'INVALID_PRESET_TYPE', + message: `Invalid preset type: ${settings.presetType}` + }); + } + + const limits = Validator.LIMITS; + const {width, height, eyeHeight} = settings.dimensions; + Validator.#validateRange(errors, 'INVALID_DIMENSIONS', 'dimensions.width', + width, limits.minDimension, limits.maxDimension); + Validator.#validateRange(errors, 'INVALID_DIMENSIONS', 'dimensions.height', + height, limits.minDimension, limits.maxDimension); + Validator.#validateRange(errors, 'INVALID_DIMENSIONS', + 'dimensions.eye_height', eyeHeight, 0, + Number.isFinite(height) ? height : limits.maxDimension); + + Validator.#validateRange(errors, 'INVALID_MOVEMENT', 'movement.speed', + settings.movement.speed, 0, limits.maxMovementSpeed); + Validator.#validateRange(errors, 'INVALID_MOVEMENT', + 'movement.step_height', settings.movement.stepHeight, 0, + limits.maxStepHeight); + + Validator.#validateNonNegative(errors, 'INVALID_ATTRIBUTES', + 'attributes.max_health', settings.attributes.maxHealth); + Validator.#validateNonNegative(errors, 'INVALID_ATTRIBUTES', + 'attributes.movement_speed', settings.attributes.movementSpeed); + Validator.#validateNonNegative(errors, 'INVALID_ATTRIBUTES', + 'attributes.follow_range', settings.attributes.followRange); + + const rendering = settings.rendering; + Validator.#validatePositive(errors, 'INVALID_RENDER_SETTINGS', + 'rendering.scale', rendering.scale); + Validator.#validateNonNegative(errors, 'INVALID_RENDER_SETTINGS', + 'rendering.shadow_radius', rendering.shadowRadius); + Validator.#validateNonNegative(errors, 'INVALID_RENDER_SETTINGS', + 'rendering.visible_bounds_width', rendering.visibleBoundsWidth ?? 0); + Validator.#validateNonNegative(errors, 'INVALID_RENDER_SETTINGS', + 'rendering.visible_bounds_height', rendering.visibleBoundsHeight ?? 0); + + const animation = settings.animation; + Validator.#validateNonNegative(errors, 'INVALID_ANIMATION_SETTINGS', + 'animation.swing_speed', animation.swingSpeed); + Validator.#validateNonNegative(errors, 'INVALID_ANIMATION_SETTINGS', + 'animation.walk_speed_multiplier', animation.walkSpeedMultiplier); + Validator.#validateNonNegative(errors, 'INVALID_ANIMATION_SETTINGS', + 'animation.idle_strength', animation.idleStrength ?? 1); + + if (ctx.hasModel === false) { + errors.push( + {code: 'MISSING_MODEL', message: 'No model present in project'}); + } + if (ctx.hasTexture === false) { + errors.push( + {code: 'MISSING_TEXTURE', message: 'No texture present in project'}); + } + (ctx.textureIssues || []).forEach((issue) => errors.push(issue)); + + const referencedTextures = ctx.referencedTextures || []; + if (referencedTextures.length > 0) { + warnings.push({ + code: 'EXTERNAL_TEXTURE_REFERENCE', + message: `${referencedTextures.join( + ', ')} stays a reference and is not ` + + 'part of the export; the mod or pack providing it has to be ' + + 'installed, or enable "Include external textures"' + }); + } + + if (Validator.#isFiniteNumber(ctx.textureWidth) + && Validator.#isFiniteNumber(ctx.textureHeight)) { + const textureSize = Math.max(ctx.textureWidth, ctx.textureHeight); + Validator.#warnIfOverBudget(warnings, textureSize, + Validator.BUDGETS.softTextureSize, Validator.BUDGETS.maxTextureSize, + { + code: 'SOFT_TEXTURE_SIZE', + message: `Texture larger than ${Validator.BUDGETS.softTextureSize}x${Validator.BUDGETS.softTextureSize}` + }, + { + code: 'LARGE_TEXTURE', + message: `Texture larger than ${Validator.BUDGETS.maxTextureSize}x${Validator.BUDGETS.maxTextureSize}` + }); + } + Validator.#warnIfOverBudget(warnings, ctx.modelFileSize, + Validator.BUDGETS.softModelFileSizeBytes, + Validator.BUDGETS.maxModelFileSizeBytes, + { + code: 'SOFT_MODEL_SIZE', + message: 'Model file larger than 1 MB' + }, + {code: 'LARGE_MODEL', message: 'Model file larger than 2 MB'}); + Validator.#warnIfOverBudget(warnings, ctx.cubeCount, + Validator.BUDGETS.softCubeCount, Validator.BUDGETS.maxCubeCount, + { + code: 'SOFT_CUBE_COUNT', + message: `More than ${Validator.BUDGETS.softCubeCount} cubes` + }, + { + code: 'HIGH_CUBE_COUNT', + message: `More than ${Validator.BUDGETS.maxCubeCount} cubes` + }); + Validator.#warnIfOverBudget(warnings, ctx.boneCount, + Validator.BUDGETS.softBoneCount, Validator.BUDGETS.maxBoneCount, + { + code: 'SOFT_BONE_COUNT', + message: `More than ${Validator.BUDGETS.softBoneCount} bones` + }, + { + code: 'HIGH_BONE_COUNT', + message: `More than ${Validator.BUDGETS.maxBoneCount} bones` + }); + Validator.#warnIfOverBudget(warnings, ctx.hierarchyDepth, + Validator.BUDGETS.softHierarchyDepth, + Validator.BUDGETS.maxHierarchyDepth, + { + code: 'SOFT_HIERARCHY_DEPTH', + message: `Hierarchy deeper than ${Validator.BUDGETS.softHierarchyDepth}` + }, + { + code: 'DEEP_HIERARCHY', + message: `Hierarchy deeper than ${Validator.BUDGETS.maxHierarchyDepth}` + }); + if (Validator.#isFiniteNumber(ctx.animationCount) + && ctx.animationCount > Validator.BUDGETS.maxAnimationCount) { + errors.push({ + code: 'HIGH_ANIMATION_COUNT', + message: `More than ${Validator.BUDGETS.maxAnimationCount} animations; the mod refuses to load the model` + }); + } + + (ctx.animations || []).forEach((animation) => { + const name = String(animation.name || '').toLowerCase(); + if (!ANIMATION_CLIPS.includes(name)) { + warnings.push({ + code: 'CUSTOM_ANIMATION_CLIP', + message: `Animation "${animation.name}" is not one of ${ANIMATION_CLIPS.join( + ', ')} and is never played automatically; it can only be played by name via command or API` + }); + } + if (animation.hasExpression) { + warnings.push({ + code: 'UNSUPPORTED_ANIMATION_EXPRESSION', + message: `Animation "${animation.name}" uses expressions; the mod only supports numeric keyframes and drops the clip` + }); + } + (animation.channels || []).forEach((channel) => { + if (!SUPPORTED_ANIMATION_CHANNELS.has(channel)) { + warnings.push({ + code: 'UNSUPPORTED_ANIMATION_CHANNEL', + message: `Animation "${animation.name}" uses the "${channel}" channel which is ignored by the mod` + }); + } + }); + }); + + const clipNames = standardClipNames(ctx.animations); + if (!blockEntity && clipNames.length > 0 + && (settings.animation.mode === 'none' + || settings.host.bodyType === 'static')) { + warnings.push({ + code: 'ANIMATIONS_NEVER_PLAYED', + message: `The preset ${settings.presetType} renders without animation, ` + + `so the mod never plays ${clipNames.join( + ', ')}; pick an animated ` + + 'preset instead' + }); + } + + Validator.getMissingBodyParts(settings.host.bodyType, + ctx.boneNames || []).forEach((part) => { + warnings.push({ + code: 'MISSING_BODY_PART', + message: `Missing recommended body part: ${part}` + }); + }); + + return {errors, warnings, valid: errors.length === 0}; + } + + static getMissingBodyParts(bodyType, boneNames) { + const required = Validator.REQUIRED_BODY_PARTS[bodyType] || []; + const present = new Set( + (boneNames || []).map((name) => String(name).toLowerCase())); + + return required.filter((part) => !present.has(part)); + } + + static validateOutputPath(relativePath) { + if (typeof relativePath !== 'string' || relativePath.length === 0) { + return { + valid: false, + code: 'INVALID_OUTPUT_PATH', + message: 'Empty output path' + }; + } + + const normalized = relativePath.replaceAll('\\', '/'); + if (normalized.includes('..')) { + return { + valid: false, + code: 'PATH_TRAVERSAL', + message: 'Path traversal detected' + }; + } + + if (/^([a-zA-Z]:\/|\/)/.test(normalized)) { + return { + valid: false, + code: 'ABSOLUTE_PATH', + message: 'Absolute paths are not allowed' + }; + } + + return {valid: true, path: normalized}; + } +} + +module.exports = {Validator}; diff --git a/plugins/easy_model_entities/src/model/VisibleBounds.js b/plugins/easy_model_entities/src/model/VisibleBounds.js new file mode 100644 index 00000000..27152f62 --- /dev/null +++ b/plugins/easy_model_entities/src/model/VisibleBounds.js @@ -0,0 +1,109 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +class VisibleBounds { + static PIXELS_PER_BLOCK = 16; + + static #empty() { + return {width: 0, height: 0, offset: [0, 0, 0]}; + } + + static #round(value) { + return Math.round(value * 1000) / 1000; + } + + static #usableCubes(cubes) { + if (!Array.isArray(cubes)) { + return []; + } + + return cubes.filter((cube) => + cube && Array.isArray(cube.from) && Array.isArray(cube.to) + && cube.from.length === 3 && cube.to.length === 3 + && [...cube.from, ...cube.to].every((value) => Number.isFinite(value))); + } + + static #bounds(cubes) { + const bounds = { + min: [Infinity, Infinity, Infinity], + max: [-Infinity, -Infinity, -Infinity] + }; + cubes.forEach((cube) => { + for (let axis = 0; axis < 3; axis++) { + bounds.min[axis] = Math.min(bounds.min[axis], cube.from[axis], + cube.to[axis]); + bounds.max[axis] = Math.max(bounds.max[axis], cube.from[axis], + cube.to[axis]); + } + }); + + return bounds; + } + + static derive(cubes) { + const usable = VisibleBounds.#usableCubes(cubes); + if (usable.length === 0) { + return VisibleBounds.#empty(); + } + + const ppb = VisibleBounds.PIXELS_PER_BLOCK; + const bounds = VisibleBounds.#bounds(usable); + const xExtent = bounds.max[0] - bounds.min[0]; + const yExtent = bounds.max[1] - bounds.min[1]; + const zExtent = bounds.max[2] - bounds.min[2]; + const width = Math.max(xExtent, zExtent) / ppb; + const height = yExtent / ppb; + + if (width <= 0 || height <= 0) { + return VisibleBounds.#empty(); + } + + return { + width: VisibleBounds.#round(width), + height: VisibleBounds.#round(height), + offset: [ + VisibleBounds.#round(((bounds.min[0] + bounds.max[0]) / 2) / ppb), + VisibleBounds.#round(bounds.min[1] / ppb), + VisibleBounds.#round(((bounds.min[2] + bounds.max[2]) / 2) / ppb) + ] + }; + } + + static hasVisibleBounds(settings) { + const rendering = settings && settings.rendering; + return !!rendering && rendering.visibleBoundsWidth > 0 + && rendering.visibleBoundsHeight > 0; + } + + static applyVisibleBounds(settings, derived, options) { + if (!derived || !settings || !settings.rendering) { + return settings; + } + if (options?.preserveExisting && VisibleBounds.hasVisibleBounds(settings)) { + return settings; + } + + settings.rendering.visibleBoundsWidth = derived.width; + settings.rendering.visibleBoundsHeight = derived.height; + settings.rendering.visibleBoundsOffset = derived.offset.slice(); + return settings; + } +} + +module.exports = {VisibleBounds}; diff --git a/plugins/easy_model_entities/src/model/exportTypes.js b/plugins/easy_model_entities/src/model/exportTypes.js new file mode 100644 index 00000000..ce590c2d --- /dev/null +++ b/plugins/easy_model_entities/src/model/exportTypes.js @@ -0,0 +1,56 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const EXPORT_TYPE_PACKS = 'packs'; +const EXPORT_TYPE_RESOURCE_PACK = 'resource_pack'; +const EXPORT_TYPE_DATA_PACK = 'data_pack'; +const EXPORT_TYPE_MOD_PROJECT = 'mod_project'; +const EXPORT_TYPE_MODEL_ONLY = 'model_only'; + +const ZIP_EXPORT_TYPES = [EXPORT_TYPE_PACKS, EXPORT_TYPE_RESOURCE_PACK, + EXPORT_TYPE_DATA_PACK]; + +function isZipExport(exportType) { + return ZIP_EXPORT_TYPES.includes(exportType); +} + +function includesDataPack(exportType) { + return exportType !== EXPORT_TYPE_RESOURCE_PACK; +} + +function includesResourcePack(exportType) { + return exportType !== EXPORT_TYPE_DATA_PACK; +} + +function isSinglePackExport(exportType) { + return exportType === EXPORT_TYPE_RESOURCE_PACK + || exportType === EXPORT_TYPE_DATA_PACK; +} + +module.exports = { + EXPORT_TYPE_PACKS, + EXPORT_TYPE_RESOURCE_PACK, + EXPORT_TYPE_DATA_PACK, + EXPORT_TYPE_MOD_PROJECT, + EXPORT_TYPE_MODEL_ONLY, + isZipExport, + includesDataPack, + includesResourcePack, + isSinglePackExport +}; diff --git a/plugins/easy_model_entities/src/model/presetTypes.js b/plugins/easy_model_entities/src/model/presetTypes.js new file mode 100644 index 00000000..a195dccd --- /dev/null +++ b/plugins/easy_model_entities/src/model/presetTypes.js @@ -0,0 +1,425 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const SCHEMA_VERSION = '0.2.0'; + +const MODEL_TYPE_ENTITY = 'entity'; +const MODEL_TYPE_BLOCK_ENTITY = 'block_entity'; +const MODEL_TYPES = [MODEL_TYPE_ENTITY, MODEL_TYPE_BLOCK_ENTITY]; + +const BODY_TYPES = [ + 'static', + 'biped', + 'quadruped', + 'aquatic', + 'amphibious', + 'winged', + 'winged_humanoid', + 'arthropod', + 'cuboid', + 'floating' +]; + +const MOVEMENT_TYPES = ['ground', 'water', 'amphibious', 'static']; +const BEHAVIOR_MODES = ['idle_only', 'ambient', 'static', 'external_owner']; +const ANIMATION_MODES = ['automatic', 'random_idle', 'none']; +const ANIMATION_CLIPS = ['idle', 'walk', 'swim', 'fly', 'hurt', 'death', + 'attack', 'sit']; +const GAIT_TYPES = ['natural', 'feline', 'ungulate']; + +const GROUND_ENTITY = 'easy_model_entities:ground_entity'; +const STATIC_ENTITY = 'easy_model_entities:static_entity'; +const AQUATIC_ENTITY = 'easy_model_entities:aquatic_entity'; +const AMPHIBIOUS_ENTITY = 'easy_model_entities:amphibious_entity'; + +const FALLBACK_DIMENSIONS = {width: 0.6, height: 1.8, eyeHeight: 1.62}; +const BLOCK_ENTITY_DIMENSIONS = {width: 1.0, height: 1.0, eyeHeight: 0.5}; + +const DEFAULT_MAX_HEALTH = 10; +const DEFAULT_FOLLOW_RANGE = 16; + +const PRESET_TYPES = [ + 'custom', + 'static', + 'statue', + 'humanoid_still', + 'humanoid_wandering', + 'quadruped_still', + 'quadruped_wandering', + 'aquatic_still', + 'aquatic_swimming', + 'amphibious_still', + 'amphibious_wandering', + 'winged_still', + 'winged_wandering', + 'winged_humanoid_still', + 'winged_humanoid_wandering', + 'arthropod_still', + 'arthropod_wandering', + 'cuboid_still', + 'cuboid_hopping', + 'floating_still' +]; + +const BLOCK_ENTITY_PRESET_TYPES = [ + 'static', + 'ticking', + 'animated', + 'animated_randomly' +]; + +const STABLE_PRESET_TYPES = new Set([ + 'static', + 'statue', + 'humanoid_still', + 'humanoid_wandering', + 'quadruped_still', + 'quadruped_wandering', + 'aquatic_still', + 'aquatic_swimming', + 'amphibious_still', + 'amphibious_wandering', + 'winged_still', + 'winged_wandering', + 'winged_humanoid_still', + 'winged_humanoid_wandering', + 'arthropod_still', + 'arthropod_wandering', + 'cuboid_still', + 'cuboid_hopping', + 'floating_still', + // Block entity stable presets. + 'animated', + 'animated_randomly' +]); + +const SELECTABLE_PRESET_TYPES = PRESET_TYPES.filter((id) => id !== 'custom'); + +function standardClipNames(animations) { + return (animations || []) + .map((animation) => String(animation.name || '').toLowerCase()) + .filter((name) => ANIMATION_CLIPS.includes(name)); +} + +function isStablePreset(presetType) { + return STABLE_PRESET_TYPES.has(presetType); +} + +function isCustom(presetType) { + return presetType === 'custom'; +} + +function isStill(presetType) { + return String(presetType).endsWith('_still'); +} + +function isMoving(presetType) { + return String(presetType).endsWith('_wandering') + || presetType === 'aquatic_swimming' + || presetType === 'cuboid_hopping'; +} + +function bodyType(presetType) { + switch (presetType) { + case 'humanoid_still': + case 'humanoid_wandering': + return 'biped'; + case 'quadruped_still': + case 'quadruped_wandering': + return 'quadruped'; + case 'aquatic_still': + case 'aquatic_swimming': + return 'aquatic'; + case 'amphibious_still': + case 'amphibious_wandering': + return 'amphibious'; + case 'winged_still': + case 'winged_wandering': + return 'winged'; + case 'winged_humanoid_still': + case 'winged_humanoid_wandering': + return 'winged_humanoid'; + case 'arthropod_still': + case 'arthropod_wandering': + return 'arthropod'; + case 'cuboid_still': + case 'cuboid_hopping': + return 'cuboid'; + case 'floating_still': + return 'floating'; + default: + return 'static'; + } +} + +function isAquatic(presetType) { + return presetType === 'aquatic_still' || presetType === 'aquatic_swimming'; +} + +function isAmphibious(presetType) { + return presetType === 'amphibious_still' + || presetType === 'amphibious_wandering'; +} + +function movementType(presetType) { + if (isAquatic(presetType)) { + return 'water'; + } + if (isAmphibious(presetType)) { + return 'amphibious'; + } + + return isMoving(presetType) ? 'ground' : 'static'; +} + +function entityType(presetType) { + if (isAquatic(presetType)) { + return AQUATIC_ENTITY; + } + if (isAmphibious(presetType)) { + return AMPHIBIOUS_ENTITY; + } + + return isMoving(presetType) || presetType === 'static' ? GROUND_ENTITY + : STATIC_ENTITY; +} + +function presetDimensions(presetType) { + switch (presetType) { + case 'quadruped_still': + case 'quadruped_wandering': + return {width: 0.9, height: 0.9, eyeHeight: 0.6}; + case 'aquatic_still': + case 'aquatic_swimming': + return {width: 0.7, height: 0.4, eyeHeight: 0.25}; + case 'amphibious_still': + case 'amphibious_wandering': + return {width: 0.9, height: 0.6, eyeHeight: 0.4}; + case 'winged_still': + case 'winged_wandering': + return {width: 0.6, height: 0.9, eyeHeight: 0.6}; + case 'winged_humanoid_still': + case 'winged_humanoid_wandering': + return {width: 0.6, height: 0.8, eyeHeight: 0.6}; + case 'arthropod_still': + case 'arthropod_wandering': + return {width: 1.4, height: 0.9, eyeHeight: 0.45}; + case 'cuboid_still': + case 'cuboid_hopping': + case 'floating_still': + return {width: 1.0, height: 1.0, eyeHeight: 0.5}; + default: + return {...FALLBACK_DIMENSIONS}; + } +} + +function presetShadowRadius(presetType) { + switch (presetType) { + case 'quadruped_still': + case 'quadruped_wandering': + case 'amphibious_still': + case 'amphibious_wandering': + return 0.45; + case 'aquatic_still': + case 'aquatic_swimming': + case 'winged_still': + case 'winged_wandering': + case 'winged_humanoid_still': + case 'winged_humanoid_wandering': + return 0.25; + case 'arthropod_still': + case 'arthropod_wandering': + return 0.7; + case 'cuboid_still': + case 'cuboid_hopping': + case 'floating_still': + return 0.5; + default: + return 0.3; + } +} + +function behaviorModeFor(presetType, move) { + if (isMoving(presetType)) { + return 'ambient'; + } + + if (isStill(presetType)) { + return 'idle_only'; + } + + if (presetType === 'custom') { + return move === 'ground' || move === 'amphibious' ? 'idle_only' : 'static'; + } + + return 'static'; +} + +function behaviorMode(presetType) { + return behaviorModeFor(presetType, movementType(presetType)); +} + +function wandersByMovement(move) { + return move === 'ground' || move === 'amphibious' || move === 'water'; +} + +function defaultSpeed(move) { + switch (move) { + case 'ground': + return 0.22; + case 'amphibious': + return 0.15; + case 'water': + return 0.1; + default: + return 0; + } +} + +function movementDefaults(presetType, move) { + return { + speed: defaultSpeed(move), + stepHeight: move === 'ground' || move === 'amphibious' ? 0.6 : 0, + gravity: presetType === 'static' || move !== 'static' + }; +} + +function animationMode(presetType) { + return presetType === 'custom' || presetType === 'static' + || presetType === 'statue' ? 'none' : 'automatic'; +} + +function blockEntityAnimationMode(presetType) { + switch (presetType) { + case 'animated': + return 'automatic'; + case 'animated_randomly': + return 'random_idle'; + default: + return 'none'; + } +} + +function defaultRenderingSettings(presetType) { + return { + scale: 1, + shadowRadius: presetShadowRadius(presetType), + visibleBoundsWidth: 0, + visibleBoundsHeight: 0, + visibleBoundsOffset: [0, 0, 0] + }; +} + +function defaultAnimationSettings(mode) { + return { + mode: mode, + swingSpeed: 1, + walkSpeedMultiplier: 1, + idleStrength: 1, + gait: 'natural' + }; +} + +function blockEntityPresetDefaults(presetType) { + return { + schemaVersion: SCHEMA_VERSION, + modelType: MODEL_TYPE_BLOCK_ENTITY, + presetType: presetType, + host: {entityType: '', movementType: 'static', bodyType: 'static'}, + dimensions: {...BLOCK_ENTITY_DIMENSIONS}, + movement: {speed: 0, stepHeight: 0, gravity: false}, + behavior: {mode: 'static', lookAtPlayers: false, randomStroll: false}, + attributes: { + maxHealth: DEFAULT_MAX_HEALTH, + movementSpeed: 0, + followRange: DEFAULT_FOLLOW_RANGE + }, + rendering: defaultRenderingSettings('cuboid_still'), + animation: defaultAnimationSettings(blockEntityAnimationMode(presetType)) + }; +} + +function presetDefaults(presetType, modelType) { + if (modelType === MODEL_TYPE_BLOCK_ENTITY) { + return blockEntityPresetDefaults(presetType); + } + + const move = movementType(presetType); + const movement = movementDefaults(presetType, move); + const mode = behaviorMode(presetType); + + return { + schemaVersion: SCHEMA_VERSION, + modelType: MODEL_TYPE_ENTITY, + presetType: presetType, + host: { + entityType: entityType(presetType), + movementType: move, + bodyType: bodyType(presetType) + }, + dimensions: presetDimensions(presetType), + movement: { + speed: movement.speed, + stepHeight: movement.stepHeight, + gravity: movement.gravity + }, + behavior: { + mode: mode, + lookAtPlayers: mode === 'idle_only' || mode === 'ambient', + randomStroll: wandersByMovement(move) && mode === 'ambient' + }, + attributes: { + maxHealth: DEFAULT_MAX_HEALTH, + movementSpeed: movement.speed, + followRange: DEFAULT_FOLLOW_RANGE + }, + rendering: defaultRenderingSettings(presetType), + animation: defaultAnimationSettings(animationMode(presetType)) + }; +} + +module.exports = { + SCHEMA_VERSION, + MODEL_TYPE_ENTITY, + MODEL_TYPE_BLOCK_ENTITY, + MODEL_TYPES, + BODY_TYPES, + MOVEMENT_TYPES, + DEFAULT_MAX_HEALTH, + DEFAULT_FOLLOW_RANGE, + BEHAVIOR_MODES, + PRESET_TYPES, + BLOCK_ENTITY_PRESET_TYPES, + ANIMATION_MODES, + ANIMATION_CLIPS, + GAIT_TYPES, + SELECTABLE_PRESET_TYPES, + standardClipNames, + isStablePreset, + isCustom, + bodyType, + behaviorModeFor, + movementDefaults, + wandersByMovement, + animationMode, + presetDimensions, + presetDefaults, + defaultRenderingSettings, + defaultAnimationSettings +}; diff --git a/plugins/easy_model_entities/src/model/templates.js b/plugins/easy_model_entities/src/model/templates.js new file mode 100644 index 00000000..6ea05b09 --- /dev/null +++ b/plugins/easy_model_entities/src/model/templates.js @@ -0,0 +1,96 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {MODEL_TYPE_ENTITY, presetDefaults} = require('./presetTypes'); +const {getDefaultVersionId} = require('./versionMatrix'); + +const DEFAULT_PRESET = 'statue'; + +const MODEL_SETTING_KEYS = [ + 'schemaVersion', + 'modelType', + 'presetType', + 'namespace', + 'profileId', + 'targetVersion', + 'customize', + 'includeExternalTextures', + 'host', + 'dimensions', + 'movement', + 'behavior', + 'attributes', + 'rendering', + 'animation', + 'lastExportedVersion' +]; + +function pickModelSettings(settings) { + if (!settings || typeof settings !== 'object') { + return settings; + } + + const picked = {}; + MODEL_SETTING_KEYS.forEach((key) => { + if (settings[key] !== undefined) { + picked[key] = settings[key]; + } + }); + + return picked; +} + +function deepMerge(target, source) { + const result = Array.isArray(target) ? target.slice() : ({ + ...target + }); + Object.keys(source).forEach((key) => { + const value = source[key]; + if (value && typeof value === 'object' && !Array.isArray(value)) { + result[key] = deepMerge(result[key] || {}, value); + } else if (Array.isArray(value)) { + result[key] = value.slice(); + } else { + result[key] = value; + } + }); + + return result; +} + +function getDefaults() { + return deepMerge(presetDefaults(DEFAULT_PRESET), { + namespace: 'example_org', + profileId: 'entity', + targetVersion: getDefaultVersionId() + }); +} + +function applyTemplate(presetType, modelType) { + return deepMerge(getDefaults(), + presetDefaults(presetType, modelType || MODEL_TYPE_ENTITY)); +} + +module.exports = { + DEFAULT_PRESET, + getDefaults, + applyTemplate, + deepMerge, + pickModelSettings +}; diff --git a/plugins/easy_model_entities/src/model/versionMatrix.js b/plugins/easy_model_entities/src/model/versionMatrix.js new file mode 100644 index 00000000..3f96f020 --- /dev/null +++ b/plugins/easy_model_entities/src/model/versionMatrix.js @@ -0,0 +1,87 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const DEFAULT_VERSION_ID = '1.20.1'; + +const VERSIONS = [ + { + id: '1.20.1', + label: 'Minecraft 1.20.1', + data: {packFormat: 15}, + resource: {packFormat: 15}, + enabled: true + }, + { + id: '1.21.1', + label: 'Minecraft 1.21.1', + data: {packFormat: 48}, + resource: {packFormat: 34}, + enabled: true + }, + { + id: '1.21.11', + label: 'Minecraft 1.21.11', + data: {packFormat: 94, minFormat: [94, 1], maxFormat: [94, 1]}, + resource: {packFormat: 75, minFormat: [75, 0], maxFormat: [75, 0]}, + enabled: true + }, + { + id: '26.1.2', + label: 'Minecraft 26.1.2', + data: {packFormat: 101, minFormat: [101, 1], maxFormat: [101, 1]}, + resource: {packFormat: 84, minFormat: [84, 0], maxFormat: [84, 0]}, + enabled: true + }, + { + id: '26.2', + label: 'Minecraft 26.2', + data: {packFormat: 101, minFormat: [101, 1], maxFormat: [101, 1]}, + resource: {packFormat: 84, minFormat: [84, 0], maxFormat: [84, 0]}, + enabled: true + } +]; + +function getVersions() { + return VERSIONS.map((version) => ({...version})); +} + +function getVersion(id) { + return VERSIONS.find((version) => version.id === id) || null; +} + +function getDefaultVersionId() { + return DEFAULT_VERSION_ID; +} + +function getPackFormats(id) { + const version = getVersion(id); + if (!version?.enabled || !version.data || !version.resource) { + return null; + } + return { + data: version.data, + resource: version.resource + }; +} + +module.exports = { + getVersions, + getDefaultVersionId, + getPackFormats +}; diff --git a/plugins/easy_model_entities/src/package-lock.json b/plugins/easy_model_entities/src/package-lock.json new file mode 100644 index 00000000..53244508 --- /dev/null +++ b/plugins/easy_model_entities/src/package-lock.json @@ -0,0 +1,5790 @@ +{ + "name": "easy_model_entities", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "easy_model_entities", + "version": "1.0.0", + "devDependencies": { + "blockbench-types": "^5.1.0", + "copy-webpack-plugin": "14.0.0", + "jest": "30.4.2", + "webpack": "5.107.2", + "webpack-cli": "7.0.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@discoveryjs/json-ext": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-1.1.0.tgz", + "integrity": "sha512-Xc3VhU02wqZ1HvHRJUwL09HkZSTvidqY5Ya0NXBSYOxAp+Ln9dcJr9fySI+CkONzP3PekQo9WdzCv0PGER/mOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.17.0" + } + }, + "node_modules/@electron-internal/extract-zip": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.4.tgz", + "integrity": "sha512-Zr1Vs7E9tpCNhZHDAbFVXc2gEVCG9RqPDjrno5+bdgB6LRAuvgyMHJut4NCVyYwtAieapMzc3fiQ3CSTi75ARg==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/get": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-5.0.0.tgz", + "integrity": "sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^3.0.0", + "graceful-fs": "^4.2.11", + "progress": "^2.0.3", + "semver": "^7.6.3", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=22.12.0" + }, + "optionalDependencies": { + "undici": "^7.24.4" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.4.1.tgz", + "integrity": "sha512-v3bhyxUh9Hgmo5p6hAOXe14/R3ZxZDOsvHleh4B07z3m/x4/ngPUXEm9XwK4sF4u+f+P2ORb0Ge+MgpaqRMVDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.4.1", + "jest-util": "30.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.4.2.tgz", + "integrity": "sha512-TZJA6cPJUFxoWhxaLo8t0VX/MZX2wPWr0uIDvLSHIvN4gu9h02vSzqI2kBADG1ExqQlC+cY09xKMSreivvrChQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.4.1", + "@jest/pattern": "30.4.0", + "@jest/reporters": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.4.1", + "jest-config": "30.4.2", + "jest-haste-map": "30.4.1", + "jest-message-util": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-resolve": "30.4.1", + "jest-resolve-dependencies": "30.4.2", + "jest-runner": "30.4.2", + "jest-runtime": "30.4.2", + "jest-snapshot": "30.4.1", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "jest-watcher": "30.4.1", + "pretty-format": "30.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", + "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.4.1.tgz", + "integrity": "sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-mock": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.4.1.tgz", + "integrity": "sha512-ginrj6TMgh2GshLUGCjO94Ptx9HhdZA/I6A9iUfyeLKFtdAjnKzHDgzgP9HYQgbxM1lbXScQ2eUBz2lGeVDPWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.4.1", + "jest-snapshot": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.4.1.tgz", + "integrity": "sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.4.1.tgz", + "integrity": "sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@sinonjs/fake-timers": "^15.4.0", + "@types/node": "*", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.4.1.tgz", + "integrity": "sha512-ZbuY4cmXC8DkxYjfvT2DbcHWL2T6vmsMhXCDcmTB2T0y0gaezBI77ufq5ZAIdcRkYZ7NEQEDg1xFeKbxUJ5v5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/expect": "30.4.1", + "@jest/types": "30.4.1", + "jest-mock": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz", + "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.4.1.tgz", + "integrity": "sha512-/SnkPCzEQpUaBH81kjdEdDdo2WZl5hxw+BmLDGWjRkm8o7XlhjwsU36cqwe5PGBE5WYpBvDzRSdXx9rbGuJtNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.4.1", + "jest-util": "30.4.1", + "jest-worker": "30.4.1", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.4.1.tgz", + "integrity": "sha512-ObY4ljvQ95mt6iwKtVLetR/4yXiAgl3H4nJxhztr0MTjrN97TwDYrnCp/kF60Ec9HdhkWTHSu+Hg05aXfngpOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.4.1.tgz", + "integrity": "sha512-/ZG7pgEiOmmWkN9TplKbOu4id2N5lh7FHwRwlkgBVAzGdRH+OkkQ8wX/kIxg4zmd3ZQvAL1RwL2yWsvNYYECTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.4.1", + "@jest/types": "30.4.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.4.1.tgz", + "integrity": "sha512-PeYE+4td5rKjoRPxztObrXU+H8hsjZfxKMXOcmrr34JerSyB/ROOxbbicz8B7A5j9R9VayDnVPvBmedqCsFCdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.4.1", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.4.1.tgz", + "integrity": "sha512-Wz0LyktlTvRefoymh+n64hQ84KNXsRGcwdoZ8CSa0Ea+fgYcHZlnk+hDP7v2MS7il2bQ5uTEIxf4/NNfhMN4KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.4.1", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-util": "30.4.1", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", + "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.4.0", + "@jest/schemas": "30.4.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.52", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.52.tgz", + "integrity": "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", + "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jquery": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.34.tgz", + "integrity": "sha512-3m3939S3erqmTLJANS/uy0B6V7BorKx7RorcGZVjZ62dF5PAGbKEDZK1CuLtKombJkFA2T1jl8LAIIs7IV6gBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sizzle": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/prismjs": { + "version": "1.26.6", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sizzle": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.10.tgz", + "integrity": "sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.129.2", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.129.2.tgz", + "integrity": "sha512-fZDNRtUFnq3baNjDiAzi4QoHYcczWtEdsduH/tpiwk91fbVk8COJiT/iLoN3tIOjLKMySoYsjUfP0VTmUfZKLw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tinycolor2": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.6.tgz", + "integrity": "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vue/compiler-sfc": { + "version": "2.7.14", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.14.tgz", + "integrity": "sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.18.4", + "postcss": "^8.4.14", + "source-map": "^0.6.1" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/babel-jest": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.4.1.tgz", + "integrity": "sha512-fATAbM8piYxkiXQp3RBXmZHxZVNJZAVXXfyeyCN2Tida3+qJ8ea9UxhiJ2y4fLO90ZImKt6k9FlcH2+rLkJGhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.4.1", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.4.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.4.0.tgz", + "integrity": "sha512-9EdtWM/sSfXLOGLwSn+GS6pIXyBnL07/8gyJlwFXjWy4DxMOyItqyUT29d4lQiS380EZwYlX7/At4PgBS+m2aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.4.0.tgz", + "integrity": "sha512-lBY4jxsNmCnSiu7kquw8ZC9F4+XLMOKypT3RnNHPvU2Kpd4W0xaPuLr5ZkRyOsvLYAY4yaW1ZwTW4xB7NIiZzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.4.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.43", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", + "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/blockbench-types": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/blockbench-types/-/blockbench-types-5.1.0.tgz", + "integrity": "sha512-JLMHJZ+2J7aFQwSacfW4N/rvd56JRqvaBsHNH2R6sjyxkwKPuNPKYIeav/WtRXnq3dmlsEoH3rtg09s7yhe3sg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7", + "@types/dompurify": "^3.0.5", + "@types/jquery": "^3.5.32", + "@types/prismjs": "^1.26.0", + "@types/three": "^0.129.2", + "@types/tinycolor2": "^1.4.6", + "dompurify": "^3.0.1", + "electron": "^40.8.0", + "prismjs": "^1.29.0", + "tinycolor2": "^1.6.0", + "typescript": "^5.8.3", + "vue": "2.7.14", + "wintersky": "^1.3.0" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", + "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001803", + "electron-to-chromium": "^1.5.389", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001805", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001805.tgz", + "integrity": "sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-webpack-plugin": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-14.0.0.tgz", + "integrity": "sha512-3JLW90aBGeaTLpM7mYQKpnVdgsUZRExY55giiZgLuX/xTQRUs1dOCwbBnWnvY6Q6rfZoXMNwzOQJCSZPppfqXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-parent": "^6.0.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^7.0.3", + "tinyglobby": "^0.2.12" + }, + "engines": { + "node": ">= 20.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dompurify": { + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz", + "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==", + "dev": true, + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron": { + "version": "40.10.6", + "resolved": "https://registry.npmjs.org/electron/-/electron-40.10.6.tgz", + "integrity": "sha512-TGjlkOU9Lg6K4KjDbsErywCWCIDaNgLh0q+xj0nlpRoQhevI7VBIxBTtJI/V30lypyLAaXMpnP9O9jui1/qRFw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron-internal/extract-zip": "^1.0.1", + "@electron/get": "^5.0.0", + "@types/node": "^24.9.0" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 22.12.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.392", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.392.tgz", + "integrity": "sha512-1yQq3VQCZRwsnYc67Oc+1fge6Lwtn0hzi6zmEVkB61Zx21kTbwJAW4dFLadl5Rc1tKhG/kSpYXnfiAhu0f0a1g==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.24.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.2.tgz", + "integrity": "sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.4.1.tgz", + "integrity": "sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.4.1", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.4.2.tgz", + "integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.4.2", + "@jest/types": "30.4.1", + "import-local": "^3.2.0", + "jest-cli": "30.4.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.4.1.tgz", + "integrity": "sha512-IuctmYrxi21iOSOaIXpJWalHyPAsVv0GeBHKDn8C1CA4W5htHn7INL+wdnL4Bo0+olEndvAFkmb++tIQJG+vvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.4.1", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.4.2.tgz", + "integrity": "sha512-rvHH7VlY6LgbJXJTQ87GW62g1FntOtbhh0zT+v04kC+pgL6aBKyYINXxWukCpj3dcIBMw5/XUbtDS9dU9JTXeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/expect": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.4.1", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-runtime": "30.4.2", + "jest-snapshot": "30.4.1", + "jest-util": "30.4.1", + "p-limit": "^3.1.0", + "pretty-format": "30.4.1", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.4.2.tgz", + "integrity": "sha512-jfA2ocvVHMXS2QijrJ0d31ektP+d/W0T5RpcTX2Pq+3sVqHlsXVCM2+FmwpL+bdY8OfHpIg9xMxLF17Zg0U49Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.4.2", + "@jest/test-result": "30.4.1", + "@jest/types": "30.4.1", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.4.2", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.4.2.tgz", + "integrity": "sha512-rNHAShJQqQwFNoL0hbf3BphSBOWnpOUAKvidLS/AjNVLPfoj5mSf4jQMfW3cYOs6hXeZC7nF7mDHaBnbxELOzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.4.0", + "@jest/test-sequencer": "30.4.1", + "@jest/types": "30.4.1", + "babel-jest": "30.4.1", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-circus": "30.4.2", + "jest-docblock": "30.4.0", + "jest-environment-node": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-resolve": "30.4.1", + "jest-runner": "30.4.2", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "parse-json": "^5.2.0", + "pretty-format": "30.4.1", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", + "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.4.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.4.0.tgz", + "integrity": "sha512-ZPMabUZCx5MpbZ2eBYSvZ0J8fvo3dR9oM+eeUpb3aKNQFuS2tu3Duw1TNlMoP8k3WQgKGJuhcMFvwcVuq6T7oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.4.1.tgz", + "integrity": "sha512-/8MJbH6fuj48TstjrMf+u/pd06Qezz5xOXvZA6442heNOWr8bdeoGZX2d9fCn028CoMgYmroH9//zky5GfyYmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.4.1", + "chalk": "^4.1.2", + "jest-util": "30.4.1", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.4.1.tgz", + "integrity": "sha512-4FZYVOk85hz2AyT6BbarKy9u37g6DbrDyCdFhsnDdXqyrueYQvB+0zO4f/kqLCRD0BsPRXPMNJeQwihKZV8naw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/fake-timers": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-mock": "30.4.1", + "jest-util": "30.4.1", + "jest-validate": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.4.1.tgz", + "integrity": "sha512-rFrcONd8jeFsyw+Z9CrScJgglRf2+NFmNam8dKu7n+SoHqNYT47mn0DdEcVUZJpvh7Iz6/si7f7yUH7GJHVgnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.4.0", + "jest-util": "30.4.1", + "jest-worker": "30.4.1", + "picomatch": "^4.0.3", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.4.1.tgz", + "integrity": "sha512-IpmyiioeHxiWDhesHnUFmOxcTzwCwKpgACgWajtAP+nYQXiY7DakTxB6Bx9JFiRMljr0AX1PvnQdaU1KFoz6NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.4.1.tgz", + "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.4.1", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", + "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.4.1", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-util": "30.4.1", + "picomatch": "^4.0.3", + "pretty-format": "30.4.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", + "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", + "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.4.1.tgz", + "integrity": "sha512-Zry8Yq/yJcNAZ7dJ5F2heic8AheXvbFZ7XI5V+h28nrYZ7Qoyy4dItq8OodjnYD270mvX+ZudmrNV9cysqhW5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.4.2.tgz", + "integrity": "sha512-gDiVh1I+GxYzz9oXlyw+1wv6VOYX1WYxMOfjsA3iGKePV2oxmbHhwxfkALxNxYy1ciw6APWwkW2zZONwP97aEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.4.0", + "jest-snapshot": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.4.2.tgz", + "integrity": "sha512-2dw0PslVYXxffXGpLo+Ejad+KcI1Qkjn7f4X4619gf21oCUmL+SPfjqIa/losUem3yEOvfNZe/F1HWUcNpODcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.4.1", + "@jest/environment": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.4.0", + "jest-environment-node": "30.4.1", + "jest-haste-map": "30.4.1", + "jest-leak-detector": "30.4.1", + "jest-message-util": "30.4.1", + "jest-resolve": "30.4.1", + "jest-runtime": "30.4.2", + "jest-util": "30.4.1", + "jest-watcher": "30.4.1", + "jest-worker": "30.4.1", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.4.2.tgz", + "integrity": "sha512-3/5e8iPz2k/VLqlr8DgTftYyLUv8Su3FkCAO2/Od81UsUTpSxOrS6O5x5KkoQwyUjmpYyDJKeyAvg2T2nvpNkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/fake-timers": "30.4.1", + "@jest/globals": "30.4.1", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-resolve": "30.4.1", + "jest-snapshot": "30.4.1", + "jest-util": "30.4.1", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.4.1.tgz", + "integrity": "sha512-tEOkkfOMppUyeiHwjZswOQ3lcnoTnws/q5FnGIaeIh/jmoU0ZlgMYRR8sTlTj+nNGCoJ0RDq6SfxGxCsyMTPmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.4.1", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.4.1", + "graceful-fs": "^4.2.11", + "jest-diff": "30.4.1", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-util": "30.4.1", + "pretty-format": "30.4.1", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.4.1.tgz", + "integrity": "sha512-PDWi4SOwLnwqNDfHZjOcsEFyZ4fc/2W2gVL3DEoyqnB6jCQMLRtfBong8s6omIw3lI0HWOus12xfnFmQtjW3fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.4.1", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.4.1.tgz", + "integrity": "sha512-/l9UonmvCwjHH7d2h3iAwIloLc1H0S8mJZ/LNK3i86hqwPAz8otUJjP9MfYtz9Tt77Su5FD2xGjZn8d31IZHlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.4.1", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.4.1.tgz", + "integrity": "sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.4.1", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", + "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loader-runner": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/molangjs": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/molangjs/-/molangjs-1.7.0.tgz", + "integrity": "sha512-slta4hSuftsCvirnepZ2L46k/uEfAmaE0o9wo/b6oES7p9dh+N7HCUErFEsoH3dGxOoQ86pW2A+GRuMWl/rlOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.4.1", + "ansi-styles": "^5.2.0", + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is-18": { + "name": "react-is", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-is-19": { + "name": "react-is", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", + "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.7.tgz", + "integrity": "sha512-YAy8Od6KV+uuwUuU50np8fGB/Aues6Y0nAhA9y/hId74PlKUcme4pXcBD46NWKr1Q4osN/iseZ17YqO1XfmI8g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.3.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.49.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.0.tgz", + "integrity": "sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/three": { + "version": "0.134.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.134.0.tgz", + "integrity": "sha512-LbBerg7GaSPjYtTOnu41AMp7tV6efUNR3p4Wk5NzkSsNTBuA5mDGOfwwZL1jhhVMLx9V20HolIUo0+U3AXehbg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", + "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.4" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.12.2", + "@unrs/resolver-binding-android-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-x64": "1.12.2", + "@unrs/resolver-binding-freebsd-x64": "1.12.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-musl": "1.12.2", + "@unrs/resolver-binding-openharmony-arm64": "1.12.2", + "@unrs/resolver-binding-wasm32-wasi": "1.12.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vue": { + "version": "2.7.14", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.14.tgz", + "integrity": "sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ==", + "deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-sfc": "2.7.14", + "csstype": "^3.1.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.107.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.107.2.tgz", + "integrity": "sha512-v7RhXaJbpMlV0D7hC7lb2EbnxkoeUqf9qhKr6lozx3Q48pmFrqqNRmZFUEGmi7pSwm6fCQ2H1IjvCkHqdpVdjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.22.0", + "es-module-lexer": "^2.1.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "loader-runner": "^4.3.2", + "mime-db": "^1.54.0", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.5.0", + "watchpack": "^2.5.1", + "webpack-sources": "^3.5.0" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-7.0.3.tgz", + "integrity": "sha512-2E2C6A1e2El7791zQgTH7LPIuwLjRliow9OHS/qlJc9pwhZlCoL/uiwqd/1WSlXT83wJfmfDbkcqHXuXoPJZ3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "^1.1.0", + "commander": "^14.0.3", + "cross-spawn": "^7.0.6", + "envinfo": "^7.14.0", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^6.0.1" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.101.0", + "webpack-bundle-analyzer": "^4.0.0 || ^5.0.0", + "webpack-dev-server": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz", + "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/wintersky": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/wintersky/-/wintersky-1.3.3.tgz", + "integrity": "sha512-1rT84FrUFu3rWLsT6WBPUFm0/zxCFrRR86D5zfzeS1XKAp3LLiQligXK9830BvAcVrAbsvBhm9vlaHsFOHkU5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "molangjs": "^1.6.6", + "three": "^0.134.0", + "tinycolor2": "^1.4.2" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/plugins/easy_model_entities/src/package.json b/plugins/easy_model_entities/src/package.json new file mode 100644 index 00000000..0c1093e2 --- /dev/null +++ b/plugins/easy_model_entities/src/package.json @@ -0,0 +1,28 @@ +{ + "name": "easy_model_entities", + "version": "1.0.0", + "description": "Export Blockbench projects into Easy Model Entities Data Pack and Resource Pack files.", + "author": "Markus Bordihn", + "private": true, + "main": "index.js", + "scripts": { + "build": "webpack", + "test": "jest" + }, + "devDependencies": { + "blockbench-types": "^5.1.0", + "copy-webpack-plugin": "14.0.0", + "jest": "30.4.2", + "webpack": "5.107.2", + "webpack-cli": "7.0.3" + }, + "jest": { + "testEnvironment": "node", + "testMatch": [ + "**/tests/**/*.test.js" + ], + "transform": { + "\\.md$": "/tests/mdTransform.js" + } + } +} diff --git a/plugins/easy_model_entities/src/resources/create_icon.svg b/plugins/easy_model_entities/src/resources/create_icon.svg new file mode 100644 index 00000000..3c75f629 --- /dev/null +++ b/plugins/easy_model_entities/src/resources/create_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/easy_model_entities/src/resources/export_icon.svg b/plugins/easy_model_entities/src/resources/export_icon.svg new file mode 100644 index 00000000..29210322 --- /dev/null +++ b/plugins/easy_model_entities/src/resources/export_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/easy_model_entities/src/resources/icon.png b/plugins/easy_model_entities/src/resources/icon.png new file mode 100644 index 00000000..7c6f764e Binary files /dev/null and b/plugins/easy_model_entities/src/resources/icon.png differ diff --git a/plugins/easy_model_entities/src/resources/readme.md b/plugins/easy_model_entities/src/resources/readme.md new file mode 100644 index 00000000..36a943e1 --- /dev/null +++ b/plugins/easy_model_entities/src/resources/readme.md @@ -0,0 +1,84 @@ +# {{id}} - Easy Model Entities pack + +A custom Blockbench model that appears in Minecraft as an entity you can spawn — no coding and no +extra mod needed for this model. Made with the **Easy Model Entities Exporter** Blockbench plugin +for **{{mcVersion}}**. + +> **Note:** This is an early release. The pack format may still change, so you might need to +> re-export this pack after updating the plugin or the mod. + +Profile ID: `{{serverProfileId}}` + +## What you need + +The **Easy Model Entities** mod for **{{mcVersion}}** (Forge, NeoForge or Fabric, depending on the +Minecraft version). Without the mod these files do nothing at all. + +- [CurseForge](https://www.curseforge.com/minecraft/mc-mods/easy-model-entities) +- [Modrinth](https://modrinth.com/mod/easy-model-entities) + +Install it like any other mod first. In multiplayer it has to be installed on the server too. + +## Installation + +Two files belong together — do not unpack them, just move them into the right folder: + +- `{{datapackFile}}` — the entity itself (size, movement, behavior) +- `{{resourcepackFile}}` — how it looks (model, texture, animations) + +### 1. Resource Pack + +Move `{{resourcepackFile}}` into your resourcepacks folder: + +- Windows: `%appdata%\.minecraft\resourcepacks\` +- Linux: `~/.minecraft/resourcepacks/` +- macOS: `~/Library/Application Support/minecraft/resourcepacks/` + +Start Minecraft and enable it under **Options > Resource Packs**. + +### 2. Data Pack + +Move `{{datapackFile}}` into your world's datapacks folder: + +- Single player: `.minecraft/saves//datapacks/` +- Dedicated server: `/world/datapacks/` + +Load the world, or run `/reload` if it is already open. + +> **Install both.** With only the resource pack the model shows up but cannot be spawned. With only +> the data pack the entity exists but renders as a pink placeholder box. + +## Using it in game + +Look for the spawn item in the creative inventory: entities are in the **Easy Model Entities** tab, +block entities in **Easy Model Block Entities**. Place it like a spawn egg. + +## What this model can do + +Movement and behavior come from a fixed set of presets built into the mod — wandering, swimming, +flying, standing still and so on. The preset was chosen at export time. + +Animations are played from the clips in the Blockbench project. These names play automatically: + +| Clip | When it plays | +|----------|----------------------| +| `idle` | standing still | +| `walk` | moving on the ground | +| `swim` | moving in water | +| `fly` | flying | +| `attack` | attacking | +| `sit` | sitting | + +Clips named `hurt` and `death` are loaded as well, but the mod does not trigger them on its own. +Clips with any other name are kept as custom clips and are played by name, for example with +`/easy_model_entities animation play entity `. + +There is no scripting or custom AI. A model without any matching clip simply renders without +animation. + +## Updating this pack + +Changed only the model, texture or animations? Re-export just the resource pack and replace that +file. Changed entity data such as size or movement? Re-export just the data pack. Either way the +updated pack still matches the one you already installed. If both changed, export the complete +bundle again and replace both files. diff --git a/plugins/easy_model_entities/src/tests/blockbenchAdapter.test.js b/plugins/easy_model_entities/src/tests/blockbenchAdapter.test.js new file mode 100644 index 00000000..d60adb13 --- /dev/null +++ b/plugins/easy_model_entities/src/tests/blockbenchAdapter.test.js @@ -0,0 +1,92 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const {BlockbenchAdapter} = require('../BlockbenchAdapter'); + +describe('writeToDirectory', () => { + let rootDir; + + beforeEach(() => { + rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'eme-test-')); + }); + + afterEach(() => { + fs.rmSync(rootDir, {recursive: true, force: true}); + }); + + test('writes files below the output root', () => { + BlockbenchAdapter.writeToDirectory(rootDir, + [{path: 'data/example/profile.json', content: '{}'}]); + expect(fs.readFileSync( + path.join(rootDir, 'data/example/profile.json'), 'utf8')).toBe('{}'); + }); + + test('rejects path traversal', () => { + expect(() => BlockbenchAdapter.writeToDirectory(rootDir, + [{path: '../escape.json', content: '{}'}])).toThrow('PATH_TRAVERSAL'); + expect(fs.existsSync(path.join(rootDir, '..', 'escape.json'))).toBe(false); + }); + + test('rejects absolute paths', () => { + expect(() => BlockbenchAdapter.writeToDirectory(rootDir, + [{path: 'C:/escape.json', content: '{}'}])).toThrow('ABSOLUTE_PATH'); + }); +}); + +describe('collectTextures', () => { + const emptyPixel = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAA' + + 'DUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=='; + + function texture(id, name) { + return { + id, name, namespace: '', folder: 'block', path: '', + getBase64: () => emptyPixel + }; + } + + afterEach(() => { + delete global.Texture; + }); + + test('uses the slot position as index, not the Blockbench texture id', () => { + global.Texture = {all: [texture('2', 'dawn_sparrow.png')]}; + + expect(BlockbenchAdapter.collectTextures().map( + (descriptor) => descriptor.index)).toEqual([0]); + }); + + test('numbers multiple textures by slot position regardless of their ids', + () => { + global.Texture = { + all: [texture('chest_texture', 'normal.png'), + texture('7', 'disguised_chestling.png')] + }; + + expect(BlockbenchAdapter.collectTextures().map( + (descriptor) => descriptor.index)).toEqual([0, 1]); + }); + + test('returns an empty list when no textures are loaded', () => { + expect(BlockbenchAdapter.collectTextures()).toEqual([]); + }); +}); diff --git a/plugins/easy_model_entities/src/tests/contractSync.test.js b/plugins/easy_model_entities/src/tests/contractSync.test.js new file mode 100644 index 00000000..240e903c --- /dev/null +++ b/plugins/easy_model_entities/src/tests/contractSync.test.js @@ -0,0 +1,122 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const contract = require('./fixtures/easy-model-api-0.2.0.json'); +const {buildRenderProfile} = require('../builders/renderProfile'); +const {buildServerProfile} = require('../builders/serverProfile'); +const {Validator} = require('../model/Validator'); +const { + SCHEMA_VERSION, + MODEL_TYPES, + BODY_TYPES, + MOVEMENT_TYPES, + BEHAVIOR_MODES, + PRESET_TYPES, + BLOCK_ENTITY_PRESET_TYPES, + ANIMATION_MODES, + ANIMATION_CLIPS, + GAIT_TYPES +} = require('../model/presetTypes'); +const {applyTemplate} = require('../model/templates'); + +function sorted(values) { + return values.slice().sort(); +} + +describe('mod API contract sync', () => { + test('schema version matches the committed mod contract', () => { + expect(SCHEMA_VERSION).toBe(contract.schema_version); + }); + + test('enum constants match the committed mod contract', () => { + expect(sorted(MODEL_TYPES)).toEqual(sorted(contract.enums.model_types)); + expect(sorted(BODY_TYPES)).toEqual(sorted(contract.enums.body_types)); + expect(sorted(MOVEMENT_TYPES)).toEqual( + sorted(contract.enums.movement_types)); + expect(sorted(BEHAVIOR_MODES)).toEqual( + sorted(contract.enums.behavior_modes)); + expect(sorted(PRESET_TYPES)).toEqual(sorted(contract.enums.preset_types)); + expect(sorted(BLOCK_ENTITY_PRESET_TYPES)).toEqual( + sorted(contract.enums.block_entity_preset_types)); + expect(sorted(ANIMATION_MODES)).toEqual( + sorted(contract.enums.animation_modes)); + expect(sorted(ANIMATION_CLIPS)).toEqual( + sorted(contract.enums.animation_clips)); + expect(sorted(GAIT_TYPES)).toEqual(sorted(contract.enums.gaits)); + }); + + test('validator budgets match the committed mod contract', () => { + const budgets = Validator.BUDGETS; + expect(budgets.maxModelFileSizeBytes).toBe( + contract.budgets.max_model_file_size_bytes); + expect(budgets.softModelFileSizeBytes).toBe( + contract.budgets.soft_model_file_size_bytes); + expect(budgets.maxTextureSize).toBe(contract.budgets.max_texture_size); + expect(budgets.softTextureSize).toBe(contract.budgets.soft_texture_size); + expect(budgets.maxBoneCount).toBe(contract.budgets.max_bone_count); + expect(budgets.softBoneCount).toBe(contract.budgets.soft_bone_count); + expect(budgets.maxCubeCount).toBe(contract.budgets.max_cube_count); + expect(budgets.softCubeCount).toBe(contract.budgets.soft_cube_count); + expect(budgets.maxHierarchyDepth).toBe( + contract.budgets.max_hierarchy_depth); + expect(budgets.softHierarchyDepth).toBe( + contract.budgets.soft_hierarchy_depth); + expect(budgets.maxAnimationCount).toBe( + contract.budgets.max_animation_count); + }); +}); + +describe('mod example style stays minimal', () => { + test('little_explorer style preset does not expand defaults', () => { + const settings = applyTemplate('humanoid_wandering'); + settings.namespace = 'easy_model_entities_examples'; + settings.profileId = 'little_explorer'; + + expect(buildServerProfile(settings)).toEqual({ + schema_version: '0.2.0', + model_type: 'entity', + preset_type: 'humanoid_wandering' + }); + expect(buildRenderProfile(settings)).toEqual({ + schema_version: '0.2.0', + preset_type: 'humanoid_wandering', + model: + 'easy_model_entities_examples:easy_model_entities/models/little_explorer' + }); + }); + + test('shrine style block entity keeps the block render override', () => { + const settings = applyTemplate('animated_randomly', 'block_entity'); + settings.namespace = 'easy_model_entities_examples'; + settings.profileId = 'shrine'; + + expect(buildServerProfile(settings)).toEqual({ + schema_version: '0.2.0', + model_type: 'block_entity', + preset_type: 'animated_randomly' + }); + expect(buildRenderProfile(settings)).toEqual({ + schema_version: '0.2.0', + preset_type: 'static', + model: 'easy_model_entities_examples:easy_model_entities/models/shrine', + rendering: {shadow_radius: 0.5}, + animation: {mode: 'random_idle'} + }); + }); +}); diff --git a/plugins/easy_model_entities/src/tests/exportDialog.test.js b/plugins/easy_model_entities/src/tests/exportDialog.test.js new file mode 100644 index 00000000..3376ce0d --- /dev/null +++ b/plugins/easy_model_entities/src/tests/exportDialog.test.js @@ -0,0 +1,215 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const { + settingsToForm, + formToSettings, + presetFormValues, + resolveExportSettings +} = require('../ui/exportSettingsMapping'); +const {presetOptions} = require('../ui/exportDialogOptions'); +const {fixtureSettings} = require('./fixtureData'); + +const MODEL_DIMS = {width: 1, height: 2, eyeHeight: 1.8}; + +describe('exportDialog mapping', () => { + test('settings survive a form round-trip', () => { + const settings = fixtureSettings(); + const restored = formToSettings(settingsToForm(settings), settings); + expect(restored).toEqual(settings); + }); + + test('form trims namespace and profile id', () => { + const settings = fixtureSettings(); + const form = settingsToForm(settings); + form.namespace = ' example '; + form.profileId = ' lizard '; + const restored = formToSettings(form, settings); + expect(restored.namespace).toBe('example'); + expect(restored.profileId).toBe('lizard'); + }); +}); + +describe('presetOptions', () => { + test('without experimental, every selectable preset is shown (custom hidden)', + () => { + const options = presetOptions('entity', false); + expect(Object.keys(options)).toEqual([ + 'static', 'statue', 'humanoid_still', 'humanoid_wandering', + 'quadruped_still', 'quadruped_wandering', 'aquatic_still', + 'aquatic_swimming', 'amphibious_still', 'amphibious_wandering', + 'winged_still', 'winged_wandering', 'winged_humanoid_still', + 'winged_humanoid_wandering', 'arthropod_still', 'arthropod_wandering', + 'cuboid_still', 'cuboid_hopping', 'floating_still' + ]); + expect(options.custom).toBeUndefined(); + expect(options.floating_still).toBeDefined(); + }); + + test('experimental reveals custom on top of every selectable preset', + () => { + const options = presetOptions('entity', true); + expect(Object.keys(options)[0]).toBe('custom'); + expect(Object.keys(options)).toHaveLength(20); + expect(options.amphibious_wandering).toBeDefined(); + expect(options.floating_still).not.toMatch(/WIP/); + }); + + test('ensure keeps an already-selected experimental preset visible', () => { + const options = presetOptions('entity', false, 'cuboid_still'); + expect(options.cuboid_still).toBeDefined(); + expect(options.custom).toBeUndefined(); + }); + + test('block entity hides ticking unless experimental is on', () => { + expect(Object.keys(presetOptions('block_entity', false))).toEqual( + ['static', 'animated', 'animated_randomly']); + expect(Object.keys(presetOptions('block_entity', true))).toEqual( + ['static', 'ticking', 'animated', 'animated_randomly']); + }); +}); + +describe('presetFormValues', () => { + test('returns advanced fields with model dimensions, no identity fields', + () => { + const values = presetFormValues('statue', 'entity', MODEL_DIMS); + expect(values.bodyType).toBe('static'); + expect(values.animationMode).toBe('none'); + expect(values.width).toBe(1); + expect(values.height).toBe(2); + expect(values.namespace).toBeUndefined(); + expect(values.profileId).toBeUndefined(); + expect(values.targetVersion).toBeUndefined(); + expect(values.version).toBeUndefined(); + }); +}); + +describe('resolveExportSettings', () => { + const base = fixtureSettings(); + + test( + 'built-in preset derives from template and model, ignoring hidden fields', + () => { + const form = { + preset: 'statue', + namespace: 'example.org', + profileId: 'statue', + targetVersion: '1.20.1', + customize: false, + width: 999, + bodyType: 'biped' + }; + const settings = resolveExportSettings(form, base, MODEL_DIMS); + expect(settings.presetType).toBe('statue'); + expect(settings.host.bodyType).toBe('static'); + expect(settings.animation.mode).toBe('none'); + expect(settings.dimensions.width).toBe(1); + expect(settings.dimensions.height).toBe(2); + expect(settings.namespace).toBe('example.org'); + expect(settings.profileId).toBe('statue'); + }); + + test('custom preset keeps the stored base settings', () => { + const form = { + preset: 'custom', + namespace: 'example.org', + profileId: 'lizard', + targetVersion: '1.20.1', + customize: false + }; + const settings = resolveExportSettings(form, base, MODEL_DIMS); + expect(settings.presetType).toBe('custom'); + expect(settings.dimensions).toEqual(base.dimensions); + expect(settings.host).toEqual(base.host); + }); + + test('customize overlays advanced form fields', () => { + const form = settingsToForm(fixtureSettings()); + form.preset = 'quadruped_wandering'; + form.customize = true; + form.width = 3.5; + form.maxHealth = 42; + const settings = resolveExportSettings(form, base, MODEL_DIMS); + expect(settings.dimensions.width).toBe(3.5); + expect(settings.attributes.maxHealth).toBe(42); + }); + + test('standalone ZIP export maps to the packs target', () => { + const form = { + exportType: 'packs', + modelType: 'entity', + preset: 'statue', + namespace: 'example', + profileId: 'statue', + targetVersion: '1.20.1' + }; + const settings = resolveExportSettings(form, base, MODEL_DIMS); + expect(settings.modelType).toBe('entity'); + expect(settings.exportTarget).toBe('packs'); + expect(settings.modelOnly).toBe(false); + }); + + test('block entity export uses the block preset and model type', () => { + const form = { + exportType: 'mod_project', + modelType: 'block_entity', + blockPreset: 'animated_randomly', + namespace: 'example', + profileId: 'shrine', + targetVersion: '1.20.1' + }; + const settings = resolveExportSettings(form, base, MODEL_DIMS); + expect(settings.modelType).toBe('block_entity'); + expect(settings.presetType).toBe('animated_randomly'); + expect(settings.exportTarget).toBe('mod_project'); + }); + + test('model-only export hides the data pack and forces mod project', () => { + const form = { + exportType: 'model_only', + modelType: 'block_entity', + preset: 'cuboid_still', + namespace: 'example', + profileId: 'chestling', + targetVersion: '1.20.1' + }; + const settings = resolveExportSettings(form, base, MODEL_DIMS); + expect(settings.modelOnly).toBe(true); + expect(settings.modelType).toBe('entity'); + expect(settings.presetType).toBe('cuboid_still'); + expect(settings.exportTarget).toBe('mod_project'); + }); + + test.each(['resource_pack', 'data_pack'])( + 'single pack export %s keeps the packs target and the model type', + (exportType) => { + const form = { + exportType, + modelType: 'block_entity', + blockPreset: 'static', + namespace: 'example', + profileId: 'shrine', + targetVersion: '1.20.1' + }; + const settings = resolveExportSettings(form, base, MODEL_DIMS); + expect(settings.exportTarget).toBe('packs'); + expect(settings.modelOnly).toBe(false); + expect(settings.modelType).toBe('block_entity'); + }); +}); diff --git a/plugins/easy_model_entities/src/tests/exportFlow.test.js b/plugins/easy_model_entities/src/tests/exportFlow.test.js new file mode 100644 index 00000000..a4f6d5c4 --- /dev/null +++ b/plugins/easy_model_entities/src/tests/exportFlow.test.js @@ -0,0 +1,127 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const {performExport, zipExportName} = require('../exportFlow'); +const {BlockbenchAdapter} = require('../BlockbenchAdapter'); +const {registerEmeCodecHooks} = require('../format/EmeCodecHooks'); +const {FORMAT_ID} = require('../format/EmeFormat'); +const {pickModelSettings} = require('../model/templates'); +const { + EXPORT_TYPE_MOD_PROJECT, + EXPORT_TYPE_RESOURCE_PACK +} = require('../model/exportTypes'); +const {fixtureSettings, fixtureTextureResolution} = require('./fixtureData'); + +function compileProject() { + const event = {model: {}}; + compileHandlers.forEach((handler) => handler(event)); + + return event.model; +} + +let compileHandlers; +let rootDir; + +beforeEach(() => { + compileHandlers = []; + rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'eme-flow-')); + global.Project = {}; + global.Format = {id: FORMAT_ID}; + global.Codecs = { + project: { + on: (event, handler) => { + if (event === 'compile') { + compileHandlers.push(handler); + } + }, + compile: () => JSON.stringify(compileProject()) + } + }; + global.Blockbench = { + showQuickMessage: () => { + }, + showMessageBox: () => { + }, + pickDirectory: () => rootDir + }; + registerEmeCodecHooks(); +}); + +afterEach(() => { + fs.rmSync(rootDir, {recursive: true, force: true}); + delete global.Project; + delete global.Format; + delete global.Codecs; + delete global.Blockbench; +}); + +describe('performExport', () => { + function modProjectSettings() { + return { + ...fixtureSettings(), + exportType: EXPORT_TYPE_MOD_PROJECT, + exportTarget: EXPORT_TYPE_MOD_PROJECT + }; + } + + test('exported model carries the settings of this very export', () => { + const settings = modProjectSettings(); + performExport(settings, EXPORT_TYPE_MOD_PROJECT, + fixtureTextureResolution(settings)); + + const exported = JSON.parse(fs.readFileSync( + path.join(rootDir, + 'assets/example/easy_model_entities/models/lizard.bbmodel'), + 'utf8')); + expect(exported.emeSettings).toEqual( + pickModelSettings(BlockbenchAdapter.loadSettings())); + expect(exported.emeSettings.presetType).toBe(settings.presetType); + expect(exported.emeSettings.profileId).toBe('lizard'); + expect(exported.emeSettings.lastExportedVersion).toMatch(/^[0-9a-f]{8}$/); + }); + + test('stores the pairing version for the next single pack export', () => { + const settings = modProjectSettings(); + performExport(settings, EXPORT_TYPE_MOD_PROJECT, + fixtureTextureResolution(settings)); + + expect(BlockbenchAdapter.loadSettings().lastExportedVersion).toMatch( + /^[0-9a-f]{8}$/); + }); + + test('an empty project property is not treated as stored settings', () => { + global.Project = {[BlockbenchAdapter.PROJECT_PROPERTY]: {}}; + + expect(BlockbenchAdapter.loadSettings()).toBeNull(); + expect(compileProject().emeSettings).toBeUndefined(); + }); +}); + +describe('zipExportName', () => { + test('names the single pack export after its pack type', () => { + expect(zipExportName({ + namespace: 'example', profileId: 'lizard', + exportType: EXPORT_TYPE_RESOURCE_PACK + })).toBe('example_lizard_resourcepack'); + }); +}); diff --git a/plugins/easy_model_entities/src/tests/exporter.test.js b/plugins/easy_model_entities/src/tests/exporter.test.js new file mode 100644 index 00000000..a33ca1a3 --- /dev/null +++ b/plugins/easy_model_entities/src/tests/exporter.test.js @@ -0,0 +1,305 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {buildPackBundle, buildModProjectFiles} = require('../builders/exporter'); +const {applyTemplate} = require('../model/templates'); +const {resolveTextures} = require('../utils/TextureResolver'); +const {fixtureSettings, fixtureExportOptions} = require('./fixtureData'); + +function fileByPath(files, filePath) { + return files.find((file) => file.path === filePath); +} + +const ENTITY_PROFILE_PATH = + 'data/example/easy_model_entities/profiles/entity/lizard.json'; + +describe('buildPackBundle', () => { + const bundle = buildPackBundle(fixtureSettings(), fixtureExportOptions()); + + test('wraps a README plus datapack and resourcepack file lists', () => { + expect(bundle.readme.path).toBe('README.md'); + expect(bundle.datapack.map((f) => f.path).sort()).toEqual([ + ENTITY_PROFILE_PATH, + 'pack.mcmeta' + ]); + expect(bundle.resourcepack.map((f) => f.path).sort()).toEqual([ + 'assets/example/easy_model_entities/models/lizard.bbmodel', + 'assets/example/easy_model_entities/render_profiles/entity/lizard.json', + 'assets/example/textures/entity/lizard.png', + 'pack.mcmeta' + ].sort()); + }); + + test('exposes profile-id-prefixed inner pack file names', () => { + expect(bundle.datapackFileName).toBe('lizard_datapack.zip'); + expect(bundle.resourcepackFileName).toBe('lizard_resourcepack.zip'); + }); + + test('README mentions the mod, Java Edition and both packs', () => { + const readme = bundle.readme.content; + expect(readme).toContain('Easy Model Entities'); + expect(readme).toContain('Minecraft: Java Edition'); + expect(readme).toContain('resourcepacks'); + expect(readme).toContain('datapacks'); + expect(readme).toContain('example:entity/lizard'); + }); + + test( + 'profiles carry model_type, preset_type, aligned model ref, no obsolete fields', + () => { + const server = JSON.parse(fileByPath(bundle.datapack, + ENTITY_PROFILE_PATH).content); + const render = JSON.parse(fileByPath(bundle.resourcepack, + 'assets/example/easy_model_entities/render_profiles/entity/lizard.json') + .content); + expect(server.model_type).toBe('entity'); + expect(server.preset_type).toBe('quadruped_wandering'); + expect(server.schema_version).toBe('0.2.0'); + expect(server.client).toBeUndefined(); + expect(render.preset_type).toBe('quadruped_wandering'); + expect(render.model).toBe('example:easy_model_entities/models/lizard'); + expect(server.host).toBeUndefined(); + expect(server.pack_pair).toBeUndefined(); + expect(server.id).toBeUndefined(); + expect(render.pack_pair).toBeUndefined(); + }); + + test('pack.mcmeta carries a pack format without pair metadata', () => { + const meta = JSON.parse(fileByPath(bundle.datapack, 'pack.mcmeta').content); + expect(meta.pack.pack_format).toBe(15); + expect(meta.easy_model_entities).toBeUndefined(); + }); + + test('resource pack export omits the data pack and the bundle README', () => { + const settings = fixtureSettings(); + settings.exportType = 'resource_pack'; + const singlePack = buildPackBundle(settings, fixtureExportOptions()); + + expect(singlePack.datapack).toBeNull(); + expect(singlePack.readme).toBeNull(); + expect(singlePack.resourcepack.map((file) => file.path)).toContain( + 'assets/example/textures/entity/lizard.png'); + }); + + test('data pack export omits the resource pack and the bundle README', () => { + const settings = fixtureSettings(); + settings.exportType = 'data_pack'; + const singlePack = buildPackBundle(settings, fixtureExportOptions()); + + expect(singlePack.resourcepack).toBeNull(); + expect(singlePack.readme).toBeNull(); + expect(singlePack.datapack.map((file) => file.path)).toContain( + ENTITY_PROFILE_PATH); + }); + + test('single pack exports keep the version of the complete export', () => { + const settings = fixtureSettings(); + const complete = buildPackBundle(settings, fixtureExportOptions()); + settings.exportType = 'resource_pack'; + const resourceOnly = buildPackBundle(settings, fixtureExportOptions()); + + expect(resourceOnly.renderProfile.version).toBe( + complete.serverProfile.version); + }); + + test('a data pack export keeps pairing after the entity data changed', () => { + const settings = fixtureSettings(); + const paired = buildPackBundle(settings, fixtureExportOptions()) + .serverProfile.version; + + settings.lastExportedVersion = paired; + settings.exportType = 'data_pack'; + settings.dimensions = {...settings.dimensions, height: 3.25}; + const dataOnly = buildPackBundle(settings, fixtureExportOptions()); + + expect(dataOnly.serverProfile.dimensions.height).toBe(3.25); + expect(dataOnly.serverProfile.version).toBe(paired); + }); + + test('a complete export mints a new version when the entity data changed', + () => { + const settings = fixtureSettings(); + const paired = buildPackBundle(settings, fixtureExportOptions()) + .serverProfile.version; + + settings.lastExportedVersion = paired; + settings.dimensions = {...settings.dimensions, height: 3.25}; + const complete = buildPackBundle(settings, fixtureExportOptions()); + + expect(complete.serverProfile.version).not.toBe(paired); + expect(complete.renderProfile.version).toBe( + complete.serverProfile.version); + }); + + test('writes model and texture bytes unchanged as binary entries', () => { + const opts = fixtureExportOptions(); + const model = fileByPath(bundle.resourcepack, + 'assets/example/easy_model_entities/models/lizard.bbmodel'); + const texture = fileByPath(bundle.resourcepack, + 'assets/example/textures/entity/lizard.png'); + expect(model.binary).toBe(true); + expect(model.content).toBe(opts.modelBytes); + expect(texture.binary).toBe(true); + expect(texture.content).toBe(opts.textureResolution.packed[0].bytes); + }); +}); + +describe('buildModProjectFiles', () => { + test('emits data and assets paths without pack.mcmeta', () => { + const {files} = buildModProjectFiles(fixtureSettings(), + fixtureExportOptions()); + expect(files.map((f) => f.path)).toEqual([ + ENTITY_PROFILE_PATH, + 'assets/example/easy_model_entities/render_profiles/entity/lizard.json', + 'assets/example/easy_model_entities/models/lizard.bbmodel', + 'assets/example/textures/entity/lizard.png' + ]); + expect(files.some((f) => f.path.endsWith('pack.mcmeta'))).toBe(false); + }); + + test('mod project profile JSON equals the bundle profile JSON', () => { + const mod = buildModProjectFiles(fixtureSettings(), fixtureExportOptions()); + const bundle = buildPackBundle(fixtureSettings(), fixtureExportOptions()); + const modProfile = fileByPath(mod.files, ENTITY_PROFILE_PATH).content; + const bundleProfile = fileByPath(bundle.datapack, ENTITY_PROFILE_PATH) + .content; + expect(modProfile).toBe(bundleProfile); + }); +}); + +describe('model-only export (mod integration)', () => { + function modelOnlySettings() { + const settings = applyTemplate('cuboid_still'); + settings.namespace = 'example'; + settings.profileId = 'chestling'; + settings.modelOnly = true; + return settings; + } + + test('writes only render profile, model and texture (no server profile)', + () => { + const settings = modelOnlySettings(); + const {files, serverProfile} = buildModProjectFiles(settings, + fixtureExportOptions(settings)); + expect(serverProfile).toBeNull(); + expect(files.map((f) => f.path)).toEqual([ + 'assets/example/easy_model_entities/render_profiles/entity/chestling.json', + 'assets/example/easy_model_entities/models/chestling.bbmodel', + 'assets/example/textures/entity/chestling.png' + ]); + expect(files.some((f) => f.path.includes('/profiles/'))).toBe(false); + }); + + test('leaves the render profile version-less (mod owns the contract)', () => { + const settings = modelOnlySettings(); + const {renderProfile} = buildModProjectFiles(settings, + fixtureExportOptions(settings)); + expect(renderProfile.version).toBeUndefined(); + }); +}); + +describe('version stamp (server-relevant content hash)', () => { + test('server and render profile share the same stamp', () => { + const bundle = buildPackBundle(fixtureSettings(), fixtureExportOptions()); + expect(bundle.serverProfile.version).toMatch(/^[0-9a-f]{8}$/); + expect(bundle.renderProfile.version).toBe(bundle.serverProfile.version); + }); + + test('a server-relevant change bumps the stamp', () => { + const base = buildPackBundle(fixtureSettings(), fixtureExportOptions()); + const changed = fixtureSettings(); + changed.dimensions.width += 1; + const next = buildPackBundle(changed, fixtureExportOptions()); + expect(next.serverProfile.version) + .not.toBe(base.serverProfile.version); + }); + + test('a texture-only change keeps the stamp stable', () => { + const settings = fixtureSettings(); + const base = buildPackBundle(settings, fixtureExportOptions()); + const extraTexture = resolveTextures([ + { + index: 0, namespace: '', folder: '', name: '', path: '', + bytes: Uint8Array.from([1]) + }, + { + index: 1, namespace: 'minecraft', folder: 'entity/chest', + name: 'normal.png', path: '', bytes: Uint8Array.from([1]) + } + ], settings); + const next = buildPackBundle(settings, + {modelBytes: 'different', textureResolution: extraTexture}); + expect(next.serverProfile.version).toBe(base.serverProfile.version); + }); +}); + +describe('mixed vanilla + custom textures', () => { + const PIXELS = Uint8Array.from([0x89, 0x50, 0x4e, 0x47]); + + function mixedOptions(settings) { + const textureResolution = resolveTextures([ + { + index: 0, namespace: 'minecraft', folder: 'entity/chest', + name: 'normal.png', path: '', bytes: PIXELS + }, + { + index: 1, namespace: '', folder: 'block', name: 'eye_texture', + path: '', bytes: PIXELS + } + ], settings); + return {modelBytes: '{}', textureResolution}; + } + + test('packs only the custom texture and references the vanilla one', () => { + const settings = fixtureSettings(); + const {files} = buildModProjectFiles(settings, mixedOptions(settings)); + const texturePaths = files.map((f) => f.path) + .filter((p) => p.includes('/textures/')); + expect(texturePaths).toEqual( + ['assets/example/textures/entity/lizard_1.png']); + + const render = JSON.parse(fileByPath(files, + 'assets/example/easy_model_entities/render_profiles/entity/lizard.json') + .content); + expect(render.texture).toBe('minecraft:textures/entity/chest/normal.png'); + expect(render.textures).toEqual( + {1: 'example:textures/entity/lizard_1.png'}); + }); +}); + +describe('block entity export uses the block_entity subfolder', () => { + function blockSettings() { + const settings = applyTemplate('animated_randomly', 'block_entity'); + settings.namespace = 'example'; + settings.profileId = 'shrine'; + return settings; + } + + test('server and render profiles land under the block_entity subfolder', + () => { + const settings = blockSettings(); + const {files} = buildModProjectFiles(settings, + fixtureExportOptions(settings)); + const paths = files.map((f) => f.path); + expect(paths).toContain( + 'data/example/easy_model_entities/profiles/block_entity/shrine.json'); + expect(paths).toContain( + 'assets/example/easy_model_entities/render_profiles/block_entity/shrine.json'); + }); +}); diff --git a/plugins/easy_model_entities/src/tests/fixtureData.js b/plugins/easy_model_entities/src/tests/fixtureData.js new file mode 100644 index 00000000..d96049e4 --- /dev/null +++ b/plugins/easy_model_entities/src/tests/fixtureData.js @@ -0,0 +1,57 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {applyTemplate} = require('../model/templates'); +const {resolveTextures} = require('../utils/TextureResolver'); + +const FIXTURE_MODEL_BYTES = '{"meta":{"format_version":"4.5"},"name":"lizard"}'; +const FIXTURE_TEXTURE_BYTES = Uint8Array.from( + [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + +function fixtureSettings() { + const settings = applyTemplate('quadruped_wandering'); + settings.namespace = 'example'; + settings.profileId = 'lizard'; + + return settings; +} + +function fixtureTextureResolution(settings) { + return resolveTextures([ + { + index: 0, name: '', namespace: '', folder: '', path: '', + bytes: FIXTURE_TEXTURE_BYTES + } + ], settings || fixtureSettings()); +} + +function fixtureExportOptions(settings) { + return { + modelBytes: FIXTURE_MODEL_BYTES, + textureResolution: fixtureTextureResolution(settings) + }; +} + +module.exports = { + FIXTURE_MODEL_BYTES, + FIXTURE_TEXTURE_BYTES, + fixtureSettings, + fixtureTextureResolution, + fixtureExportOptions +}; diff --git a/plugins/easy_model_entities/src/tests/fixtures/easy-model-api-0.2.0.json b/plugins/easy_model_entities/src/tests/fixtures/easy-model-api-0.2.0.json new file mode 100644 index 00000000..78b69beb --- /dev/null +++ b/plugins/easy_model_entities/src/tests/fixtures/easy-model-api-0.2.0.json @@ -0,0 +1,163 @@ +{ + "schema_version": "0.2.0", + "api_version": "2.0.0", + "supported_schema_versions": [ + "0.1.0", + "0.2.0" + ], + "budgets": { + "max_model_file_size_bytes": 2097152, + "soft_model_file_size_bytes": 1048576, + "max_texture_size": 2048, + "soft_texture_size": 128, + "max_bone_count": 128, + "soft_bone_count": 96, + "max_cube_count": 512, + "soft_cube_count": 384, + "max_hierarchy_depth": 32, + "soft_hierarchy_depth": 24, + "max_animation_count": 16 + }, + "enums": { + "model_types": [ + "entity", + "block_entity" + ], + "body_types": [ + "static", + "biped", + "quadruped", + "aquatic", + "amphibious", + "winged", + "winged_humanoid", + "arthropod", + "cuboid", + "floating" + ], + "movement_types": [ + "ground", + "water", + "amphibious", + "static" + ], + "behavior_modes": [ + "idle_only", + "ambient", + "static", + "external_owner" + ], + "preset_types": [ + "custom", + "static", + "statue", + "aquatic_still", + "aquatic_swimming", + "amphibious_still", + "amphibious_wandering", + "arthropod_still", + "arthropod_wandering", + "cuboid_hopping", + "cuboid_still", + "floating_still", + "humanoid_still", + "humanoid_wandering", + "quadruped_still", + "quadruped_wandering", + "winged_humanoid_still", + "winged_humanoid_wandering", + "winged_still", + "winged_wandering" + ], + "block_entity_preset_types": [ + "static", + "ticking", + "animated", + "animated_randomly" + ], + "animation_modes": [ + "automatic", + "random_idle", + "none" + ], + "animation_states": [ + "auto", + "idle", + "walk", + "run", + "hurt", + "death", + "swim", + "fly", + "attack", + "sit" + ], + "animation_clips": [ + "idle", + "walk", + "swim", + "fly", + "hurt", + "death", + "attack", + "sit" + ], + "animation_loops": [ + "clip", + "loop", + "once" + ], + "animation_switch_timings": [ + "immediate", + "after_current" + ], + "playback_modes": [ + "once", + "loop", + "repeat" + ], + "animation_types": [ + "standard", + "custom" + ], + "gaits": [ + "natural", + "feline", + "ungulate" + ] + }, + "status_codes": { + "server": [ + "ACTIVE", + "INVALID_JSON", + "INVALID_SCHEMA_VERSION", + "INVALID_MODEL_TYPE", + "INVALID_RESOURCE_LOCATION", + "INVALID_DIMENSIONS", + "INVALID_HOST_ENTITY", + "INVALID_HOST_BLOCK_ENTITY", + "DISABLED" + ], + "render": [ + "ACTIVE", + "INVALID_JSON", + "INVALID_SCHEMA_VERSION", + "INVALID_RESOURCE_LOCATION", + "INVALID_BODY_TYPE", + "INVALID_ANIMATION_MODE", + "INVALID_RENDER_SETTINGS", + "MISSING_RENDER_PROFILE", + "MISSING_MODEL", + "MISSING_TEXTURE", + "MODEL_DECODE_FAILED", + "CLIENT_ASSET_MISMATCH", + "CLIENT_BODY_TYPE_MISMATCH", + "FALLBACK_ACTIVE" + ] + }, + "diagnostic_severities": [ + "INFO", + "WARNING", + "ERROR" + ] +} diff --git a/plugins/easy_model_entities/src/tests/mdTransform.js b/plugins/easy_model_entities/src/tests/mdTransform.js new file mode 100644 index 00000000..ad8b4f40 --- /dev/null +++ b/plugins/easy_model_entities/src/tests/mdTransform.js @@ -0,0 +1,24 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +module.exports = { + process(sourceText) { + return {code: `module.exports = ${JSON.stringify(sourceText)};`}; + } +}; diff --git a/plugins/easy_model_entities/src/tests/modUpdate.test.js b/plugins/easy_model_entities/src/tests/modUpdate.test.js new file mode 100644 index 00000000..47a42559 --- /dev/null +++ b/plugins/easy_model_entities/src/tests/modUpdate.test.js @@ -0,0 +1,146 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {presetDefaults} = require('../model/presetTypes'); +const {applyTemplate, pickModelSettings} = require('../model/templates'); +const {buildRenderProfile} = require('../builders/renderProfile'); +const {VisibleBounds} = require('../model/VisibleBounds'); + +describe('amphibious / aquatic mapping mirrors the mod', () => { + test('amphibious_wandering uses the amphibious host and movement defaults', + () => { + const d = presetDefaults('amphibious_wandering'); + expect(d.host).toEqual({ + entityType: 'easy_model_entities:amphibious_entity', + movementType: 'amphibious', + bodyType: 'amphibious' + }); + expect(d.movement).toEqual( + {speed: 0.15, stepHeight: 0.6, gravity: true}); + expect(d.behavior).toMatchObject( + {mode: 'ambient', randomStroll: true}); + expect(d.dimensions).toEqual({width: 0.9, height: 0.6, eyeHeight: 0.4}); + }); + + test('aquatic presets use the water host and movement defaults', () => { + const swimming = presetDefaults('aquatic_swimming'); + expect(swimming.host.entityType).toBe('easy_model_entities:aquatic_entity'); + expect(swimming.host.movementType).toBe('water'); + expect(swimming.movement).toEqual( + {speed: 0.1, stepHeight: 0, gravity: true}); + expect(swimming.behavior).toMatchObject( + {mode: 'ambient', randomStroll: true}); + + const still = presetDefaults('aquatic_still'); + expect(still.host.movementType).toBe('water'); + expect(still.behavior.mode).toBe('idle_only'); + }); +}); + +describe('pickModelSettings keeps reopen-critical settings', () => { + test('keeps customize / targetVersion and drops export-only keys', () => { + const full = { + ...applyTemplate('humanoid_wandering'), + targetVersion: '1.21.11', + customize: true, + exportType: 'packs', + exportTarget: 'packs', + modelOnly: false, + experimental: true, + lastExportedVersion: 'abc123' + }; + const picked = pickModelSettings(full); + expect(picked.targetVersion).toBe('1.21.11'); + expect(picked.customize).toBe(true); + expect(picked.lastExportedVersion).toBe('abc123'); + expect(picked.exportType).toBeUndefined(); + expect(picked.exportTarget).toBeUndefined(); + expect(picked.modelOnly).toBeUndefined(); + expect(picked.experimental).toBeUndefined(); + expect(picked.schemaVersion).toBe('0.2.0'); + expect(picked.presetType).toBe('humanoid_wandering'); + expect(picked.host).toEqual(full.host); + expect(picked.rendering).toEqual(full.rendering); + expect(picked.animation).toEqual(full.animation); + }); +}); + +describe('render profile emits the new optional fields when non-default', + () => { + test('gait, idle_strength and visible_bounds are written', () => { + const settings = applyTemplate('humanoid_wandering'); + settings.rendering.visibleBoundsWidth = 2; + settings.rendering.visibleBoundsHeight = 3; + settings.animation.idleStrength = 1.5; + settings.animation.gait = 'feline'; + + const render = buildRenderProfile(settings, null); + expect(render.rendering.visible_bounds_width).toBe(2); + expect(render.rendering.visible_bounds_height).toBe(3); + expect(render.rendering.visible_bounds_offset).toBeUndefined(); + expect(render.animation.idle_strength).toBe(1.5); + expect(render.animation.gait).toBe('feline'); + }); + + test('defaults (natural gait, idle 1, zero bounds) stay omitted', () => { + const render = buildRenderProfile(applyTemplate('humanoid_wandering'), + null); + // Every value matches the preset defaults, so neither section is written. + expect(render.rendering).toBeUndefined(); + expect(render.animation).toBeUndefined(); + }); + }); + +describe('VisibleBounds', () => { + test('derives a conservative box from a single cube', () => { + expect(VisibleBounds.derive([{from: [-8, 0, -8], to: [8, 16, 8]}])) + .toEqual({width: 1, height: 1, offset: [0, 0, 0]}); + }); + + test('includes thin / far protrusions instead of trimming them', () => { + const cubes = [ + {from: [0, 0, 0], to: [16, 16, 16]}, + {from: [7, 16, 8], to: [8, 48, 9]} + ]; + const derived = VisibleBounds.derive(cubes); + expect(derived.height).toBe(3); + expect(derived.width).toBe(1); + expect(derived.offset).toEqual([0.5, 0, 0.5]); + }); + + test('empty geometry falls back to vanilla culling (0 / 0)', () => { + expect(VisibleBounds.derive([])).toEqual( + {width: 0, height: 0, offset: [0, 0, 0]}); + }); + + test('applyVisibleBounds can preserve a saved manual override', () => { + const settings = applyTemplate('humanoid_wandering'); + settings.rendering.visibleBoundsWidth = 2; + settings.rendering.visibleBoundsHeight = 3; + settings.rendering.visibleBoundsOffset = [0, 1, 0]; + + VisibleBounds.applyVisibleBounds(settings, + {width: 1, height: 1, offset: [0, 0, 0]}, + {preserveExisting: true}); + + expect(settings.rendering.visibleBoundsWidth).toBe(2); + expect(settings.rendering.visibleBoundsHeight).toBe(3); + expect(settings.rendering.visibleBoundsOffset).toEqual([0, 1, 0]); + }); +}); diff --git a/plugins/easy_model_entities/src/tests/modelDimensions.test.js b/plugins/easy_model_entities/src/tests/modelDimensions.test.js new file mode 100644 index 00000000..d050b549 --- /dev/null +++ b/plugins/easy_model_entities/src/tests/modelDimensions.test.js @@ -0,0 +1,71 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {ModelDimensions} = require('../model/ModelDimensions'); + +describe('deriveDimensions', () => { + test('derives blocks from pixel bounds and uses the widest horizontal extent', + () => { + const bounds = {minX: 0, minY: 0, minZ: 0, maxX: 16, maxY: 32, maxZ: 8}; + const dims = ModelDimensions.deriveDimensions(bounds, 'biped'); + expect(dims.width).toBe(1); // 16px / 16 + expect(dims.height).toBe(2); // 32px / 16 + expect(dims.eyeHeight).toBe(1.8); // 2 * 0.9 (biped) + }); + + test('eye height factor depends on body type', () => { + const bounds = {minX: 0, minY: 0, minZ: 0, maxX: 16, maxY: 16, maxZ: 16}; + expect(ModelDimensions.deriveDimensions(bounds, 'biped').eyeHeight).toBe( + 0.9); + expect( + ModelDimensions.deriveDimensions(bounds, 'quadruped').eyeHeight).toBe( + 0.8); + expect(ModelDimensions.deriveDimensions(bounds, 'static').eyeHeight).toBe( + 0.85); + }); + + test('falls back for missing, empty or degenerate bounds', () => { + expect(ModelDimensions.deriveDimensions(null, 'biped')).toEqual( + ModelDimensions.FALLBACK); + expect(ModelDimensions.deriveDimensions( + {minX: NaN, minY: 0, minZ: 0, maxX: 1, maxY: 1, maxZ: 1}, + 'biped')).toEqual(ModelDimensions.FALLBACK); + expect(ModelDimensions.deriveDimensions( + {minX: 5, minY: 5, minZ: 5, maxX: 5, maxY: 5, maxZ: 5}, + 'biped')).toEqual(ModelDimensions.FALLBACK); + }); +}); + +describe('applyModelDimensions', () => { + const dims = {width: 1, height: 2, eyeHeight: 1.8}; + + function settings() { + return {dimensions: {width: 0, height: 0, eyeHeight: 0}}; + } + + test('overlays dimensions', () => { + const result = ModelDimensions.applyModelDimensions(settings(), dims); + expect(result.dimensions).toEqual({width: 1, height: 2, eyeHeight: 1.8}); + }); + + test('returns settings unchanged when no dimensions are given', () => { + const input = settings(); + expect(ModelDimensions.applyModelDimensions(input, null)).toBe(input); + }); +}); diff --git a/plugins/easy_model_entities/src/tests/packMeta.test.js b/plugins/easy_model_entities/src/tests/packMeta.test.js new file mode 100644 index 00000000..dfabe0ef --- /dev/null +++ b/plugins/easy_model_entities/src/tests/packMeta.test.js @@ -0,0 +1,62 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const { + buildDataPackMcmeta, + buildResourcePackMcmeta +} = require('../builders/packMeta'); + +describe('packMeta', () => { + test('legacy versions emit a single integer pack_format', () => { + expect(buildDataPackMcmeta({targetVersion: '1.20.1'}).pack) + .toMatchObject({pack_format: 15}); + expect(buildResourcePackMcmeta({targetVersion: '1.21.1'}).pack) + .toMatchObject({pack_format: 34}); + expect(buildDataPackMcmeta({targetVersion: '1.20.1'}).pack.min_format) + .toBeUndefined(); + }); + + test('1.21.11 emits pack_format plus min/max format ranges', () => { + const data = buildDataPackMcmeta({targetVersion: '1.21.11'}).pack; + expect(data.pack_format).toBe(94); + expect(data.min_format).toEqual([94, 1]); + expect(data.max_format).toEqual([94, 1]); + + const resource = buildResourcePackMcmeta({targetVersion: '1.21.11'}).pack; + expect(resource.pack_format).toBe(75); + expect(resource.min_format).toEqual([75, 0]); + expect(resource.max_format).toEqual([75, 0]); + }); + + test('26.1.2 emits pack_format plus min/max format ranges', () => { + const data = buildDataPackMcmeta({targetVersion: '26.1.2'}).pack; + expect(data.pack_format).toBe(101); + expect(data.min_format).toEqual([101, 1]); + expect(data.max_format).toEqual([101, 1]); + + const resource = buildResourcePackMcmeta({targetVersion: '26.1.2'}).pack; + expect(resource.pack_format).toBe(84); + expect(resource.min_format).toEqual([84, 0]); + expect(resource.max_format).toEqual([84, 0]); + }); + + test('unknown versions throw', () => { + expect(() => buildDataPackMcmeta({targetVersion: 'nope'})).toThrow(); + }); +}); diff --git a/plugins/easy_model_entities/src/tests/presetDetector.test.js b/plugins/easy_model_entities/src/tests/presetDetector.test.js new file mode 100644 index 00000000..e250c43a --- /dev/null +++ b/plugins/easy_model_entities/src/tests/presetDetector.test.js @@ -0,0 +1,105 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {PresetDetector} = require('../model/PresetDetector'); + +function stats(boneNames) { + return {boneNames}; +} + +describe('PresetDetector.detectPresetType', () => { + test('detects a quadruped from front/back legs', () => { + expect(PresetDetector.detectPresetType( + stats(['root', 'body', 'head', 'front_left_leg', 'back_right_leg']))) + .toBe('quadruped_still'); + }); + + test('detects four legs without front/back naming', () => { + expect(PresetDetector.detectPresetType( + stats(['leg1', 'leg2', 'leg3', 'leg4']))).toBe('quadruped_still'); + }); + + test('detects a humanoid from arms', () => { + expect(PresetDetector.detectPresetType( + stats(['root', 'head', 'body', 'left_arm', 'right_arm', 'left_leg']))) + .toBe('humanoid_still'); + }); + + test('detects winged from wing bones', () => { + expect(PresetDetector.detectPresetType( + stats(['body', 'left_wing', 'right_wing']))).toBe('winged_still'); + }); + + test('detects winged humanoid when wings and arms are present', () => { + expect(PresetDetector.detectPresetType( + stats(['body', 'left_arm', 'right_arm', 'left_wing', 'right_wing']))) + .toBe('winged_humanoid_still'); + }); + + test('detects aquatic from fins', () => { + expect(PresetDetector.detectPresetType( + stats(['body', 'dorsal_fin', 'tail_fin']))).toBe('aquatic_still'); + }); + + test('detects arthropod from six or more legs', () => { + expect(PresetDetector.detectPresetType( + stats(['leg1', 'leg2', 'leg3', 'leg4', 'leg5', 'leg6']))).toBe( + 'arthropod_still'); + }); + + test('matches bone names case-insensitively', () => { + expect(PresetDetector.detectPresetType(stats(['Left_Arm', 'RIGHT_ARM']))) + .toBe('humanoid_still'); + }); + + test('uses cube-like bounds to suggest cuboid when no limbs', () => { + const bounds = {minX: 0, minY: 0, minZ: 0, maxX: 16, maxY: 16, maxZ: 16}; + expect(PresetDetector.detectPresetType(stats(['root', 'body']), bounds)) + .toBe('cuboid_still'); + }); + + test('falls back to statue without limbs or usable bounds', () => { + expect(PresetDetector.detectPresetType(stats(['root', 'body', 'head']))) + .toBe('statue'); + expect(PresetDetector.detectPresetType(stats([]))).toBe('statue'); + expect(PresetDetector.detectPresetType(stats(undefined))).toBe('statue'); + }); + + test('prefers an animated preset over statue when clips exist', () => { + const animated = { + boneNames: ['root', 'body', 'head'], + animations: [{name: 'idle'}, {name: 'dance'}] + }; + expect(PresetDetector.detectPresetType(animated)).toBe('floating_still'); + }); + + test('keeps statue when only custom clips exist', () => { + const animated = { + boneNames: ['root', 'body', 'head'], + animations: [{name: 'dance'}] + }; + expect(PresetDetector.detectPresetType(animated)).toBe('statue'); + }); + + test('detect returns a human readable reason', () => { + const result = PresetDetector.detect(stats(['front_left_leg'])); + expect(result.presetType).toBe('quadruped_still'); + expect(typeof result.reason).toBe('string'); + }); +}); diff --git a/plugins/easy_model_entities/src/tests/readme.test.js b/plugins/easy_model_entities/src/tests/readme.test.js new file mode 100644 index 00000000..b920fea6 --- /dev/null +++ b/plugins/easy_model_entities/src/tests/readme.test.js @@ -0,0 +1,63 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {buildReadme} = require('../builders/readme'); +const {fixtureSettings} = require('./fixtureData'); + +describe('buildReadme', () => { + const readme = buildReadme(fixtureSettings(), { + datapack: 'lizard_datapack.zip', + resourcepack: 'lizard_resourcepack.zip' + }); + + test('states the mod requirement and Java Edition target', () => { + expect(readme).toContain('Easy Model Entities'); + expect(readme).toContain('Minecraft: Java Edition'); + expect(readme).toMatch(/mod .*installed|installed .*mod/i); + }); + + test('explains where to install both packs as ready-to-use ZIPs', () => { + expect(readme).toContain('resourcepacks'); + expect(readme).toContain('saves//datapacks'); + expect(readme).toContain('resourcepack.zip'); + expect(readme).toContain('datapack.zip'); + expect(readme).toContain('Resource Pack'); + expect(readme).toContain('Data Pack'); + }); + + test('names the profile-id-prefixed pack files', () => { + expect(readme).toContain('lizard_datapack.zip'); + expect(readme).toContain('lizard_resourcepack.zip'); + }); + + test('links to the CurseForge and Modrinth distribution pages', () => { + expect(readme).toContain( + 'https://www.curseforge.com/minecraft/mc-mods/easy-model-entities'); + expect(readme).toContain( + 'https://modrinth.com/mod/easy-model-entities'); + }); + + test('references the profile id', () => { + expect(readme).toContain('example:lizard'); + }); + + test('names the target Minecraft version', () => { + expect(readme).toContain('Minecraft: Java Edition 1.20.1'); + }); +}); diff --git a/plugins/easy_model_entities/src/tests/resourceLocation.test.js b/plugins/easy_model_entities/src/tests/resourceLocation.test.js new file mode 100644 index 00000000..f83f0129 --- /dev/null +++ b/plugins/easy_model_entities/src/tests/resourceLocation.test.js @@ -0,0 +1,130 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {ResourceLocation} = require('../utils/ResourceLocation'); + +describe('resourceLocation', () => { + test('accepts valid namespaces', () => { + expect(ResourceLocation.isValidNamespace('example')).toBe(true); + expect(ResourceLocation.isValidNamespace('easy_model_entities')).toBe(true); + expect(ResourceLocation.isValidNamespace('mod.id-1')).toBe(true); + }); + + test('rejects invalid namespaces', () => { + expect(ResourceLocation.isValidNamespace('Example')).toBe(false); + expect(ResourceLocation.isValidNamespace('with space')).toBe(false); + expect(ResourceLocation.isValidNamespace('')).toBe(false); + expect(ResourceLocation.isValidNamespace('a/b')).toBe(false); + }); + + test('paths allow slashes but reject traversal', () => { + expect(ResourceLocation.isValidPath('models/lizard')).toBe(true); + expect(ResourceLocation.isValidPath('lizard')).toBe(true); + expect(ResourceLocation.isValidPath('../escape')).toBe(false); + expect(ResourceLocation.isValidPath('Upper')).toBe(false); + }); + + test('validates full resource locations', () => { + expect(ResourceLocation.isValidResourceLocation('example:lizard')).toBe( + true); + expect(ResourceLocation.isValidResourceLocation( + 'easy_model_entities:ground_entity')).toBe(true); + expect(ResourceLocation.isValidResourceLocation('no_colon')).toBe(false); + expect(ResourceLocation.isValidResourceLocation('a:b:c')).toBe(false); + }); +}); + +describe('sanitizeProfileId', () => { + test('strips extension, lowercases and replaces invalid characters', () => { + expect(ResourceLocation.sanitizeProfileId('My Model.bbmodel')).toBe( + 'my_model'); + expect(ResourceLocation.sanitizeProfileId('Stone Turtle')).toBe( + 'stone_turtle'); + expect(ResourceLocation.sanitizeProfileId('Lizard')).toBe('lizard'); + }); + + test('collapses repeats and trims separators, result is a valid path', () => { + const id = ResourceLocation.sanitizeProfileId(' --Cool!!Robot-- '); + expect(id).toBe('cool_robot'); + expect(ResourceLocation.isValidPath(id)).toBe(true); + }); + + test('falls back to "entity" for empty names', () => { + expect(ResourceLocation.sanitizeProfileId('')).toBe('entity'); + expect(ResourceLocation.sanitizeProfileId(' ')).toBe('entity'); + expect(ResourceLocation.sanitizeProfileId(undefined)).toBe('entity'); + expect(ResourceLocation.sanitizeProfileId(null)).toBe('entity'); + }); + + test('keeps dots that are not a model file extension', () => { + expect(ResourceLocation.sanitizeProfileId('Stone Turtle v1.5')).toBe( + 'stone_turtle_v1.5'); + expect(ResourceLocation.sanitizeProfileId('turtle.json')).toBe('turtle'); + }); + + test('transliterates umlauts and accents instead of dropping them', () => { + expect(ResourceLocation.sanitizeProfileId('Bär')).toBe('baer'); + expect(ResourceLocation.sanitizeProfileId('Wächter Groß')).toBe( + 'waechter_gross'); + expect(ResourceLocation.sanitizeProfileId('Café Crème')).toBe('cafe_creme'); + expect(ResourceLocation.sanitizeProfileId('Blåhaj Øst')).toBe( + 'blaahaj_oest'); + expect(ResourceLocation.sanitizeProfileId('Ñandú')).toBe('nandu'); + expect(ResourceLocation.sanitizeProfileId('Cat & Dog')).toBe('cat_and_dog'); + }); + + test('gives non latin names a stable and unique id', () => { + const japanese = ResourceLocation.sanitizeProfileId('モデル'); + const cyrillic = ResourceLocation.sanitizeProfileId('Модель'); + const emoji = ResourceLocation.sanitizeProfileId('🐢'); + + expect(ResourceLocation.isValidPath(japanese)).toBe(true); + expect(ResourceLocation.isValidPath(cyrillic)).toBe(true); + expect(ResourceLocation.isValidPath(emoji)).toBe(true); + expect(new Set([japanese, cyrillic, emoji]).size).toBe(3); + expect(ResourceLocation.sanitizeProfileId('モデル')).toBe(japanese); + }); + + test('every sanitized id is a valid path', () => { + const names = [ + 'My Model.bbmodel', ' --Cool!!Robot-- ', 'Bär', 'モデル', '🐢', '!!!', + 'a/../b', '///', 'ΑΒΓ αβγ', 'Ünïcødé Tëst', 'Angel', 'İstanbul' + ]; + names.forEach((name) => { + expect(ResourceLocation.isValidPath( + ResourceLocation.sanitizeProfileId(name))).toBe(true); + }); + }); +}); + +describe('sanitizeNamespace', () => { + test('produces a valid namespace without slashes', () => { + expect(ResourceLocation.sanitizeNamespace('Beispiel Örg')).toBe( + 'beispiel_oerg'); + expect(ResourceLocation.sanitizeNamespace('my/namespace')).toBe( + 'my_namespace'); + expect(ResourceLocation.isValidNamespace( + ResourceLocation.sanitizeNamespace('モデル'))).toBe(true); + }); + + test('falls back to the default namespace', () => { + expect(ResourceLocation.sanitizeNamespace('')).toBe('example_org'); + expect(ResourceLocation.sanitizeNamespace(undefined)).toBe('example_org'); + }); +}); diff --git a/plugins/easy_model_entities/src/tests/schemaConsistency.test.js b/plugins/easy_model_entities/src/tests/schemaConsistency.test.js new file mode 100644 index 00000000..543d673e --- /dev/null +++ b/plugins/easy_model_entities/src/tests/schemaConsistency.test.js @@ -0,0 +1,129 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {applyTemplate, deepMerge} = require('../model/templates'); +const {SELECTABLE_PRESET_TYPES} = require('../model/presetTypes'); +const {buildServerProfile} = require('../builders/serverProfile'); +const {buildRenderProfile} = require('../builders/renderProfile'); + +function withId(settings) { + settings.namespace = 'example'; + settings.profileId = 'lizard'; + return settings; +} + +describe('preset defaults mirror the mod (minimal output)', () => { + describe.each(SELECTABLE_PRESET_TYPES)('%s', (preset) => { + const settings = withId(applyTemplate(preset)); + + test('server profile reduces to identity only', () => { + expect(buildServerProfile(settings)).toEqual({ + schema_version: '0.2.0', + model_type: 'entity', + preset_type: preset + }); + }); + + test('render profile reduces to schema_version, preset_type and model', + () => { + expect(buildRenderProfile(settings)).toEqual({ + schema_version: '0.2.0', + preset_type: preset, + model: 'example:easy_model_entities/models/lizard' + }); + }); + }); +}); + +describe('custom preset writes the mod-required entity and dimensions', () => { + const settings = withId(applyTemplate('custom')); + + test('server profile includes the full entity block and dimensions', () => { + const profile = buildServerProfile(settings); + expect(profile.model_type).toBe('entity'); + expect(profile.preset_type).toBe('custom'); + expect(profile.entity).toEqual({ + type: 'easy_model_entities:static_entity', + movement_type: 'static', + body_type: 'static' + }); + expect(profile.dimensions).toEqual( + {width: 0.6, height: 1.8, eye_height: 1.62}); + }); + + test('render profile includes the required body_type', () => { + expect(buildRenderProfile(settings).body_type).toBe('static'); + }); +}); + +describe('block entity profiles', () => { + function blockSettings(preset) { + const settings = applyTemplate(preset, 'block_entity'); + settings.namespace = 'example'; + settings.profileId = 'shrine'; + return settings; + } + + test('server profile carries model_type and no entity behavior', + () => { + const profile = buildServerProfile(blockSettings('animated_randomly')); + expect(profile).toEqual({ + schema_version: '0.2.0', + model_type: 'block_entity', + preset_type: 'animated_randomly' + }); + expect(profile.entity).toBeUndefined(); + expect(profile.movement).toBeUndefined(); + expect(profile.behavior).toBeUndefined(); + expect(profile.attributes).toBeUndefined(); + }); + + test('render profile uses the static render preset with mapped animation', + () => { + const render = buildRenderProfile(blockSettings('animated_randomly')); + expect(render.preset_type).toBe('static'); + expect(render.body_type).toBeUndefined(); + expect(render.animation.mode).toBe('random_idle'); + }); + + test('static block entity emits no animation override', () => { + const render = buildRenderProfile(blockSettings('static')); + expect(render.preset_type).toBe('static'); + expect(render.animation).toBeUndefined(); + }); +}); + +describe('customized settings emit only the deviating values', () => { + const settings = deepMerge(withId(applyTemplate('quadruped_wandering')), { + dimensions: {width: 1.2, height: 0.85, eyeHeight: 0.6}, + movement: {speed: 0.06, stepHeight: 0.4, gravity: true}, + attributes: {maxHealth: 12, movementSpeed: 0.06, followRange: 12} + }); + + test('server profile contains the stone turtle overrides only', () => { + expect(buildServerProfile(settings)).toEqual({ + schema_version: '0.2.0', + model_type: 'entity', + preset_type: 'quadruped_wandering', + dimensions: {width: 1.2, height: 0.85}, + movement: {speed: 0.06, step_height: 0.4}, + attributes: {max_health: 12, follow_range: 12} + }); + }); +}); diff --git a/plugins/easy_model_entities/src/tests/templates.test.js b/plugins/easy_model_entities/src/tests/templates.test.js new file mode 100644 index 00000000..c54dbf31 --- /dev/null +++ b/plugins/easy_model_entities/src/tests/templates.test.js @@ -0,0 +1,117 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const { + getDefaults, + applyTemplate, + DEFAULT_PRESET, + deepMerge +} = require('../model/templates'); +const { + presetDefaults, + PRESET_TYPES, + SELECTABLE_PRESET_TYPES +} = require('../model/presetTypes'); + +describe('templates', () => { + test('exposes the 20 mod preset types', () => { + expect(PRESET_TYPES).toHaveLength(20); + expect(PRESET_TYPES).toContain('custom'); + expect(PRESET_TYPES).toContain('quadruped_wandering'); + expect(PRESET_TYPES).toContain('amphibious_still'); + expect(PRESET_TYPES).toContain('amphibious_wandering'); + expect(PRESET_TYPES).toContain('floating_still'); + }); + + test('selectable presets exclude custom', () => { + expect(SELECTABLE_PRESET_TYPES).not.toContain('custom'); + expect(SELECTABLE_PRESET_TYPES).toHaveLength(19); + }); + + test('getDefaults carries identity fields and the default preset', () => { + const d = getDefaults(); + expect(d.namespace).toBe('example_org'); + expect(d.profileId).toBe('entity'); + expect(d.targetVersion).toBe('1.20.1'); + expect(d.presetType).toBe(DEFAULT_PRESET); + }); + + test('getDefaults returns an independent copy', () => { + const a = getDefaults(); + a.dimensions.width = 99; + expect(getDefaults().dimensions.width).not.toBe(99); + }); + + test('applyTemplate merges preset defaults onto the identity fields', () => { + const s = applyTemplate('humanoid_wandering'); + expect(s.namespace).toBe('example_org'); + expect(s.presetType).toBe('humanoid_wandering'); + expect(s.host).toEqual({ + entityType: 'easy_model_entities:ground_entity', + movementType: 'ground', + bodyType: 'biped' + }); + expect(s.behavior.mode).toBe('ambient'); + expect(s.animation.mode).toBe('automatic'); + }); + + test('still presets are static hosts that idle', () => { + const s = applyTemplate('quadruped_still'); + expect(s.host.movementType).toBe('static'); + expect(s.host.entityType).toBe('easy_model_entities:static_entity'); + expect(s.behavior.mode).toBe('idle_only'); + expect(s.movement.speed).toBe(0); + }); + + test('statue disables movement and animation', () => { + const s = applyTemplate('statue'); + expect(s.host.bodyType).toBe('static'); + expect(s.behavior.mode).toBe('static'); + expect(s.animation.mode).toBe('none'); + }); +}); + +describe('presetDefaults', () => { + test.each(SELECTABLE_PRESET_TYPES)( + 'produces a complete settings object for %s', + (preset) => { + const d = presetDefaults(preset); + expect(d.presetType).toBe(preset); + expect(d.host.bodyType).toBeTruthy(); + expect(Number.isFinite(d.dimensions.width)).toBe(true); + expect(Number.isFinite(d.dimensions.height)).toBe(true); + expect(d.dimensions.eyeHeight).toBeLessThanOrEqual(d.dimensions.height); + }); + + test('moving presets enable ground movement and ambient strolling', () => { + const d = presetDefaults('quadruped_wandering'); + expect(d.host.movementType).toBe('ground'); + expect(d.movement.speed).toBe(0.22); + expect(d.behavior).toMatchObject( + {mode: 'ambient', lookAtPlayers: true, randomStroll: true}); + }); + + test('deepMerge overrides nested values and preserves untouched defaults', + () => { + const merged = deepMerge(presetDefaults('cuboid_still'), + {rendering: {scale: 2}}); + expect(merged.rendering.scale).toBe(2); + expect(merged.rendering.shadowRadius).toBe(0.5); + }); +}); diff --git a/plugins/easy_model_entities/src/tests/textureResolver.test.js b/plugins/easy_model_entities/src/tests/textureResolver.test.js new file mode 100644 index 00000000..88b5a99d --- /dev/null +++ b/plugins/easy_model_entities/src/tests/textureResolver.test.js @@ -0,0 +1,248 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const { + parseExternalLocation, + resolveTextures, + describeTextureSource +} = require('../utils/TextureResolver'); + +const SETTINGS = {namespace: 'example', profileId: 'lizard'}; +const BYTES = Uint8Array.from([1, 2, 3]); + +describe('parseExternalLocation', () => { + test('derives from an assets path', () => { + expect(parseExternalLocation({ + path: 'C:/mc/assets/minecraft/textures/entity/chest/normal.png' + })).toBe('minecraft:textures/entity/chest/normal.png'); + }); + + test('derives from namespace + folder + name', () => { + expect(parseExternalLocation({ + namespace: 'minecraft', folder: 'entity/chest', name: 'normal.png' + })).toBe('minecraft:textures/entity/chest/normal.png'); + }); + + test('treats a foreign mod namespace as external', () => { + expect(parseExternalLocation({ + namespace: 'othermod', folder: 'entity', name: 'thing' + })).toBe('othermod:textures/entity/thing.png'); + }); + + test('returns null for an empty namespace and no assets path', () => { + expect(parseExternalLocation({ + namespace: '', folder: 'block', name: 'eye_texture', path: '' + })).toBeNull(); + }); + + test('keeps a non-minecraft assets path custom when no namespace is set', + () => { + expect(parseExternalLocation({ + namespace: '', folder: 'block', name: 'disguised_chestling', + path: 'D:/x/assets/easy_model_entities_examples/textures/entity/' + + 'disguised_chestling_1.png' + })).toBeNull(); + }); +}); + +describe('describeTextureSource', () => { + test('labels a no-namespace texture as custom', () => { + expect(describeTextureSource({ + namespace: '', folder: 'block', name: 'disguised_chestling.png', path: '' + })).toEqual({label: 'Custom Texture'}); + }); + + test('labels a namespaced texture with its compact location', () => { + expect(describeTextureSource({ + namespace: 'minecraft', folder: 'entity/chest', name: 'normal.png' + })).toEqual({label: 'minecraft:entity/chest/normal'}); + }); +}); + +describe('resolveTextures', () => { + test('single custom texture is packed and referenced explicitly', () => { + const result = resolveTextures( + [{ + index: 0, namespace: '', folder: '', name: '', path: '', + bytes: BYTES + }], + SETTINGS); + expect(result.texture).toBe('example:textures/entity/lizard.png'); + expect(result.textures).toEqual({}); + expect(result.packed).toEqual( + [{path: 'assets/example/textures/entity/lizard.png', bytes: BYTES}]); + expect(result.referencedOnly).toEqual([]); + expect(result.issues).toEqual([]); + }); + + test('foreign-namespace texture is packed under its own namespace', () => { + const result = resolveTextures( + [{ + index: 0, namespace: 'othermod', folder: 'entity', name: 'thing', + path: '', bytes: BYTES + }], + SETTINGS); + expect(result.texture).toBe('othermod:textures/entity/thing.png'); + expect(result.textures).toEqual({}); + expect(result.packed).toEqual( + [{path: 'assets/othermod/textures/entity/thing.png', bytes: BYTES}]); + expect(result.referencedOnly).toEqual([]); + expect(result.issues).toEqual([]); + }); + + test('foreign-namespace texture stays a reference when excluded', () => { + const result = resolveTextures( + [{ + index: 0, namespace: 'othermod', folder: 'entity', name: 'thing', + path: '', bytes: BYTES + }], + {...SETTINGS, includeExternalTextures: false}); + expect(result.texture).toBe('othermod:textures/entity/thing.png'); + expect(result.packed).toEqual([]); + expect(result.referencedOnly).toEqual( + ['othermod:textures/entity/thing.png']); + expect(result.issues).toEqual([]); + }); + + test('vanilla textures are never packed and never reported', () => { + const result = resolveTextures( + [{ + index: 0, namespace: 'minecraft', folder: 'entity/chest', + name: 'normal.png', path: '', bytes: BYTES + }], + {...SETTINGS, includeExternalTextures: true}); + expect(result.texture).toBe('minecraft:textures/entity/chest/normal.png'); + expect(result.packed).toEqual([]); + expect(result.referencedOnly).toEqual([]); + }); + + test('packs a custom index 1 that lives under a non-minecraft assets tree', + () => { + const result = resolveTextures([ + { + index: 0, namespace: 'minecraft', folder: 'entity/chest', + name: 'normal.png', path: '', bytes: BYTES + }, + { + index: 1, namespace: '', folder: 'block', + name: 'disguised_chestling.png', + path: 'D:/x/assets/easy_model_entities_examples/textures/entity/' + + 'disguised_chestling_1.png', + bytes: BYTES + } + ], SETTINGS); + + expect(result.texture).toBe( + 'minecraft:textures/entity/chest/normal.png'); + expect(result.textures).toEqual( + {1: 'example:textures/entity/lizard_1.png'}); + expect(result.packed).toEqual([ + { + path: 'assets/example/textures/entity/lizard_1.png', bytes: BYTES + }]); + }); + + test('mixes a vanilla index 0 with a custom index 1', () => { + const result = resolveTextures([ + { + index: 0, namespace: 'minecraft', folder: 'entity/chest', + name: 'normal.png', path: '', bytes: BYTES + }, + { + index: 1, namespace: '', folder: 'block', name: 'eye_texture', + path: '', bytes: BYTES + } + ], SETTINGS); + + expect(result.texture).toBe('minecraft:textures/entity/chest/normal.png'); + expect(result.textures).toEqual( + {1: 'example:textures/entity/lizard_1.png'}); + expect(result.packed).toEqual( + [{path: 'assets/example/textures/entity/lizard_1.png', bytes: BYTES}]); + expect(result.issues).toEqual([]); + }); + + test('mixes a custom index 0 with a vanilla index 1', () => { + const result = resolveTextures([ + { + index: 0, namespace: '', folder: 'block', name: 'body', path: '', + bytes: BYTES + }, + { + index: 1, namespace: 'minecraft', folder: 'entity/chest', + name: 'normal.png', path: '', bytes: BYTES + } + ], SETTINGS); + + expect(result.texture).toBe('example:textures/entity/lizard.png'); + expect(result.textures).toEqual( + {1: 'minecraft:textures/entity/chest/normal.png'}); + expect(result.packed).toEqual( + [{path: 'assets/example/textures/entity/lizard.png', bytes: BYTES}]); + expect(result.issues).toEqual([]); + }); + + test('reports a texture location with an unusable namespace', () => { + const result = resolveTextures( + [{ + index: 0, namespace: 'My Mod', folder: 'entity', name: 'thing', + path: '', bytes: BYTES + }], + SETTINGS); + + expect(result.texture).toBeNull(); + expect(result.issues.map((issue) => issue.code)).toEqual( + ['INVALID_TEXTURE_LOCATION']); + }); + + test('reports a duplicate texture slot instead of overwriting it', () => { + const result = resolveTextures([ + { + index: 1, namespace: 'minecraft', folder: 'entity/chest', + name: 'normal.png', path: '', bytes: BYTES + }, + { + index: 1, namespace: 'othermod', folder: 'entity', name: 'thing', + path: '', bytes: BYTES + } + ], SETTINGS); + + expect(result.textures).toEqual( + {1: 'minecraft:textures/entity/chest/normal.png'}); + expect(result.issues.map((issue) => issue.code)).toEqual( + ['DUPLICATE_TEXTURE_INDEX', 'MISSING_TEXTURE_INDEX']); + }); + + test('reports a model that has no texture in slot 0', () => { + const result = resolveTextures( + [{ + index: 1, namespace: 'othermod', folder: 'entity', name: 'thing', + path: '', bytes: BYTES + }], + SETTINGS); + + expect(result.texture).toBeNull(); + expect(result.issues.map((issue) => issue.code)).toEqual( + ['MISSING_TEXTURE_INDEX']); + }); + + test('reports nothing for a project without any texture', () => { + expect(resolveTextures([], SETTINGS).issues).toEqual([]); + }); +}); diff --git a/plugins/easy_model_entities/src/tests/validation.test.js b/plugins/easy_model_entities/src/tests/validation.test.js new file mode 100644 index 00000000..7a90803f --- /dev/null +++ b/plugins/easy_model_entities/src/tests/validation.test.js @@ -0,0 +1,327 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {Validator} = require('../model/Validator'); +const {fixtureSettings} = require('./fixtureData'); + +function codes(list) { + return list.map((entry) => entry.code); +} + +const QUADRUPED_BONES = ['root', 'body', 'head', 'front_left_leg', + 'front_right_leg', 'back_left_leg', 'back_right_leg']; + +describe('validateSettings', () => { + test('valid default settings produce no errors', () => { + const result = Validator.validateSettings(fixtureSettings(), { + hasModel: true, + hasTexture: true, + boneNames: QUADRUPED_BONES + }); + expect(result.valid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + test('rejects invalid namespace and profile id', () => { + const s = fixtureSettings(); + s.namespace = 'Bad NS'; + s.profileId = '../escape'; + const result = Validator.validateSettings(s, + {hasModel: true, hasTexture: true}); + expect(result.valid).toBe(false); + expect(codes(result.errors)).toEqual( + expect.arrayContaining(['INVALID_NAMESPACE', 'INVALID_PROFILE_ID'])); + }); + + test('rejects an unknown preset type', () => { + const s = fixtureSettings(); + s.presetType = 'bogus'; + const result = Validator.validateSettings(s, + {hasModel: true, hasTexture: true}); + expect(codes(result.errors)).toContain('INVALID_PRESET_TYPE'); + }); + + test('rejects non-positive dimensions and eye height above height', () => { + const s = fixtureSettings(); + s.dimensions = {width: 0, height: -1, eyeHeight: 5}; + const result = Validator.validateSettings(s, + {hasModel: true, hasTexture: true}); + expect(codes(result.errors).filter( + (c) => c === 'INVALID_DIMENSIONS').length).toBeGreaterThanOrEqual(2); + }); + + test('rejects dimensions larger than the mod accepts', () => { + const s = fixtureSettings(); + s.dimensions = {width: 9, height: 12, eyeHeight: 1}; + const result = Validator.validateSettings(s, + {hasModel: true, hasTexture: true}); + expect(codes(result.errors).filter( + (c) => c === 'INVALID_DIMENSIONS').length).toBe(2); + }); + + test('rejects movement values outside the mod range', () => { + const s = fixtureSettings(); + s.movement = {speed: 3, stepHeight: -0.5, gravity: true}; + const result = Validator.validateSettings(s, + {hasModel: true, hasTexture: true}); + expect(codes(result.errors).filter( + (c) => c === 'INVALID_MOVEMENT').length).toBe(2); + }); + + test('rejects negative attributes', () => { + const s = fixtureSettings(); + s.attributes = {maxHealth: -1, movementSpeed: -1, followRange: -1}; + const result = Validator.validateSettings(s, + {hasModel: true, hasTexture: true}); + expect(codes(result.errors).filter( + (c) => c === 'INVALID_ATTRIBUTES').length).toBe(3); + }); + + test('rejects a non-positive scale and negative render settings', () => { + const s = fixtureSettings(); + s.rendering.scale = 0; + s.rendering.shadowRadius = -0.5; + s.rendering.visibleBoundsWidth = -1; + const result = Validator.validateSettings(s, + {hasModel: true, hasTexture: true}); + expect(codes(result.errors).filter( + (c) => c === 'INVALID_RENDER_SETTINGS').length).toBe(3); + }); + + test('rejects negative animation settings', () => { + const s = fixtureSettings(); + s.animation.swingSpeed = -1; + s.animation.idleStrength = -1; + const result = Validator.validateSettings(s, + {hasModel: true, hasTexture: true}); + expect(codes(result.errors).filter( + (c) => c === 'INVALID_ANIMATION_SETTINGS').length).toBe(2); + }); + + test('rejects NaN/Infinity dimensions', () => { + const s = fixtureSettings(); + s.dimensions = {width: NaN, height: Infinity, eyeHeight: 0.5}; + const result = Validator.validateSettings(s, + {hasModel: true, hasTexture: true}); + expect(codes(result.errors)).toContain('INVALID_NUMERIC'); + }); + + test('blocks the export on texture resolution issues', () => { + const result = Validator.validateSettings(fixtureSettings(), { + hasModel: true, + hasTexture: true, + boneNames: QUADRUPED_BONES, + textureIssues: [ + {code: 'INVALID_TEXTURE_LOCATION', message: 'bad location'}] + }); + expect(result.valid).toBe(false); + expect(codes(result.errors)).toContain('INVALID_TEXTURE_LOCATION'); + }); + + test('flags missing model and texture', () => { + const result = Validator.validateSettings(fixtureSettings(), + {hasModel: false, hasTexture: false}); + expect(codes(result.errors)).toEqual( + expect.arrayContaining(['MISSING_MODEL', 'MISSING_TEXTURE'])); + }); + + test('allows multiple textures without warning', () => { + const result = Validator.validateSettings(fixtureSettings(), { + hasModel: true, + hasTexture: true, + boneNames: QUADRUPED_BONES + }); + expect(result.valid).toBe(true); + expect(codes(result.warnings)).not.toContain('MULTIPLE_TEXTURES'); + }); + + test('warns about performance budgets without blocking', () => { + const result = Validator.validateSettings(fixtureSettings(), { + hasModel: true, + hasTexture: true, + textureWidth: 4096, + textureHeight: 4096, + modelFileSize: 5 * 1024 * 1024, + cubeCount: 1000, + boneCount: 200, + hierarchyDepth: 50, + boneNames: QUADRUPED_BONES + }); + expect(result.valid).toBe(true); + expect(codes(result.warnings)).toEqual( + expect.arrayContaining( + ['LARGE_TEXTURE', 'LARGE_MODEL', 'HIGH_CUBE_COUNT', + 'HIGH_BONE_COUNT', 'DEEP_HIERARCHY']) + ); + }); + + test('warns about soft performance budgets without blocking', () => { + const result = Validator.validateSettings(fixtureSettings(), { + hasModel: true, + hasTexture: true, + textureWidth: 256, + textureHeight: 128, + modelFileSize: 1536 * 1024, + cubeCount: 400, + boneCount: 100, + hierarchyDepth: 25, + animationCount: 16, + boneNames: QUADRUPED_BONES + }); + expect(result.valid).toBe(true); + expect(codes(result.warnings)).toEqual( + expect.arrayContaining( + ['SOFT_TEXTURE_SIZE', 'SOFT_MODEL_SIZE', 'SOFT_CUBE_COUNT', + 'SOFT_BONE_COUNT', 'SOFT_HIERARCHY_DEPTH']) + ); + expect(codes(result.warnings)).not.toContain('HIGH_ANIMATION_COUNT'); + }); + + test('rejects more animations than the mod accepts', () => { + const result = Validator.validateSettings(fixtureSettings(), { + hasModel: true, + hasTexture: true, + animationCount: 17, + boneNames: QUADRUPED_BONES + }); + expect(result.valid).toBe(false); + expect(codes(result.errors)).toContain('HIGH_ANIMATION_COUNT'); + }); + + test('standard animation clips produce no warnings', () => { + const result = Validator.validateSettings(fixtureSettings(), { + hasModel: true, + hasTexture: true, + animationCount: 3, + animations: [ + {name: 'idle', channels: ['rotation'], hasExpression: false}, + { + name: 'WALK', + channels: ['rotation', 'position'], + hasExpression: false + }, + {name: 'sit', channels: ['rotation'], hasExpression: false} + ], + boneNames: QUADRUPED_BONES + }); + expect(result.valid).toBe(true); + expect(result.warnings).toHaveLength(0); + }); + + test('warns about custom clips and animations the mod drops', () => { + const result = Validator.validateSettings(fixtureSettings(), { + hasModel: true, + hasTexture: true, + animationCount: 3, + animations: [ + {name: 'dance', channels: ['rotation'], hasExpression: false}, + {name: 'walk', channels: ['rotation'], hasExpression: true}, + {name: 'idle', channels: ['rotation', 'scale'], hasExpression: false} + ], + boneNames: QUADRUPED_BONES + }); + expect(result.valid).toBe(true); + expect(codes(result.warnings)).toEqual( + expect.arrayContaining( + ['CUSTOM_ANIMATION_CLIP', 'UNSUPPORTED_ANIMATION_EXPRESSION', + 'UNSUPPORTED_ANIMATION_CHANNEL']) + ); + }); + + test('warns when the preset silences the clips of the project', () => { + const s = fixtureSettings(); + s.presetType = 'statue'; + s.host.bodyType = 'static'; + s.animation.mode = 'none'; + const result = Validator.validateSettings(s, { + hasModel: true, + hasTexture: true, + animations: [{name: 'idle'}, {name: 'walk'}] + }); + expect(result.valid).toBe(true); + expect(codes(result.warnings)).toContain('ANIMATIONS_NEVER_PLAYED'); + expect(result.warnings.find( + (warning) => warning.code === 'ANIMATIONS_NEVER_PLAYED').message) + .toContain('idle, walk'); + }); + + test('animated presets with clips produce no animation warning', () => { + const result = Validator.validateSettings(fixtureSettings(), { + hasModel: true, + hasTexture: true, + boneNames: QUADRUPED_BONES, + animations: [{name: 'idle'}] + }); + expect(codes(result.warnings)).not.toContain('ANIMATIONS_NEVER_PLAYED'); + }); + + test('warns about textures that stay a reference', () => { + const result = Validator.validateSettings(fixtureSettings(), { + hasModel: true, + hasTexture: true, + boneNames: QUADRUPED_BONES, + referencedTextures: ['othermod:textures/entity/thing.png'] + }); + expect(result.valid).toBe(true); + expect(codes(result.warnings)).toContain('EXTERNAL_TEXTURE_REFERENCE'); + }); + + test('warns about missing body parts per body type', () => { + const result = Validator.validateSettings(fixtureSettings(), + {hasModel: true, hasTexture: true, boneNames: ['root', 'body']}); + expect(codes(result.warnings).filter( + (c) => c === 'MISSING_BODY_PART').length).toBeGreaterThan(0); + }); +}); + +describe('getMissingBodyParts', () => { + test('static body type requires no parts', () => { + expect(Validator.getMissingBodyParts('static', [])).toEqual([]); + }); + + test('unknown body types require no parts', () => { + expect(Validator.getMissingBodyParts('aquatic', [])).toEqual([]); + }); + + test('biped reports missing limbs case-insensitively', () => { + expect(Validator.getMissingBodyParts('biped', + ['ROOT', 'Head', 'Body'])).toEqual([ + 'left_arm', + 'right_arm', + 'left_leg', + 'right_leg' + ]); + }); +}); + +describe('validateOutputPath', () => { + test('accepts a clean relative path', () => { + expect(Validator.validateOutputPath( + 'data/example/profiles/lizard.json').valid).toBe(true); + }); + + test('rejects traversal and absolute paths', () => { + expect(Validator.validateOutputPath('../../etc/passwd').code).toBe( + 'PATH_TRAVERSAL'); + expect(Validator.validateOutputPath('/etc/passwd').code).toBe( + 'ABSOLUTE_PATH'); + expect(Validator.validateOutputPath('C:/Windows/system32').code).toBe( + 'ABSOLUTE_PATH'); + }); +}); diff --git a/plugins/easy_model_entities/src/tests/versionMatrix.test.js b/plugins/easy_model_entities/src/tests/versionMatrix.test.js new file mode 100644 index 00000000..5bf8775c --- /dev/null +++ b/plugins/easy_model_entities/src/tests/versionMatrix.test.js @@ -0,0 +1,69 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const { + getVersions, + getDefaultVersionId, + getPackFormats +} = require('../model/versionMatrix'); + +describe('versionMatrix', () => { + test('default version is 1.20.1; all five versions are enabled', () => { + expect(getDefaultVersionId()).toBe('1.20.1'); + const enabled = getVersions().filter((v) => v.enabled).map((v) => v.id); + expect(enabled).toEqual(['1.20.1', '1.21.1', '1.21.11', '26.1.2', '26.2']); + }); + + test('exposes the full selectable version list', () => { + expect(getVersions().map((v) => v.id)).toEqual( + ['1.20.1', '1.21.1', '1.21.11', '26.1.2', '26.2']); + }); + + test('legacy versions use a single integer pack_format', () => { + expect(getPackFormats('1.20.1')).toEqual( + {data: {packFormat: 15}, resource: {packFormat: 15}}); + expect(getPackFormats('1.21.1')).toEqual( + {data: {packFormat: 48}, resource: {packFormat: 34}}); + }); + + test('1.21.11 uses the min/max format scheme', () => { + expect(getPackFormats('1.21.11')).toEqual({ + data: {packFormat: 94, minFormat: [94, 1], maxFormat: [94, 1]}, + resource: {packFormat: 75, minFormat: [75, 0], maxFormat: [75, 0]} + }); + }); + + test('26.1.2 uses the min/max format scheme', () => { + expect(getPackFormats('26.1.2')).toEqual({ + data: {packFormat: 101, minFormat: [101, 1], maxFormat: [101, 1]}, + resource: {packFormat: 84, minFormat: [84, 0], maxFormat: [84, 0]} + }); + }); + + test('26.2 uses the min/max format scheme', () => { + expect(getPackFormats('26.2')).toEqual({ + data: {packFormat: 101, minFormat: [101, 1], maxFormat: [101, 1]}, + resource: {packFormat: 84, minFormat: [84, 0], maxFormat: [84, 0]} + }); + }); + + test('unknown versions return no pack formats', () => { + expect(getPackFormats('does-not-exist')).toBeNull(); + }); +}); diff --git a/plugins/easy_model_entities/src/ui/exportDialog.js b/plugins/easy_model_entities/src/ui/exportDialog.js new file mode 100644 index 00000000..39bc1717 --- /dev/null +++ b/plugins/easy_model_entities/src/ui/exportDialog.js @@ -0,0 +1,342 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const { + MODEL_TYPE_ENTITY, + MODEL_TYPE_BLOCK_ENTITY +} = require('../model/presetTypes'); +const {EXPORT_TYPE_DATA_PACK} = require('../model/exportTypes'); +const {Validator} = require('../model/Validator'); +const {t} = require('../i18n/translations'); +const { + presetOptions, + modelTypeOptions, + exportTypeOptions, + hostEntityTypeOptions, + movementTypeOptions, + bodyTypeOptions, + gaitOptions, + behaviorModeOptions, + animationModeOptions, + versionOptions +} = require('./exportDialogOptions'); +const { + settingsToForm, + activeModelType, + activePreset, + presetFormValues, + resolveExportSettings +} = require('./exportSettingsMapping'); + +function advancedField(field, showCustomize) { + return Object.assign({condition: showCustomize}, field); +} + +function buildFormConfig(settings, ui) { + const limits = Validator.LIMITS; + const values = settingsToForm(settings); + const state = ui || {}; + const allowCustomize = !!state.showCustomization; + + const standalone = (form) => form.exportType !== 'model_only'; + const blockEntitySelected = (form) => + standalone(form) && form.modelType === MODEL_TYPE_BLOCK_ENTITY; + const customizeOn = (form) => allowCustomize && !!form.customize; + const showServer = (form) => customizeOn(form) && standalone(form); + const showEntity = (form) => showServer(form) && !blockEntitySelected(form); + const showRender = customizeOn; + + const experimental = !!state.experimental; + const entityValue = state.preset || 'statue'; + const blockValue = state.blockPreset || 'static'; + + const config = { + exportType: { + label: t('eme.field.exportType'), + type: 'select', + options: exportTypeOptions(), + value: state.exportType || 'packs' + }, + modelType: { + label: t('eme.field.modelType'), + type: 'select', + options: modelTypeOptions(), + value: state.modelType || MODEL_TYPE_ENTITY, + condition: standalone + }, + preset: { + label: t('eme.field.preset'), + type: 'select', + options: presetOptions(MODEL_TYPE_ENTITY, experimental, entityValue), + value: entityValue, + condition: (form) => !blockEntitySelected(form) + }, + blockPreset: { + label: t('eme.field.preset'), + type: 'select', + options: presetOptions(MODEL_TYPE_BLOCK_ENTITY, experimental, blockValue), + value: blockValue, + condition: blockEntitySelected + }, + namespace: { + label: t('eme.field.namespace'), + type: 'text', + value: values.namespace, + placeholder: 'your_mod_id' + }, + profileId: { + label: t('eme.field.profileId'), type: 'text', + value: values.profileId + }, + targetVersion: { + label: t('eme.field.targetVersion'), + type: 'select', + options: versionOptions(), + value: values.targetVersion + } + }; + + if (state.hasExternalTextures) { + config.includeExternalTextures = { + label: t('eme.field.includeExternalTextures'), + description: t('eme.field.includeExternalTexturesDescription'), + type: 'checkbox', + value: settings.includeExternalTextures !== false, + condition: (form) => form.exportType !== EXPORT_TYPE_DATA_PACK + }; + } + + if (allowCustomize) { + config.customize = { + label: t('eme.field.customize'), + type: 'checkbox', + value: !!state.customize + }; + } + + Object.assign(config, { + host_header: advancedField( + {type: 'info', text: `### ${t('eme.section.host')}`}, showEntity), + hostEntityType: advancedField({ + label: t('eme.field.hostEntityType'), + type: 'select', + options: hostEntityTypeOptions(), + value: values.hostEntityType + }, showEntity), + movementType: advancedField({ + label: t('eme.field.movementType'), + type: 'select', + options: movementTypeOptions(), + value: values.movementType + }, showEntity), + bodyType: advancedField({ + label: t('eme.field.bodyType'), + type: 'select', + options: bodyTypeOptions(), + value: values.bodyType + }, showEntity), + + dimensions_header: advancedField( + {type: 'info', text: `### ${t('eme.section.dimensions')}`}, + showServer), + width: advancedField( + { + label: t('eme.field.width'), type: 'number', value: values.width, + step: 0.1, min: limits.minDimension, max: limits.maxDimension + }, showServer), + height: advancedField( + { + label: t('eme.field.height'), type: 'number', value: values.height, + step: 0.1, min: limits.minDimension, max: limits.maxDimension + }, showServer), + eyeHeight: advancedField( + { + label: t('eme.field.eyeHeight'), type: 'number', + value: values.eyeHeight, step: 0.1, min: 0, + max: limits.maxDimension + }, showServer), + + movement_header: advancedField( + {type: 'info', text: `### ${t('eme.section.movement')}`}, showEntity), + speed: advancedField( + { + label: t('eme.field.speed'), type: 'number', value: values.speed, + step: 0.01, min: 0, max: limits.maxMovementSpeed + }, showEntity), + stepHeight: advancedField( + { + label: t('eme.field.stepHeight'), type: 'number', + value: values.stepHeight, step: 0.1, min: 0, + max: limits.maxStepHeight + }, showEntity), + gravity: advancedField( + { + label: t('eme.field.gravity'), type: 'checkbox', + value: values.gravity + }, showEntity), + + behavior_header: advancedField( + {type: 'info', text: `### ${t('eme.section.behavior')}`}, showEntity), + behaviorMode: advancedField({ + label: t('eme.field.behaviorMode'), + type: 'select', + options: behaviorModeOptions(), + value: values.behaviorMode + }, showEntity), + + attributes_header: advancedField( + {type: 'info', text: `### ${t('eme.section.attributes')}`}, + showEntity), + maxHealth: advancedField( + { + label: t('eme.field.maxHealth'), type: 'number', + value: values.maxHealth, step: 0.5, min: 0 + }, showEntity), + movementSpeed: advancedField( + { + label: t('eme.field.movementSpeed'), type: 'number', + value: values.movementSpeed, step: 0.01, min: 0 + }, showEntity), + followRange: advancedField( + { + label: t('eme.field.followRange'), type: 'number', + value: values.followRange, step: 1, min: 0 + }, showEntity), + + rendering_header: advancedField( + {type: 'info', text: `### ${t('eme.section.rendering')}`}, + showRender), + scale: advancedField( + { + label: t('eme.field.scale'), type: 'number', value: values.scale, + step: 0.1, min: limits.minDimension + }, showRender), + shadowRadius: advancedField( + { + label: t('eme.field.shadowRadius'), type: 'number', + value: values.shadowRadius, step: 0.1, min: 0 + }, showRender), + visibleBoundsWidth: advancedField( + { + label: t('eme.field.visibleBoundsWidth'), type: 'number', + value: values.visibleBoundsWidth, step: 0.1, min: 0 + }, showRender), + visibleBoundsHeight: advancedField( + { + label: t('eme.field.visibleBoundsHeight'), type: 'number', + value: values.visibleBoundsHeight, step: 0.1, min: 0 + }, showRender), + visibleBoundsOffsetX: advancedField( + { + label: t('eme.field.visibleBoundsOffsetX'), type: 'number', + value: values.visibleBoundsOffsetX, step: 0.1 + }, showRender), + visibleBoundsOffsetY: advancedField( + { + label: t('eme.field.visibleBoundsOffsetY'), type: 'number', + value: values.visibleBoundsOffsetY, step: 0.1 + }, showRender), + visibleBoundsOffsetZ: advancedField( + { + label: t('eme.field.visibleBoundsOffsetZ'), type: 'number', + value: values.visibleBoundsOffsetZ, step: 0.1 + }, showRender), + + animation_header: advancedField( + {type: 'info', text: `### ${t('eme.section.animation')}`}, + showRender), + animationMode: advancedField({ + label: t('eme.field.animationMode'), + type: 'select', + options: animationModeOptions(), + value: values.animationMode + }, showRender), + swingSpeed: advancedField( + { + label: t('eme.field.swingSpeed'), type: 'number', + value: values.swingSpeed, step: 0.1, min: 0 + }, showRender), + walkSpeedMultiplier: advancedField( + { + label: t('eme.field.walkSpeedMultiplier'), type: 'number', + value: values.walkSpeedMultiplier, step: 0.1, min: 0 + }, showRender), + idleStrength: advancedField( + { + label: t('eme.field.idleStrength'), type: 'number', + value: values.idleStrength, step: 0.1, min: 0 + }, showRender), + gait: advancedField({ + label: t('eme.field.gait'), + type: 'select', + options: gaitOptions(), + value: values.gait + }, showRender) + }); + + return config; +} + +function openExportDialog(options) { + const settings = options.settings; + const modelDimensions = options.modelDimensions; + const visibleBounds = options.visibleBounds; + let lastKey = `${activeModelType(options)}|${activePreset(options)}`; + + const dialog = new Dialog({ + id: 'eme_export_dialog', + title: t('eme.dialog.title'), + width: 560, + form: buildFormConfig(settings, { + preset: options.preset, + blockPreset: options.blockPreset, + modelType: options.modelType, + exportType: options.exportType, + customize: options.customize, + showCustomization: options.showCustomization, + experimental: options.experimental, + hasExternalTextures: options.hasExternalTextures + }), + onFormChange(form) { + const modelType = activeModelType(form); + const preset = activePreset(form); + const key = `${modelType}|${preset}`; + if (key !== lastKey) { + lastKey = key; + if (modelType !== MODEL_TYPE_ENTITY || preset !== 'custom') { + dialog.setFormValues( + presetFormValues(preset, modelType, modelDimensions, + visibleBounds), false); + } + } + }, + onConfirm(form) { + const finalSettings = resolveExportSettings(form, settings, + modelDimensions, visibleBounds); + options.onExport(finalSettings, finalSettings.exportTarget); + } + }); + dialog.show(); + + return dialog; +} + +module.exports = { + openExportDialog +}; diff --git a/plugins/easy_model_entities/src/ui/exportDialogOptions.js b/plugins/easy_model_entities/src/ui/exportDialogOptions.js new file mode 100644 index 00000000..097ad545 --- /dev/null +++ b/plugins/easy_model_entities/src/ui/exportDialogOptions.js @@ -0,0 +1,148 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {getVersions} = require('../model/versionMatrix'); +const { + SELECTABLE_PRESET_TYPES, + BLOCK_ENTITY_PRESET_TYPES, + MOVEMENT_TYPES, + BODY_TYPES, + GAIT_TYPES, + BEHAVIOR_MODES, + ANIMATION_MODES, + isStablePreset, + MODEL_TYPE_ENTITY, + MODEL_TYPE_BLOCK_ENTITY +} = require('../model/presetTypes'); +const { + EXPORT_TYPE_PACKS, + EXPORT_TYPE_RESOURCE_PACK, + EXPORT_TYPE_DATA_PACK, + EXPORT_TYPE_MOD_PROJECT, + EXPORT_TYPE_MODEL_ONLY +} = require('../model/exportTypes'); +const {t} = require('../i18n/translations'); + +function optionsFromEnum(values, prefix) { + const options = {}; + values.forEach((id) => { + options[id] = t(`${prefix}.${id}`); + }); + + return options; +} + +function presetLabel(id) { + return id === 'custom' ? t('eme.preset.custom') : t(`eme.preset.${id}`); +} + +function entityPresetIds(experimental) { + return experimental + ? ['custom', ...SELECTABLE_PRESET_TYPES] + : SELECTABLE_PRESET_TYPES.filter(isStablePreset); +} + +function blockPresetIds(experimental) { + return experimental + ? [...BLOCK_ENTITY_PRESET_TYPES] + : BLOCK_ENTITY_PRESET_TYPES.filter(isStablePreset); +} + +function presetOptions(modelType, experimental, ensure) { + const ids = modelType === MODEL_TYPE_BLOCK_ENTITY + ? blockPresetIds(experimental) : entityPresetIds(experimental); + if (ensure && !ids.includes(ensure)) { + ids.push(ensure); + } + const options = {}; + ids.forEach((id) => { + options[id] = presetLabel(id); + }); + + return options; +} + +function modelTypeOptions() { + return { + [MODEL_TYPE_ENTITY]: t('eme.modelType.entity'), + [MODEL_TYPE_BLOCK_ENTITY]: t('eme.modelType.block_entity') + }; +} + +function exportTypeOptions() { + return { + [EXPORT_TYPE_PACKS]: t('eme.exportType.packs'), + [EXPORT_TYPE_RESOURCE_PACK]: t('eme.exportType.resource_pack'), + [EXPORT_TYPE_DATA_PACK]: t('eme.exportType.data_pack'), + [EXPORT_TYPE_MOD_PROJECT]: t('eme.exportType.mod_project'), + [EXPORT_TYPE_MODEL_ONLY]: t('eme.exportType.model_only') + }; +} + +function hostEntityTypeOptions() { + return { + 'easy_model_entities:ground_entity': t('eme.entity.ground_entity'), + 'easy_model_entities:static_entity': t('eme.entity.static_entity'), + 'easy_model_entities:aquatic_entity': t('eme.entity.aquatic_entity'), + 'easy_model_entities:amphibious_entity': t('eme.entity.amphibious_entity') + }; +} + +function movementTypeOptions() { + return optionsFromEnum(MOVEMENT_TYPES, 'eme.movement'); +} + +function bodyTypeOptions() { + return optionsFromEnum(BODY_TYPES, 'eme.body'); +} + +function gaitOptions() { + return optionsFromEnum(GAIT_TYPES, 'eme.gait'); +} + +function behaviorModeOptions() { + return optionsFromEnum(BEHAVIOR_MODES, 'eme.behavior'); +} + +function animationModeOptions() { + return optionsFromEnum(ANIMATION_MODES, 'eme.animation'); +} + +function versionOptions() { + const options = {}; + getVersions().forEach((version) => { + options[version.id] = version.enabled ? version.label + : `${version.label} (coming soon)`; + }); + + return options; +} + +module.exports = { + presetOptions, + modelTypeOptions, + exportTypeOptions, + hostEntityTypeOptions, + movementTypeOptions, + bodyTypeOptions, + gaitOptions, + behaviorModeOptions, + animationModeOptions, + versionOptions +}; diff --git a/plugins/easy_model_entities/src/ui/exportSettingsMapping.js b/plugins/easy_model_entities/src/ui/exportSettingsMapping.js new file mode 100644 index 00000000..581d4e2b --- /dev/null +++ b/plugins/easy_model_entities/src/ui/exportSettingsMapping.js @@ -0,0 +1,188 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {getDefaults, applyTemplate, deepMerge} = require('../model/templates'); +const { + MODEL_TYPE_ENTITY, + MODEL_TYPE_BLOCK_ENTITY +} = require('../model/presetTypes'); +const { + EXPORT_TYPE_PACKS, + EXPORT_TYPE_MODEL_ONLY, + EXPORT_TYPE_MOD_PROJECT, + isZipExport +} = require('../model/exportTypes'); +const {ModelDimensions} = require('../model/ModelDimensions'); +const {VisibleBounds} = require('../model/VisibleBounds'); + +function settingsToForm(settings) { + const offset = settings.rendering.visibleBoundsOffset || [0, 0, 0]; + + return { + namespace: settings.namespace, + profileId: settings.profileId, + targetVersion: settings.targetVersion, + hostEntityType: settings.host.entityType, + movementType: settings.host.movementType, + bodyType: settings.host.bodyType, + width: settings.dimensions.width, + height: settings.dimensions.height, + eyeHeight: settings.dimensions.eyeHeight, + speed: settings.movement.speed, + stepHeight: settings.movement.stepHeight, + gravity: settings.movement.gravity, + behaviorMode: settings.behavior.mode, + maxHealth: settings.attributes.maxHealth, + movementSpeed: settings.attributes.movementSpeed, + followRange: settings.attributes.followRange, + scale: settings.rendering.scale, + shadowRadius: settings.rendering.shadowRadius, + visibleBoundsWidth: settings.rendering.visibleBoundsWidth ?? 0, + visibleBoundsHeight: settings.rendering.visibleBoundsHeight ?? 0, + visibleBoundsOffsetX: offset[0], + visibleBoundsOffsetY: offset[1], + visibleBoundsOffsetZ: offset[2], + animationMode: settings.animation.mode, + swingSpeed: settings.animation.swingSpeed, + walkSpeedMultiplier: settings.animation.walkSpeedMultiplier, + idleStrength: settings.animation.idleStrength ?? 1, + gait: settings.animation.gait || 'natural' + }; +} + +function formToSettings(form, base) { + return { + schemaVersion: base.schemaVersion, + modelType: base.modelType, + presetType: base.presetType, + namespace: String(form.namespace || '').trim(), + profileId: String(form.profileId || '').trim(), + targetVersion: base.targetVersion, + host: { + entityType: form.hostEntityType, + movementType: form.movementType, + bodyType: form.bodyType + }, + dimensions: { + width: Number(form.width), + height: Number(form.height), + eyeHeight: Number(form.eyeHeight) + }, + movement: { + speed: Number(form.speed), + stepHeight: Number(form.stepHeight), + gravity: !!form.gravity + }, + behavior: { + mode: form.behaviorMode, + lookAtPlayers: base.behavior ? base.behavior.lookAtPlayers : true, + randomStroll: base.behavior ? base.behavior.randomStroll : false + }, + attributes: { + maxHealth: Number(form.maxHealth), + movementSpeed: Number(form.movementSpeed), + followRange: Number(form.followRange) + }, + rendering: { + scale: Number(form.scale), + shadowRadius: Number(form.shadowRadius), + visibleBoundsWidth: Number(form.visibleBoundsWidth), + visibleBoundsHeight: Number(form.visibleBoundsHeight), + visibleBoundsOffset: [ + Number(form.visibleBoundsOffsetX), + Number(form.visibleBoundsOffsetY), + Number(form.visibleBoundsOffsetZ) + ] + }, + animation: { + mode: form.animationMode, + swingSpeed: Number(form.swingSpeed), + walkSpeedMultiplier: Number(form.walkSpeedMultiplier), + idleStrength: Number(form.idleStrength), + gait: form.gait + } + }; +} + +function activeModelType(form) { + return form.exportType === EXPORT_TYPE_MODEL_ONLY + ? MODEL_TYPE_ENTITY : (form.modelType || MODEL_TYPE_ENTITY); +} + +function activePreset(form) { + return activeModelType(form) === MODEL_TYPE_BLOCK_ENTITY + ? (form.blockPreset || 'static') : (form.preset || 'custom'); +} + +function presetFormValues(presetType, modelType, modelDimensions, + visibleBounds) { + const settings = ModelDimensions.applyModelDimensions( + applyTemplate(presetType, modelType), modelDimensions); + VisibleBounds.applyVisibleBounds(settings, visibleBounds); + const form = settingsToForm(settings); + delete form.namespace; + delete form.profileId; + delete form.targetVersion; + + return form; +} + +function resolveExportSettings(form, base, modelDimensions, visibleBounds) { + const exportType = form.exportType || EXPORT_TYPE_PACKS; + const modelType = activeModelType(form); + const preset = activePreset(form); + + let settings; + if (modelType !== MODEL_TYPE_BLOCK_ENTITY && preset === 'custom') { + settings = deepMerge(getDefaults(), base); + } else { + settings = ModelDimensions.applyModelDimensions( + applyTemplate(preset, modelType), modelDimensions); + VisibleBounds.applyVisibleBounds(settings, visibleBounds); + } + + settings.schemaVersion = base.schemaVersion || settings.schemaVersion; + settings.modelType = modelType; + settings.presetType = preset; + settings.namespace = String(form.namespace || '').trim(); + settings.profileId = String(form.profileId || '').trim(); + settings.targetVersion = form.targetVersion; + + if (form.customize) { + settings = formToSettings(form, settings); + } + settings.customize = !!form.customize; + settings.includeExternalTextures = form.includeExternalTextures !== false; + settings.lastExportedVersion = base.lastExportedVersion; + settings.exportType = exportType; + settings.exportTarget = isZipExport(exportType) + ? EXPORT_TYPE_PACKS : EXPORT_TYPE_MOD_PROJECT; + settings.modelOnly = exportType === EXPORT_TYPE_MODEL_ONLY; + + return settings; +} + +module.exports = { + settingsToForm, + formToSettings, + activeModelType, + activePreset, + presetFormValues, + resolveExportSettings +}; diff --git a/plugins/easy_model_entities/src/ui/texturePanel.js b/plugins/easy_model_entities/src/ui/texturePanel.js new file mode 100644 index 00000000..1d3a90fc --- /dev/null +++ b/plugins/easy_model_entities/src/ui/texturePanel.js @@ -0,0 +1,71 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {describeTextureSource} = require('../utils/TextureResolver'); + +let patchedVue = null; +let originalGetDescription = null; + +function texturesPanelVue() { + const panel = (typeof Panels !== 'undefined' && Panels.textures) + || (typeof Interface !== 'undefined' && Interface.Panels + && Interface.Panels.textures); + + return (panel && panel.inside_vue) || null; +} + +function patchTexturePanel(formatId) { + const vue = texturesPanelVue(); + if (!vue || typeof vue.getDescription !== 'function' + || originalGetDescription) { + return; + } + + const original = vue.getDescription.bind(vue); + patchedVue = vue; + originalGetDescription = original; + vue.getDescription = function (texture) { + const base = original(texture); + try { + if (typeof Format !== 'undefined' && Format && Format.id === formatId) { + const {label} = describeTextureSource({ + namespace: texture.namespace, + folder: texture.folder, + name: texture.name, + path: texture.path + }); + + return `${base} - ${label}`; + } + } catch (error) { + } + + return base; + }; +} + +function unpatchTexturePanel() { + if (patchedVue && originalGetDescription) { + patchedVue.getDescription = originalGetDescription; + } + patchedVue = null; + originalGetDescription = null; +} + +module.exports = {patchTexturePanel, unpatchTexturePanel}; diff --git a/plugins/easy_model_entities/src/utils/ResourceLocation.js b/plugins/easy_model_entities/src/utils/ResourceLocation.js new file mode 100644 index 00000000..06ae9dba --- /dev/null +++ b/plugins/easy_model_entities/src/utils/ResourceLocation.js @@ -0,0 +1,96 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {hashString} = require('./hash'); + +class ResourceLocation { + static #NAMESPACE_PATTERN = /^[a-z0-9_.-]+$/; + static #PATH_PATTERN = /^[a-z0-9_./-]+$/; + static #MODEL_EXTENSION_PATTERN = /\.(bbmodel|json|gltf|glb|obj|geo)$/i; + static #COMBINING_MARK_PATTERN = /[\u0300-\u036f]/g; + static #TRANSLITERATIONS = new Map(Object.entries({ + 'ä': 'ae', 'ö': 'oe', 'ü': 'ue', 'ß': 'ss', 'å': 'aa', 'æ': 'ae', + 'ø': 'oe', 'œ': 'oe', 'þ': 'th', 'ð': 'dh', 'đ': 'd', 'ł': 'l', + 'ı': 'i', 'ij': 'ij', 'ŋ': 'ng', '&': '_and_', '+': '_plus_' + })); + + static isValidNamespace(namespace) { + return typeof namespace === 'string' + && ResourceLocation.#NAMESPACE_PATTERN.test(namespace); + } + + static isValidPath(path) { + return typeof path === 'string' && ResourceLocation.#PATH_PATTERN.test(path) + && !path.includes('..'); + } + + static isValidResourceLocation(value) { + if (typeof value !== 'string' || !value.includes(':')) { + return false; + } + const parts = value.split(':'); + if (parts.length !== 2) { + return false; + } + return ResourceLocation.isValidNamespace(parts[0]) + && ResourceLocation.isValidPath(parts[1]); + } + + static buildResourceLocation(namespace, path) { + return `${namespace}:${path}`; + } + + static transliterate(value) { + let transliterated = ''; + for (const character of String(value ?? '').toLowerCase()) { + const replacement = ResourceLocation.#TRANSLITERATIONS.get(character); + transliterated += replacement ?? character; + } + + return transliterated.normalize('NFKD') + .replace(ResourceLocation.#COMBINING_MARK_PATTERN, ''); + } + + static sanitizeProfileId(name) { + const rawName = String(name ?? '').trim(); + const base = ResourceLocation.transliterate( + rawName.replace(ResourceLocation.#MODEL_EXTENSION_PATTERN, '')) + .replace(/[^a-z0-9_./-]+/g, '_') + .replace(/_{2,}/g, '_') + .split('/') + .filter((segment) => segment !== '' && segment !== '.' && segment !== '..') + .join('/') + .replace(/^[_./-]+|[_./-]+$/g, ''); + if (base.length > 0) { + return base; + } + + return rawName.length > 0 ? `model_${hashString(rawName)}` : 'entity'; + } + + static sanitizeNamespace(namespace) { + const base = ResourceLocation.transliterate(namespace) + .replace(/[^a-z0-9_.-]+/g, '_') + .replace(/_{2,}/g, '_') + .replace(/^[_.-]+|[_.-]+$/g, ''); + return base.length > 0 ? base : 'example_org'; + } +} + +module.exports = {ResourceLocation}; diff --git a/plugins/easy_model_entities/src/utils/TextureResolver.js b/plugins/easy_model_entities/src/utils/TextureResolver.js new file mode 100644 index 00000000..5ba49a73 --- /dev/null +++ b/plugins/easy_model_entities/src/utils/TextureResolver.js @@ -0,0 +1,139 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const {ResourceLocation} = require('./ResourceLocation'); + +const ASSETS_PATTERN = /assets\/([a-z0-9_.-]+)\/textures\/(.+?)(?:\.png)?$/i; + +function stripPng(value) { + return value.replace(/\.png$/i, ''); +} + +function trimSlashes(value) { + return value.replace(/^\/+|\/+$/g, ''); +} + +function customFileName(settings, index) { + return index === 0 + ? `${settings.profileId}.png` + : `${settings.profileId}_${index}.png`; +} + +function customLocation(settings, index) { + return `${settings.namespace}:textures/entity/${customFileName(settings, + index)}`; +} + +function parseExternalLocation(descriptor) { + const namespace = String(descriptor.namespace || '').trim().toLowerCase(); + if (namespace) { + const folder = trimSlashes(String(descriptor.folder || '').trim()); + const name = stripPng(String(descriptor.name || '').trim()).toLowerCase(); + if (!name) { + return null; + } + const rest = folder ? `${folder.toLowerCase()}/${name}` : name; + + return `${namespace}:textures/${rest}.png`; + } + + const path = String(descriptor.path || '').replace(/\\/g, '/'); + const match = path.match(ASSETS_PATTERN); + if (match && match[1].toLowerCase() === 'minecraft') { + return `minecraft:textures/${match[2].toLowerCase()}.png`; + } + + return null; +} + +function packPath(location) { + const [namespace, resourcePath] = location.split(':'); + + return `assets/${namespace}/${resourcePath}`; +} + +function describeTextureSource(descriptor) { + const location = parseExternalLocation(descriptor); + return location + ? {label: location.replace(':textures/', ':').replace(/\.png$/i, '')} + : {label: 'Custom Texture'}; +} + +function resolveTextures(descriptors, settings) { + const textures = {}; + const packed = []; + const issues = []; + const referencedOnly = []; + const resolvedIndices = new Set(); + const includeExternal = settings.includeExternalTextures !== false; + let texture = null; + + (descriptors || []).forEach((descriptor, position) => { + const index = Number.isInteger(descriptor.index) ? descriptor.index + : position; + if (resolvedIndices.has(index)) { + issues.push({ + code: 'DUPLICATE_TEXTURE_INDEX', + message: `Texture slot ${index} is assigned more than once` + }); + return; + } + + const externalLocation = parseExternalLocation(descriptor); + if (externalLocation + && !ResourceLocation.isValidResourceLocation(externalLocation)) { + issues.push({ + code: 'INVALID_TEXTURE_LOCATION', + message: `Texture slot ${index} points to the invalid location ` + + `${externalLocation}, check its namespace and folder` + }); + return; + } + + resolvedIndices.add(index); + const location = externalLocation || customLocation(settings, index); + const vanilla = location.startsWith('minecraft:'); + if (!externalLocation || (includeExternal && !vanilla)) { + packed.push({path: packPath(location), bytes: descriptor.bytes}); + } else if (!vanilla) { + referencedOnly.push(location); + } + + if (index === 0) { + texture = location; + } else { + textures[index] = location; + } + }); + + if (!texture && resolvedIndices.size > 0) { + issues.push({ + code: 'MISSING_TEXTURE_INDEX', + message: 'Texture slot 0 is missing, the model has no primary texture' + }); + } + + return {texture, textures, packed, referencedOnly, issues}; +} + +module.exports = { + resolveTextures, + parseExternalLocation, + describeTextureSource +}; diff --git a/plugins/easy_model_entities/src/utils/hash.js b/plugins/easy_model_entities/src/utils/hash.js new file mode 100644 index 00000000..a3e4d48d --- /dev/null +++ b/plugins/easy_model_entities/src/utils/hash.js @@ -0,0 +1,31 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +function hashString(value) { + let hash = 0x811c9dc5; + const text = String(value); + for (let i = 0; i < text.length; i++) { + hash ^= text.codePointAt(i); + hash = Math.imul(hash, 0x01000193); + } + + return (hash >>> 0).toString(16).padStart(8, '0'); +} + +module.exports = {hashString}; diff --git a/plugins/easy_model_entities/src/webpack.config.js b/plugins/easy_model_entities/src/webpack.config.js new file mode 100644 index 00000000..e3983758 --- /dev/null +++ b/plugins/easy_model_entities/src/webpack.config.js @@ -0,0 +1,48 @@ +/* + * Copyright 2026 Markus Bordihn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +const path = require('path'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); + +module.exports = { + entry: './index.js', + output: { + filename: 'easy_model_entities.js', + path: path.resolve(__dirname, '..') + }, + target: 'node', + module: { + rules: [ + {test: /\.svg$/, type: 'asset/source'}, + {test: /\.md$/, type: 'asset/source'} + ] + }, + plugins: [ + new CopyWebpackPlugin({ + patterns: [ + {from: './LICENSE.MD', to: './LICENSE.MD', noErrorOnMissing: true}, + {from: './resources/icon.png', to: './icon.png', noErrorOnMissing: true} + ] + }) + ], + mode: 'production', + optimization: { + minimize: false + } +};