diff --git a/build.gradle b/build.gradle index 737dcf8..5185de6 100644 --- a/build.gradle +++ b/build.gradle @@ -1,10 +1,11 @@ plugins { id 'java' + id "com.gradleup.shadow" version "9.6.1" id "io.papermc.paperweight.userdev" version "${paperWeightUserDevVersion}" } group = 'fr.openmc.riftengine' -version = '0.6-SNAPSHOT' +version = '0.7-SNAPSHOT' repositories { mavenCentral() @@ -17,6 +18,8 @@ repositories { maven {url = "https://repo.papermc.io/repository/maven-public/" } // * ItemsAdder maven {url "https://maven.devs.beer/"} + // * Jitpack + maven { url = "https://jitpack.io" } } dependencies { @@ -28,6 +31,8 @@ dependencies { compileOnly "org.geysermc.geyser:api:${geyserApiVersion}" compileOnly "org.geysermc.floodgate:api:${floodgateApiVersion}" + implementation("com.github.minecraft-library:asset-renderer:master-SNAPSHOT") + compileOnly files("libs/OpenMC.jar") implementation "org.jetbrains:annotations:${jetbrainsAnnotationsVersion}" compileOnly "org.projectlombok:lombok:${lombokVersion}" @@ -59,12 +64,17 @@ tasks.jar { destinationDirectory.set(file('./builds/')) } +tasks.shadowJar { + destinationDirectory.set(file('./builds/')) + archiveFileName.set('RiftEngine.jar') +} + tasks.register('copyJarToServer', Copy) { - dependsOn tasks.jar + dependsOn tasks.shadowJar - from tasks.jar + from tasks.shadowJar if (localProperties.getProperty('minecraftServerDir')) { into file("${localProperties.getProperty('minecraftServerDir')}/plugins") } -} +} \ No newline at end of file diff --git a/libs/OpenMC.jar b/libs/OpenMC.jar index d5c2c2a..035cf13 100644 Binary files a/libs/OpenMC.jar and b/libs/OpenMC.jar differ diff --git a/src/main/java/fr/openmc/riftengine/core/RiftLog.java b/src/main/java/fr/openmc/riftengine/core/RiftLog.java index a0b9b16..236b178 100644 --- a/src/main/java/fr/openmc/riftengine/core/RiftLog.java +++ b/src/main/java/fr/openmc/riftengine/core/RiftLog.java @@ -22,12 +22,15 @@ public enum RiftLog { GLYPH_NOT_FOUND("Glyph {} introuvable", LogType.WARN), FLIPBOOK_NULL("Flipbook est null pour ce tooltip {}", LogType.WARN), NULL("{} est null", LogType.WARN), + ENTRY_EQUIPMENT_NULL("{} est null, item qui se fait passer pour une piece d'armure mais que la texture/model est géré autrement ", LogType.WARN), + + MODEL_PARENT("le model ({}) convertit est null, le model hérite surement de parent et doit etre géré autrement", LogType.WARN), PATH_CANT_REDUCED_TO_TEXTURES("resource {} ne peut pas etre réduite à un chemin de texture", LogType.WARN), ; public static void logBanner(String pluginVersion, String server, String javaVersion) { - OMCLogger.info("\u001B[1;35m██████╗ ██╗███████╗████████╗ \u001B[0;95mRIFT v{}\u001B[0m", pluginVersion); + OMCLogger.info("\u001B[1;35m██████╗ ██╗███████╗████████╗ \u001B[0;95mRiftEngine v{}\u001B[0m", pluginVersion); OMCLogger.info("\u001B[1;35m██╔══██╗██║██╔════╝╚══██╔══╝ \u001B[0;90mPaper {}\u001B[0m", server); OMCLogger.info("\u001B[1;95m██████╔╝██║█████╗ ██║ \u001B[0;90mJava {}\u001B[0m", javaVersion); OMCLogger.info("\u001B[1;95m██╔══██╗██║██╔══╝ ██║ \u001B[0m"); diff --git a/src/main/java/fr/openmc/riftengine/core/RiftPlugin.java b/src/main/java/fr/openmc/riftengine/core/RiftPlugin.java index cc369fe..c1f5b19 100644 --- a/src/main/java/fr/openmc/riftengine/core/RiftPlugin.java +++ b/src/main/java/fr/openmc/riftengine/core/RiftPlugin.java @@ -86,6 +86,10 @@ public void onDisable() { RiftLog.RIFT_DISABLE.send(); } + public Path getOutputDir() { + return this.getDataFolder().toPath().resolve("output"); + } + public void updateBedrockResourcePack() { try { Path generatedBedrockPack = converterManager.generateConvertedPack(); diff --git a/src/main/java/fr/openmc/riftengine/core/converter/ConverterManager.java b/src/main/java/fr/openmc/riftengine/core/converter/ConverterManager.java index 7e642f4..4d3ba79 100644 --- a/src/main/java/fr/openmc/riftengine/core/converter/ConverterManager.java +++ b/src/main/java/fr/openmc/riftengine/core/converter/ConverterManager.java @@ -1,6 +1,10 @@ package fr.openmc.riftengine.core.converter; import com.comphenix.protocol.ProtocolLibrary; +import dev.simplified.image.ImageData; +import dev.simplified.image.ImageFactory; +import dev.simplified.image.ImageFormat; +import dev.simplified.image.pixel.Background; import fr.openmc.core.OMCRegistry; import fr.openmc.core.bootstrap.integration.OMCLogger; import fr.openmc.core.utils.FilesUtils; @@ -20,6 +24,12 @@ import fr.openmc.riftengine.core.converter.writers.items.ItemsMappingWriter; import fr.openmc.riftengine.core.converter.writers.items.ItemsTextureJsonWriter; import fr.openmc.riftengine.core.converter.writers.items.ItemsTextureWriter; +import fr.openmc.riftengine.core.converter.writers.items.specials.armor.CustomArmorAttachableWriter; +import fr.openmc.riftengine.core.converter.writers.items.specials.armor.CustomArmorTextureWriter; +import fr.openmc.riftengine.core.converter.writers.items.specials.model.ItemModelAnimationWriter; +import fr.openmc.riftengine.core.converter.writers.items.specials.model.ItemModelAttachableWriter; +import fr.openmc.riftengine.core.converter.writers.items.specials.model.ItemModelGeometryWriter; +import fr.openmc.riftengine.core.converter.writers.items.specials.model.ItemModelTextureWriter; import fr.openmc.riftengine.core.converter.writers.manifest.IconWriter; import fr.openmc.riftengine.core.converter.writers.manifest.ManifestWriter; import fr.openmc.riftengine.core.converter.writers.manifest.PackIdentity; @@ -36,6 +46,9 @@ import fr.openmc.riftengine.core.resolvers.mcmeta.animation.AnimationMcMetaResolver; import fr.openmc.riftengine.core.resolvers.mcmeta.gui.GuiMcMetaResolver; import fr.openmc.riftengine.core.resolvers.tooltip.TooltipResourceResolver; +import fr.openmc.riftengine.core.utils.AssetRender; +import lib.minecraft.renderer.AtlasRenderer; +import lib.minecraft.renderer.call.request.AtlasOptions; import org.bukkit.plugin.java.JavaPlugin; import java.io.File; @@ -47,6 +60,7 @@ public class ConverterManager { private final RiftPlugin plugin; + private AssetRender assetRender; private final List writers = new ArrayList<>(); @@ -85,6 +99,14 @@ public ConverterManager(RiftPlugin plugin) { register("Fichiers de textures", new ItemsTextureWriter(items, itemResolver)); register("Mappings des items", new ItemsMappingWriter(items, itemResolver)); + register("Textures des armures", new CustomArmorTextureWriter(items)); + register("Génération des attachables pour les armures", new CustomArmorAttachableWriter(items, itemResolver)); + + register("Textures des items 3d", new ItemModelTextureWriter(items)); + register("Génération des fichiers de géometrie", new ItemModelGeometryWriter(items)); + register("Générations des attachables pour les items 3D", new ItemModelAttachableWriter(items, itemResolver)); + register("Générations des animations pour les items 3D", new ItemModelAnimationWriter(items)); + register("Textures des tooltips", new TooltipTextureWriter(items, guiResolver, tooltipResolver)); register("Assignations des glyphs pour les tooltips", new TooltipGlyphAssignatorWriter(items, tooltipResolver)); @@ -112,13 +134,27 @@ private void register(String name, PackWriter writer) { public Path generateConvertedPack() throws Exception { Path javaPackPath = getJavaPackPath(RiftPlugin.getInstance()); - Path outputDir = plugin.getDataFolder().toPath().resolve("output"); + this.assetRender = new AssetRender(javaPackPath.toFile()); + + AtlasOptions options = AtlasOptions.builder() + .filter(id -> !id.startsWith("minecraft:")) // omit for every block and item in the game + .tileSize(128) + .columns(100) + .background(Background.checkerboard()) + .progressLogging(false) // on by default; a library consumer wants it off + .build(); + + ImageData sheet = new AtlasRenderer(assetRender.getContext()).render(options).image(); + + Path outputDir = plugin.getOutputDir(); + FilesUtils.deleteDirectory(outputDir.toFile()); Files.createDirectories(outputDir); - Path workDir = outputDir.resolve("internal"); - Path javaExtractDir = workDir.resolve("java"); - Path bedrockBuildDir = workDir.resolve("bedrock"); + ImageFactory.toFile(sheet, ImageFormat.PNG, outputDir.resolve("aywenite_block.png").toFile()); + + Path javaExtractDir = outputDir.resolve("java"); + Path bedrockBuildDir = outputDir.resolve("bedrock"); Files.createDirectories(bedrockBuildDir); ZipUtils.unzip(javaPackPath, javaExtractDir); diff --git a/src/main/java/fr/openmc/riftengine/core/converter/writers/blocks/BlocksMappingWriter.java b/src/main/java/fr/openmc/riftengine/core/converter/writers/blocks/BlocksMappingWriter.java index 8eab87e..826d64f 100644 --- a/src/main/java/fr/openmc/riftengine/core/converter/writers/blocks/BlocksMappingWriter.java +++ b/src/main/java/fr/openmc/riftengine/core/converter/writers/blocks/BlocksMappingWriter.java @@ -42,11 +42,12 @@ public void write(Path bedrockRootPath, Path javaRootPath) throws Exception { List resources = resolver.resolve(javaRootPath, item); + String parentModel = item.getGraphics().parent(); BlockStateScanner.CustomBlockState state = blockBehaviour.blockState(); JsonObject def = new JsonObject(); def.addProperty("name", item.getKey()); - def.addProperty("geometry", "minecraft:geometry.full_block"); + def.addProperty("geometry", getGeometryBlockFromParent(parentModel)); def.add("material_instances", createMaterialInstance(resources)); blockStatesByBlock.computeIfAbsent(state.javaBlock(), _ -> new JsonObject()) @@ -69,6 +70,7 @@ public void write(Path bedrockRootPath, Path javaRootPath) throws Exception { Files.deleteIfExists(outputFile); this.writeJsonFile(outputFile, root); + this.writeJsonFile(RiftPlugin.getInstance().getOutputDir().resolve(OUTPUT_FILE), root); } private JsonObject createMaterialInstance(List faceResources) { @@ -103,4 +105,15 @@ private JsonObject materialObj(String texture) { m.addProperty("ambient_occlusion", true); return m; } + + private String getGeometryBlockFromParent(String parentModel) { + String defaultGeometry = "minecraft:geometry.full_block"; + if (parentModel == null) return defaultGeometry; + String parent = IdentifierUtils.normalizeId(parentModel); + + if (!parent.startsWith("minecraft:")) return defaultGeometry; + if (parentModel.contains("block/cross")) + return "minecraft:geometry.cross"; + return defaultGeometry; + } } \ No newline at end of file diff --git a/src/main/java/fr/openmc/riftengine/core/converter/writers/items/ItemsMappingWriter.java b/src/main/java/fr/openmc/riftengine/core/converter/writers/items/ItemsMappingWriter.java index 8354be5..b358c8b 100644 --- a/src/main/java/fr/openmc/riftengine/core/converter/writers/items/ItemsMappingWriter.java +++ b/src/main/java/fr/openmc/riftengine/core/converter/writers/items/ItemsMappingWriter.java @@ -36,15 +36,20 @@ public void write(Path bedrockRootPath, Path javaRootPath) throws Exception { for (ItemEntry item : items) { List resources = resolver.resolve(javaRootPath, item); + String material = item.getMaterial().getKey().asString(); + + if (material.equals("minecraft:tripwire")) + material = "minecraft:tripwire_hook"; + JsonArray definitions = byMaterial.computeIfAbsent( - item.getMaterial().getKey().asString(), + material, _ -> new JsonArray() ); if (resources.size() == 1) { // * Gestion des items a une texture - definitions.add(handleUniqueTexture(item, resources.getFirst())); - } else if (resources.size() > 1) { + definitions.add(handleUniqueTexture(item, resolver, resources.getFirst())); + } else if (resources.size() > 1 && item.getBehaviour().isItem()) { // * Gestion des items a plusieurs textures JsonObject def = new JsonObject(); def.addProperty("type", "group"); @@ -52,6 +57,24 @@ public void write(Path bedrockRootPath, Path javaRootPath) throws Exception { def.add("definitions", handleMultipleTexture(item, resources)); definitions.add(def); + } else if (resources.size() > 1 && !item.getBehaviour().isItem()) { + // * Gestion des non items (ex block qui sont géré par le block mapping) + JsonObject def = new JsonObject(); + String bedrockIdentifier = resources.getFirst().bedrockIdentifier(); + def.addProperty("type", "definition"); + def.addProperty("model", resources.getFirst().modelResourceId()); + def.addProperty("bedrock_identifier", bedrockIdentifier); + + if (item.getBehaviour().isItem()) { + JsonObject options = new JsonObject(); + options.addProperty("icon", bedrockIdentifier); + def.add("bedrock_options", options); + } + + definitions.add(def); + } else { + // * Gestion des items qui n'a pas de textures (donc dans notre cas model) + definitions.add(handleUniqueTexture(item, resolver, null)); } } @@ -63,23 +86,27 @@ public void write(Path bedrockRootPath, Path javaRootPath) throws Exception { .resolve(OUTPUT_FILE); Files.deleteIfExists(outputFile); this.writeJsonFile(outputFile, root); + this.writeJsonFile(RiftPlugin.getInstance().getOutputDir().resolve(OUTPUT_FILE), root); } - private JsonObject handleUniqueTexture(ItemEntry item, ResolvedItemResource resource) { + private JsonObject handleUniqueTexture(ItemEntry item, ItemResourceResolver resolver, ResolvedItemResource resource) { JsonObject def = new JsonObject(); if (item.getCustomModelData() != null) { def.addProperty("type", "legacy"); def.addProperty("custom_model_data", item.getCustomModelData()); - } else { + } else if (resource != null) { def.addProperty("type", "definition"); def.addProperty("model", resource.modelResourceId()); } - def.addProperty("bedrock_identifier", resource.bedrockIdentifier()); + String bedrockIdentifier = resolver.resolveBedrockIdentifier(item); + if (resource != null) + bedrockIdentifier = resource.bedrockIdentifier(); + def.addProperty("bedrock_identifier", bedrockIdentifier); if (item.getBehaviour().isItem()) { JsonObject options = new JsonObject(); - options.addProperty("icon", resource.bedrockIdentifier()); + options.addProperty("icon", bedrockIdentifier); def.add("bedrock_options", options); } diff --git a/src/main/java/fr/openmc/riftengine/core/converter/writers/items/ItemsTextureJsonWriter.java b/src/main/java/fr/openmc/riftengine/core/converter/writers/items/ItemsTextureJsonWriter.java index 1ff34c9..fc4174e 100644 --- a/src/main/java/fr/openmc/riftengine/core/converter/writers/items/ItemsTextureJsonWriter.java +++ b/src/main/java/fr/openmc/riftengine/core/converter/writers/items/ItemsTextureJsonWriter.java @@ -32,6 +32,13 @@ public void write(Path bedrockRootPath, Path javaRootPath) throws IOException { for (ItemEntry itemEntry : items) { List resolvedItemResources = resolver.resolve(javaRootPath, itemEntry); + if (resolvedItemResources.isEmpty()) { + JsonObject itemData = new JsonObject(); + itemData.addProperty("textures", "textures/misc/missing_texture"); + textureData.add(resolver.resolveBedrockIdentifier(itemEntry), itemData); + continue; + } + for (ResolvedItemResource resource : resolvedItemResources) { JsonObject itemData = new JsonObject(); itemData.addProperty("textures", resource.bedrockTexturePath()); diff --git a/src/main/java/fr/openmc/riftengine/core/converter/writers/items/specials/armor/CustomArmorAttachableWriter.java b/src/main/java/fr/openmc/riftengine/core/converter/writers/items/specials/armor/CustomArmorAttachableWriter.java new file mode 100644 index 0000000..b2d33b9 --- /dev/null +++ b/src/main/java/fr/openmc/riftengine/core/converter/writers/items/specials/armor/CustomArmorAttachableWriter.java @@ -0,0 +1,100 @@ +package fr.openmc.riftengine.core.converter.writers.items.specials.armor; + +import com.google.gson.JsonObject; +import fr.openmc.core.utils.JsonUtils; +import fr.openmc.riftengine.api.scanner.equipements.EquipementEntry; +import fr.openmc.riftengine.api.scanner.items.ItemEntry; +import fr.openmc.riftengine.api.scanner.items.entry.behaviours.armors.ArmorType; +import fr.openmc.riftengine.api.scanner.items.entry.behaviours.armors.ArmorsBehaviour; +import fr.openmc.riftengine.core.RiftLog; +import fr.openmc.riftengine.core.converter.writers.PackWriter; +import fr.openmc.riftengine.core.resolvers.items.ItemResourceResolver; +import fr.openmc.riftengine.core.resolvers.items.ResolvedItemResource; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +public class CustomArmorAttachableWriter implements PackWriter { + private final List items; + private final ItemResourceResolver resolver; + + public CustomArmorAttachableWriter(List items, ItemResourceResolver resolver) { + this.items = items; + this.resolver = resolver; + } + + private final String TEMPLATE_ATTACHABLE = """ + { + "format_version": "1.8.0", + "minecraft:attachable": { + "description": { + "identifier": "", + "materials": { + "default": "armor", + "enchanted": "armor_enchanted" + }, + "textures": { + "default": "", + "enchanted": "textures/misc/enchanted_actor_glint", + "baby": "" + }, + "geometry": { + "default": "geometry.humanoid.armor.", + "baby": "geometry.humanoid.baby.armor." + }, + "scripts": { + // Prevents parts of the player's skin from sticking through the armor + "parent_setup": "v._layer_visible = false;", + "pre_animation": [ + // Variable used by the render controller to determine whether the "baby" geometry and texture should be used + "v.use_baby_geo = q.is_baby && c.owning_entity->v.use_dedicated_baby_armor;" + ] + }, + "render_controllers": ["controller.render.armor.v2"] + } + } + } + """; + + @Override + public void write(Path bedrockRootPath, Path javaRootPath) throws IOException { + for (ItemEntry item : items) { + if (!item.getBehaviour().isArmor()) continue; + if (!(item.getBehaviour() instanceof ArmorsBehaviour( + EquipementEntry entry, + ArmorType type + ))) continue; + if (entry == null) { + RiftLog.ENTRY_EQUIPMENT_NULL.send(item.namespacedId()); + continue; + } + + List resolvedItems = resolver.resolve(javaRootPath, item); + + String bedrockIdentifier; + if (resolvedItems.isEmpty()) { + bedrockIdentifier = resolver.resolveBedrockIdentifier(item, "null"); + } else { + bedrockIdentifier = resolvedItems.getFirst().bedrockIdentifier(); + } + + if (bedrockIdentifier == null) continue; + + JsonObject root = JsonUtils.parseJson(TEMPLATE_ATTACHABLE + .replace("", "geyser_custom:" + bedrockIdentifier) + .replace("", "textures/" + type.selectLayer( + entry.layer1Path(), entry.layer2Path() + )) + .replace("", type.getType()) + .replace("", type.getShortType()) + ); + + + Path outputFile = bedrockRootPath + .resolve("attachables") + .resolve(bedrockIdentifier + ".json"); + this.writeJsonFile(outputFile, root); + } + } +} diff --git a/src/main/java/fr/openmc/riftengine/core/converter/writers/items/specials/armor/CustomArmorTextureWriter.java b/src/main/java/fr/openmc/riftengine/core/converter/writers/items/specials/armor/CustomArmorTextureWriter.java new file mode 100644 index 0000000..3d87497 --- /dev/null +++ b/src/main/java/fr/openmc/riftengine/core/converter/writers/items/specials/armor/CustomArmorTextureWriter.java @@ -0,0 +1,34 @@ +package fr.openmc.riftengine.core.converter.writers.items.specials.armor; + +import fr.openmc.riftengine.api.scanner.equipements.EquipementEntry; +import fr.openmc.riftengine.api.scanner.items.ItemEntry; +import fr.openmc.riftengine.api.scanner.items.entry.behaviours.armors.ArmorsBehaviour; +import fr.openmc.riftengine.core.RiftLog; +import fr.openmc.riftengine.core.converter.writers.PackWriter; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +public class CustomArmorTextureWriter implements PackWriter { + private final List items; + + public CustomArmorTextureWriter(List items) { + this.items = items; + } + + @Override + public void write(Path bedrockRootPath, Path javaRootPath) throws IOException { + for (ItemEntry item : items) { + if (!item.getBehaviour().isArmor()) continue; + if (!(item.getBehaviour() instanceof ArmorsBehaviour(EquipementEntry entry, _))) continue; + if (entry == null) { + RiftLog.ENTRY_EQUIPMENT_NULL.send(item.namespacedId()); + continue; + } + + this.copyFile(entry.getLayer1Path(javaRootPath), entry.getLayer1BedrockPath(bedrockRootPath)); + this.copyFile(entry.getLayer2Path(javaRootPath), entry.getLayer2BedrockPath(bedrockRootPath)); + } + } +} diff --git a/src/main/java/fr/openmc/riftengine/core/converter/writers/items/specials/model/ItemModelAnimationWriter.java b/src/main/java/fr/openmc/riftengine/core/converter/writers/items/specials/model/ItemModelAnimationWriter.java new file mode 100644 index 0000000..65f3914 --- /dev/null +++ b/src/main/java/fr/openmc/riftengine/core/converter/writers/items/specials/model/ItemModelAnimationWriter.java @@ -0,0 +1,183 @@ +package fr.openmc.riftengine.core.converter.writers.items.specials.model; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import fr.openmc.core.utils.JsonUtils; +import fr.openmc.riftengine.api.scanner.items.ItemEntry; +import fr.openmc.riftengine.api.scanner.items.entry.ItemModel; +import fr.openmc.riftengine.api.utils.IdentifierUtils; +import fr.openmc.riftengine.core.converter.writers.PackWriter; +import fr.openmc.riftengine.core.converter.writers.items.specials.model.converter.JavaModelToGeometryConverter; + +import java.nio.file.Path; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * les bedrock_root/animations/ servent a positioner les items correctement. + * Code fait main et les méthodes handAnimation et dérivés ont été faite via IA. + */ +public class ItemModelAnimationWriter implements PackWriter { + private static final String ROOT = JavaModelToGeometryConverter.ROOT_BONE; + private static final double HEAD_SCALE = 0.625; + + // * Échelle du model en 1ère personne d'apres des repos de Geyser et autres. + private static final double FIRST_PERSON_ROOT_SCALE = 1.5; + + private final List items; + + public ItemModelAnimationWriter(List items) { + this.items = items; + } + + @Override + public void write(Path bedrockRootPath, Path javaRootPath) throws Exception { + Set modelCreated = new HashSet<>(); + + for (ItemEntry item : items) { + if (!item.getModel().hasModel()) continue; + + ItemModel model = item.getModel(); + String modelName = model.modelId(); + + if (!modelCreated.add(modelName)) continue; + + JsonObject rootModelJson = JsonUtils.loadJson(IdentifierUtils.resolveModelId(javaRootPath, modelName)); + if (rootModelJson == null) continue; + + JsonObject display = JsonUtils.getJsonObject(rootModelJson, "display", new JsonObject()); + + String id = model.toBedrockAnimationId(); + JsonObject animations = new JsonObject(); + animations.add(id + ".thirdperson_main_hand", handAnimation(display, "thirdperson_righthand", false, true)); + animations.add(id + ".thirdperson_off_hand", handAnimation(display, "thirdperson_lefthand", false, true)); + animations.add(id + ".head", headAnimation(display)); + animations.add(id + ".firstperson_main_hand", handAnimation(display, "firstperson_righthand", true, true)); + animations.add(id + ".firstperson_off_hand", handAnimation(display, "firstperson_lefthand", true, false)); + animations.add(id + ".disable", disableAnimation()); + + JsonObject root = new JsonObject(); + root.addProperty("format_version", "1.8.0"); + root.add("animations", animations); + + this.writeJsonFile(bedrockRootPath.resolve(model.toBedrockAnimationPath()), root); + } + } + + private JsonObject handAnimation(JsonObject display, String slot, boolean firstPerson, boolean negateX) { + double[] rot = vec(display, slot, "rotation"); + double[] pos = vec(display, slot, "translation"); + double[] scl = vec(display, slot, "scale"); + + JsonObject bones = new JsonObject(); + + JsonObject root = new JsonObject(); + if (firstPerson) { + root.add("rotation", array(90, 60, -40)); + root.add("position", array(4, 10, 4)); + root.addProperty("scale", FIRST_PERSON_ROOT_SCALE); + } else { + root.add("rotation", array(90, 0, 0)); + root.add("position", array(0, 13, -3)); + } + bones.add(ROOT, root); + + JsonObject x = new JsonObject(); + if (rot != null) { + x.add("rotation", array(-rot[0], 0, 0)); + } else if (firstPerson) { + x.add("rotation", array(0.1, 0.1, 0.1)); + } + if (pos != null) { + double k = firstPerson ? 1.0 / FIRST_PERSON_ROOT_SCALE : 1.0; + x.add("position", array( + (negateX ? -pos[0] : pos[0]) * k, + pos[1] * k, + (firstPerson ? -pos[2] : pos[2]) * k)); + } + if (scl != null) x.add("scale", array(scl[0], scl[1], scl[2])); + if (x.size() > 0) bones.add(ROOT + "_x", x); + + addYZ(bones, rot); + return animation(bones); + } + + private JsonObject headAnimation(JsonObject display) { + double[] rot = vec(display, "head", "rotation"); + double[] pos = vec(display, "head", "translation"); + double[] scl = vec(display, "head", "scale"); + + JsonObject bones = new JsonObject(); + + JsonObject root = new JsonObject(); + root.add("position", array(0, 19.5, 0)); + bones.add(ROOT, root); + + JsonObject x = new JsonObject(); + if (rot != null) x.add("rotation", array(-rot[0], 0, 0)); + if (pos != null) { + x.add("position", array(-pos[0] * HEAD_SCALE, pos[1] * HEAD_SCALE, pos[2] * HEAD_SCALE)); + } + if (scl != null) { + x.add("scale", array(scl[0] * HEAD_SCALE, scl[1] * HEAD_SCALE, scl[2] * HEAD_SCALE)); + } else { + x.addProperty("scale", HEAD_SCALE); + } + bones.add(ROOT + "_x", x); + + addYZ(bones, rot); + return animation(bones); + } + + private JsonObject disableAnimation() { + JsonObject root = new JsonObject(); + root.addProperty("scale", 0); + + JsonObject bones = new JsonObject(); + bones.add(ROOT, root); + + JsonObject animation = animation(bones); + animation.addProperty("override_previous_animation", true); + return animation; + } + + private void addYZ(JsonObject bones, double[] rot) { + if (rot == null) return; + + JsonObject y = new JsonObject(); + y.add("rotation", array(0, -rot[1], 0)); + bones.add(ROOT + "_y", y); + + JsonObject z = new JsonObject(); + z.add("rotation", array(0, 0, rot[2])); + bones.add(ROOT + "_z", z); + } + + private JsonObject animation(JsonObject bones) { + JsonObject animation = new JsonObject(); + animation.addProperty("loop", true); + animation.add("bones", bones); + return animation; + } + + private double[] vec(JsonObject display, String slot, String field) { + JsonObject transform = JsonUtils.getJsonObject(display, slot, null); + if (transform == null) return null; + JsonArray a = JsonUtils.getJsonArray(transform, field, null); + if (a == null || a.size() < 3) return null; + return new double[]{a.get(0).getAsDouble(), a.get(1).getAsDouble(), a.get(2).getAsDouble()}; + } + + private JsonArray array(double x, double y, double z) { + JsonArray a = new JsonArray(); + a.add(round4(x)); + a.add(round4(y)); + a.add(round4(z)); + return a; + } + + private double round4(double v) { + return Math.round(v * 10000.0) / 10000.0; + } +} diff --git a/src/main/java/fr/openmc/riftengine/core/converter/writers/items/specials/model/ItemModelAttachableWriter.java b/src/main/java/fr/openmc/riftengine/core/converter/writers/items/specials/model/ItemModelAttachableWriter.java new file mode 100644 index 0000000..0877ca7 --- /dev/null +++ b/src/main/java/fr/openmc/riftengine/core/converter/writers/items/specials/model/ItemModelAttachableWriter.java @@ -0,0 +1,107 @@ +package fr.openmc.riftengine.core.converter.writers.items.specials.model; + +import com.google.gson.JsonObject; +import fr.openmc.core.utils.JsonUtils; +import fr.openmc.riftengine.api.scanner.items.ItemEntry; +import fr.openmc.riftengine.api.scanner.items.entry.ItemModel; +import fr.openmc.riftengine.core.converter.writers.PackWriter; +import fr.openmc.riftengine.core.resolvers.items.ItemResourceResolver; +import fr.openmc.riftengine.core.resolvers.items.ResolvedItemResource; + +import java.nio.file.Path; +import java.util.List; + +public class ItemModelAttachableWriter implements PackWriter { + private final List items; + private final ItemResourceResolver resolver; + + public ItemModelAttachableWriter(List items, ItemResourceResolver resolver) { + this.items = items; + this.resolver = resolver; + } + + private final String TEMPLATE_ATTACHABLE = """ + { + "format_version": "1.21.50", + "minecraft:attachable": { + "description": { + "identifier": "", + "materials": { + "default": "", + "enchanted": "entity_alphatest_glint" + }, + "textures": { + "default": "", + "enchanted": "textures/misc/enchanted_item_glint" + }, + "geometry": { + "default": "" + }, + "scripts": { + "pre_animation": [ + "v.main_hand = c.item_slot == 'main_hand';", + "v.off_hand = c.item_slot == 'off_hand';", + "v.head = c.item_slot == 'head';" + ], + "animate": [ + {"thirdperson_main_hand": "v.main_hand && !c.is_first_person"}, + {"thirdperson_off_hand": "v.off_hand && !c.is_first_person"}, + {"thirdperson_head": "v.head && !c.is_first_person"}, + {"firstperson_main_hand": "v.main_hand && c.is_first_person"}, + {"firstperson_off_hand": "v.off_hand && c.is_first_person"}, + {"firstperson_head": "c.is_first_person && v.head"} + ] + }, + "animations": { + "thirdperson_main_hand": ".thirdperson_main_hand", + "thirdperson_off_hand": ".thirdperson_off_hand", + "thirdperson_head": ".head", + "firstperson_main_hand": ".firstperson_main_hand", + "firstperson_off_hand": ".firstperson_off_hand", + "firstperson_head": ".disable" + }, + "render_controllers": ["controller.render.item_default"] + } + } + } + """; + + + + @Override + public void write(Path bedrockRootPath, Path javaRootPath) throws Exception { + for (ItemEntry item : items) { + if (!item.getModel().hasModel()) continue; + + List resolvedItems = resolver.resolve(javaRootPath, item); + + String bedrockIdentifier; + if (resolvedItems.isEmpty()) { + bedrockIdentifier = resolver.resolveBedrockIdentifier(item, ItemEntry.DEFAULT_STATE); + } else { + bedrockIdentifier = resolvedItems.getFirst().bedrockIdentifier(); + } + + if (bedrockIdentifier == null) continue; + + ItemModel model = item.getModel(); + + List textures = model.texturesPath(javaRootPath); + + if (textures.isEmpty()) continue; + + JsonObject root = JsonUtils.parseJson(TEMPLATE_ATTACHABLE + .replace("", "geyser_custom:" + bedrockIdentifier) + .replace("", "entity_alphablend") + .replace("", model.toBedrockAtlasPath()) + .replace("", model.toBedrockGeometryId()) + .replace("", model.toBedrockAnimationId()) + ); + + Path outputFile = bedrockRootPath + .resolve("attachables") + .resolve(bedrockIdentifier + ".json"); + this.writeJsonFile(outputFile, root); + } + } +} diff --git a/src/main/java/fr/openmc/riftengine/core/converter/writers/items/specials/model/ItemModelGeometryWriter.java b/src/main/java/fr/openmc/riftengine/core/converter/writers/items/specials/model/ItemModelGeometryWriter.java new file mode 100644 index 0000000..a631698 --- /dev/null +++ b/src/main/java/fr/openmc/riftengine/core/converter/writers/items/specials/model/ItemModelGeometryWriter.java @@ -0,0 +1,61 @@ +package fr.openmc.riftengine.core.converter.writers.items.specials.model; + +import com.google.gson.JsonObject; +import fr.openmc.core.utils.JsonUtils; +import fr.openmc.riftengine.api.scanner.items.ItemEntry; +import fr.openmc.riftengine.api.scanner.items.entry.ItemModel; +import fr.openmc.riftengine.api.utils.IdentifierUtils; +import fr.openmc.riftengine.core.RiftLog; +import fr.openmc.riftengine.core.converter.writers.PackWriter; +import fr.openmc.riftengine.core.converter.writers.items.specials.model.converter.JavaModelToGeometryConverter; +import fr.openmc.riftengine.core.converter.writers.items.specials.model.converter.ModelTextureAtlas; + +import java.nio.file.Path; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class ItemModelGeometryWriter implements PackWriter { + private final List items; + + public ItemModelGeometryWriter(List items) { + this.items = items; + } + + @Override + public void write(Path bedrockRootPath, Path javaRootPath) throws Exception { + Set modelCreated = new HashSet<>(); + + for (ItemEntry item : items) { + if (!item.getModel().hasModel()) continue; + + ItemModel model = item.getModel(); + String modelName = model.modelId(); + + if (modelCreated.contains(modelName)) continue; + modelCreated.add(modelName); + + Path targetJson = IdentifierUtils.resolveModelId(javaRootPath, modelName); + JsonObject rootModelJson = JsonUtils.loadJson(targetJson); + + if (rootModelJson == null) continue; + + ModelTextureAtlas atlas = ModelTextureAtlas.build(javaRootPath, rootModelJson, model.namespace()); + + JsonObject rootModelGeoJson = JavaModelToGeometryConverter.convert( + rootModelJson, + model.toBedrockGeometryId(), + true, + atlas + ); + + if (rootModelGeoJson == null) { + RiftLog.MODEL_PARENT.send(item.namespacedId()); + continue; + } + + Path output = bedrockRootPath.resolve(model.toBedrockModelPath()); + this.writeJsonFile(output, rootModelGeoJson); + } + } +} diff --git a/src/main/java/fr/openmc/riftengine/core/converter/writers/items/specials/model/ItemModelTextureWriter.java b/src/main/java/fr/openmc/riftengine/core/converter/writers/items/specials/model/ItemModelTextureWriter.java new file mode 100644 index 0000000..235a088 --- /dev/null +++ b/src/main/java/fr/openmc/riftengine/core/converter/writers/items/specials/model/ItemModelTextureWriter.java @@ -0,0 +1,52 @@ +package fr.openmc.riftengine.core.converter.writers.items.specials.model; + +import com.google.gson.JsonObject; +import fr.openmc.core.utils.JsonUtils; +import fr.openmc.riftengine.api.scanner.items.ItemEntry; +import fr.openmc.riftengine.api.scanner.items.entry.ItemModel; +import fr.openmc.riftengine.api.utils.IdentifierUtils; +import fr.openmc.riftengine.core.converter.writers.PackWriter; +import fr.openmc.riftengine.core.converter.writers.items.specials.model.converter.ModelTextureAtlas; + +import javax.imageio.ImageIO; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class ItemModelTextureWriter implements PackWriter { + private final List items; + + public ItemModelTextureWriter(List items) { + this.items = items; + } + + @Override + public void write(Path bedrockRootPath, Path javaRootPath) throws Exception { + Set modelCreated = new HashSet<>(); + + for (ItemEntry item : items) { + if (!item.getModel().hasModel()) continue; + + ItemModel model = item.getModel(); + String modelName = model.modelId(); + + if (modelName.startsWith("minecraft:")) continue; + + if (modelCreated.contains(modelName)) continue; + modelCreated.add(modelName); + + JsonObject rootModelJson = JsonUtils.loadJson(IdentifierUtils.resolveModelId(javaRootPath, modelName)); + if (rootModelJson == null) continue; + + ModelTextureAtlas atlas = ModelTextureAtlas.build(javaRootPath, rootModelJson, model.namespace()); + if (atlas == null) continue; + + Path output = bedrockRootPath.resolve(model.toBedrockAtlasPath() + ".png"); + Files.createDirectories(output.getParent()); + ImageIO.write(atlas.getImage(), "png", output.toFile()); + + } + } +} diff --git a/src/main/java/fr/openmc/riftengine/core/converter/writers/items/specials/model/converter/JavaModelToGeometryConverter.java b/src/main/java/fr/openmc/riftengine/core/converter/writers/items/specials/model/converter/JavaModelToGeometryConverter.java new file mode 100644 index 0000000..8aa2265 --- /dev/null +++ b/src/main/java/fr/openmc/riftengine/core/converter/writers/items/specials/model/converter/JavaModelToGeometryConverter.java @@ -0,0 +1,264 @@ +package fr.openmc.riftengine.core.converter.writers.items.specials.model.converter; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +/** + * Convertit un modèle Java Edition (JSON Blockbench) en géométrie Bedrock (.geo.json). + * + * Classe généré par IA, je précise. Assez complexe de définir les formules et de les appliquer sans + * s'aider d'une IA + */ +public final class JavaModelToGeometryConverter { + + private static final double CENTRE_X = 8.0; + private static final double CENTRE_Y = 0.0; + private static final double CENTRE_Z = 8.0; + + /** + * @param javaModel le JSON du modèle Java + * @param geometryId ex. "geometry.omc_dream.orb.orb_cloud" + * @param itemBinding true pour ajouter le binding d'item tenu en main (attachable) + * @param atlas l'atlas de textures (donc le regroupement des textures) + * @return le contenu du .geo.json, ou null si aucun élément exploitable + */ + public static JsonObject convert(JsonObject javaModel, String geometryId, boolean itemBinding, + ModelTextureAtlas atlas) { + if (!javaModel.has("elements") || !javaModel.get("elements").isJsonArray()) return null; + + int texW = 16; + int texH = 16; + if (javaModel.has("texture_size") && javaModel.get("texture_size").isJsonArray()) { + JsonArray ts = javaModel.getAsJsonArray("texture_size"); + if (ts.size() >= 2) { + texW = ts.get(0).getAsInt(); + texH = ts.get(1).getAsInt(); + } + } + if (atlas != null) { + texW = atlas.width(); + texH = atlas.height(); + } + double scaleU = texW / 16.0; + double scaleV = texH / 16.0; + + JsonArray cubes = new JsonArray(); + for (JsonElement el : javaModel.getAsJsonArray("elements")) { + if (!el.isJsonObject()) continue; + JsonObject element = el.getAsJsonObject(); + if (!element.has("from") || !element.has("to")) continue; + cubes.add(convertElement(element, scaleU, scaleV, atlas)); + } + if (cubes.isEmpty()) return null; + + JsonArray bones = new JsonArray(); + bones.add(bone(ROOT_BONE, null, itemBinding, null)); + bones.add(bone(ROOT_BONE + "_x", ROOT_BONE, false, null)); + bones.add(bone(ROOT_BONE + "_y", ROOT_BONE + "_x", false, null)); + bones.add(bone(ROOT_BONE + "_z", ROOT_BONE + "_y", false, cubes)); + + JsonObject description = new JsonObject(); + description.addProperty("identifier", geometryId); + description.addProperty("visible_bounds_width", 4.0); + description.addProperty("visible_bounds_height", 4.0); + description.add("visible_bounds_offset", toArray(0, 0.75, 0)); + description.addProperty("texture_width", texW); + description.addProperty("texture_height", texH); + + JsonObject geoEntry = new JsonObject(); + geoEntry.add("description", description); + geoEntry.add("bones", bones); + + JsonArray geoArray = new JsonArray(); + geoArray.add(geoEntry); + + JsonObject root = new JsonObject(); + root.addProperty("format_version", "1.21.0"); + root.add("minecraft:geometry", geoArray); + return root; + } + + private static JsonObject convertElement(JsonObject element, double scaleU, double scaleV, ModelTextureAtlas atlas) { + JsonArray from = element.getAsJsonArray("from"); + JsonArray to = element.getAsJsonArray("to"); + + double rawFromX = from.get(0).getAsDouble(); + double rawFromY = from.get(1).getAsDouble(); + double rawFromZ = from.get(2).getAsDouble(); + double rawToX = to.get(0).getAsDouble(); + double rawToY = to.get(1).getAsDouble(); + double rawToZ = to.get(2).getAsDouble(); + + double fromX = rawFromX - CENTRE_X; + double fromY = rawFromY - CENTRE_Y; + double fromZ = rawFromZ - CENTRE_Z; + double toX = rawToX - CENTRE_X; + double toY = rawToY - CENTRE_Y; + double toZ = rawToZ - CENTRE_Z; + + double originX = Math.min(fromX, toX); + double originY = Math.min(fromY, toY); + double originZ = Math.min(fromZ, toZ); + double sizeX = Math.max(fromX, toX) - originX; + double sizeY = Math.max(fromY, toY) - originY; + double sizeZ = Math.max(fromZ, toZ) - originZ; + + originX = -(originX + sizeX); + int invertedAxes = (rawFromX > rawToX ? 1 : 0) + (rawFromY > rawToY ? 1 : 0) + (rawFromZ > rawToZ ? 1 : 0); + if (invertedAxes % 2 == 1) { + originX += sizeX; originY += sizeY; originZ += sizeZ; + sizeX = -sizeX; sizeY = -sizeY; sizeZ = -sizeZ; + } + + JsonObject cube = new JsonObject(); + cube.add("origin", toArray(originX, originY, originZ)); + cube.add("size", toArray(sizeX, sizeY, sizeZ)); + + if (element.has("rotation") && element.get("rotation").isJsonObject()) { + JsonObject javaRot = element.getAsJsonObject("rotation"); + double angle = javaRot.has("angle") ? javaRot.get("angle").getAsDouble() : 0; + String axis = javaRot.has("axis") ? javaRot.get("axis").getAsString() : "y"; + + if (angle != 0) { + JsonArray rotation = new JsonArray(); + switch (axis) { + case "x" -> { + rotation.add(-angle); + rotation.add(0); + rotation.add(0); + } + case "y" -> { + rotation.add(0); + rotation.add(-angle); + rotation.add(0); + } + case "z" -> { + rotation.add(0); + rotation.add(0); + rotation.add(angle); + } + default -> { + rotation.add(0); + rotation.add(0); + rotation.add(0); + } + } + cube.add("rotation", rotation); + + if (javaRot.has("origin") && javaRot.get("origin").isJsonArray()) { + JsonArray o = javaRot.getAsJsonArray("origin"); + double px = -(o.get(0).getAsDouble() - CENTRE_X); + double py = o.get(1).getAsDouble() - CENTRE_Y; + double pz = o.get(2).getAsDouble() - CENTRE_Z; + if (px != 0 || py != 0 || pz != 0) cube.add("pivot", toArray(px, py, pz)); + } + } + } + + if (element.has("faces") && element.get("faces").isJsonObject()) { + JsonObject javaFaces = element.getAsJsonObject("faces"); + JsonObject bedrockUV = new JsonObject(); + + for (String faceName : new String[]{"north", "east", "south", "west", "up", "down"}) { + if (!javaFaces.has(faceName) || !javaFaces.get(faceName).isJsonObject()) continue; + JsonObject javaFace = javaFaces.getAsJsonObject(faceName); + + double[] uv; + if (javaFace.has("uv") && javaFace.get("uv").isJsonArray() && javaFace.getAsJsonArray("uv").size() >= 4) { + JsonArray a = javaFace.getAsJsonArray("uv"); + uv = new double[]{ + a.get(0).getAsDouble(), a.get(1).getAsDouble(), + a.get(2).getAsDouble(), a.get(3).getAsDouble() + }; + } else { + uv = defaultFaceUV(faceName, rawFromX, rawFromY, rawFromZ, rawToX, rawToY, rawToZ); + } + + int faceRotation = 0; + if (javaFace.has("rotation") && javaFace.get("rotation").isJsonPrimitive()) { + int normalized = ((javaFace.get("rotation").getAsInt() % 360) + 360) % 360; + if (normalized % 90 == 0) faceRotation = normalized; + } + + boolean flip = faceName.equals("up") || faceName.equals("down"); + double sU = scaleU, sV = scaleV, offU = 0, offV = 0; + if (atlas != null) { + String ref = javaFace.has("texture") ? javaFace.get("texture").getAsString() : "#0"; + ModelTextureAtlas.Sprite s = atlas.sprite(ref); + if (s != null) { + sU = s.width() / 16.0; sV = s.height() / 16.0; + offU = s.x(); offV = s.y(); + } + } + bedrockUV.add(faceName, convertFaceUV(uv, flip, sU, sV, offU, offV, faceRotation)); + } + if (bedrockUV.size() > 0) cube.add("uv", bedrockUV); + } + return cube; + } + + private static double[] defaultFaceUV(String face, double fx, double fy, double fz, double tx, double ty, double tz) { + return switch (face) { + case "north" -> new double[]{16 - tx, 16 - ty, 16 - fx, 16 - fy}; + case "south" -> new double[]{fx, 16 - ty, tx, 16 - fy}; + case "west" -> new double[]{fz, 16 - ty, tz, 16 - fy}; + case "east" -> new double[]{16 - tz, 16 - ty, 16 - fz, 16 - fy}; + case "up" -> new double[]{fx, fz, tx, tz}; + case "down" -> new double[]{fx, 16 - tz, tx, 16 - fz}; + default -> new double[]{0, 0, 16, 16}; + }; + } + + private static JsonObject convertFaceUV(double[] uv, boolean flipUpDown, double scaleU, double scaleV, + double offU, double offV, int rotation) { + double u1 = uv[0] * scaleU + offU; + double v1 = uv[1] * scaleV + offV; + double u2 = uv[2] * scaleU + offU; + double v2 = uv[3] * scaleV + offV; + + JsonArray uvArr = new JsonArray(); + JsonArray sizeArr = new JsonArray(); + if (flipUpDown) { + uvArr.add(round4(u2)); + uvArr.add(round4(v2)); + sizeArr.add(round4(u1 - u2)); + sizeArr.add(round4(v1 - v2)); + } else { + uvArr.add(round4(u1)); + uvArr.add(round4(v1)); + sizeArr.add(round4(u2 - u1)); + sizeArr.add(round4(v2 - v1)); + } + + JsonObject face = new JsonObject(); + face.add("uv", uvArr); + face.add("uv_size", sizeArr); + if (rotation != 0) face.addProperty("uv_rotation", rotation); + return face; + } + + private static JsonArray toArray(double x, double y, double z) { + JsonArray arr = new JsonArray(); + arr.add(round4(x)); + arr.add(round4(y)); + arr.add(round4(z)); + return arr; + } + + public static final String ROOT_BONE = "bone"; + + private static JsonObject bone(String name, String parent, boolean binding, JsonArray cubes) { + JsonObject b = new JsonObject(); + b.addProperty("name", name); + if (parent != null) b.addProperty("parent", parent); + if (binding) b.addProperty("binding", "c.item_slot == 'head' ? 'head' : q.item_slot_to_bone_name(c.item_slot)"); + b.add("pivot", toArray(0, 8, 0)); + if (cubes != null) b.add("cubes", cubes); + return b; + } + + private static double round4(double v) { + return Math.round(v * 10000.0) / 10000.0; + } +} \ No newline at end of file diff --git a/src/main/java/fr/openmc/riftengine/core/converter/writers/items/specials/model/converter/ModelTextureAtlas.java b/src/main/java/fr/openmc/riftengine/core/converter/writers/items/specials/model/converter/ModelTextureAtlas.java new file mode 100644 index 0000000..aa94afb --- /dev/null +++ b/src/main/java/fr/openmc/riftengine/core/converter/writers/items/specials/model/converter/ModelTextureAtlas.java @@ -0,0 +1,135 @@ +package fr.openmc.riftengine.core.converter.writers.items.specials.model.converter; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import fr.openmc.core.utils.JsonUtils; +import fr.openmc.riftengine.api.utils.IdentifierUtils; +import lombok.Getter; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; + +/** + * Adapte les textures d'un modèle car bedrock ne supporte pas les plusieures textures par modeles :/ + * + * Classe généré par IA :{ + * todo: la refaire à ma sauce, un ImageUtils car le probleme pour faire un atlas je l'avais aussi dans FrameTextureConverter + */ +@Getter +public final class ModelTextureAtlas { + + public record Sprite(int x, int y, int width, int height) { + } + + private final BufferedImage image; + private final Map spritesByKey; + + private ModelTextureAtlas(BufferedImage image, Map spritesByKey) { + this.image = image; + this.spritesByKey = spritesByKey; + } + + public int width() { + return image.getWidth(); + } + + public int height() { + return image.getHeight(); + } + + public Sprite sprite(String ref) { + if (ref == null) return null; + return spritesByKey.get(ref.startsWith("#") ? ref.substring(1) : ref); + } + + public static ModelTextureAtlas build(Path javaRootPath, JsonObject model, String defaultNamespace) throws IOException { + JsonObject textures = JsonUtils.getJsonObject(model, "textures", null); + + if (textures == null) return null; + + Map keyToTexture = new LinkedHashMap<>(); + for (String key : textures.keySet()) { + if (key.equals("particle")) continue; + String id = resolveAlias(textures, key, defaultNamespace); + if (id != null) keyToTexture.put(key, id); + } + + Map images = new LinkedHashMap<>(); + for (String id : new LinkedHashSet<>(keyToTexture.values())) { + BufferedImage img = loadFirstFrame(javaRootPath, id); + if (img != null) images.put(id, img); + } + if (images.isEmpty()) return null; + + int totalWidth = 0, maxHeight = 0; + for (BufferedImage img : images.values()) { + totalWidth += img.getWidth(); + maxHeight = Math.max(maxHeight, img.getHeight()); + } + + BufferedImage atlas = new BufferedImage(totalWidth, maxHeight, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = atlas.createGraphics(); + Map spriteByTexture = new HashMap<>(); + int x = 0; + for (Map.Entry entry : images.entrySet()) { + BufferedImage img = entry.getValue(); + g.drawImage(img, x, 0, null); + spriteByTexture.put(entry.getKey(), new Sprite(x, 0, img.getWidth(), img.getHeight())); + x += img.getWidth(); + } + g.dispose(); + + Map spritesByKey = new HashMap<>(); + for (Map.Entry entry : keyToTexture.entrySet()) { + Sprite sprite = spriteByTexture.get(entry.getValue()); + if (sprite != null) spritesByKey.put(entry.getKey(), sprite); + } + return new ModelTextureAtlas(atlas, spritesByKey); + } + + private static String resolveAlias(JsonObject textures, String key, String defaultNamespace) { + JsonElement element = textures.get(key); + for (int i = 0; i < 10 && element != null && element.isJsonPrimitive(); i++) { + String value = element.getAsString(); + if (!value.startsWith("#")) + return IdentifierUtils.normalizeId(value.startsWith("_") ? value.substring(1) : value, defaultNamespace); + element = textures.get(value.substring(1)); + } + return null; + } + + private static BufferedImage loadFirstFrame(Path javaRootPath, String textureId) throws IOException { + Path png = IdentifierUtils.resolveTextureId(javaRootPath, textureId); + if (!Files.exists(png)) return null; + + BufferedImage img = ImageIO.read(png.toFile()); + if (img == null) return null; + + Path meta = IdentifierUtils.resolveMetaId(javaRootPath, textureId, ".png"); + if (!Files.exists(meta)) return img; + + JsonObject metaJson; + try { + metaJson = JsonUtils.loadJson(meta); + } catch (RuntimeException e) { + return img; + } + if (metaJson == null || !metaJson.has("animation")) return img; + + JsonObject animation = JsonUtils.getJsonObject(metaJson, "animation", new JsonObject()); + int frameWidth = JsonUtils.getInt(animation, "width", img.getWidth()); + int frameHeight = JsonUtils.getInt(animation, "height", frameWidth); + + frameWidth = Math.min(frameWidth, img.getWidth()); + frameHeight = Math.min(frameHeight, img.getHeight()); + return img.getSubimage(0, 0, frameWidth, frameHeight); + } +} diff --git a/src/main/java/fr/openmc/riftengine/core/converter/writers/tooltip/textures/TooltipTextureWriter.java b/src/main/java/fr/openmc/riftengine/core/converter/writers/tooltip/textures/TooltipTextureWriter.java index 02e70e2..1fad735 100644 --- a/src/main/java/fr/openmc/riftengine/core/converter/writers/tooltip/textures/TooltipTextureWriter.java +++ b/src/main/java/fr/openmc/riftengine/core/converter/writers/tooltip/textures/TooltipTextureWriter.java @@ -70,7 +70,7 @@ private void handleTextureAnimated(Path bedrockRootPath, ResolvedGuiMcMeta resol FrameTextureConverter.convert( resolvedGuiMcMeta, imagePath, - bedrockRootPath.resolve( IdentifierUtils.toBedrockTexturePath(reducedImagePath.toString())) + bedrockRootPath.resolve(IdentifierUtils.toBedrockTexturePath(reducedImagePath.toString())) ); } } \ No newline at end of file diff --git a/src/main/java/fr/openmc/riftengine/core/resolvers/items/ItemResourceResolver.java b/src/main/java/fr/openmc/riftengine/core/resolvers/items/ItemResourceResolver.java index ccf6b59..d9968fc 100644 --- a/src/main/java/fr/openmc/riftengine/core/resolvers/items/ItemResourceResolver.java +++ b/src/main/java/fr/openmc/riftengine/core/resolvers/items/ItemResourceResolver.java @@ -29,7 +29,7 @@ public List resolve(Path javaRootPath, ItemEntry item) { String state = entry.getKey(); String resourceId = entry.getValue(); Path bedrockTexturePath = bedrockTexturePaths.get(state); - String bedrockIdentifier = state.equals(ItemEntry.DEFAULT_STATE) ? item.getKey() : item.getKey() + "_" + state; + String bedrockIdentifier = resolveBedrockIdentifier(item, state); String bedrockTextureValue = null; String modelResourceId = item.getNamespace() + ":ia_auto/" + item.getKey(); @@ -70,4 +70,14 @@ public List resolve(Path javaRootPath, ItemEntry item) { return resolved; } + + public String resolveBedrockIdentifier(ItemEntry item, String state) { + return state.equals(ItemEntry.DEFAULT_STATE) ? resolveBedrockIdentifier(item) : item.getKey() + "_" + state; + + } + + public String resolveBedrockIdentifier(ItemEntry item) { + return item.getKey(); + + } } diff --git a/src/main/java/fr/openmc/riftengine/core/utils/AssetRender.java b/src/main/java/fr/openmc/riftengine/core/utils/AssetRender.java new file mode 100644 index 0000000..86d3fad --- /dev/null +++ b/src/main/java/fr/openmc/riftengine/core/utils/AssetRender.java @@ -0,0 +1,35 @@ +package fr.openmc.riftengine.core.utils; + + +import dev.simplified.collection.Concurrent; +import lib.minecraft.renderer.BlockRenderer; +import lib.minecraft.renderer.call.request.BlockOptions; +import lib.minecraft.renderer.call.result.RenderResult; +import lib.minecraft.renderer.content.client.ClientAcquisition; +import lib.minecraft.renderer.content.client.ClientAssets; +import lib.minecraft.renderer.content.client.ClientOptions; +import lib.minecraft.renderer.content.index.RendererContext; +import lombok.Getter; + +import java.io.File; + +public class AssetRender { + @Getter + private final RendererContext context; + public AssetRender(File javaResourcePack) { + ClientOptions clientOptions = ClientOptions.builder() + .version("26.3") + .texturePacks(Concurrent.newList(javaResourcePack)) + .build(); + ClientAssets assets = ClientAcquisition.acquire(clientOptions); + this.context = RendererContext.load(assets); + + } + + public RenderResult render(String blockId) { + return new BlockRenderer(context).render(BlockOptions.builder() + .blockId(blockId) + .type(BlockOptions.Type.ISOMETRIC_3D) + .build()); + } +}