From 3500327af5cb771ee6e9e72f8142d67f1d437381 Mon Sep 17 00:00:00 2001 From: SnaveSutit Date: Sun, 21 Apr 2024 19:32:18 -0400 Subject: [PATCH] Resource Pack Exporter - Improved dev debugging by adding `sourceRoot: true` to esbuild `buildDev()` options. - Removed unused `CustomModelData` class. - Fixed some weird incorrect `.png` extensions. - Fixed `rigRenderer.ts` failing to export any textures on variant models. - Completed the first mostly-functional build of the resource pack compiling systems. - Made `exportRig()` crash-safe. - Made `exportRig()` actually respect the "export data pack" and "export resource pack" blueprint settings. - Removed left-over `scoreboards.ts` file. - Made some small fixes to the data pack compiler. - Removed redundant try catch statement from data pack compiler. - Added `saveBlueprint()` function to `blueprintFormat.ts` - Renamed internal variables for `root_entity_summon_commands` to `custom_summon_commands` to match the user-facing naming. - Added some proper default values to some blueprint settings. - Added `Project.last_used_export_namespace` to keep track of previous exports, and clean up old files a little better. - --- src/blockbenchTypeMods.d.ts | 3 +- src/blueprintFormat.ts | 12 + src/blueprintSettings.ts | 6 +- src/components/blueprintSettingsDialog.svelte | 6 +- src/interface/blueprintSettingsDialog.ts | 4 +- src/lang/en.yml | 4 +- src/mods/saveProjectActionMod.ts | 4 +- src/systems/datapackCompiler.ts | 70 +++--- .../datapackCompiler/animated_java.mcb | 63 ++--- src/systems/datapackCompiler/scoreboards.ts | 27 --- src/systems/exporter.ts | 99 +++++--- src/systems/resourcepackCompiler.ts | 217 +++++++++++++++++- src/systems/rigRenderer.ts | 45 ++-- src/systems/util.ts | 7 + src/util/minecraftUtil.ts | 7 +- tools/esbuild.ts | 1 + 16 files changed, 400 insertions(+), 175 deletions(-) delete mode 100644 src/systems/datapackCompiler/scoreboards.ts diff --git a/src/blockbenchTypeMods.d.ts b/src/blockbenchTypeMods.d.ts index c937d7c4..f526024b 100644 --- a/src/blockbenchTypeMods.d.ts +++ b/src/blockbenchTypeMods.d.ts @@ -23,8 +23,9 @@ declare global { enable_data_pack: boolean enable_advanced_data_pack_settings: boolean data_pack: string - root_entity_summon_commands: string + custom_summon_commands: string } + last_used_export_namespace: string variants: Variant[] } diff --git a/src/blueprintFormat.ts b/src/blueprintFormat.ts index 9cc25af3..13753fa9 100644 --- a/src/blueprintFormat.ts +++ b/src/blueprintFormat.ts @@ -85,6 +85,7 @@ export interface IBlueprintFormatJSON { box_uv: boolean backup: boolean save_location: string + last_used_export_namespace: string } /** * The project settings of the Blueprint @@ -174,6 +175,9 @@ export const BLUEPRINT_CODEC = new Blockbench.Codec('animated_java_blueprint', { Project.animated_java = { ...Project.animated_java, ...model.project_settings } } + Project.last_used_export_namespace = + model.meta.last_used_export_namespace || Project.animated_java.export_namespace + if (model.textures) { for (const texture of model.textures) { const newTexture = new Texture(texture, texture.uuid).add(false) @@ -301,6 +305,7 @@ export const BLUEPRINT_CODEC = new Blockbench.Codec('animated_java_blueprint', { format_version: PACKAGE.version, uuid: Project.uuid, save_location: Project.save_path, + last_used_export_namespace: Project.last_used_export_namespace, }, project_settings: Project.animated_java, resolution: { @@ -446,6 +451,7 @@ export const BLUEPRINT_FORMAT = new Blockbench.ModelFormat({ } Project.variants ??= [] + Project.last_used_export_namespace = Project.animated_java.export_namespace if (newModel) { new Variant('Default', true) } @@ -508,3 +514,9 @@ BLUEPRINT_CODEC.format = BLUEPRINT_FORMAT export function isCurrentFormat() { return Format.id === BLUEPRINT_FORMAT.id } + +export function saveBlueprint() { + if (!Project || !Format) return + if (Format !== BLUEPRINT_FORMAT) return + BLUEPRINT_CODEC.write(BLUEPRINT_CODEC.compile(), Project.save_path) +} diff --git a/src/blueprintSettings.ts b/src/blueprintSettings.ts index 2b111df5..e2164907 100644 --- a/src/blueprintSettings.ts +++ b/src/blueprintSettings.ts @@ -1,10 +1,10 @@ export const defaultValues: ModelProject['animated_java'] = { - export_namespace: '', + export_namespace: 'blueprint', // Plugin Settings enable_plugin_mode: false, // Resource Pack Settings enable_resource_pack: true, - display_item: '', + display_item: 'minecraft:white_dye', customModelDataOffset: 0, enable_advanced_resource_pack_settings: false, resource_pack: '', @@ -15,5 +15,5 @@ export const defaultValues: ModelProject['animated_java'] = { enable_data_pack: true, enable_advanced_data_pack_settings: false, data_pack: '', - root_entity_summon_commands: '', + custom_summon_commands: '', } diff --git a/src/components/blueprintSettingsDialog.svelte b/src/components/blueprintSettingsDialog.svelte index 8db94115..0efe15f5 100644 --- a/src/components/blueprintSettingsDialog.svelte +++ b/src/components/blueprintSettingsDialog.svelte @@ -381,10 +381,8 @@ /> {/if} {/if} diff --git a/src/interface/blueprintSettingsDialog.ts b/src/interface/blueprintSettingsDialog.ts index 739801bd..3b2aceb4 100644 --- a/src/interface/blueprintSettingsDialog.ts +++ b/src/interface/blueprintSettingsDialog.ts @@ -67,7 +67,7 @@ function getSettings(): Record> { Project!.animated_java.enable_advanced_data_pack_settings ), dataPack: new Valuable(Project!.animated_java.data_pack), - rootEntitySummonCommands: new Valuable(Project!.animated_java.root_entity_summon_commands), + rootEntitySummonCommands: new Valuable(Project!.animated_java.custom_summon_commands), } } @@ -94,7 +94,7 @@ function setSettings(settings: any) { Project.animated_java.enable_advanced_data_pack_settings = settings.enableAdvancedDataPackSettings.get() Project.animated_java.data_pack = settings.dataPack.get() - Project.animated_java.root_entity_summon_commands = settings.rootEntitySummonCommands.get() + Project.animated_java.custom_summon_commands = settings.rootEntitySummonCommands.get() console.log('Successfully saved project settings', Project) } diff --git a/src/lang/en.yml b/src/lang/en.yml index c7e6a418..b53d45f1 100644 --- a/src/lang/en.yml +++ b/src/lang/en.yml @@ -97,8 +97,8 @@ animated_java.dialog.blueprint_settings.data_pack.error.not_a_folder: The select animated_java.dialog.blueprint_settings.data_pack.error.missing_pack_mcmeta: The selected folder is missing a pack.mcmeta file! animated_java.dialog.blueprint_settings.data_pack.error.missing_data_folder: The selected Data Pack is missing a data folder! -animated_java.dialog.blueprint_settings.root_entity_summon_commands.title: Custom On-Summon Commands -animated_java.dialog.blueprint_settings.root_entity_summon_commands.description: Commands to run as the root entity when summoned. Treat this text input as a normal mcfunction. +animated_java.dialog.blueprint_settings.custom_summon_commands.title: Custom On-Summon Commands +animated_java.dialog.blueprint_settings.custom_summon_commands.description: Commands to run as the root entity when summoned. Treat this text input as a normal mcfunction. ## Bone Config Dialog animated_java.dialog.bone_config.title: Bone Config diff --git a/src/mods/saveProjectActionMod.ts b/src/mods/saveProjectActionMod.ts index 297d232a..016ae13a 100644 --- a/src/mods/saveProjectActionMod.ts +++ b/src/mods/saveProjectActionMod.ts @@ -1,4 +1,4 @@ -import { BLUEPRINT_CODEC, BLUEPRINT_FORMAT } from '../blueprintFormat' +import { BLUEPRINT_FORMAT, saveBlueprint } from '../blueprintFormat' import { PACKAGE } from '../constants' import { createBlockbenchMod } from '../util/moddingTools' @@ -12,7 +12,7 @@ createBlockbenchMod( context.action.click = (event: Event) => { if (!Project || !Format) return if (Format === BLUEPRINT_FORMAT) { - BLUEPRINT_CODEC.write(BLUEPRINT_CODEC.compile(), Project.save_path) + saveBlueprint() } else { context.originalClick.call(context.action, event) } diff --git a/src/systems/datapackCompiler.ts b/src/systems/datapackCompiler.ts index 59f5b164..802b04e9 100644 --- a/src/systems/datapackCompiler.ts +++ b/src/systems/datapackCompiler.ts @@ -2,7 +2,6 @@ import { Compiler, Parser, Tokenizer, SyncIo } from 'mc-build' import { VariableMap } from 'mc-build/dist/mcl/Compiler' import { isFunctionTagPath } from '../util/fileUtil' import datapackTemplate from './datapackCompiler/animated_java.mcb' -import { openUnexpectedErrorDialog } from '../interface/unexpectedErrorDialog' import { IRenderedNodes, IRenderedRig } from './rigRenderer' import { IRenderedAnimation } from './animationRenderer' import { Variant } from '../variants' @@ -183,41 +182,40 @@ function createCustomSyncIO(): SyncIo { return io } -export function compileDataPack(rig: IRenderedRig, animations: IRenderedAnimation[]) { - try { - console.log('Compiling Data Pack...') - const compiler = new Compiler('src/', { - libDir: null, - generatedDirName: 'zzz', - internalScoreboardName: 'aj.i', - eqVarScoreboardName: null, - eqConstScoreboardName: null, - header: '# This file was generated by Animated Java via MC-Build. It is not recommended to edit this file directly.', - ioThreadCount: null, - setup: null, - }) - compiler.io = createCustomSyncIO() - compiler.disableRequire = true - - const variables = { - export_namespace: Project!.animated_java.export_namespace, - rig, - animations, - variants: Variant.all, - export_version: Math.random().toString().substring(2, 12), - root_entity_passengers: generateRootEntityPassengers(rig), - TAGS, - } +export function compileDataPack(options: { rig: IRenderedRig; animations: IRenderedAnimation[] }) { + const { rig, animations } = options + console.log('Compiling Data Pack...') + const compiler = new Compiler('src/', { + libDir: null, + generatedDirName: 'zzz', + internalScoreboardName: 'aj.i', + eqVarScoreboardName: null, + eqConstScoreboardName: null, + header: '# This file was generated by Animated Java via MC-Build. It is not recommended to edit this file directly.', + ioThreadCount: null, + setup: null, + }) + compiler.io = createCustomSyncIO() + compiler.disableRequire = true - console.time('Data Pack Compilation') - compiler.addFile( - 'src/animated_java.mcb', - Parser.parseMcbFile(Tokenizer.tokenize(datapackTemplate, 'src/animated_java.mcb')) - ) - compiler.compile(VariableMap.fromObject(variables)) - console.timeEnd('Data Pack Compilation') - } catch (e: any) { - openUnexpectedErrorDialog(e as Error) - console.error(e) + const aj = Project!.animated_java + + const variables = { + export_namespace: aj.export_namespace, + rig, + animations, + variants: Variant.all, + export_version: Math.random().toString().substring(2, 10), + root_entity_passengers: generateRootEntityPassengers(rig), + TAGS, + custom_summon_commands: aj.custom_summon_commands, } + + console.time('Data Pack Compilation') + compiler.addFile( + 'src/animated_java.mcb', + Parser.parseMcbFile(Tokenizer.tokenize(datapackTemplate, 'src/animated_java.mcb')) + ) + compiler.compile(VariableMap.fromObject(variables)) + console.timeEnd('Data Pack Compilation') } diff --git a/src/systems/datapackCompiler/animated_java.mcb b/src/systems/datapackCompiler/animated_java.mcb index 85e24708..f0dc89a8 100644 --- a/src/systems/datapackCompiler/animated_java.mcb +++ b/src/systems/datapackCompiler/animated_java.mcb @@ -7,8 +7,8 @@ dir global { scoreboard objectives add aj.frame dummy scoreboard objectives add aj.rig_is_loaded dummy - scoreboard players set aj.true aj.i 1 - scoreboard players set aj.false aj.i 0 + scoreboard players set aj.1b aj.i 1 + scoreboard players set aj.0b aj.i 0 scoreboard players add aj.last_id aj.id 0 scoreboard players reset * aj.rig_is_loaded @@ -54,7 +54,19 @@ dir global { dir errors { function function_not_executed_as_root_entity { # Make this text fancier in the actual implementation. - $tellraw @a 'The function $(function_path) must be executed as the root entity of the rig.' + $tellraw @a "The function $(function_path) must be executed as the root entity of the rig." + } + } + + dir remove { + # Removes all instances of all rigs from the world. + function everything { + # Args: {confirm: boolean} + $execute if score aj.$(confirm) aj.i matches 0 run tellraw @a "Are you sure you want to remove all Animated Java rigs from the world? Run this command again with {confirm:true} to confirm." + $execute if score aj.$(confirm) aj.i matches 1 run { + kill @e[type=item_display,tag=aj.rig_entity] + tellraw @a "All Animated Java rigs have been removed from the world." + } } } } @@ -143,54 +155,54 @@ dir <%export_namespace%> { function play { execute unless entity @s[type=item_display,tag=aj.root_entity] run return run \ function *global/errors/function_not_executed_as_root_entity \ - with {'function_path': 'animated_java:<%export_namespace%>/animations/<%animation.name%>/play'} + {'function_path': 'animated_java:<%export_namespace%>/animations/<%animation.name%>/play'} tag @s add aj.<%export_namespace%>.animation.<%animation.name%>.playing } function stop { execute unless entity @s[type=item_display,tag=aj.root_entity] run return run \ function *global/errors/function_not_executed_as_root_entity \ - with {'function_path': 'animated_java:<%export_namespace%>/animations/<%animation.name%>/stop'} + {'function_path': 'animated_java:<%export_namespace%>/animations/<%animation.name%>/stop'} tag @s remove aj.<%export_namespace%>.animation.<%animation.name%>.playing } function pause { execute unless entity @s[type=item_display,tag=aj.root_entity] run return run \ function *global/errors/function_not_executed_as_root_entity \ - with {'function_path': 'animated_java:<%export_namespace%>/animations/<%animation.name%>/pause'} + {'function_path': 'animated_java:<%export_namespace%>/animations/<%animation.name%>/pause'} tag @s remove aj.<%export_namespace%>.animation.<%animation.name%>.playing } function resume { execute unless entity @s[type=item_display,tag=aj.root_entity] run return run \ function *global/errors/function_not_executed_as_root_entity \ - with {'function_path': 'animated_java:<%export_namespace%>/animations/<%animation.name%>/resume'} + {'function_path': 'animated_java:<%export_namespace%>/animations/<%animation.name%>/resume'} tag @s add aj.<%export_namespace%>.animation.<%animation.name%>.playing } function reset { execute unless entity @s[type=item_display,tag=aj.root_entity] run return run \ function *global/errors/function_not_executed_as_root_entity \ - with {'function_path': 'animated_java:<%export_namespace%>/animations/<%animation.name%>/reset'} + {'function_path': 'animated_java:<%export_namespace%>/animations/<%animation.name%>/reset'} tag @s remove aj.<%export_namespace%>.animation.<%animation.name%>.playing } function next_frame { execute unless entity @s[type=item_display,tag=aj.root_entity] run return run \ function *global/errors/function_not_executed_as_root_entity \ - with {'function_path': 'animated_java:<%export_namespace%>/animations/<%animation.name%>/next_frame'} + {'function_path': 'animated_java:<%export_namespace%>/animations/<%animation.name%>/next_frame'} } function apply_frame { # ARGS: {frame: int} execute unless entity @s[type=item_display,tag=aj.root_entity] run return run \ function *global/errors/function_not_executed_as_root_entity \ - with {'function_path': 'animated_java:<%export_namespace%>/animations/<%animation.name%>/apply_frame'} + {'function_path': 'animated_java:<%export_namespace%>/animations/<%animation.name%>/apply_frame'} } function tween_play { execute unless entity @s[type=item_display,tag=aj.root_entity] run return run \ function *global/errors/function_not_executed_as_root_entity \ - with {'function_path': 'animated_java:<%export_namespace%>/animations/<%animation.name%>/tween_play'} + {'function_path': 'animated_java:<%export_namespace%>/animations/<%animation.name%>/tween_play'} tag @s add aj.<%export_namespace%>.animation.<%animation.name%>.tween_playing } function tween_resume { execute unless entity @s[type=item_display,tag=aj.root_entity] run return run \ function *global/errors/function_not_executed_as_root_entity \ - with {'function_path': 'animated_java:<%export_namespace%>/animations/<%animation.name%>/tween_resume'} + {'function_path': 'animated_java:<%export_namespace%>/animations/<%animation.name%>/tween_resume'} tag @s add aj.<%export_namespace%>.animation.<%animation.name%>.tween_playing } dir zzz { @@ -215,26 +227,26 @@ dir <%export_namespace%> { execute store result score @s aj.id run scoreboard players add aj.last_id aj.id 1 tag @s remove aj.new - data set value aj:temp args set value {variant:'', animation:'', frame: 0} + data modify storage aj:temp args set value {variant:'', animation:'', frame: 0} $execute store success score #success aj.i run data modify storage aj:temp args set value $(args) # Variant Arguement execute if data storage aj:temp args.variant run { - execute if data storage aj:temp {args:{variant:''}} run return run tellraw @a 'The variant argument cannot be an empty string.' + execute if data storage aj:temp {args:{variant:''}} run return run tellraw @a "The variant argument cannot be an empty string." # Attempt to apply the variant, if it fails, print an error. execute store success score #success aj.i run { with storage aj:temp args $execute store success score #success aj.i run function *<%export_namespace%>/variants/$(variant)/apply execute if score #success aj.i matches 1 run return 1 return fail } - $execute unless score #success aj.i matches 1 run return run tellraw @a 'The variant $(variant) does not exist.' + $execute unless score #success aj.i matches 1 run return run tellraw @a "The variant $(variant) does not exist." } # Animation, Frame, and Start Animation Arguments execute if data storage aj:temp args.animation run { - execute if data storage aj:temp {args:{animation:''}} run return run tellraw @a 'The animation argument cannot be an empty string.' + execute if data storage aj:temp {args:{animation:''}} run return run tellraw @a "The animation argument cannot be an empty string." execute store result score #frame aj.i run data get storage aj:temp args.frame - execute if score #frame aj.i matches ..-1 run return run tellraw @a 'The frame argument must be a non-negative integer.' + execute if score #frame aj.i matches ..-1 run return run tellraw @a "The frame argument must be a non-negative integer." execute store result storage aj:temp args.frame int 1 run scoreboard players get #frame aj.i # Attempt to apply the animation frame, if it fails, print an error. execute store success score #success aj.i run { with storage aj:temp args @@ -242,11 +254,15 @@ dir <%export_namespace%> { execute if score #success aj.i matches 1 run return 1 return fail } - $execute unless score #success aj.i matches 1 run return run tellraw @a 'The animation $(animation) does not exist.' + $execute unless score #success aj.i matches 1 run return run tellraw @a "The animation $(animation) does not exist." execute if data storage aj:temp {args:{start_animation: true}} run { with storage aj:temp args $function *<%export_namespace%>/animations/$(animation)/resume } } + # Custom Summon Commands + <%custom_summon_commands%> + # Custom Summon Commands + # Run the on_summon function for the root entity. function #*<%export_namespace%>/as_root/on_summon } @@ -274,15 +290,6 @@ dir <%export_namespace%> { execute on passengers run kill @s kill @s } - # Removes all instances of all rigs from the world. - function everything { - # Args: {confirm: boolean} - $execute if score aj.$(confirm) aj.i matches 0 run tellraw @a 'Are you sure you want to remove all Animated Java rigs from the world? Run this command again with {confirm:true} to confirm.' - $execute if score aj.$(confirm) aj.i matches 1 run { - kill @e[type=item_display,tag=aj.rig_entity] - tellraw @a 'All Animated Java rigs have been removed from the world.' - } - } } dir variants { @@ -312,7 +319,7 @@ dir <%export_namespace%> { dir zzz { function check_if_rig_outdated { - execute unless score @s aj.<%export_version%> matches <%export_version%> run function #animated_java:<%export_namespace%>/zzz/update_outdated_rig + execute unless score @s aj.export_version matches <%export_version%> run function #animated_java:<%export_namespace%>/zzz/update_outdated_rig } function update_outdated_rig { diff --git a/src/systems/datapackCompiler/scoreboards.ts b/src/systems/datapackCompiler/scoreboards.ts deleted file mode 100644 index a2d318f9..00000000 --- a/src/systems/datapackCompiler/scoreboards.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { JsonText } from '../../util/jsonText' - -export class Scoreboard { - public static all: Scoreboard[] = [] - constructor(public name: string, public displayName?: JsonText, public type = 'dummy') {} - - public toString() { - return this.name - } - - public toInitializer() { - return `scoreboard objectives add ${this.name} ${this.type}${ - this.displayName ? ' ' + this.displayName.toString() : '' - }` - } - - public static get(name: string) { - return Scoreboard.all.find(scoreboard => scoreboard.name === name) - } -} - -export function generateScoreboard() { - new Scoreboard('aj.i') - new Scoreboard('aj.id') - new Scoreboard('aj.frame') - new Scoreboard('aj.rig_is_loaded') -} diff --git a/src/systems/exporter.ts b/src/systems/exporter.ts index 4cd527da..36eac0d3 100644 --- a/src/systems/exporter.ts +++ b/src/systems/exporter.ts @@ -1,44 +1,71 @@ +import { saveBlueprint } from '../blueprintFormat' +import { openUnexpectedErrorDialog } from '../interface/unexpectedErrorDialog' import { renderProjectAnimations } from './animationRenderer' import { compileDataPack } from './datapackCompiler' +import { compileResourcePack } from './resourcepackCompiler' import { renderRig } from './rigRenderer' export function exportProject() { - if (!Project) return // TODO: Handle this error better - const aj = Project.animated_java - - let resourcePackFolder: string, - dataPackFolder: string, - textureExportFolder: string, - modelExportFolder: string, - displayItemPath: string - - resourcePackFolder = aj.resource_pack - dataPackFolder = aj.data_pack - - if (aj.enable_advanced_resource_pack_settings) { - modelExportFolder = aj.model_folder - textureExportFolder = aj.texture_folder - displayItemPath = aj.display_item_path - } else { - modelExportFolder = PathModule.join( - resourcePackFolder, - 'assets/animated_java/models/', - aj.export_namespace - ) - textureExportFolder = PathModule.join( - resourcePackFolder, - 'assets/animated_java/textures/', - aj.export_namespace - ) - displayItemPath = PathModule.join( - resourcePackFolder, - 'assets/minecraft/models/item/', - aj.display_item + '.json' - ) - } + try { + if (!Project) return // TODO: Handle this error better + const aj = Project.animated_java + + let resourcePackFolder: string, + dataPackFolder: string, + textureExportFolder: string, + modelExportFolder: string, + displayItemPath: string + + resourcePackFolder = aj.resource_pack + dataPackFolder = aj.data_pack + + if (aj.enable_advanced_resource_pack_settings) { + modelExportFolder = aj.model_folder + textureExportFolder = aj.texture_folder + displayItemPath = aj.display_item_path + } else { + modelExportFolder = PathModule.join( + resourcePackFolder, + 'assets/animated_java/models/', + aj.export_namespace + ) + textureExportFolder = PathModule.join( + resourcePackFolder, + 'assets/animated_java/textures/', + aj.export_namespace + ) + displayItemPath = PathModule.join( + resourcePackFolder, + 'assets/minecraft/models/item/', + aj.display_item.split(':').at(-1)! + '.json' + ) + } - const rig = renderRig(modelExportFolder, textureExportFolder) - const animations = renderProjectAnimations(Project, rig) + const rig = renderRig(modelExportFolder, textureExportFolder) + const animations = renderProjectAnimations(Project, rig) - compileDataPack(rig, animations) + if (aj.enable_resource_pack) { + compileResourcePack({ + rig, + animations, + displayItemPath, + resourcePackFolder, + textureExportFolder, + modelExportFolder, + dataPackFolder, + }) + } + + if (aj.enable_data_pack) { + compileDataPack({ rig, animations }) + } + + Project.last_used_export_namespace = aj.export_namespace + + saveBlueprint() + Blockbench.showQuickMessage('Project exported successfully!', 2000) + } catch (e: any) { + console.error(e) + openUnexpectedErrorDialog(e as Error) + } } diff --git a/src/systems/resourcepackCompiler.ts b/src/systems/resourcepackCompiler.ts index 7b36480d..35e57cd5 100644 --- a/src/systems/resourcepackCompiler.ts +++ b/src/systems/resourcepackCompiler.ts @@ -1,3 +1,216 @@ -export function compileResourcePack() { - // TODO Compile resource pack +import { isResourcePackPath, toSafeFuntionName } from '../util/minecraftUtil' +import { IRenderedAnimation } from './animationRenderer' +import { IRenderedNodes, IRenderedRig } from './rigRenderer' +import { replacePathPart } from './util' + +interface IPredicateItemModel { + parent: string + textures: any + overrides: Array<{ + predicate: { custom_model_data: number } + model: string + }> + animated_java: { + rigs: Record + } +} + +class PredicateItemModel { + private lastOverrideId = 1 + private overrides = new Map() + public usedIds = new Set() + public rigs: Record = {} + + // constructor() {} + + setOverride(id: number, model: string) { + if (!this.usedIds.has(id)) this.usedIds.add(id) + this.overrides.set(id, model) + } + + addOverride(model: string) { + let id = this.lastOverrideId + while (this.overrides.get(id) !== undefined) id++ + this.lastOverrideId = id + this.usedIds.add(id) + this.overrides.set(id, model) + return id + } + + assertOverride(id: number, model: string) { + if (this.overrides.get(id) === undefined) this.setOverride(id, model) + } + + readExisting(path: string) { + let file: IPredicateItemModel + try { + file = JSON.parse(fs.readFileSync(path, 'utf-8')) + } catch (e) { + console.error('Failed to read existing display item model:', e) + return + } + + if (typeof file.animated_java !== 'object') { + // TODO Inform the user that they are attempting to merge into a non-animated_java model. And give them the option to cancel. + } + + // Assert important fields + file.overrides ??= [] + file.animated_java ??= { rigs: {} } + file.animated_java.rigs ??= {} + + for (const [name, rig] of Object.entries(file.animated_java.rigs)) { + const namespace = Project!.animated_java.export_namespace + const lastNamespace = Project!.last_used_export_namespace + if (name === namespace || name === lastNamespace) { + file.overrides = file.overrides.filter( + override => !rig.used_ids.includes(override.predicate.custom_model_data) + ) + if (name === lastNamespace && namespace !== lastNamespace) + delete file.animated_java.rigs[lastNamespace] + continue + } + + rig.used_ids.forEach(id => this.usedIds.add(id)) + this.rigs[name] = rig + } + } + + toJSON(): IPredicateItemModel { + const [displayItemNamespace, displayItemName] = + Project!.animated_java.display_item.split(':') + const exportNamespace = Project!.animated_java.export_namespace + return { + parent: 'item/generated', + textures: { + layer0: `${displayItemNamespace}:item/${displayItemName}`, + }, + overrides: [...this.overrides.entries()].map(([id, model]) => ({ + predicate: { custom_model_data: id }, + model, + })), + animated_java: { + rigs: { + ...this.rigs, + [exportNamespace]: { + used_ids: [...this.usedIds.values()], + }, + }, + }, + } + } +} + +export function compileResourcePack(options: { + rig: IRenderedRig + animations: IRenderedAnimation[] + displayItemPath: string + resourcePackFolder: string + textureExportFolder: string + modelExportFolder: string + dataPackFolder: string +}) { + const { + rig, + // animations, + displayItemPath, + resourcePackFolder, + textureExportFolder, + modelExportFolder, + // dataPackFolder, + } = options + const aj = Project!.animated_java + const lastUsedExportNamespace = Project!.last_used_export_namespace + console.log('Compiling resource pack...', options) + + // Internal Models + fs.mkdirSync(PathModule.join(resourcePackFolder, 'assets/animated_java/models/'), { + recursive: true, + }) + fs.writeFileSync( + PathModule.join(resourcePackFolder, 'assets/animated_java/models/empty.json'), + '{}' + ) + + // Display Item + const displayItemModel = new PredicateItemModel() + if (fs.existsSync(displayItemPath)) { + console.warn('Display item already exists! Attempting to merge...') + displayItemModel.readExisting(displayItemPath) + } + + // Empty model for hiding bones / snowballs + displayItemModel.assertOverride(1, 'animated_java:item/empty') + + // Models + fs.rmSync(replacePathPart(modelExportFolder, aj.export_namespace, lastUsedExportNamespace), { + recursive: true, + force: true, + }) + fs.mkdirSync(modelExportFolder, { recursive: true }) + for (const [boneUuid, model] of Object.entries(rig.models)) { + const bone = rig.nodeMap[boneUuid] as IRenderedNodes['Bone'] + bone.customModelData = displayItemModel.addOverride(bone.resourceLocation) + fs.writeFileSync( + PathModule.join(modelExportFolder, bone.name + '.json'), + JSON.stringify(model) + ) + } + + // Textures + fs.rmSync(replacePathPart(textureExportFolder, aj.export_namespace, lastUsedExportNamespace), { + recursive: true, + force: true, + }) + fs.mkdirSync(textureExportFolder, { recursive: true }) + for (const texture of Object.values(rig.textures)) { + let image: Buffer | undefined + let mcmeta: Buffer | undefined + let optifineEmissive: Buffer | undefined + if (texture.source?.startsWith('data:')) { + image = Buffer.from(texture.source.split(',')[1], 'base64') + } else if (texture.path && fs.existsSync(texture.path)) { + if (!isResourcePackPath(texture.path)) { + image = fs.readFileSync(texture.path) + const mcmetaPath = texture.path + '.mcmeta' + const emissivePath = texture.path.replace('.png', '_e.png') + if (fs.existsSync(mcmetaPath)) mcmeta = fs.readFileSync(mcmetaPath) + if (fs.existsSync(emissivePath)) optifineEmissive = fs.readFileSync(emissivePath) + } + } else { + throw new Error(`Texture ${texture.name} has no image!`) + } + + const textureName = toSafeFuntionName(texture.name) + fs.writeFileSync(PathModule.join(textureExportFolder, textureName), image!) + if (mcmeta !== undefined) + fs.writeFileSync(PathModule.join(textureExportFolder, textureName + '.mcmeta'), mcmeta) + if (optifineEmissive !== undefined) + fs.writeFileSync( + PathModule.join(textureExportFolder, textureName + '_e.png'), + optifineEmissive + ) + } + + // Variant Models + for (const [variantName, models] of Object.entries(rig.variantModels)) { + fs.mkdirSync(PathModule.join(modelExportFolder, variantName), { recursive: true }) + for (const [boneUuid, variantModel] of Object.entries(models)) { + const bone = rig.nodeMap[boneUuid] as IRenderedNodes['Bone'] + variantModel.customModelData = displayItemModel.addOverride( + variantModel.resourceLocation + ) + fs.writeFileSync( + PathModule.join(modelExportFolder, variantName, bone.name + '.json'), + JSON.stringify(variantModel.model) + ) + } + } + + // Write display item model + console.log('Display Item Model', displayItemModel.toJSON()) + fs.mkdirSync(PathModule.dirname(displayItemPath), { recursive: true }) + fs.writeFileSync(displayItemPath, JSON.stringify(displayItemModel.toJSON())) + + console.log('Resource pack compiled!') } diff --git a/src/systems/rigRenderer.ts b/src/systems/rigRenderer.ts index 8cd1ab2d..3ebdbaa5 100644 --- a/src/systems/rigRenderer.ts +++ b/src/systems/rigRenderer.ts @@ -154,23 +154,6 @@ export interface IRenderedRig { textureExportFolder: string } -export class CustomModelData { - private static current = 0 - public static usedIds: number[] = [] - - public static get() { - let id = this.current - while (this.usedIds.includes(id)) id++ - this.current = id + 1 - this.usedIds.push(id) - return id - } - - public static set(value: number) { - this.current = value - } -} - // function countNodesRecursive(nodes: OutlinerNode[] = Outliner.root): number { // let count = 0 // for (const node of nodes) { @@ -251,8 +234,6 @@ function renderCube(cube: Cube, rig: IRenderedRig, model: IRenderedModel) { } if (Object.keys(element.faces).length === 0) return - // progress.add(1) - // progress.update() return element } @@ -261,7 +242,7 @@ export function getTextureResourceLocation(texture: Texture, rig: IRenderedRig) const parsed = parseResourcePackPath(texture.path) if (parsed) return parsed } - const path = PathModule.join(rig.textureExportFolder, toSafeFuntionName(texture.name) + '.png') + const path = PathModule.join(rig.textureExportFolder, toSafeFuntionName(texture.name)) const parsed = parseResourcePackPath(path) if (parsed) return parsed @@ -355,7 +336,6 @@ function renderGroup(group: Group, rig: IRenderedRig) { } else { console.warn(`Encountered unknown node type:`, node) } - // progress.add(1) } // Don't export groups without a model. @@ -374,8 +354,8 @@ function renderGroup(group: Group, rig: IRenderedRig) { element.rotation.origin = element.rotation.origin.map(v => v * scale + 8) } } - renderedBone.scale = 1 / scale + renderedBone.scale = 1 / scale rig.models[group.uuid] = renderedBone.model rig.nodeMap[group.uuid] = renderedBone return structure @@ -413,7 +393,6 @@ function renderCamera(camera: ICamera, rig: IRenderedRig): INodeStructure { } rig.nodeMap[camera.uuid] = renderedCamera - // progress.add(1) return { uuid: camera.uuid, children: [], @@ -423,21 +402,23 @@ function renderCamera(camera: ICamera, rig: IRenderedRig): INodeStructure { function renderVariantModels(variant: Variant, rig: IRenderedRig) { const bones: Record = {} + // TODO Remove elements if they are entirely transparent. + for (const [uuid, bone] of Object.entries(rig.nodeMap)) { if (bone.type !== 'bone') continue const textures: IRenderedModel['textures'] = {} - for (const [fromUUID, toUUID] of Object.entries(variant.textureMap.map) as Array< - [string, string] - >) { + + for (const [fromUUID, toUUID] of variant.textureMap.map.entries()) { if (!(fromUUID && toUUID)) throw new Error( `Invalid texture mapping found while exporting variant models. If you're seeing this error something has gone horribly wrong.` ) - const fromTexture = rig.textures[fromUUID] - if (!fromTexture) throw new Error(`Texture not found: ${fromUUID}`) - const toTexture = rig.textures[toUUID] - if (!toTexture) throw new Error(`Texture not found: ${toUUID}`) + const fromTexture = Texture.all.find(t => t.uuid === fromUUID) + if (!fromTexture) throw new Error(`From texture not found: ${fromUUID}`) + const toTexture = Texture.all.find(t => t.uuid === toUUID) + if (!toTexture) throw new Error(`To texture not found: ${toUUID}`) textures[fromTexture.id] = getTextureResourceLocation(toTexture, rig).resourceLocation + rig.textures[toTexture.id] = toTexture } const parsed = PathModule.parse(bone.modelPath) @@ -445,6 +426,9 @@ function renderVariantModels(variant: Variant, rig: IRenderedRig) { const parsedModelPath = parseResourcePackPath(modelPath) if (!parsedModelPath) throw new Error(`Invalid variant model path: ${modelPath}`) + // Don't export models without any texture changes + if (Object.keys(textures).length === 0) continue + bones[uuid] = { model: { parent: bone.resourceLocation, @@ -468,7 +452,6 @@ function getDefaultPose(rig: IRenderedRig) { } export function renderRig(modelExportFolder: string, textureExportFolder: string): IRenderedRig { - CustomModelData.set(1) Texture.all.forEach((t, i) => (t.id = String(i))) Animator.showDefaultPose() diff --git a/src/systems/util.ts b/src/systems/util.ts index 42902f54..5bc72a83 100644 --- a/src/systems/util.ts +++ b/src/systems/util.ts @@ -8,3 +8,10 @@ export function matrixToNbtFloatArray(matrix: THREE.Matrix4) { const matrixArray = new THREE.Matrix4().copy(matrix).transpose().toArray() return arrayToNbtFloatArray(matrixArray) } + +export function replacePathPart(path: string, oldPart: string, newPart: string) { + return path + .split(PathModule.sep) + .map(v => (v === oldPart ? newPart : v)) + .join(PathModule.sep) +} diff --git a/src/util/minecraftUtil.ts b/src/util/minecraftUtil.ts index 14dbecff..ab5a612a 100644 --- a/src/util/minecraftUtil.ts +++ b/src/util/minecraftUtil.ts @@ -7,6 +7,11 @@ export function toSafeFuntionName(name: string): string { .replace(/_+/g, '_') } +export function isResourcePackPath(path: string) { + const parsed = parseResourcePackPath(path) + return parsed && parsed.namespace && parsed.resourcePath +} + export function parseResourcePackPath(path: string) { path = path.replaceAll(/\\/g, '/') const parts = path.split('/') @@ -19,7 +24,7 @@ export function parseResourcePackPath(path: string) { const resourcePath = parts.slice(assetsIndex + 3).join('/') const fileName = pathjs.basename(path).split('.')[0] if (fileName !== fileName.toLowerCase()) return undefined - const resourceLocation = namespace + ':' + resourcePath + '/' + fileName + const resourceLocation = namespace + ':' + resourcePath return { resourcePackRoot, diff --git a/tools/esbuild.ts b/tools/esbuild.ts index 5d02b93d..000ad043 100644 --- a/tools/esbuild.ts +++ b/tools/esbuild.ts @@ -153,6 +153,7 @@ async function buildDev() { minify: false, platform: 'node', sourcemap: 'inline', + sourceRoot: 'http://animated-java/', loader: { '.svg': 'dataurl', '.ttf': 'binary', '.mcb': 'text' }, plugins: [ inlineImage({