diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 688654e..e0494b5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,12 +33,19 @@ jobs: env: BUILD_NUMBER: ${{ steps.release-info.outputs.curentRelease }} + - name: Publish to Maven Repository + if: ${{ success() && github.repository == 'GeyserMC/Rainbow' && github.ref_name == 'master' }} + run: ./gradlew publish + env: + ORG_GRADLE_PROJECT_geysermcUsername: ${{ vars.DEPLOY_USER }} + ORG_GRADLE_PROJECT_geysermcPassword: ${{ secrets.DEPLOY_PASS }} + - name: Archive Artifacts uses: GeyserMC/actions/upload-multi-artifact@master if: success() with: artifacts: | - rainbow:build/libs/Rainbow.jar + rainbow:client/build/libs/Rainbow.jar - name: Get Version if: ${{ (success() || failure()) && github.repository == 'GeyserMC/Rainbow' }} @@ -69,7 +76,7 @@ jobs: privateKey: ${{ secrets.DOWNLOADS_PRIVATE_KEY }} host: ${{ secrets.DOWNLOADS_SERVER_IP }} files: | - build/libs/Rainbow.jar + client/build/libs/Rainbow.jar changelog: ${{ steps.metadata.outputs.body }} # - name: Publish to Modrinth @@ -87,4 +94,4 @@ jobs: discordWebhook: ${{ secrets.DISCORD_WEBHOOK }} status: ${{ job.status }} body: ${{ steps.metadata.outputs.body }} - includeDownloads: ${{ success() && github.repository == 'GeyserMC/Rainbow' && github.ref_name == 'master' }} + includeDownloads: ${{ github.ref_name == 'master' }} diff --git a/README.md b/README.md index f3998ce..4d0297e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Discord](https://img.shields.io/discord/613163671870242838.svg?color=%237289da&label=discord)](https://discord.gg/geysermc) Rainbow is a client-side Minecraft mod for the Fabric modloader to generate Geyser item mappings and bedrock resourcepacks -for use with Geyser's [custom item API (v2)](https://github.com/geyserMC/geyser/pull/5189). Rainbow is available for Minecraft 1.21.7 and 1.21.8. +for use with Geyser's [custom item API (v2)](https://github.com/geyserMC/geyser/pull/5189). Rainbow is available for Minecraft 1.21.9 and 1.21.10. Rainbow is currently experimental and capable of the following: diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts new file mode 100644 index 0000000..3c6afdc --- /dev/null +++ b/build-logic/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + `kotlin-dsl` +} + +repositories { + maven { + name = "Fabric" + url = uri("https://maven.fabricmc.net/") + } + gradlePluginPortal() +} + +dependencies { + // Very ugly... https://github.com/gradle/gradle/issues/15383 + implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) + + implementation(libs.fabric.loom) +} diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts new file mode 100644 index 0000000..b5a0fab --- /dev/null +++ b/build-logic/settings.gradle.kts @@ -0,0 +1,7 @@ +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} diff --git a/build-logic/src/main/kotlin/libs.kt b/build-logic/src/main/kotlin/libs.kt new file mode 100644 index 0000000..1214d49 --- /dev/null +++ b/build-logic/src/main/kotlin/libs.kt @@ -0,0 +1,6 @@ +import org.gradle.accessors.dm.LibrariesForLibs +import org.gradle.api.Project +import org.gradle.kotlin.dsl.getByType + +val Project.libs: LibrariesForLibs + get() = rootProject.extensions.getByType() diff --git a/build-logic/src/main/kotlin/rainbow.base-conventions.gradle.kts b/build-logic/src/main/kotlin/rainbow.base-conventions.gradle.kts new file mode 100644 index 0000000..8717da3 --- /dev/null +++ b/build-logic/src/main/kotlin/rainbow.base-conventions.gradle.kts @@ -0,0 +1,87 @@ +plugins { + id("fabric-loom") +} + +version = properties["mod_version"]!! as String +group = properties["maven_group"]!! as String + +val archivesBaseName = properties["archives_base_name"]!! as String +val targetJavaVersion = 21 + +base { + archivesName = archivesBaseName +} + +repositories { + maven { + name = "ParchmentMC" + url = uri("https://maven.parchmentmc.org") + } + + maven { + name = "Jitpack" + url = uri("https://jitpack.io") + } + + maven { + name = "Open Collaboration" + url = uri("https://repo.opencollab.dev/main") + } +} + +dependencies { + minecraft(libs.minecraft) + mappings(loom.layered { + officialMojangMappings() + parchment(libs.parchment) + }) + + modImplementation(libs.fabric.loader) + modImplementation(libs.fabric.api) +} + +tasks { + processResources { + inputs.property("version", version) + inputs.property("supported_versions", libs.versions.minecraft.supported.get()) + inputs.property("loader_version", libs.versions.fabric.loader.get()) + filteringCharset = "UTF-8" + + filesMatching("fabric.mod.json") { + expand( + mapOf( + "version" to version, + "supported_versions" to libs.versions.minecraft.supported.get(), + "loader_version" to libs.versions.fabric.loader.get() + ) + ) + } + } + + jar { + from("LICENSE") { + rename { "${it}_${archivesBaseName}" } + } + } + + withType().configureEach { + options.encoding = "UTF-8" + options.release = targetJavaVersion + } +} + +java { + val javaVersion = JavaVersion.toVersion(targetJavaVersion) + if (JavaVersion.current() < javaVersion) { + toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion) + } + withSourcesJar() +} + +loom { + runs { + named("server") { + runDir = "run-server" + } + } +} diff --git a/build-logic/src/main/kotlin/rainbow.publish-conventions.gradle.kts b/build-logic/src/main/kotlin/rainbow.publish-conventions.gradle.kts new file mode 100644 index 0000000..a434afb --- /dev/null +++ b/build-logic/src/main/kotlin/rainbow.publish-conventions.gradle.kts @@ -0,0 +1,27 @@ +plugins { + `maven-publish` +} + +val archivesBaseName = properties["archives_base_name"]!! as String + +publishing { + repositories { + maven { + name = "geysermc" + url = uri( + when { + version.toString().endsWith("-SNAPSHOT") -> "https://repo.opencollab.dev/maven-snapshots" + else -> "https://repo.opencollab.dev/maven-releases" + } + ) + credentials(PasswordCredentials::class) + } + } + + publications { + register("publish", MavenPublication::class) { + artifactId = archivesBaseName + from(project.components["java"]) + } + } +} diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 3912291..0000000 --- a/build.gradle +++ /dev/null @@ -1,76 +0,0 @@ -plugins { - id 'fabric-loom' version '1.11-SNAPSHOT' - id 'maven-publish' -} - -repositories { - maven { - url = "https://maven.parchmentmc.org" - } -} - -dependencies { - minecraft "com.mojang:minecraft:${project.minecraft_version}" - mappings(loom.layered { - officialMojangMappings() - parchment("org.parchmentmc.data:parchment-${project.parchment_version}@zip") - }) - - modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" - - modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" -} - -processResources { - inputs.property "version", project.mod_version - inputs.property "supported_versions", project.supported_versions - inputs.property "loader_version", project.loader_version - filteringCharset "UTF-8" - - filesMatching("fabric.mod.json") { - expand "version": project.mod_version, - "supported_versions": project.supported_versions, - "loader_version": project.loader_version - } -} - -def targetJavaVersion = 21 -tasks.withType(JavaCompile).configureEach { - it.options.encoding = "UTF-8" - if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) { - it.options.release.set(targetJavaVersion) - } -} - -java { - def javaVersion = JavaVersion.toVersion(targetJavaVersion) - if (JavaVersion.current() < javaVersion) { - toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion) - } - withSourcesJar() -} - -jar { - from("LICENSE") { - rename { "${it}_${project.archivesBaseName}" } - } -} - -publishing { - publications { - create("mavenJava", MavenPublication) { - artifactId = project.archives_base_name - from components.java - } - } - - repositories {} -} - -loom { - runs { - named("server") { - runDir = "run-server" - } - } -} diff --git a/client/build.gradle.kts b/client/build.gradle.kts new file mode 100644 index 0000000..f490a7d --- /dev/null +++ b/client/build.gradle.kts @@ -0,0 +1,31 @@ +import net.fabricmc.loom.task.RemapJarTask + +plugins { + id("rainbow.base-conventions") + id("rainbow.publish-conventions") +} + +dependencies { + // Implement namedElements so IDEs can use it correctly, but include the remapped build + implementation(project(path = ":rainbow", configuration = "namedElements")) + include(project(":rainbow")) +} + +tasks { + val copyJarTask = register("copyRainbowClientJar") { + group = "build" + + val remapJarTask = getByName("remapJar") + dependsOn(remapJarTask) + + from(remapJarTask.archiveFile) + rename { + "Rainbow.jar" + } + into(project.layout.buildDirectory.file("libs")) + } + + named("build") { + dependsOn(copyJarTask) + } +} diff --git a/client/gradle.properties b/client/gradle.properties new file mode 100644 index 0000000..3e65201 --- /dev/null +++ b/client/gradle.properties @@ -0,0 +1 @@ +archives_base_name=rainbow-client diff --git a/client/src/main/java/org/geysermc/rainbow/client/MinecraftAssetResolver.java b/client/src/main/java/org/geysermc/rainbow/client/MinecraftAssetResolver.java new file mode 100644 index 0000000..7a4acab --- /dev/null +++ b/client/src/main/java/org/geysermc/rainbow/client/MinecraftAssetResolver.java @@ -0,0 +1,51 @@ +package org.geysermc.rainbow.client; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.item.ClientItem; +import net.minecraft.client.resources.model.EquipmentAssetManager; +import net.minecraft.client.resources.model.EquipmentClientInfo; +import net.minecraft.client.resources.model.ModelManager; +import net.minecraft.client.resources.model.ResolvedModel; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.world.item.equipment.EquipmentAsset; +import org.geysermc.rainbow.client.accessor.ResolvedModelAccessor; +import org.geysermc.rainbow.client.mixin.EntityRenderDispatcherAccessor; +import org.geysermc.rainbow.mapping.AssetResolver; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Optional; + +public class MinecraftAssetResolver implements AssetResolver { + private final ModelManager modelManager; + private final EquipmentAssetManager equipmentAssetManager; + private final ResourceManager resourceManager; + + public MinecraftAssetResolver(Minecraft minecraft) { + modelManager = minecraft.getModelManager(); + equipmentAssetManager = ((EntityRenderDispatcherAccessor) minecraft.getEntityRenderDispatcher()).getEquipmentAssets(); + resourceManager = minecraft.getResourceManager(); + } + + @Override + public Optional getResolvedModel(ResourceLocation location) { + return ((ResolvedModelAccessor) modelManager).rainbow$getResolvedModel(location); + } + + @Override + public Optional getClientItem(ResourceLocation location) { + return ((ResolvedModelAccessor) modelManager).rainbow$getClientItem(location); + } + + @Override + public Optional getEquipmentInfo(ResourceKey key) { + return Optional.of(equipmentAssetManager.get(key)); + } + + @Override + public InputStream openAsset(ResourceLocation location) throws IOException { + return resourceManager.open(location); + } +} diff --git a/client/src/main/java/org/geysermc/rainbow/client/MinecraftPackSerializer.java b/client/src/main/java/org/geysermc/rainbow/client/MinecraftPackSerializer.java new file mode 100644 index 0000000..f7fdbaa --- /dev/null +++ b/client/src/main/java/org/geysermc/rainbow/client/MinecraftPackSerializer.java @@ -0,0 +1,44 @@ +package org.geysermc.rainbow.client; + +import com.google.gson.JsonElement; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.JsonOps; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.core.HolderLookup; +import net.minecraft.resources.RegistryOps; +import org.geysermc.rainbow.CodecUtil; +import org.geysermc.rainbow.RainbowIO; +import org.geysermc.rainbow.mapping.PackSerializer; + +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.nio.file.Path; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +public class MinecraftPackSerializer implements PackSerializer { + private final HolderLookup.Provider registries; + + public MinecraftPackSerializer(Minecraft minecraft) { + registries = Objects.requireNonNull(minecraft.level).registryAccess(); + } + + @Override + public CompletableFuture saveJson(Codec codec, T object, Path path) { + DynamicOps ops = RegistryOps.create(JsonOps.INSTANCE, registries); + return CompletableFuture.runAsync(() -> RainbowIO.safeIO(() -> CodecUtil.trySaveJson(codec, object, path.resolveSibling(path.getFileName() + ".json"), ops)), + Util.backgroundExecutor().forName("PackSerializer-saveJson")); + } + + @Override + public CompletableFuture saveTexture(byte[] texture, Path path) { + return CompletableFuture.runAsync(() -> RainbowIO.safeIO(() -> { + CodecUtil.ensureDirectoryExists(path.getParent()); + try (OutputStream outputTexture = new FileOutputStream(path.toFile())) { + outputTexture.write(texture); + } + }), Util.backgroundExecutor().forName("PackSerializer-saveTexture")); + } +} diff --git a/client/src/main/java/org/geysermc/rainbow/client/PackManager.java b/client/src/main/java/org/geysermc/rainbow/client/PackManager.java new file mode 100644 index 0000000..172e576 --- /dev/null +++ b/client/src/main/java/org/geysermc/rainbow/client/PackManager.java @@ -0,0 +1,130 @@ +package org.geysermc.rainbow.client; + +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.SplashRenderer; +import net.minecraft.util.ProblemReporter; +import net.minecraft.util.RandomSource; +import net.minecraft.util.StringUtil; +import org.geysermc.rainbow.CodecUtil; +import org.geysermc.rainbow.Rainbow; +import org.geysermc.rainbow.RainbowIO; +import org.geysermc.rainbow.client.mixin.SplashRendererAccessor; +import org.geysermc.rainbow.client.render.MinecraftGeometryRenderer; +import org.geysermc.rainbow.pack.BedrockItem; +import org.geysermc.rainbow.pack.BedrockPack; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +public final class PackManager { + private static final List PACK_SUMMARY_COMMENTS = List.of("Use the custom item API v2 build!", "bugrock moment", "RORY", + "use !!plshelp", "*message was deleted*", "welcome to the internet!", "beep beep. boop boop?", "FROG", "it is frog day", "it is cat day!", + "eclipse will hear about this.", "you must now say the word 'frog' in the #general channel", "You Just Lost The Game", "you are now breathing manually", + "you are now blinking manually", "you're eligible for a free hug token! <3", "don't mind me!", "hissss", "Gayser and Floodgayte, my favourite plugins.", + "meow", "we'll be done here soon™", "got anything else to say?", "we're done now!", "this will be fixed by v6053", "expect it to be done within 180 business days!", + "any colour you like", "someone tell Mojang about this", "you can't unbake baked models, so we'll store the unbaked models", "soon fully datagen ready", + "packconverter when", "codecs ftw"); + private static final RandomSource RANDOM = RandomSource.create(); + + private static final Path EXPORT_DIRECTORY = FabricLoader.getInstance().getGameDir().resolve(Rainbow.MOD_ID); + private static final Path PACK_DIRECTORY = Path.of("pack"); + private static final Path MAPPINGS_FILE = Path.of("geyser_mappings"); + private static final Path PACK_ZIP_FILE = Path.of("pack.zip"); + private static final Path REPORT_FILE = Path.of("report.txt"); + + private Optional currentPack = Optional.empty(); + + public void startPack(String name) throws IOException { + if (currentPack.isPresent()) { + throw new IllegalStateException("Already started a pack (" + currentPack.get().name() + ")"); + } + + Path packDirectory = createPackDirectory(name); + BedrockPack pack = BedrockPack.builder(name, packDirectory.resolve(MAPPINGS_FILE), packDirectory.resolve(PACK_DIRECTORY), + new MinecraftPackSerializer(Minecraft.getInstance()), new MinecraftAssetResolver(Minecraft.getInstance())) + .withPackZipFile(packDirectory.resolve(PACK_ZIP_FILE)) + .withGeometryRenderer(MinecraftGeometryRenderer.INSTANCE) + .reportSuccesses() + .build(); + currentPack = Optional.of(pack); + } + + public void run(Consumer consumer) { + currentPack.ifPresent(consumer); + } + + public void runOrElse(Consumer consumer, Runnable runnable) { + currentPack.ifPresentOrElse(consumer, runnable); + } + + public Optional getExportPath() { + return currentPack.map(pack -> EXPORT_DIRECTORY.resolve(pack.name())); + } + + public boolean finish() { + currentPack.map(pack -> { + RainbowIO.safeIO(() -> Files.writeString(getExportPath().orElseThrow().resolve(REPORT_FILE), createPackSummary(pack))); + return pack.save(); + }).ifPresent(CompletableFuture::join); + boolean wasPresent = currentPack.isPresent(); + currentPack = Optional.empty(); + return wasPresent; + } + + private static String createPackSummary(BedrockPack pack) { + String problems = ((ProblemReporter.Collector) pack.getReporter()).getTreeReport(); + if (StringUtil.isBlank(problems)) { + problems = "Well that's odd... there's nothing here!"; + } + + Set bedrockItems = pack.getBedrockItems(); + //long attachables = bedrockItems.stream().filter(item -> item.attachableCreator().isPresent()).count(); + long geometries = bedrockItems.stream().filter(item -> item.geometryContext().geometry().isPresent()).count(); + long animations = bedrockItems.stream().filter(item -> item.geometryContext().animation().isPresent()).count(); + + return """ +-- PACK GENERATION REPORT -- +// %s + +Generated pack: %s +Mappings written: %d +Item texture atlas size: %d +Attachables tried to export: FIXME +Geometry files tried to export: %d +Animations tried to export: %d +Textures tried to export: FIXME + +-- MAPPING TREE REPORT -- +%s +""".formatted(randomSummaryComment(), pack.name(), pack.getMappings(), pack.getItemTextureAtlasSize(), + geometries, animations, problems); + } + + private static String randomSummaryComment() { + if (RANDOM.nextDouble() < 0.5) { + SplashRenderer splash = Minecraft.getInstance().getSplashManager().getSplash(); + if (splash == null) { + return "Undefined Undefined :("; + } + return ((SplashRendererAccessor) splash).getSplash(); + } + return randomBuiltinSummaryComment(); + } + + private static String randomBuiltinSummaryComment() { + return PACK_SUMMARY_COMMENTS.get(RANDOM.nextInt(PACK_SUMMARY_COMMENTS.size())); + } + + private static Path createPackDirectory(String name) throws IOException { + Path path = EXPORT_DIRECTORY.resolve(name); + CodecUtil.ensureDirectoryExists(path); + return path; + } +} diff --git a/src/main/java/org/geysermc/rainbow/Rainbow.java b/client/src/main/java/org/geysermc/rainbow/client/RainbowClient.java similarity index 50% rename from src/main/java/org/geysermc/rainbow/Rainbow.java rename to client/src/main/java/org/geysermc/rainbow/client/RainbowClient.java index f0f4eba..131846b 100644 --- a/src/main/java/org/geysermc/rainbow/Rainbow.java +++ b/client/src/main/java/org/geysermc/rainbow/client/RainbowClient.java @@ -1,22 +1,17 @@ -package org.geysermc.rainbow; +package org.geysermc.rainbow.client; -import com.mojang.logging.LogUtils; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.command.v2.ArgumentTypeRegistry; import net.minecraft.commands.synchronization.SingletonArgumentInfo; -import net.minecraft.resources.ResourceLocation; -import org.geysermc.rainbow.command.CommandSuggestionsArgumentType; -import org.geysermc.rainbow.command.PackGeneratorCommand; -import org.geysermc.rainbow.mapper.PackMapper; -import org.slf4j.Logger; +import org.geysermc.rainbow.Rainbow; +import org.geysermc.rainbow.RainbowIO; +import org.geysermc.rainbow.client.command.CommandSuggestionsArgumentType; +import org.geysermc.rainbow.client.command.PackGeneratorCommand; +import org.geysermc.rainbow.client.mapper.PackMapper; -public class Rainbow implements ClientModInitializer { - - public static final String MOD_ID = "rainbow"; - public static final String MOD_NAME = "Rainbow"; - public static final Logger LOGGER = LogUtils.getLogger(); +public class RainbowClient implements ClientModInitializer { private final PackManager packManager = new PackManager(); private final PackMapper packMapper = new PackMapper(packManager); @@ -27,15 +22,9 @@ public class Rainbow implements ClientModInitializer { ClientCommandRegistrationCallback.EVENT.register((dispatcher, buildContext) -> PackGeneratorCommand.register(dispatcher, packManager, packMapper)); ClientTickEvents.START_CLIENT_TICK.register(packMapper::tick); - ArgumentTypeRegistry.registerArgumentType(getModdedLocation("command_suggestions"), + ArgumentTypeRegistry.registerArgumentType(Rainbow.getModdedLocation("command_suggestions"), CommandSuggestionsArgumentType.class, SingletonArgumentInfo.contextFree(CommandSuggestionsArgumentType::new)); - } - public static ResourceLocation getModdedLocation(String path) { - return ResourceLocation.fromNamespaceAndPath(MOD_ID, path); - } - - public static String fileSafeResourceLocation(ResourceLocation location) { - return location.toString().replace(':', '.').replace('/', '_'); + RainbowIO.registerExceptionListener(new RainbowClientIOHandler()); } } diff --git a/client/src/main/java/org/geysermc/rainbow/client/RainbowClientIOHandler.java b/client/src/main/java/org/geysermc/rainbow/client/RainbowClientIOHandler.java new file mode 100644 index 0000000..f31f1f8 --- /dev/null +++ b/client/src/main/java/org/geysermc/rainbow/client/RainbowClientIOHandler.java @@ -0,0 +1,18 @@ +package org.geysermc.rainbow.client; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.toasts.SystemToast; +import net.minecraft.network.chat.Component; +import org.geysermc.rainbow.RainbowIO; + +import java.io.IOException; + +public class RainbowClientIOHandler implements RainbowIO.IOExceptionListener { + private static final SystemToast.SystemToastId TOAST_ID = new SystemToast.SystemToastId(); + + @Override + public void error(IOException exception) { + Minecraft.getInstance().getToastManager().addToast(new SystemToast(TOAST_ID, + Component.translatable("toast.rainbow.io_exception.title"), Component.translatable("toast.rainbow.io_exception.description"))); + } +} diff --git a/src/main/java/org/geysermc/rainbow/accessor/ResolvedModelAccessor.java b/client/src/main/java/org/geysermc/rainbow/client/accessor/ResolvedModelAccessor.java similarity index 93% rename from src/main/java/org/geysermc/rainbow/accessor/ResolvedModelAccessor.java rename to client/src/main/java/org/geysermc/rainbow/client/accessor/ResolvedModelAccessor.java index c8f0aae..c3a2cd3 100644 --- a/src/main/java/org/geysermc/rainbow/accessor/ResolvedModelAccessor.java +++ b/client/src/main/java/org/geysermc/rainbow/client/accessor/ResolvedModelAccessor.java @@ -1,4 +1,4 @@ -package org.geysermc.rainbow.accessor; +package org.geysermc.rainbow.client.accessor; import net.minecraft.client.renderer.item.ClientItem; import net.minecraft.client.resources.model.ResolvedModel; diff --git a/src/main/java/org/geysermc/rainbow/command/CommandSuggestionsArgumentType.java b/client/src/main/java/org/geysermc/rainbow/client/command/CommandSuggestionsArgumentType.java similarity index 98% rename from src/main/java/org/geysermc/rainbow/command/CommandSuggestionsArgumentType.java rename to client/src/main/java/org/geysermc/rainbow/client/command/CommandSuggestionsArgumentType.java index f1f812a..23add2a 100644 --- a/src/main/java/org/geysermc/rainbow/command/CommandSuggestionsArgumentType.java +++ b/client/src/main/java/org/geysermc/rainbow/client/command/CommandSuggestionsArgumentType.java @@ -1,4 +1,4 @@ -package org.geysermc.rainbow.command; +package org.geysermc.rainbow.client.command; import com.mojang.brigadier.StringReader; import com.mojang.brigadier.arguments.ArgumentType; diff --git a/src/main/java/org/geysermc/rainbow/command/PackGeneratorCommand.java b/client/src/main/java/org/geysermc/rainbow/client/command/PackGeneratorCommand.java similarity index 89% rename from src/main/java/org/geysermc/rainbow/command/PackGeneratorCommand.java rename to client/src/main/java/org/geysermc/rainbow/client/command/PackGeneratorCommand.java index 2743826..7b36272 100644 --- a/src/main/java/org/geysermc/rainbow/command/PackGeneratorCommand.java +++ b/client/src/main/java/org/geysermc/rainbow/client/command/PackGeneratorCommand.java @@ -1,4 +1,4 @@ -package org.geysermc.rainbow.command; +package org.geysermc.rainbow.client.command; import com.mojang.brigadier.Command; import com.mojang.brigadier.CommandDispatcher; @@ -10,9 +10,9 @@ import net.minecraft.network.chat.ClickEvent; import net.minecraft.network.chat.Component; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.item.ItemStack; -import org.geysermc.rainbow.PackManager; -import org.geysermc.rainbow.mapper.InventoryMapper; -import org.geysermc.rainbow.mapper.PackMapper; +import org.geysermc.rainbow.client.PackManager; +import org.geysermc.rainbow.client.mapper.InventoryMapper; +import org.geysermc.rainbow.client.mapper.PackMapper; import org.geysermc.rainbow.pack.BedrockPack; import java.nio.file.Path; @@ -113,14 +113,13 @@ public class PackGeneratorCommand { .then(ClientCommandManager.literal("finish") .executes(context -> { Optional exportPath = packManager.getExportPath(); - packManager.finish().ifPresentOrElse(success -> { - if (!success) { - context.getSource().sendError(Component.translatable("commands.rainbow.pack_finished_error")); - } else { - context.getSource().sendFeedback(Component.translatable("commands.rainbow.pack_finished_successfully") - .withStyle(style -> style.withUnderlined(true).withClickEvent(new ClickEvent.OpenFile(exportPath.orElseThrow())))); - } - }, () -> context.getSource().sendError(NO_PACK_CREATED)); + if (packManager.finish()) { + // TODO error when exporting fails + context.getSource().sendFeedback(Component.translatable("commands.rainbow.pack_finished_successfully") + .withStyle(style -> style.withUnderlined(true).withClickEvent(new ClickEvent.OpenFile(exportPath.orElseThrow())))); + } else { + context.getSource().sendError(NO_PACK_CREATED); + } return 0; }) ) diff --git a/src/main/java/org/geysermc/rainbow/mapper/CustomItemProvider.java b/client/src/main/java/org/geysermc/rainbow/client/mapper/CustomItemProvider.java similarity index 88% rename from src/main/java/org/geysermc/rainbow/mapper/CustomItemProvider.java rename to client/src/main/java/org/geysermc/rainbow/client/mapper/CustomItemProvider.java index 8a8c7ad..8708f5d 100644 --- a/src/main/java/org/geysermc/rainbow/mapper/CustomItemProvider.java +++ b/client/src/main/java/org/geysermc/rainbow/client/mapper/CustomItemProvider.java @@ -1,4 +1,4 @@ -package org.geysermc.rainbow.mapper; +package org.geysermc.rainbow.client.mapper; import net.minecraft.client.multiplayer.ClientPacketListener; import net.minecraft.client.player.LocalPlayer; diff --git a/src/main/java/org/geysermc/rainbow/mapper/InventoryMapper.java b/client/src/main/java/org/geysermc/rainbow/client/mapper/InventoryMapper.java similarity index 93% rename from src/main/java/org/geysermc/rainbow/mapper/InventoryMapper.java rename to client/src/main/java/org/geysermc/rainbow/client/mapper/InventoryMapper.java index 5cfde28..4bac740 100644 --- a/src/main/java/org/geysermc/rainbow/mapper/InventoryMapper.java +++ b/client/src/main/java/org/geysermc/rainbow/client/mapper/InventoryMapper.java @@ -1,4 +1,4 @@ -package org.geysermc.rainbow.mapper; +package org.geysermc.rainbow.client.mapper; import net.minecraft.client.multiplayer.ClientPacketListener; import net.minecraft.client.player.LocalPlayer; diff --git a/src/main/java/org/geysermc/rainbow/mapper/ItemSuggestionProvider.java b/client/src/main/java/org/geysermc/rainbow/client/mapper/ItemSuggestionProvider.java similarity index 97% rename from src/main/java/org/geysermc/rainbow/mapper/ItemSuggestionProvider.java rename to client/src/main/java/org/geysermc/rainbow/client/mapper/ItemSuggestionProvider.java index fea136a..33ef127 100644 --- a/src/main/java/org/geysermc/rainbow/mapper/ItemSuggestionProvider.java +++ b/client/src/main/java/org/geysermc/rainbow/client/mapper/ItemSuggestionProvider.java @@ -1,4 +1,4 @@ -package org.geysermc.rainbow.mapper; +package org.geysermc.rainbow.client.mapper; import net.minecraft.client.multiplayer.ClientPacketListener; import net.minecraft.client.player.LocalPlayer; diff --git a/src/main/java/org/geysermc/rainbow/mapper/PackMapper.java b/client/src/main/java/org/geysermc/rainbow/client/mapper/PackMapper.java similarity index 94% rename from src/main/java/org/geysermc/rainbow/mapper/PackMapper.java rename to client/src/main/java/org/geysermc/rainbow/client/mapper/PackMapper.java index ce9666a..cb56500 100644 --- a/src/main/java/org/geysermc/rainbow/mapper/PackMapper.java +++ b/client/src/main/java/org/geysermc/rainbow/client/mapper/PackMapper.java @@ -1,14 +1,13 @@ -package org.geysermc.rainbow.mapper; +package org.geysermc.rainbow.client.mapper; import net.minecraft.client.Minecraft; import net.minecraft.client.multiplayer.ClientPacketListener; import net.minecraft.client.player.LocalPlayer; import net.minecraft.network.chat.Component; -import org.geysermc.rainbow.PackManager; +import org.geysermc.rainbow.client.PackManager; import org.geysermc.rainbow.pack.BedrockPack; import java.util.Objects; -import java.util.Optional; public class PackMapper { private final PackManager packManager; diff --git a/src/main/java/org/geysermc/rainbow/mixin/EntityRenderDispatcherAccessor.java b/client/src/main/java/org/geysermc/rainbow/client/mixin/EntityRenderDispatcherAccessor.java similarity index 89% rename from src/main/java/org/geysermc/rainbow/mixin/EntityRenderDispatcherAccessor.java rename to client/src/main/java/org/geysermc/rainbow/client/mixin/EntityRenderDispatcherAccessor.java index 6dcc361..2267bd9 100644 --- a/src/main/java/org/geysermc/rainbow/mixin/EntityRenderDispatcherAccessor.java +++ b/client/src/main/java/org/geysermc/rainbow/client/mixin/EntityRenderDispatcherAccessor.java @@ -1,4 +1,4 @@ -package org.geysermc.rainbow.mixin; +package org.geysermc.rainbow.client.mixin; import net.minecraft.client.renderer.entity.EntityRenderDispatcher; import net.minecraft.client.resources.model.EquipmentAssetManager; diff --git a/src/main/java/org/geysermc/rainbow/mixin/GuiItemRenderStateMixin.java b/client/src/main/java/org/geysermc/rainbow/client/mixin/GuiItemRenderStateMixin.java similarity index 92% rename from src/main/java/org/geysermc/rainbow/mixin/GuiItemRenderStateMixin.java rename to client/src/main/java/org/geysermc/rainbow/client/mixin/GuiItemRenderStateMixin.java index 70e1b9d..8b76205 100644 --- a/src/main/java/org/geysermc/rainbow/mixin/GuiItemRenderStateMixin.java +++ b/client/src/main/java/org/geysermc/rainbow/client/mixin/GuiItemRenderStateMixin.java @@ -1,4 +1,4 @@ -package org.geysermc.rainbow.mixin; +package org.geysermc.rainbow.client.mixin; import net.minecraft.client.gui.render.state.GuiItemRenderState; import net.minecraft.client.gui.render.state.ScreenArea; diff --git a/src/main/java/org/geysermc/rainbow/mixin/ModelManagerMixin.java b/client/src/main/java/org/geysermc/rainbow/client/mixin/ModelManagerMixin.java similarity index 96% rename from src/main/java/org/geysermc/rainbow/mixin/ModelManagerMixin.java rename to client/src/main/java/org/geysermc/rainbow/client/mixin/ModelManagerMixin.java index f3d92dd..acd0a57 100644 --- a/src/main/java/org/geysermc/rainbow/mixin/ModelManagerMixin.java +++ b/client/src/main/java/org/geysermc/rainbow/client/mixin/ModelManagerMixin.java @@ -1,4 +1,4 @@ -package org.geysermc.rainbow.mixin; +package org.geysermc.rainbow.client.mixin; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; @@ -9,7 +9,7 @@ import net.minecraft.client.resources.model.ModelManager; import net.minecraft.client.resources.model.ResolvedModel; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.resources.PreparableReloadListener; -import org.geysermc.rainbow.accessor.ResolvedModelAccessor; +import org.geysermc.rainbow.client.accessor.ResolvedModelAccessor; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; diff --git a/src/main/java/org/geysermc/rainbow/mixin/PictureInPictureRendererAccessor.java b/client/src/main/java/org/geysermc/rainbow/client/mixin/PictureInPictureRendererAccessor.java similarity index 88% rename from src/main/java/org/geysermc/rainbow/mixin/PictureInPictureRendererAccessor.java rename to client/src/main/java/org/geysermc/rainbow/client/mixin/PictureInPictureRendererAccessor.java index 4c6d5e4..4a51697 100644 --- a/src/main/java/org/geysermc/rainbow/mixin/PictureInPictureRendererAccessor.java +++ b/client/src/main/java/org/geysermc/rainbow/client/mixin/PictureInPictureRendererAccessor.java @@ -1,4 +1,4 @@ -package org.geysermc.rainbow.mixin; +package org.geysermc.rainbow.client.mixin; import com.mojang.blaze3d.textures.GpuTexture; import net.minecraft.client.gui.render.pip.PictureInPictureRenderer; diff --git a/src/main/java/org/geysermc/rainbow/mixin/PictureInPictureRendererMixin.java b/client/src/main/java/org/geysermc/rainbow/client/mixin/PictureInPictureRendererMixin.java similarity index 90% rename from src/main/java/org/geysermc/rainbow/mixin/PictureInPictureRendererMixin.java rename to client/src/main/java/org/geysermc/rainbow/client/mixin/PictureInPictureRendererMixin.java index a665dee..3329a71 100644 --- a/src/main/java/org/geysermc/rainbow/mixin/PictureInPictureRendererMixin.java +++ b/client/src/main/java/org/geysermc/rainbow/client/mixin/PictureInPictureRendererMixin.java @@ -1,8 +1,8 @@ -package org.geysermc.rainbow.mixin; +package org.geysermc.rainbow.client.mixin; import com.mojang.blaze3d.textures.GpuTexture; import net.minecraft.client.gui.render.pip.PictureInPictureRenderer; -import org.geysermc.rainbow.render.PictureInPictureCopyRenderer; +import org.geysermc.rainbow.client.render.PictureInPictureCopyRenderer; import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; diff --git a/src/main/java/org/geysermc/rainbow/mixin/SplashRendererAccessor.java b/client/src/main/java/org/geysermc/rainbow/client/mixin/SplashRendererAccessor.java similarity index 85% rename from src/main/java/org/geysermc/rainbow/mixin/SplashRendererAccessor.java rename to client/src/main/java/org/geysermc/rainbow/client/mixin/SplashRendererAccessor.java index 95fcb8e..0f5a4a3 100644 --- a/src/main/java/org/geysermc/rainbow/mixin/SplashRendererAccessor.java +++ b/client/src/main/java/org/geysermc/rainbow/client/mixin/SplashRendererAccessor.java @@ -1,4 +1,4 @@ -package org.geysermc.rainbow.mixin; +package org.geysermc.rainbow.client.mixin; import net.minecraft.client.gui.components.SplashRenderer; import org.spongepowered.asm.mixin.Mixin; diff --git a/src/main/java/org/geysermc/rainbow/mapping/geometry/GeometryRenderer.java b/client/src/main/java/org/geysermc/rainbow/client/render/MinecraftGeometryRenderer.java similarity index 70% rename from src/main/java/org/geysermc/rainbow/mapping/geometry/GeometryRenderer.java rename to client/src/main/java/org/geysermc/rainbow/client/render/MinecraftGeometryRenderer.java index f215134..b86ba75 100644 --- a/src/main/java/org/geysermc/rainbow/mapping/geometry/GeometryRenderer.java +++ b/client/src/main/java/org/geysermc/rainbow/client/render/MinecraftGeometryRenderer.java @@ -1,4 +1,4 @@ -package org.geysermc.rainbow.mapping.geometry; +package org.geysermc.rainbow.client.render; import com.mojang.blaze3d.buffers.GpuBuffer; import com.mojang.blaze3d.platform.NativeImage; @@ -14,20 +14,19 @@ import net.minecraft.client.gui.render.state.pip.OversizedItemRenderState; import net.minecraft.client.renderer.item.TrackingItemStackRenderState; import net.minecraft.world.item.ItemDisplayContext; import net.minecraft.world.item.ItemStack; -import org.geysermc.rainbow.CodecUtil; -import org.geysermc.rainbow.mixin.PictureInPictureRendererAccessor; -import org.geysermc.rainbow.render.PictureInPictureCopyRenderer; +import org.geysermc.rainbow.mapping.geometry.GeometryRenderer; +import org.geysermc.rainbow.client.mixin.PictureInPictureRendererAccessor; import org.joml.Matrix3x2fStack; -import java.io.IOException; -import java.nio.file.Path; import java.util.Objects; // TODO maybe just use this even for normal 2D items, not sure, could be useful for composite models and stuff // TODO output in a size bedrock likes -public class GeometryRenderer { +public class MinecraftGeometryRenderer implements GeometryRenderer { + public static final MinecraftGeometryRenderer INSTANCE = new MinecraftGeometryRenderer(); - public static void render(ItemStack stack, Path path) { + @Override + public NativeImage render(ItemStack stack) { TrackingItemStackRenderState itemRenderState = new TrackingItemStackRenderState(); Minecraft.getInstance().getItemModelResolver().updateForTopItem(itemRenderState, stack, ItemDisplayContext.GUI, null, null, 0); itemRenderState.setOversizedInGui(true); @@ -41,12 +40,12 @@ public class GeometryRenderer { //noinspection DataFlowIssue ((PictureInPictureCopyRenderer) itemRenderer).rainbow$allowTextureCopy(); itemRenderer.prepare(oversizedRenderState, new GuiRenderState(), 4); - writeAsPNG(path, ((PictureInPictureRendererAccessor) itemRenderer).getTexture()); + return writeToImage(((PictureInPictureRendererAccessor) itemRenderer).getTexture()); } } - // Simplified TextureUtil#writeAsPNG with some modifications to flip the image and just generate it at full size - private static void writeAsPNG(Path path, GpuTexture texture) { + // Simplified TextureUtil#writeAsPNG with some modifications to just write to a NativeImage, flip the image and just generate it at full size + private static NativeImage writeToImage(GpuTexture texture) { RenderSystem.assertOnRenderThread(); int width = texture.getWidth(0); int height = texture.getHeight(0); @@ -55,25 +54,21 @@ public class GeometryRenderer { GpuBuffer buffer = RenderSystem.getDevice().createBuffer(() -> "Texture output buffer", GpuBuffer.USAGE_COPY_DST | GpuBuffer.USAGE_MAP_READ, bufferSize); CommandEncoder commandEncoder = RenderSystem.getDevice().createCommandEncoder(); + NativeImage image = new NativeImage(width, height, false); Runnable writer = () -> { try (GpuBuffer.MappedView mappedView = commandEncoder.mapBuffer(buffer, true, false)) { - try (NativeImage nativeImage = new NativeImage(width, height, false)) { - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - int colour = mappedView.data().getInt((x + y * width) * texture.getFormat().pixelSize()); - nativeImage.setPixelABGR(x, height - y - 1, colour); - } + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int colour = mappedView.data().getInt((x + y * width) * texture.getFormat().pixelSize()); + image.setPixelABGR(x, height - y - 1, colour); } - - CodecUtil.ensureDirectoryExists(path.getParent()); - nativeImage.writeToFile(path); - } catch (IOException var19) { - // TODO } } buffer.close(); }; commandEncoder.copyTextureToBuffer(texture, buffer, 0, writer, 0); + + return image; } } diff --git a/src/main/java/org/geysermc/rainbow/render/PictureInPictureCopyRenderer.java b/client/src/main/java/org/geysermc/rainbow/client/render/PictureInPictureCopyRenderer.java similarity index 66% rename from src/main/java/org/geysermc/rainbow/render/PictureInPictureCopyRenderer.java rename to client/src/main/java/org/geysermc/rainbow/client/render/PictureInPictureCopyRenderer.java index df2a497..d4c6da8 100644 --- a/src/main/java/org/geysermc/rainbow/render/PictureInPictureCopyRenderer.java +++ b/client/src/main/java/org/geysermc/rainbow/client/render/PictureInPictureCopyRenderer.java @@ -1,4 +1,4 @@ -package org.geysermc.rainbow.render; +package org.geysermc.rainbow.client.render; public interface PictureInPictureCopyRenderer { diff --git a/src/main/resources/assets/rainbow/icon.png b/client/src/main/resources/assets/rainbow/icon.png similarity index 100% rename from src/main/resources/assets/rainbow/icon.png rename to client/src/main/resources/assets/rainbow/icon.png diff --git a/src/main/resources/assets/rainbow/lang/en_us.json b/client/src/main/resources/assets/rainbow/lang/en_us.json similarity index 87% rename from src/main/resources/assets/rainbow/lang/en_us.json rename to client/src/main/resources/assets/rainbow/lang/en_us.json index 7fe4de1..cc89808 100644 --- a/src/main/resources/assets/rainbow/lang/en_us.json +++ b/client/src/main/resources/assets/rainbow/lang/en_us.json @@ -13,5 +13,7 @@ "commands.rainbow.pack_created": "Created pack with name %s", "commands.rainbow.pack_finished_error": "Errors occurred whilst writing the pack to disk!", "commands.rainbow.pack_finished_successfully": "Wrote pack to disk", - "commands.rainbow.stopped_automatic_mapping": "Stopped automatic mapping of custom items" + "commands.rainbow.stopped_automatic_mapping": "Stopped automatic mapping of custom items", + "toast.rainbow.io_exception.description": "Please check your game logs for more information", + "toast.rainbow.io_exception.title": "A filesystem error occurred in Rainbow!" } \ No newline at end of file diff --git a/src/main/resources/fabric.mod.json b/client/src/main/resources/fabric.mod.json similarity index 88% rename from src/main/resources/fabric.mod.json rename to client/src/main/resources/fabric.mod.json index a3e78ce..0dd34a6 100644 --- a/src/main/resources/fabric.mod.json +++ b/client/src/main/resources/fabric.mod.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "id": "rainbow", + "id": "rainbow-client", "version": "${version}", "name": "Rainbow", "description": "Rainbow is a mod to generate Geyser item mappings and bedrock resourcepacks for use with Geyser's custom item API (v2)", @@ -17,11 +17,11 @@ "environment": "client", "entrypoints": { "client": [ - "org.geysermc.rainbow.Rainbow" + "org.geysermc.rainbow.client.RainbowClient" ] }, "mixins": [ - "rainbow.mixins.json" + "rainbow-client.mixins.json" ], "depends": { "fabricloader": ">=${loader_version}", diff --git a/src/main/resources/rainbow.mixins.json b/client/src/main/resources/rainbow-client.mixins.json similarity index 63% rename from src/main/resources/rainbow.mixins.json rename to client/src/main/resources/rainbow-client.mixins.json index 2d5bb60..a55d09a 100644 --- a/src/main/resources/rainbow.mixins.json +++ b/client/src/main/resources/rainbow-client.mixins.json @@ -1,19 +1,15 @@ { "required": true, "minVersion": "0.8", - "package": "org.geysermc.rainbow.mixin", + "package": "org.geysermc.rainbow.client.mixin", "compatibilityLevel": "JAVA_21", - "mixins": [], "client": [ "EntityRenderDispatcherAccessor", "GuiItemRenderStateMixin", - "LateBoundIdMapperAccessor", "ModelManagerMixin", "PictureInPictureRendererAccessor", "PictureInPictureRendererMixin", - "RangeSelectItemModelAccessor", - "SplashRendererAccessor", - "TextureSlotsAccessor" + "SplashRendererAccessor" ], "injectors": { "defaultRequire": 1 diff --git a/datagen/build.gradle.kts b/datagen/build.gradle.kts new file mode 100644 index 0000000..d0fae2f --- /dev/null +++ b/datagen/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("rainbow.base-conventions") + id("rainbow.publish-conventions") +} + +dependencies { + implementation(project(path = ":rainbow", configuration = "namedElements")) +} + +loom { + accessWidenerPath = file("src/main/resources/rainbow-datagen.accesswidener") +} diff --git a/datagen/gradle.properties b/datagen/gradle.properties new file mode 100644 index 0000000..6ea0225 --- /dev/null +++ b/datagen/gradle.properties @@ -0,0 +1 @@ +archives_base_name=rainbow-datagen diff --git a/datagen/src/main/java/org/geysermc/rainbow/datagen/ClientPackLoader.java b/datagen/src/main/java/org/geysermc/rainbow/datagen/ClientPackLoader.java new file mode 100644 index 0000000..2b62fa0 --- /dev/null +++ b/datagen/src/main/java/org/geysermc/rainbow/datagen/ClientPackLoader.java @@ -0,0 +1,67 @@ +package org.geysermc.rainbow.datagen; + +import joptsimple.ArgumentAcceptingOptionSpec; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import joptsimple.OptionSpec; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.resources.ClientPackSource; +import net.minecraft.client.resources.IndexedAssetSource; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.repository.Pack; +import net.minecraft.server.packs.repository.PackRepository; +import net.minecraft.server.packs.resources.CloseableResourceManager; +import net.minecraft.server.packs.resources.MultiPackResourceManager; +import net.minecraft.world.level.validation.DirectoryValidator; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +class ClientPackLoader { + + static ClientPackSource loadClientPackSource() { + OptionParser parser = new OptionParser(); + OptionSpec assetsDirSpec = parser.accepts("assetsDir").withRequiredArg().ofType(File.class); + OptionSpec assetIndexSpec = parser.accepts("assetIndex").withRequiredArg(); + parser.allowsUnrecognizedOptions(); + OptionSet parsed = parser.parse(FabricLoader.getInstance().getLaunchArguments(false)); + + return new ClientPackSource(getExternalAssetSource(parseArgument(parsed, assetIndexSpec), parseArgument(parsed, assetsDirSpec)), + new DirectoryValidator(path -> false)); + } + + static CompletableFuture openClientResources() { + return CompletableFuture.supplyAsync(() -> { + ClientPackSource packSource = loadClientPackSource(); + PackRepository repository = new PackRepository(packSource); + repository.reload(); + return new MultiPackResourceManager(PackType.CLIENT_RESOURCES, repository.getAvailablePacks().stream() + .map(Pack::open) + .toList()); + }); + } + + private static Path getExternalAssetSource(String assetIndex, File assetDirectory) { + return assetIndex == null ? assetDirectory.toPath() : IndexedAssetSource.createIndexFs(assetDirectory.toPath(), assetIndex); + } + + // From Mojang's client/Main.java + @Nullable + private static T parseArgument(OptionSet set, OptionSpec spec) { + try { + return set.valueOf(spec); + } catch (Throwable exception) { + if (spec instanceof ArgumentAcceptingOptionSpec argumentAccepting) { + List list = argumentAccepting.defaultValues(); + if (!list.isEmpty()) { + return list.getFirst(); + } + } + + throw exception; + } + } +} diff --git a/datagen/src/main/java/org/geysermc/rainbow/datagen/RainbowModelProvider.java b/datagen/src/main/java/org/geysermc/rainbow/datagen/RainbowModelProvider.java new file mode 100644 index 0000000..b6496d2 --- /dev/null +++ b/datagen/src/main/java/org/geysermc/rainbow/datagen/RainbowModelProvider.java @@ -0,0 +1,202 @@ +package org.geysermc.rainbow.datagen; + +import com.google.common.hash.HashCode; +import com.mojang.serialization.Codec; +import net.fabricmc.fabric.api.client.datagen.v1.provider.FabricModelProvider; +import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput; +import net.minecraft.Util; +import net.minecraft.client.data.models.model.ModelInstance; +import net.minecraft.client.renderer.block.model.BlockModel; +import net.minecraft.client.renderer.block.model.ItemModelGenerator; +import net.minecraft.client.renderer.item.ClientItem; +import net.minecraft.client.resources.model.EquipmentClientInfo; +import net.minecraft.client.resources.model.ResolvedModel; +import net.minecraft.client.resources.model.UnbakedModel; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.component.DataComponentPatch; +import net.minecraft.data.CachedOutput; +import net.minecraft.data.DataProvider; +import net.minecraft.data.PackOutput; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.util.ProblemReporter; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.equipment.EquipmentAsset; +import org.geysermc.rainbow.Rainbow; +import org.geysermc.rainbow.RainbowIO; +import org.geysermc.rainbow.mapping.AssetResolver; +import org.geysermc.rainbow.mapping.PackSerializer; +import org.geysermc.rainbow.pack.BedrockPack; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +public abstract class RainbowModelProvider extends FabricModelProvider { + private static final Logger PROBLEM_LOGGER = LoggerFactory.getLogger(Rainbow.MOD_ID); + + private final CompletableFuture registries; + private final Map, EquipmentClientInfo> equipmentInfos; + private final Path outputRoot; + private Map itemInfos; + private Map models; + + protected RainbowModelProvider(FabricDataOutput output, CompletableFuture registries, + Map, EquipmentClientInfo> equipmentInfos, ResourceLocation outputRoot) { + super(output); + this.registries = registries; + this.equipmentInfos = equipmentInfos; + this.outputRoot = output.createPathProvider(PackOutput.Target.RESOURCE_PACK, outputRoot.getPath()) + .file(outputRoot, "").getParent(); + } + + protected RainbowModelProvider(FabricDataOutput output, CompletableFuture registries, + Map, EquipmentClientInfo> equipmentInfos) { + this(output, registries, equipmentInfos, ResourceLocation.withDefaultNamespace("bedrock")); + } + + protected RainbowModelProvider(FabricDataOutput output, CompletableFuture registries) { + this(output, registries, Map.of()); + } + + @Override + public @NotNull CompletableFuture run(CachedOutput output) { + CompletableFuture vanillaModels = super.run(output); + + CompletableFuture bedrockPack = ClientPackLoader.openClientResources() + .thenCompose(resourceManager -> registries.thenApply(registries -> { + try (resourceManager) { + BedrockPack pack = createBedrockPack(outputRoot, new Serializer(output, registries), + new DatagenResolver(resourceManager, equipmentInfos, itemInfos, models)).build(); + + for (Item item : itemInfos.keySet()) { + pack.map(getVanillaItem(item).builtInRegistryHolder(), getVanillaDataComponentPatch(item)); + } + return pack; + } + })); + + return CompletableFuture.allOf(vanillaModels, bedrockPack.thenCompose(BedrockPack::save)); + } + + protected BedrockPack.Builder createBedrockPack(Path outputRoot, PackSerializer serializer, AssetResolver resolver) { + return BedrockPack.builder("rainbow", outputRoot.resolve("geyser_mappings.json"), outputRoot.resolve("pack"), serializer, resolver) + .withReporter(path -> new ProblemReporter.ScopedCollector(path, PROBLEM_LOGGER)); + } + + protected abstract Item getVanillaItem(Item modded); + + protected DataComponentPatch getVanillaDataComponentPatch(Item modded) { + DataComponentPatch.Builder builder = DataComponentPatch.builder(); + modded.components().forEach(builder::set); + return builder.build(); + } + + @ApiStatus.Internal + public void setItemInfos(Map itemInfos) { + this.itemInfos = itemInfos; + } + + @ApiStatus.Internal + public void setModels(Map models) { + this.models = models; + } + + private record Serializer(CachedOutput output, HolderLookup.Provider registries) implements PackSerializer { + + @Override + public CompletableFuture saveJson(Codec codec, T object, Path path) { + return DataProvider.saveStable(output, registries, codec, object, path); + } + + @Override + public CompletableFuture saveTexture(byte[] texture, Path path) { + return CompletableFuture.runAsync(() -> { + try { + output.writeIfNeeded(path, texture, HashCode.fromBytes(texture)); + } catch (IOException exception) { + LOGGER.error("Failed to save texture to {}", path, exception); + } + }, Util.backgroundExecutor().forName("PackSerializer-saveTexture")); + } + } + + private static class DatagenResolver implements AssetResolver { + private final ResourceManager resourceManager; + private final Map, EquipmentClientInfo> equipmentInfos; + private final Map itemInfos; + private final Map models; + private final Map> resolvedModelCache = new HashMap<>(); + + private DatagenResolver(ResourceManager resourceManager, Map, EquipmentClientInfo> equipmentInfos, + Map itemInfos, Map models) { + this.resourceManager = resourceManager; + this.equipmentInfos = equipmentInfos; + this.itemInfos = new HashMap<>(); + for (Map.Entry entry : itemInfos.entrySet()) { + this.itemInfos.put(entry.getKey().builtInRegistryHolder().key().location(), entry.getValue()); + } + this.models = models; + } + + @Override + public Optional getResolvedModel(ResourceLocation location) { + return resolvedModelCache.computeIfAbsent(location, key -> Optional.ofNullable(models.get(location)) + .map(instance -> BlockModel.fromStream(new StringReader(instance.get().toString()))) + .or(() -> { + if (location.equals(ItemModelGenerator.GENERATED_ITEM_MODEL_ID)) { + return Optional.of(new ItemModelGenerator()); + } + return Optional.empty(); + }) + .or(() -> RainbowIO.safeIO(() -> { + try (BufferedReader reader = resourceManager.openAsReader(location.withPrefix("models/").withSuffix(".json"))) { + return BlockModel.fromStream(reader); + } + })) + .map(model -> new ResolvedModel() { + @Override + public @NotNull UnbakedModel wrapped() { + return model; + } + + @Override + public @Nullable ResolvedModel parent() { + return Optional.ofNullable(model.parent()).flatMap(parent -> getResolvedModel(parent)).orElse(null); + } + + @Override + public @NotNull String debugName() { + return location.toString(); + } + })); + } + + @Override + public Optional getClientItem(ResourceLocation location) { + return Optional.ofNullable(itemInfos.get(location)); + } + + @Override + public Optional getEquipmentInfo(ResourceKey key) { + return Optional.ofNullable(equipmentInfos.get(key)); + } + + @Override + public InputStream openAsset(ResourceLocation location) throws IOException { + return resourceManager.open(location); + } + } +} diff --git a/datagen/src/main/java/org/geysermc/rainbow/datagen/mixin/ItemInfoCollectorAccessor.java b/datagen/src/main/java/org/geysermc/rainbow/datagen/mixin/ItemInfoCollectorAccessor.java new file mode 100644 index 0000000..b9450ae --- /dev/null +++ b/datagen/src/main/java/org/geysermc/rainbow/datagen/mixin/ItemInfoCollectorAccessor.java @@ -0,0 +1,16 @@ +package org.geysermc.rainbow.datagen.mixin; + +import net.minecraft.client.data.models.ModelProvider; +import net.minecraft.client.renderer.item.ClientItem; +import net.minecraft.world.item.Item; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.Map; + +@Mixin(ModelProvider.ItemInfoCollector.class) +public interface ItemInfoCollectorAccessor { + + @Accessor + Map getItemInfos(); +} diff --git a/datagen/src/main/java/org/geysermc/rainbow/datagen/mixin/ModelProviderMixin.java b/datagen/src/main/java/org/geysermc/rainbow/datagen/mixin/ModelProviderMixin.java new file mode 100644 index 0000000..69842c4 --- /dev/null +++ b/datagen/src/main/java/org/geysermc/rainbow/datagen/mixin/ModelProviderMixin.java @@ -0,0 +1,26 @@ +package org.geysermc.rainbow.datagen.mixin; + +import com.llamalad7.mixinextras.sugar.Local; +import net.minecraft.client.data.models.ModelProvider; +import net.minecraft.data.CachedOutput; +import net.minecraft.data.DataProvider; +import org.geysermc.rainbow.datagen.RainbowModelProvider; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.concurrent.CompletableFuture; + +@Mixin(ModelProvider.class) +public abstract class ModelProviderMixin implements DataProvider { + + @Inject(method = "run", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/data/models/BlockModelGenerators;run()V")) + public void setItemInfosInRainbowModelProvider(CachedOutput output, CallbackInfoReturnable> callbackInfoReturnable, + @Local ModelProvider.ItemInfoCollector itemInfoCollector, @Local ModelProvider.SimpleModelCollector simpleModelCollector) { + if ((Object) this instanceof RainbowModelProvider rainbowModelProvider) { + rainbowModelProvider.setItemInfos(((ItemInfoCollectorAccessor) itemInfoCollector).getItemInfos()); + rainbowModelProvider.setModels(((SimpleModelCollectorAccessor) simpleModelCollector).getModels()); + } + } +} diff --git a/datagen/src/main/java/org/geysermc/rainbow/datagen/mixin/SimpleModelCollectorAccessor.java b/datagen/src/main/java/org/geysermc/rainbow/datagen/mixin/SimpleModelCollectorAccessor.java new file mode 100644 index 0000000..0f85752 --- /dev/null +++ b/datagen/src/main/java/org/geysermc/rainbow/datagen/mixin/SimpleModelCollectorAccessor.java @@ -0,0 +1,16 @@ +package org.geysermc.rainbow.datagen.mixin; + +import net.minecraft.client.data.models.ModelProvider; +import net.minecraft.client.data.models.model.ModelInstance; +import net.minecraft.resources.ResourceLocation; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.Map; + +@Mixin(ModelProvider.SimpleModelCollector.class) +public interface SimpleModelCollectorAccessor { + + @Accessor + Map getModels(); +} diff --git a/datagen/src/main/resources/fabric.mod.json b/datagen/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..95fa4bd --- /dev/null +++ b/datagen/src/main/resources/fabric.mod.json @@ -0,0 +1,33 @@ +{ + "schemaVersion": 1, + "id": "rainbow-datagen", + "version": "${version}", + "name": "Rainbow", + "description": "Rainbow is a mod to generate Geyser item mappings and bedrock resourcepacks for use with Geyser's custom item API (v2)", + "authors": [ + "GeyserMC contributors" + ], + "contact": { + "homepage": "https://github.com/GeyserMC/rainbow", + "issues": "https://github.com/GeyserMC/rainbow/issues", + "sources": "https://github.com/GeyserMC/rainbow" + }, + "license": "MIT", + "environment": "client", + "mixins": [ + "rainbow-datagen.mixins.json" + ], + "accessWidener": "rainbow-datagen.accesswidener", + "depends": { + "fabricloader": ">=${loader_version}", + "fabric-api": "*", + "minecraft": "${supported_versions}" + }, + "custom": { + "modmenu": { + "links": { + "modmenu.discord": "https://discord.gg/GeyserMC" + } + } + } +} diff --git a/datagen/src/main/resources/rainbow-datagen.accesswidener b/datagen/src/main/resources/rainbow-datagen.accesswidener new file mode 100644 index 0000000..5ab114f --- /dev/null +++ b/datagen/src/main/resources/rainbow-datagen.accesswidener @@ -0,0 +1,3 @@ +accessWidener v2 named +accessible class net/minecraft/client/data/models/ModelProvider$ItemInfoCollector +accessible class net/minecraft/client/data/models/ModelProvider$SimpleModelCollector diff --git a/datagen/src/main/resources/rainbow-datagen.mixins.json b/datagen/src/main/resources/rainbow-datagen.mixins.json new file mode 100644 index 0000000..574147d --- /dev/null +++ b/datagen/src/main/resources/rainbow-datagen.mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "org.geysermc.rainbow.datagen.mixin", + "compatibilityLevel": "JAVA_21", + "client": [ + "ItemInfoCollectorAccessor", + "ModelProviderMixin", + "SimpleModelCollectorAccessor" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/gradle.properties b/gradle.properties index 79e8f2f..12d685f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,15 +1,5 @@ org.gradle.jvmargs=-Xmx1G -# Fabric Properties -minecraft_version=1.21.10 -parchment_version=1.21.10:2025.10.12 -loader_version=0.17.3 - # Mod Properties -mod_version=0.1.0-1.21.10 -supported_versions=>=1.21.9 <=1.21.10 -maven_group=org.geysermc -archives_base_name=rainbow - -# Dependencies -fabric_version=0.135.0+1.21.10 +mod_version=0.2.0-1.21.10-SNAPSHOT +maven_group=org.geysermc.rainbow diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..cfbb800 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,26 @@ +[versions] +minecraft = "1.21.10" +minecraft-supported = ">=1.21.9 <=1.21.10" +parchment = "2025.10.12" + +fabric-loom = "1.11-SNAPSHOT" +fabric-loader = "0.17.3" +fabric-api = "0.135.0+1.21.10" + +creative = "817fa982c4" +packconverter = "3.4.1-20251013.173215-13" + +[libraries] +minecraft = {group = "com.mojang", name = "minecraft", version.ref = "minecraft"} +parchment = {group = "org.parchmentmc.data", name = "parchment-1.21.10", version.ref = "parchment"} + +fabric-loom = {group = "net.fabricmc", name = "fabric-loom", version.ref = "fabric-loom"} +fabric-loader = {group = "net.fabricmc", name = "fabric-loader", version.ref = "fabric-loader"} +fabric-api = {group = "net.fabricmc.fabric-api", name = "fabric-api", version.ref = "fabric-api"} + +creative-api = {group = "com.github.GeyserMC.unnamed-creative", name = "creative-api", version.ref = "creative"} +creative-serializer-minecraft = {group = "com.github.GeyserMC.unnamed-creative", name = "creative-serializer-minecraft", version.ref = "creative"} +packconverter = {group = "org.geysermc.pack", name = "converter", version.ref = "packconverter"} + +[plugins] +fabric-loom = {id = "fabric-loom", version.ref = "fabric-loom"} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 1b33c55..8bdaf60 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ff23a68..2e11132 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 23d15a9..adff685 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -114,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -172,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -212,7 +210,6 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" diff --git a/gradlew.bat b/gradlew.bat index db3a6ac..c4bdd3a 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,10 @@ goto fail :execute @rem Setup the command line -set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/rainbow/build.gradle.kts b/rainbow/build.gradle.kts new file mode 100644 index 0000000..f89f209 --- /dev/null +++ b/rainbow/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + id("rainbow.base-conventions") + id("rainbow.publish-conventions") +} diff --git a/rainbow/gradle.properties b/rainbow/gradle.properties new file mode 100644 index 0000000..048092a --- /dev/null +++ b/rainbow/gradle.properties @@ -0,0 +1 @@ +archives_base_name=rainbow-core diff --git a/src/main/java/org/geysermc/rainbow/CodecUtil.java b/rainbow/src/main/java/org/geysermc/rainbow/CodecUtil.java similarity index 100% rename from src/main/java/org/geysermc/rainbow/CodecUtil.java rename to rainbow/src/main/java/org/geysermc/rainbow/CodecUtil.java diff --git a/src/main/java/org/geysermc/rainbow/PackConstants.java b/rainbow/src/main/java/org/geysermc/rainbow/PackConstants.java similarity index 100% rename from src/main/java/org/geysermc/rainbow/PackConstants.java rename to rainbow/src/main/java/org/geysermc/rainbow/PackConstants.java diff --git a/rainbow/src/main/java/org/geysermc/rainbow/Rainbow.java b/rainbow/src/main/java/org/geysermc/rainbow/Rainbow.java new file mode 100644 index 0000000..f2e80b1 --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/Rainbow.java @@ -0,0 +1,28 @@ +package org.geysermc.rainbow; + +import com.mojang.logging.LogUtils; +import net.minecraft.resources.ResourceLocation; +import org.slf4j.Logger; + +public class Rainbow { + + public static final String MOD_ID = "rainbow"; + public static final String MOD_NAME = "Rainbow"; + public static final Logger LOGGER = LogUtils.getLogger(); + + public static ResourceLocation getModdedLocation(String path) { + return ResourceLocation.fromNamespaceAndPath(MOD_ID, path); + } + + public static String safeResourceLocation(ResourceLocation location) { + return location.toString().replace(':', '.').replace('/', '_'); + } + + public static ResourceLocation decorateResourceLocation(ResourceLocation location, String type, String extension) { + return location.withPath(path -> type + "/" + path + "." + extension); + } + + public static ResourceLocation decorateTextureLocation(ResourceLocation location) { + return decorateResourceLocation(location, "textures", "png"); + } +} diff --git a/rainbow/src/main/java/org/geysermc/rainbow/RainbowIO.java b/rainbow/src/main/java/org/geysermc/rainbow/RainbowIO.java new file mode 100644 index 0000000..5068ef6 --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/RainbowIO.java @@ -0,0 +1,59 @@ +package org.geysermc.rainbow; + +import com.mojang.logging.LogUtils; +import org.slf4j.Logger; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public final class RainbowIO { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final List listeners = new ArrayList<>(); + + private RainbowIO() {} + + public static Optional safeIO(IOSupplier supplier) { + try { + return Optional.ofNullable(supplier.get()); + } catch (IOException exception) { + LOGGER.error("Failed to perform IO operation!", exception); + listeners.forEach(listener -> listener.error(exception)); + return Optional.empty(); + } + } + + public static T safeIO(IOSupplier supplier, T defaultValue) { + return safeIO(supplier).orElse(defaultValue); + } + + public static void safeIO(IORunnable runnable) { + safeIO(() -> { + runnable.run(); + return null; + }); + } + + public static void registerExceptionListener(IOExceptionListener listener) { + listeners.add(listener); + } + + @FunctionalInterface + public interface IOSupplier { + + T get() throws IOException; + } + + @FunctionalInterface + public interface IORunnable { + + void run() throws IOException; + } + + @FunctionalInterface + public interface IOExceptionListener { + + void error(IOException exception); + } +} diff --git a/src/main/java/org/geysermc/rainbow/mapping/geyser/GeyserBaseDefinition.java b/rainbow/src/main/java/org/geysermc/rainbow/definition/GeyserBaseDefinition.java similarity index 95% rename from src/main/java/org/geysermc/rainbow/mapping/geyser/GeyserBaseDefinition.java rename to rainbow/src/main/java/org/geysermc/rainbow/definition/GeyserBaseDefinition.java index f00522c..66f3f04 100644 --- a/src/main/java/org/geysermc/rainbow/mapping/geyser/GeyserBaseDefinition.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/definition/GeyserBaseDefinition.java @@ -1,4 +1,4 @@ -package org.geysermc.rainbow.mapping.geyser; +package org.geysermc.rainbow.definition; import com.mojang.serialization.Codec; import com.mojang.serialization.MapCodec; @@ -8,7 +8,7 @@ import net.minecraft.core.component.DataComponentType; import net.minecraft.core.component.DataComponents; import net.minecraft.resources.ResourceLocation; import org.geysermc.rainbow.Rainbow; -import org.geysermc.rainbow.mapping.geyser.predicate.GeyserPredicate; +import org.geysermc.rainbow.definition.predicate.GeyserPredicate; import java.util.List; import java.util.Optional; @@ -61,7 +61,7 @@ public record GeyserBaseDefinition(ResourceLocation bedrockIdentifier, Optional< } public String textureName() { - return bedrockOptions.icon.orElse(Rainbow.fileSafeResourceLocation(bedrockIdentifier)); + return bedrockOptions.icon.orElse(Rainbow.safeResourceLocation(bedrockIdentifier)); } public record BedrockOptions(Optional icon, boolean allowOffhand, boolean displayHandheld, int protectionValue, List tags) { diff --git a/src/main/java/org/geysermc/rainbow/mapping/geyser/GeyserGroupDefinition.java b/rainbow/src/main/java/org/geysermc/rainbow/definition/GeyserGroupDefinition.java similarity index 59% rename from src/main/java/org/geysermc/rainbow/mapping/geyser/GeyserGroupDefinition.java rename to rainbow/src/main/java/org/geysermc/rainbow/definition/GeyserGroupDefinition.java index 3199f47..9f8a611 100644 --- a/src/main/java/org/geysermc/rainbow/mapping/geyser/GeyserGroupDefinition.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/definition/GeyserGroupDefinition.java @@ -1,11 +1,14 @@ -package org.geysermc.rainbow.mapping.geyser; +package org.geysermc.rainbow.definition; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import java.util.Comparator; import java.util.List; import java.util.Optional; +import java.util.function.Function; import java.util.stream.Stream; public record GeyserGroupDefinition(Optional model, List definitions) implements GeyserMapping { @@ -18,7 +21,9 @@ public record GeyserGroupDefinition(Optional model, List model) { @@ -53,4 +58,26 @@ public record GeyserGroupDefinition(Optional model, List otherModel, List otherDefinitions)) { + if (model.isPresent() && otherModel.isPresent()) { + return model.get().compareTo(otherModel.get()); + } else if (model.isPresent()) { + return 1; // Groups with models are always greater than groups without + } else if (otherModel.isPresent()) { + return -1; + } else if (definitions.isEmpty() && otherDefinitions.isEmpty()) { + return 0; + } else if (definitions.isEmpty()) { + return -1; // Groups with definitions are always greater than groups without + } else if (otherDefinitions.isEmpty()) { + return 1; + } + // Compare the first definition as a last resort + return definitions.getFirst().compareTo(otherDefinitions.getFirst()); + } + return 1; // Groups are always greater than individual mappings + } } diff --git a/rainbow/src/main/java/org/geysermc/rainbow/definition/GeyserItemDefinition.java b/rainbow/src/main/java/org/geysermc/rainbow/definition/GeyserItemDefinition.java new file mode 100644 index 0000000..1d3fe96 --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/definition/GeyserItemDefinition.java @@ -0,0 +1,21 @@ +package org.geysermc.rainbow.definition; + +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; + +public interface GeyserItemDefinition extends GeyserMapping { + + GeyserBaseDefinition base(); + + boolean conflictsWith(Optional parentModel, GeyserItemDefinition other); + + @Override + default int compareTo(@NotNull GeyserMapping other) { + if (other instanceof GeyserItemDefinition itemDefinition) { + return base().bedrockIdentifier().compareTo(itemDefinition.base().bedrockIdentifier()); + } + return -1; // Groups are always greater than individual mappings + } +} diff --git a/src/main/java/org/geysermc/rainbow/mapping/geyser/GeyserLegacyDefinition.java b/rainbow/src/main/java/org/geysermc/rainbow/definition/GeyserLegacyDefinition.java similarity index 96% rename from src/main/java/org/geysermc/rainbow/mapping/geyser/GeyserLegacyDefinition.java rename to rainbow/src/main/java/org/geysermc/rainbow/definition/GeyserLegacyDefinition.java index bff6ba8..0f79aae 100644 --- a/src/main/java/org/geysermc/rainbow/mapping/geyser/GeyserLegacyDefinition.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/definition/GeyserLegacyDefinition.java @@ -1,4 +1,4 @@ -package org.geysermc.rainbow.mapping.geyser; +package org.geysermc.rainbow.definition; import com.mojang.serialization.Codec; import com.mojang.serialization.MapCodec; diff --git a/src/main/java/org/geysermc/rainbow/mapping/geyser/GeyserMapping.java b/rainbow/src/main/java/org/geysermc/rainbow/definition/GeyserMapping.java similarity index 93% rename from src/main/java/org/geysermc/rainbow/mapping/geyser/GeyserMapping.java rename to rainbow/src/main/java/org/geysermc/rainbow/definition/GeyserMapping.java index 14c8ffc..a46fd1c 100644 --- a/src/main/java/org/geysermc/rainbow/mapping/geyser/GeyserMapping.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/definition/GeyserMapping.java @@ -1,4 +1,4 @@ -package org.geysermc.rainbow.mapping.geyser; +package org.geysermc.rainbow.definition; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; @@ -6,7 +6,7 @@ import com.mojang.serialization.MapCodec; import net.minecraft.util.StringRepresentable; import org.jetbrains.annotations.NotNull; -public interface GeyserMapping { +public interface GeyserMapping extends Comparable { Codec CODEC = Codec.lazyInitialized(() -> Type.CODEC.dispatch(GeyserMapping::type, Type::codec)); // Not perfect since we're not checking single definitions in groups without a model... but good enough diff --git a/src/main/java/org/geysermc/rainbow/mapping/geyser/GeyserMappings.java b/rainbow/src/main/java/org/geysermc/rainbow/definition/GeyserMappings.java similarity index 94% rename from src/main/java/org/geysermc/rainbow/mapping/geyser/GeyserMappings.java rename to rainbow/src/main/java/org/geysermc/rainbow/definition/GeyserMappings.java index 96fb307..40e789e 100644 --- a/src/main/java/org/geysermc/rainbow/mapping/geyser/GeyserMappings.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/definition/GeyserMappings.java @@ -1,4 +1,4 @@ -package org.geysermc.rainbow.mapping.geyser; +package org.geysermc.rainbow.definition; import com.google.common.collect.Multimap; import com.google.common.collect.MultimapBuilder; @@ -11,6 +11,7 @@ import org.geysermc.rainbow.CodecUtil; import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Optional; @@ -26,7 +27,10 @@ public class GeyserMappings { ).apply(instance, (format, mappings) -> new GeyserMappings(mappings)) ); - private final Multimap, GeyserMapping> mappings = MultimapBuilder.hashKeys().hashSetValues().build(); + private final Multimap, GeyserMapping> mappings = MultimapBuilder + .hashKeys() + .treeSetValues(Comparator.comparing(mapping -> mapping)) + .build(); public GeyserMappings() {} diff --git a/src/main/java/org/geysermc/rainbow/mapping/geyser/GeyserSingleDefinition.java b/rainbow/src/main/java/org/geysermc/rainbow/definition/GeyserSingleDefinition.java similarity index 96% rename from src/main/java/org/geysermc/rainbow/mapping/geyser/GeyserSingleDefinition.java rename to rainbow/src/main/java/org/geysermc/rainbow/definition/GeyserSingleDefinition.java index 85f02ce..b3e6066 100644 --- a/src/main/java/org/geysermc/rainbow/mapping/geyser/GeyserSingleDefinition.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/definition/GeyserSingleDefinition.java @@ -1,4 +1,4 @@ -package org.geysermc.rainbow.mapping.geyser; +package org.geysermc.rainbow.definition; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; diff --git a/src/main/java/org/geysermc/rainbow/mapping/geyser/predicate/GeyserConditionPredicate.java b/rainbow/src/main/java/org/geysermc/rainbow/definition/predicate/GeyserConditionPredicate.java similarity index 98% rename from src/main/java/org/geysermc/rainbow/mapping/geyser/predicate/GeyserConditionPredicate.java rename to rainbow/src/main/java/org/geysermc/rainbow/definition/predicate/GeyserConditionPredicate.java index 7e1d49c..79adca7 100644 --- a/src/main/java/org/geysermc/rainbow/mapping/geyser/predicate/GeyserConditionPredicate.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/definition/predicate/GeyserConditionPredicate.java @@ -1,4 +1,4 @@ -package org.geysermc.rainbow.mapping.geyser.predicate; +package org.geysermc.rainbow.definition.predicate; import com.google.common.base.Suppliers; import com.mojang.serialization.Codec; diff --git a/src/main/java/org/geysermc/rainbow/mapping/geyser/predicate/GeyserMatchPredicate.java b/rainbow/src/main/java/org/geysermc/rainbow/definition/predicate/GeyserMatchPredicate.java similarity index 98% rename from src/main/java/org/geysermc/rainbow/mapping/geyser/predicate/GeyserMatchPredicate.java rename to rainbow/src/main/java/org/geysermc/rainbow/definition/predicate/GeyserMatchPredicate.java index ecd558c..4e793a6 100644 --- a/src/main/java/org/geysermc/rainbow/mapping/geyser/predicate/GeyserMatchPredicate.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/definition/predicate/GeyserMatchPredicate.java @@ -1,4 +1,4 @@ -package org.geysermc.rainbow.mapping.geyser.predicate; +package org.geysermc.rainbow.definition.predicate; import com.mojang.serialization.Codec; import com.mojang.serialization.MapCodec; diff --git a/src/main/java/org/geysermc/rainbow/mapping/geyser/predicate/GeyserPredicate.java b/rainbow/src/main/java/org/geysermc/rainbow/definition/predicate/GeyserPredicate.java similarity index 95% rename from src/main/java/org/geysermc/rainbow/mapping/geyser/predicate/GeyserPredicate.java rename to rainbow/src/main/java/org/geysermc/rainbow/definition/predicate/GeyserPredicate.java index fb753ba..7c6b0f3 100644 --- a/src/main/java/org/geysermc/rainbow/mapping/geyser/predicate/GeyserPredicate.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/definition/predicate/GeyserPredicate.java @@ -1,4 +1,4 @@ -package org.geysermc.rainbow.mapping.geyser.predicate; +package org.geysermc.rainbow.definition.predicate; import com.mojang.serialization.Codec; import com.mojang.serialization.MapCodec; diff --git a/src/main/java/org/geysermc/rainbow/mapping/geyser/predicate/GeyserRangeDispatchPredicate.java b/rainbow/src/main/java/org/geysermc/rainbow/definition/predicate/GeyserRangeDispatchPredicate.java similarity index 97% rename from src/main/java/org/geysermc/rainbow/mapping/geyser/predicate/GeyserRangeDispatchPredicate.java rename to rainbow/src/main/java/org/geysermc/rainbow/definition/predicate/GeyserRangeDispatchPredicate.java index 67f3643..d58c7bb 100644 --- a/src/main/java/org/geysermc/rainbow/mapping/geyser/predicate/GeyserRangeDispatchPredicate.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/definition/predicate/GeyserRangeDispatchPredicate.java @@ -1,4 +1,4 @@ -package org.geysermc.rainbow.mapping.geyser.predicate; +package org.geysermc.rainbow.definition.predicate; import com.google.common.base.Suppliers; import com.mojang.serialization.Codec; @@ -24,7 +24,7 @@ public record GeyserRangeDispatchPredicate(Property property, float threshold, f @Override public Type type() { - return null; + return Type.RANGE_DISPATCH; } public interface Property { diff --git a/rainbow/src/main/java/org/geysermc/rainbow/image/NativeImageUtil.java b/rainbow/src/main/java/org/geysermc/rainbow/image/NativeImageUtil.java new file mode 100644 index 0000000..e79fbd8 --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/image/NativeImageUtil.java @@ -0,0 +1,44 @@ +package org.geysermc.rainbow.image; + +import com.mojang.blaze3d.platform.NativeImage; +import org.geysermc.rainbow.mixin.NativeImageAccessor; +import org.lwjgl.stb.STBImage; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.Pipe; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; + +public class NativeImageUtil { + + // Adjusted NativeImage#writeToFile + @SuppressWarnings("DataFlowIssue") + public static byte[] writeToByteArray(NativeImage image) throws IOException { + if (!image.format().supportedByStb()) { + throw new UnsupportedOperationException("Don't know how to write format " + image.format()); + } else { + ((NativeImageAccessor) (Object) image).invokeCheckAllocated(); + Pipe pipe = Pipe.open(); + try (WritableByteChannel outputChannel = pipe.sink()) { + if (!((NativeImageAccessor) (Object) image).invokeWriteToChannel(outputChannel)) { + throw new IOException("Could not write image to pipe: " + STBImage.stbi_failure_reason()); + } + } + + try (ReadableByteChannel inputChannel = pipe.source()) { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + ByteBuffer buffer = ByteBuffer.allocate(4096); + while (inputChannel.read(buffer) != -1) { + buffer.flip(); + while (buffer.hasRemaining()) { + bytes.write(buffer.get()); + } + buffer.clear(); + } + return bytes.toByteArray(); + } + } + } +} diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mapping/AssetResolver.java b/rainbow/src/main/java/org/geysermc/rainbow/mapping/AssetResolver.java new file mode 100644 index 0000000..922017e --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/mapping/AssetResolver.java @@ -0,0 +1,23 @@ +package org.geysermc.rainbow.mapping; + +import net.minecraft.client.renderer.item.ClientItem; +import net.minecraft.client.resources.model.EquipmentClientInfo; +import net.minecraft.client.resources.model.ResolvedModel; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.equipment.EquipmentAsset; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Optional; + +public interface AssetResolver { + + Optional getResolvedModel(ResourceLocation location); + + Optional getClientItem(ResourceLocation location); + + Optional getEquipmentInfo(ResourceKey key); + + InputStream openAsset(ResourceLocation location) throws IOException; +} diff --git a/src/main/java/org/geysermc/rainbow/mapping/BedrockItemConsumer.java b/rainbow/src/main/java/org/geysermc/rainbow/mapping/BedrockItemConsumer.java similarity index 100% rename from src/main/java/org/geysermc/rainbow/mapping/BedrockItemConsumer.java rename to rainbow/src/main/java/org/geysermc/rainbow/mapping/BedrockItemConsumer.java diff --git a/src/main/java/org/geysermc/rainbow/mapping/BedrockItemMapper.java b/rainbow/src/main/java/org/geysermc/rainbow/mapping/BedrockItemMapper.java similarity index 67% rename from src/main/java/org/geysermc/rainbow/mapping/BedrockItemMapper.java rename to rainbow/src/main/java/org/geysermc/rainbow/mapping/BedrockItemMapper.java index 412d4e3..8d7747a 100644 --- a/src/main/java/org/geysermc/rainbow/mapping/BedrockItemMapper.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/mapping/BedrockItemMapper.java @@ -1,6 +1,5 @@ package org.geysermc.rainbow.mapping; -import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.item.BlockModelWrapper; import net.minecraft.client.renderer.item.ClientItem; import net.minecraft.client.renderer.item.ConditionalItemModel; @@ -9,25 +8,27 @@ import net.minecraft.client.renderer.item.ItemModels; import net.minecraft.client.renderer.item.RangeSelectItemModel; import net.minecraft.client.renderer.item.SelectItemModel; import net.minecraft.client.renderer.item.properties.conditional.Broken; +import net.minecraft.client.renderer.item.properties.conditional.ConditionalItemModelProperties; +import net.minecraft.client.renderer.item.properties.conditional.ConditionalItemModelProperty; import net.minecraft.client.renderer.item.properties.conditional.CustomModelDataProperty; import net.minecraft.client.renderer.item.properties.conditional.Damaged; import net.minecraft.client.renderer.item.properties.conditional.FishingRodCast; import net.minecraft.client.renderer.item.properties.conditional.HasComponent; -import net.minecraft.client.renderer.item.properties.conditional.ItemModelPropertyTest; import net.minecraft.client.renderer.item.properties.numeric.BundleFullness; import net.minecraft.client.renderer.item.properties.numeric.Count; import net.minecraft.client.renderer.item.properties.numeric.Damage; +import net.minecraft.client.renderer.item.properties.numeric.RangeSelectItemModelProperties; import net.minecraft.client.renderer.item.properties.numeric.RangeSelectItemModelProperty; import net.minecraft.client.renderer.item.properties.select.Charge; import net.minecraft.client.renderer.item.properties.select.ContextDimension; import net.minecraft.client.renderer.item.properties.select.DisplayContext; +import net.minecraft.client.renderer.item.properties.select.SelectItemModelProperties; import net.minecraft.client.renderer.item.properties.select.TrimMaterialProperty; -import net.minecraft.client.resources.model.Material; -import net.minecraft.client.resources.model.ResolvedModel; import net.minecraft.core.component.DataComponents; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.tags.ItemTags; +import net.minecraft.util.ExtraCodecs; import net.minecraft.util.ProblemReporter; import net.minecraft.world.entity.ai.attributes.AttributeModifier; import net.minecraft.world.entity.ai.attributes.Attributes; @@ -38,26 +39,19 @@ import net.minecraft.world.item.component.ItemAttributeModifiers; import net.minecraft.world.item.equipment.trim.TrimMaterial; import net.minecraft.world.level.Level; import org.apache.commons.lang3.ArrayUtils; -import org.geysermc.rainbow.accessor.ResolvedModelAccessor; -import org.geysermc.rainbow.mapping.animation.AnimationMapper; -import org.geysermc.rainbow.mapping.animation.BedrockAnimationContext; import org.geysermc.rainbow.mapping.attachable.AttachableMapper; import org.geysermc.rainbow.mapping.geometry.BedrockGeometryContext; -import org.geysermc.rainbow.mapping.geometry.GeometryMapper; -import org.geysermc.rainbow.mapping.geometry.GeometryRenderer; -import org.geysermc.rainbow.mapping.geyser.GeyserBaseDefinition; -import org.geysermc.rainbow.mapping.geyser.GeyserItemDefinition; -import org.geysermc.rainbow.mapping.geyser.GeyserLegacyDefinition; -import org.geysermc.rainbow.mapping.geyser.GeyserSingleDefinition; -import org.geysermc.rainbow.mapping.geyser.predicate.GeyserConditionPredicate; -import org.geysermc.rainbow.mapping.geyser.predicate.GeyserMatchPredicate; -import org.geysermc.rainbow.mapping.geyser.predicate.GeyserPredicate; -import org.geysermc.rainbow.mapping.geyser.predicate.GeyserRangeDispatchPredicate; +import org.geysermc.rainbow.definition.GeyserBaseDefinition; +import org.geysermc.rainbow.definition.GeyserItemDefinition; +import org.geysermc.rainbow.definition.GeyserLegacyDefinition; +import org.geysermc.rainbow.definition.GeyserSingleDefinition; +import org.geysermc.rainbow.definition.predicate.GeyserConditionPredicate; +import org.geysermc.rainbow.definition.predicate.GeyserMatchPredicate; +import org.geysermc.rainbow.definition.predicate.GeyserPredicate; +import org.geysermc.rainbow.definition.predicate.GeyserRangeDispatchPredicate; import org.geysermc.rainbow.mixin.LateBoundIdMapperAccessor; import org.geysermc.rainbow.mixin.RangeSelectItemModelAccessor; -import org.geysermc.rainbow.mixin.TextureSlotsAccessor; import org.geysermc.rainbow.pack.BedrockItem; -import org.geysermc.rainbow.pack.BedrockTextures; import java.util.List; import java.util.Optional; @@ -65,31 +59,26 @@ import java.util.function.Function; import java.util.stream.Stream; public class BedrockItemMapper { - private static final List HANDHELD_MODELS = Stream.of("item/handheld", "item/handheld_rod", "item/handheld_mace") - .map(ResourceLocation::withDefaultNamespace) - .toList(); private static final List TRIMMABLE_ARMOR_TAGS = Stream.of("is_armor", "trimmable_armors") .map(ResourceLocation::withDefaultNamespace) .toList(); - private static ResolvedModelAccessor getModels() { - return (ResolvedModelAccessor) Minecraft.getInstance().getModelManager(); - } - - private static ResourceLocation getModelId(ItemModel.Unbaked model) { + private static ResourceLocation getId(ExtraCodecs.LateBoundIdMapper mapper, + T type) { //noinspection unchecked - return ((LateBoundIdMapperAccessor) ItemModels.ID_MAPPER).getIdToValue().inverse().get(model.type()); + return ((LateBoundIdMapperAccessor) mapper).getIdToValue().inverse().get(type); } public static void tryMapStack(ItemStack stack, ResourceLocation modelLocation, ProblemReporter reporter, PackContext context) { - getModels().rainbow$getClientItem(modelLocation).map(ClientItem::model) + context.assetResolver().getClientItem(modelLocation).map(ClientItem::model) .ifPresentOrElse(model -> mapItem(model, stack, reporter.forChild(() -> "client item definition " + modelLocation + " "), base -> new GeyserSingleDefinition(base, Optional.of(modelLocation)), context), () -> reporter.report(() -> "missing client item definition " + modelLocation)); } public static void tryMapStack(ItemStack stack, int customModelData, ProblemReporter reporter, PackContext context) { - ItemModel.Unbaked vanillaModel = getModels().rainbow$getClientItem(stack.get(DataComponents.ITEM_MODEL)).map(ClientItem::model).orElseThrow(); - ProblemReporter childReporter = reporter.forChild(() -> "item model " + vanillaModel + " with custom model data " + customModelData + " "); + ResourceLocation itemModel = stack.get(DataComponents.ITEM_MODEL); + ItemModel.Unbaked vanillaModel = context.assetResolver().getClientItem(itemModel).map(ClientItem::model).orElseThrow(); + ProblemReporter childReporter = reporter.forChild(() -> "item model " + itemModel + " with custom model data " + customModelData + " "); if (vanillaModel instanceof RangeSelectItemModel.Unbaked(RangeSelectItemModelProperty property, float scale, List entries, Optional fallback)) { // WHY, Mojang? if (property instanceof net.minecraft.client.renderer.item.properties.numeric.CustomModelDataProperty(int index)) { @@ -123,19 +112,15 @@ public class BedrockItemMapper { case ConditionalItemModel.Unbaked conditional -> mapConditionalModel(conditional, context.child("condition model ")); case RangeSelectItemModel.Unbaked rangeSelect -> mapRangeSelectModel(rangeSelect, context.child("range select model ")); case SelectItemModel.Unbaked select -> mapSelectModel(select, context.child("select model ")); - default -> context.reporter.report(() -> "unsupported item model " + getModelId(model)); + default -> context.report("unsupported item model " + getId(ItemModels.ID_MAPPER, model.type())); } } private static void mapBlockModelWrapper(BlockModelWrapper.Unbaked model, MappingContext context) { ResourceLocation itemModelLocation = model.model(); - getModels().rainbow$getResolvedModel(itemModelLocation) + context.packContext().assetResolver().getResolvedModel(itemModelLocation) .ifPresentOrElse(itemModel -> { - ResolvedModel parentModel = itemModel.parent(); - // debugName() returns the resource location of the model as a string - boolean handheld = parentModel != null && HANDHELD_MODELS.contains(ResourceLocation.parse(parentModel.debugName())); - ResourceLocation bedrockIdentifier; if (itemModelLocation.getNamespace().equals(ResourceLocation.DEFAULT_NAMESPACE)) { bedrockIdentifier = ResourceLocation.fromNamespaceAndPath("geyser_mc", itemModelLocation.getPath()); @@ -143,33 +128,17 @@ public class BedrockItemMapper { bedrockIdentifier = itemModelLocation; } - Material layer0Texture = itemModel.getTopTextureSlots().getMaterial("layer0"); - Optional texture; - Optional customGeometry; - if (layer0Texture != null) { - texture = Optional.of(layer0Texture.texture()); - customGeometry = Optional.empty(); - } else { - // We can't stitch multiple textures together yet, so we just grab the first one we see - // This will only work properly for models with just one texture - texture = ((TextureSlotsAccessor) itemModel.getTopTextureSlots()).getResolvedValues().values().stream() - .map(Material::texture) - .findAny(); - // Unknown texture (doesn't use layer0), so we immediately assume the geometry is custom - // This check should probably be done differently - customGeometry = Optional.of(itemModel); - } - - texture.ifPresentOrElse(itemTexture -> { + BedrockGeometryContext geometry = BedrockGeometryContext.create(bedrockIdentifier, context.stack, itemModel, context.packContext); + if (context.packContext.reportSuccesses()) { // Not a problem, but just report to get the model printed in the report file - context.reporter.report(() -> "creating mapping for block model " + itemModelLocation); - context.create(bedrockIdentifier, itemTexture, handheld, customGeometry); - }, () -> context.reporter.report(() -> "not mapping block model " + itemModelLocation + " because it has no texture")); - }, () -> context.reporter.report(() -> "missing block model " + itemModelLocation)); + context.report("creating mapping for block model " + itemModelLocation); + } + context.create(bedrockIdentifier, geometry); + }, () -> context.report("missing block model " + itemModelLocation)); } private static void mapConditionalModel(ConditionalItemModel.Unbaked model, MappingContext context) { - ItemModelPropertyTest property = model.property(); + ConditionalItemModelProperty property = model.property(); GeyserConditionPredicate.Property predicateProperty = switch (property) { case Broken ignored -> GeyserConditionPredicate.BROKEN; case Damaged ignored -> GeyserConditionPredicate.DAMAGED; @@ -182,7 +151,7 @@ public class BedrockItemMapper { ItemModel.Unbaked onFalse = model.onFalse(); if (predicateProperty == null) { - context.reporter.report(() -> "unsupported conditional model property " + property + ", only mapping on_false"); + context.report("unsupported conditional model property " + getId(ConditionalItemModelProperties.ID_MAPPER, property.type()) + ", only mapping on_false"); mapItem(onFalse, context.child("condition on_false (unsupported property)")); return; } @@ -203,7 +172,7 @@ public class BedrockItemMapper { }; if (predicateProperty == null) { - context.reporter.report(() -> "unsupported range dispatch model property " + property + ", only mapping fallback, if it is present"); + context.report("unsupported range dispatch model property " + getId(RangeSelectItemModelProperties.ID_MAPPER, property.type()) + ", only mapping fallback, if it is present"); } else { for (RangeSelectItemModel.Entry entry : model.entries()) { mapItem(entry.model(), context.with(new GeyserRangeDispatchPredicate(predicateProperty, entry.threshold(), model.scale()), "threshold " + entry.threshold())); @@ -229,7 +198,7 @@ public class BedrockItemMapper { if (dataConstructor == null) { if (unbakedSwitch.property() instanceof DisplayContext) { - context.reporter.report(() -> "unsupported select model property display_context, only mapping \"gui\" case, if it exists"); + context.report("unsupported select model property display_context, only mapping \"gui\" case, if it exists"); for (SelectItemModel.SwitchCase switchCase : cases) { if (switchCase.values().contains(ItemDisplayContext.GUI)) { mapItem(switchCase.model(), context.child("select GUI display_context case (unsupported property) ")); @@ -237,7 +206,7 @@ public class BedrockItemMapper { } } } - context.reporter.report(() -> "unsupported select model property " + unbakedSwitch.property() + ", only mapping fallback, if present"); + context.report("unsupported select model property " + getId(SelectItemModelProperties.ID_MAPPER, unbakedSwitch.property().type()) + ", only mapping fallback, if present"); model.fallback().ifPresent(fallback -> mapItem(fallback, context.child("select fallback case (unsupported property) "))); return; } @@ -261,17 +230,11 @@ public class BedrockItemMapper { return new MappingContext(predicateStack, stack, reporter.forChild(() -> childName), definitionCreator, packContext); } - public void create(ResourceLocation bedrockIdentifier, ResourceLocation texture, boolean displayHandheld, - Optional customModel) { - List tags; - if (stack.is(ItemTags.TRIMMABLE_ARMOR)) { - tags = TRIMMABLE_ARMOR_TAGS; - } else { - tags = List.of(); - } + public void create(ResourceLocation bedrockIdentifier, BedrockGeometryContext geometry) { + List tags = stack.is(ItemTags.TRIMMABLE_ARMOR) ? TRIMMABLE_ARMOR_TAGS : List.of(); - GeyserBaseDefinition base = new GeyserBaseDefinition(bedrockIdentifier, Optional.of(stack.getHoverName().getString()), predicateStack, - new GeyserBaseDefinition.BedrockOptions(Optional.empty(), true, displayHandheld, calculateProtectionValue(stack), tags), + GeyserBaseDefinition base = new GeyserBaseDefinition(bedrockIdentifier, Optional.ofNullable(stack.getHoverName().tryCollapseToString()), predicateStack, + new GeyserBaseDefinition.BedrockOptions(Optional.empty(), true, geometry.handheld(), calculateProtectionValue(stack), tags), stack.getComponentsPatch()); try { packContext.mappings().map(stack.getItemHolder(), definitionCreator.apply(base)); @@ -280,24 +243,12 @@ public class BedrockItemMapper { return; } - // TODO Should probably get a better way to get geometry texture - String safeIdentifier = base.textureName(); - String bone = "bone"; - ResourceLocation geometryTexture = texture; - Optional bedrockGeometry = customModel.map(model -> GeometryMapper.mapGeometry(safeIdentifier, bone, model, geometryTexture)); - Optional bedrockAnimation = customModel.map(model -> AnimationMapper.mapAnimation(safeIdentifier, bone, model.getTopTransforms())); + packContext.itemConsumer().accept(new BedrockItem(bedrockIdentifier, base.textureName(), geometry, + AttachableMapper.mapItem(packContext.assetResolver(), geometry, stack.getComponentsPatch()))); + } - boolean exportTexture = true; - if (customModel.isPresent()) { - texture = texture.withPath(path -> path + "_icon"); - GeometryRenderer.render(stack, packContext.packPath().resolve(BedrockTextures.TEXTURES_FOLDER + texture.getPath() + ".png")); - exportTexture = false; - packContext.additionalTextureConsumer().accept(geometryTexture); - } - - packContext.itemConsumer().accept(new BedrockItem(bedrockIdentifier, base.textureName(), texture, exportTexture, - AttachableMapper.mapItem(stack.getComponentsPatch(), bedrockIdentifier, bedrockGeometry, bedrockAnimation, packContext.additionalTextureConsumer()), - bedrockGeometry.map(BedrockGeometryContext::geometry), bedrockAnimation.map(BedrockAnimationContext::animation))); + public void report(String problem) { + reporter.report(() -> problem); } private static int calculateProtectionValue(ItemStack stack) { diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mapping/PackContext.java b/rainbow/src/main/java/org/geysermc/rainbow/mapping/PackContext.java new file mode 100644 index 0000000..96ad637 --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/mapping/PackContext.java @@ -0,0 +1,10 @@ +package org.geysermc.rainbow.mapping; + +import org.geysermc.rainbow.mapping.geometry.GeometryRenderer; +import org.geysermc.rainbow.definition.GeyserMappings; +import org.geysermc.rainbow.pack.PackPaths; + +import java.util.Optional; + +public record PackContext(GeyserMappings mappings, PackPaths paths, BedrockItemConsumer itemConsumer, AssetResolver assetResolver, + Optional geometryRenderer, boolean reportSuccesses) {} diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mapping/PackSerializer.java b/rainbow/src/main/java/org/geysermc/rainbow/mapping/PackSerializer.java new file mode 100644 index 0000000..ef1b9e2 --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/mapping/PackSerializer.java @@ -0,0 +1,13 @@ +package org.geysermc.rainbow.mapping; + +import com.mojang.serialization.Codec; + +import java.nio.file.Path; +import java.util.concurrent.CompletableFuture; + +public interface PackSerializer { + + CompletableFuture saveJson(Codec codec, T object, Path path); + + CompletableFuture saveTexture(byte[] texture, Path path); +} diff --git a/src/main/java/org/geysermc/rainbow/mapping/animation/AnimationMapper.java b/rainbow/src/main/java/org/geysermc/rainbow/mapping/animation/AnimationMapper.java similarity index 72% rename from src/main/java/org/geysermc/rainbow/mapping/animation/AnimationMapper.java rename to rainbow/src/main/java/org/geysermc/rainbow/mapping/animation/AnimationMapper.java index 3901502..30a641d 100644 --- a/src/main/java/org/geysermc/rainbow/mapping/animation/AnimationMapper.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/mapping/animation/AnimationMapper.java @@ -6,15 +6,12 @@ import org.geysermc.rainbow.pack.animation.BedrockAnimation; import org.joml.Vector3f; import org.joml.Vector3fc; -// TODO these offset values are completely wrong, I think +// TODO these offset values are still not entirely right, I think public class AnimationMapper { - // These aren't perfect... but I spent over 1.5 hours trying to get these. It's good enough for me. private static final Vector3fc FIRST_PERSON_POSITION_OFFSET = new Vector3f(-7.0F, 22.5F, -7.0F); private static final Vector3fc FIRST_PERSON_ROTATION_OFFSET = new Vector3f(-22.5F, 50.0F, -32.5F); - private static final Vector3fc THIRD_PERSON_POSITION_OFFSET = new Vector3f(0.0F, 13.0F, -3.0F); - private static final Vector3fc THIRD_PERSON_ROTATION_OFFSET = new Vector3f(90.0F, -90.0F, 0.0F); - + // These transformations perfect... but I spent over 3 hours trying to get these. It's good enough for me. public static BedrockAnimationContext mapAnimation(String identifier, String bone, ItemTransforms transforms) { // I don't think it's possible to display separate animations for left- and right hands ItemTransform firstPerson = transforms.firstPersonRightHand(); @@ -23,8 +20,10 @@ public class AnimationMapper { Vector3f firstPersonScale = new Vector3f(firstPerson.scale()); ItemTransform thirdPerson = transforms.thirdPersonRightHand(); - Vector3f thirdPersonPosition = THIRD_PERSON_POSITION_OFFSET.add(thirdPerson.translation(), new Vector3f()); - Vector3f thirdPersonRotation = THIRD_PERSON_ROTATION_OFFSET.add(-thirdPerson.rotation().x(), thirdPerson.rotation().y(), thirdPerson.rotation().z(), new Vector3f()); + // Translation Y/Z axes are swapped on bedrock, bedrock displays the model lower than Java does, and the X/Y axes (Java) is inverted on bedrock + Vector3f thirdPersonPosition = new Vector3f(-thirdPerson.translation().x(), 10.0F + thirdPerson.translation().z(), -thirdPerson.translation().y()); + // Rotation X/Y axes are inverted on bedrock, bedrock needs a +90-degree rotation on the X axis, and I couldn't figure out how the Z axis works + Vector3f thirdPersonRotation = new Vector3f(-thirdPerson.rotation().x() + 90.0F, -thirdPerson.rotation().y(), 0.0F); Vector3f thirdPersonScale = new Vector3f(thirdPerson.scale()); return new BedrockAnimationContext(BedrockAnimation.builder() diff --git a/src/main/java/org/geysermc/rainbow/mapping/animation/BedrockAnimationContext.java b/rainbow/src/main/java/org/geysermc/rainbow/mapping/animation/BedrockAnimationContext.java similarity index 100% rename from src/main/java/org/geysermc/rainbow/mapping/animation/BedrockAnimationContext.java rename to rainbow/src/main/java/org/geysermc/rainbow/mapping/animation/BedrockAnimationContext.java diff --git a/src/main/java/org/geysermc/rainbow/mapping/attachable/AttachableMapper.java b/rainbow/src/main/java/org/geysermc/rainbow/mapping/attachable/AttachableMapper.java similarity index 63% rename from src/main/java/org/geysermc/rainbow/mapping/attachable/AttachableMapper.java rename to rainbow/src/main/java/org/geysermc/rainbow/mapping/attachable/AttachableMapper.java index 6995474..7b9579b 100644 --- a/src/main/java/org/geysermc/rainbow/mapping/attachable/AttachableMapper.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/mapping/attachable/AttachableMapper.java @@ -1,7 +1,6 @@ package org.geysermc.rainbow.mapping.attachable; import com.mojang.datafixers.util.Pair; -import net.minecraft.client.Minecraft; import net.minecraft.client.resources.model.EquipmentAssetManager; import net.minecraft.client.resources.model.EquipmentClientInfo; import net.minecraft.core.component.DataComponentPatch; @@ -9,9 +8,10 @@ import net.minecraft.core.component.DataComponents; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.item.equipment.Equippable; -import org.geysermc.rainbow.mapping.animation.BedrockAnimationContext; +import org.geysermc.rainbow.mapping.AssetResolver; import org.geysermc.rainbow.mapping.geometry.BedrockGeometryContext; -import org.geysermc.rainbow.mixin.EntityRenderDispatcherAccessor; +import org.geysermc.rainbow.mapping.geometry.StitchedGeometry; +import org.geysermc.rainbow.mapping.geometry.TextureHolder; import org.geysermc.rainbow.pack.attachable.BedrockAttachable; import java.util.List; @@ -20,31 +20,27 @@ import java.util.function.Consumer; public class AttachableMapper { - public static Optional mapItem(DataComponentPatch components, ResourceLocation bedrockIdentifier, Optional customGeometry, - Optional customAnimation, Consumer textureConsumer) { + public static AttachableCreator mapItem(AssetResolver assetResolver, BedrockGeometryContext geometryContext, DataComponentPatch components) { // Crazy optional statement // Unfortunately we can't have both equippables and custom models, so we prefer the latter :( - return customGeometry - .map(geometry -> BedrockAttachable.geometry(bedrockIdentifier, geometry.geometry().definitions().getFirst(), geometry.texture().getPath())) + return (bedrockIdentifier, stitchedGeometry, textureConsumer) -> stitchedGeometry + .map(stitched -> BedrockAttachable.geometry(bedrockIdentifier, stitched.geometry().definitions().getFirst(), stitched.stitchedTextures().location().getPath())) .or(() -> Optional.ofNullable(components.get(DataComponents.EQUIPPABLE)) .flatMap(optional -> (Optional) optional) - .flatMap(equippable -> { - EquipmentAssetManager equipmentAssets = ((EntityRenderDispatcherAccessor) Minecraft.getInstance().getEntityRenderDispatcher()).getEquipmentAssets(); - return equippable.assetId().map(asset -> Pair.of(equippable.slot(), equipmentAssets.get(asset))); - }) + .flatMap(equippable -> equippable.assetId().flatMap(assetResolver::getEquipmentInfo).map(info -> Pair.of(equippable.slot(), info))) .filter(assetInfo -> assetInfo.getSecond() != EquipmentAssetManager.MISSING) .map(assetInfo -> assetInfo .mapSecond(info -> info.getLayers(getLayer(assetInfo.getFirst())))) .filter(assetInfo -> !assetInfo.getSecond().isEmpty()) .map(assetInfo -> { - ResourceLocation texture = getTexture(assetInfo.getSecond(), getLayer(assetInfo.getFirst())); - textureConsumer.accept(texture); - return BedrockAttachable.equipment(bedrockIdentifier, assetInfo.getFirst(), texture.getPath()); + ResourceLocation equipmentTexture = getTexture(assetInfo.getSecond(), getLayer(assetInfo.getFirst())); + textureConsumer.accept(TextureHolder.createFromResources(equipmentTexture)); + return BedrockAttachable.equipment(bedrockIdentifier, assetInfo.getFirst(), equipmentTexture.getPath()); })) .map(attachable -> { - customAnimation.ifPresent(context -> { - attachable.withAnimation("first_person", context.firstPerson()); - attachable.withAnimation("third_person", context.thirdPerson()); + geometryContext.animation().ifPresent(animation -> { + attachable.withAnimation("first_person", animation.firstPerson()); + attachable.withAnimation("third_person", animation.thirdPerson()); attachable.withScript("animate", "first_person", "context.is_first_person == 1.0"); attachable.withScript("animate", "third_person", "context.is_first_person == 0.0"); }); @@ -59,4 +55,10 @@ public class AttachableMapper { private static ResourceLocation getTexture(List info, EquipmentClientInfo.LayerType layer) { return info.getFirst().textureId().withPath(path -> "entity/equipment/" + layer.getSerializedName() + "/" + path); } + + @FunctionalInterface + public interface AttachableCreator { + + Optional create(ResourceLocation bedrockIdentifier, Optional geometry, Consumer textureConsumer); + } } diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/BedrockGeometryContext.java b/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/BedrockGeometryContext.java new file mode 100644 index 0000000..25730ff --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/BedrockGeometryContext.java @@ -0,0 +1,62 @@ +package org.geysermc.rainbow.mapping.geometry; + +import com.google.common.base.Suppliers; +import net.minecraft.client.renderer.block.model.TextureSlots; +import net.minecraft.client.resources.model.Material; +import net.minecraft.client.resources.model.ResolvedModel; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import org.geysermc.rainbow.Rainbow; +import org.geysermc.rainbow.mapping.PackContext; +import org.geysermc.rainbow.mapping.animation.AnimationMapper; +import org.geysermc.rainbow.mapping.animation.BedrockAnimationContext; +import org.geysermc.rainbow.pack.geometry.BedrockGeometry; + +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.stream.Stream; + +public record BedrockGeometryContext(Optional> geometry, + Optional animation, TextureHolder icon, + boolean handheld) { + private static final List HANDHELD_MODELS = Stream.of("item/handheld", "item/handheld_rod", "item/handheld_mace") + .map(ResourceLocation::withDefaultNamespace) + .toList(); + + public static BedrockGeometryContext create(ResourceLocation bedrockIdentifier, ItemStack stackToRender, ResolvedModel model, PackContext context) { + ResolvedModel parentModel = model.parent(); + // debugName() returns the resource location of the model as a string + boolean handheld = parentModel != null && HANDHELD_MODELS.contains(ResourceLocation.parse(parentModel.debugName())); + + TextureSlots textures = model.getTopTextureSlots(); + Material layer0Texture = textures.getMaterial("layer0"); + Optional> geometry; + Optional animation; + TextureHolder icon; + + if (layer0Texture != null) { + geometry = Optional.empty(); + animation = Optional.empty(); + icon = TextureHolder.createFromResources(layer0Texture.texture()); + } else { + // Unknown model (doesn't use layer0), so we immediately assume the geometry is custom + // This check should probably be done differently (actually check if the model is 2D or 3D) + + ResourceLocation modelLocation = ResourceLocation.parse(model.debugName()); + String safeIdentifier = Rainbow.safeResourceLocation(bedrockIdentifier); + + geometry = Optional.of(Suppliers.memoize(() -> { + StitchedTextures stitchedTextures = StitchedTextures.stitchModelTextures(textures, context); + BedrockGeometry mappedGeometry = GeometryMapper.mapGeometry(safeIdentifier, "bone", model, stitchedTextures); + return new StitchedGeometry(mappedGeometry, TextureHolder.createProvided(modelLocation.withSuffix("_stitched"), stitchedTextures.stitched())); + })); + + animation = Optional.of(AnimationMapper.mapAnimation(safeIdentifier, "bone", model.getTopTransforms())); + icon = context.geometryRenderer().isPresent() ? TextureHolder.createProvided(modelLocation, () -> context.geometryRenderer().orElseThrow().render(stackToRender)) + : TextureHolder.createNonExistent(modelLocation); + } + + return new BedrockGeometryContext(geometry, animation, icon, handheld); + } +} diff --git a/src/main/java/org/geysermc/rainbow/mapping/geometry/GeometryMapper.java b/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/GeometryMapper.java similarity index 61% rename from src/main/java/org/geysermc/rainbow/mapping/geometry/GeometryMapper.java rename to rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/GeometryMapper.java index 58eed4f..4ae3d2e 100644 --- a/src/main/java/org/geysermc/rainbow/mapping/geometry/GeometryMapper.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/GeometryMapper.java @@ -5,8 +5,9 @@ import net.minecraft.client.renderer.block.model.BlockElementFace; import net.minecraft.client.renderer.block.model.BlockElementRotation; import net.minecraft.client.renderer.block.model.SimpleUnbakedGeometry; import net.minecraft.client.resources.model.ResolvedModel; +import net.minecraft.client.resources.model.UnbakedGeometry; import net.minecraft.core.Direction; -import net.minecraft.resources.ResourceLocation; +import org.geysermc.rainbow.mixin.FaceBakeryAccessor; import org.geysermc.rainbow.pack.geometry.BedrockGeometry; import org.joml.Vector2f; import org.joml.Vector3f; @@ -17,26 +18,30 @@ import java.util.Map; public class GeometryMapper { private static final Vector3fc CENTRE_OFFSET = new Vector3f(8.0F, 0.0F, 8.0F); - public static BedrockGeometryContext mapGeometry(String identifier, String boneName, ResolvedModel model, ResourceLocation texture) { + public static BedrockGeometry mapGeometry(String identifier, String boneName, ResolvedModel model, StitchedTextures textures) { + UnbakedGeometry top = model.getTopGeometry(); + if (top == UnbakedGeometry.EMPTY) { + return BedrockGeometry.EMPTY; + } + BedrockGeometry.Builder builder = BedrockGeometry.builder(identifier); // Blockbench seems to always use these values TODO that's wrong builder.withVisibleBoundsWidth(4.0F); builder.withVisibleBoundsHeight(4.0F); builder.withVisibleBoundsOffset(new Vector3f(0.0F, 0.75F, 0.0F)); - // TODO proper texture size - builder.withTextureWidth(16); - builder.withTextureHeight(16); + builder.withTextureWidth(textures.width()); + builder.withTextureHeight(textures.height()); BedrockGeometry.Bone.Builder bone = BedrockGeometry.bone(boneName); Vector3f min = new Vector3f(Float.MAX_VALUE); Vector3f max = new Vector3f(Float.MIN_VALUE); - SimpleUnbakedGeometry geometry = (SimpleUnbakedGeometry) model.getTopGeometry(); + SimpleUnbakedGeometry geometry = (SimpleUnbakedGeometry) top; for (BlockElement element : geometry.elements()) { // TODO the origin here is wrong, some models seem to be mirrored weirdly in blockbench - BedrockGeometry.Cube cube = mapBlockElement(element).build(); + BedrockGeometry.Cube cube = mapBlockElement(element, textures).build(); bone.withCube(cube); min.min(cube.origin()); max.max(cube.origin().add(cube.size(), new Vector3f())); @@ -48,35 +53,43 @@ public class GeometryMapper { // Bind to the bone of the current item slot bone.withBinding("q.item_slot_to_bone_name(context.item_slot)"); - return new BedrockGeometryContext(builder.withBone(bone).build(), texture); + return builder.withBone(bone).build(); } - private static BedrockGeometry.Cube.Builder mapBlockElement(BlockElement element) { + private static BedrockGeometry.Cube.Builder mapBlockElement(BlockElement element, StitchedTextures textures) { // The centre of the model is back by 8 in the X and Z direction on Java, so move the origin of the cube and the pivot like that BedrockGeometry.Cube.Builder builder = BedrockGeometry.cube(element.from().sub(CENTRE_OFFSET, new Vector3f()), element.to().sub(element.from(), new Vector3f())); for (Map.Entry faceEntry : element.faces().entrySet()) { - // TODO texture key Direction direction = faceEntry.getKey(); BlockElementFace face = faceEntry.getValue(); Vector2f uvOrigin; Vector2f uvSize; BlockElementFace.UVs uvs = face.uvs(); - if (uvs != null) { - // Up and down faces are special - if (direction.getAxis() == Direction.Axis.Y) { - uvOrigin = new Vector2f(uvs.maxU(), uvs.maxV()); - uvSize = new Vector2f(uvs.minU() - uvs.maxU(), uvs.minV() - uvs.maxV()); - } else { - uvOrigin = new Vector2f(uvs.minU(), uvs.minV()); - uvSize = new Vector2f(uvs.maxU() - uvs.minU(), uvs.maxV() - uvs.minV()); - } - } else { - uvOrigin = new Vector2f(); - uvSize = new Vector2f(); + if (uvs == null) { + // Java defaults to a set of UV values determined by the position of the face if no UV values were specified + uvs = FaceBakeryAccessor.invokeDefaultFaceUV(element.from(), element.to(), direction); } + // Up and down faces are special and have their UVs flipped + if (direction.getAxis() == Direction.Axis.Y) { + uvOrigin = new Vector2f(uvs.maxU(), uvs.maxV()); + uvSize = new Vector2f(uvs.minU() - uvs.maxU(), uvs.minV() - uvs.maxV()); + } else { + uvOrigin = new Vector2f(uvs.minU(), uvs.minV()); + uvSize = new Vector2f(uvs.maxU() - uvs.minU(), uvs.maxV() - uvs.minV()); + } + + // If the texture was stitched (which it should have been, unless it doesn't exist), s UV values on Java are always in the [0;16] range, adjust the values properly to the texture size, + // and offset the UVs by the texture's starting UV + textures.getSprite(face.texture()).ifPresent(sprite -> { + float widthMultiplier = sprite.contents().width() / 16.0F; + float heightMultiplier = sprite.contents().height() / 16.0F; + uvOrigin.mul(widthMultiplier, heightMultiplier); + uvSize.mul(widthMultiplier, heightMultiplier); + uvOrigin.add(sprite.getX(), sprite.getY()); + }); builder.withFace(direction, uvOrigin, uvSize, face.rotation()); } diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/GeometryRenderer.java b/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/GeometryRenderer.java new file mode 100644 index 0000000..4607641 --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/GeometryRenderer.java @@ -0,0 +1,9 @@ +package org.geysermc.rainbow.mapping.geometry; + +import com.mojang.blaze3d.platform.NativeImage; +import net.minecraft.world.item.ItemStack; + +public interface GeometryRenderer { + + NativeImage render(ItemStack stack); +} diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/StitchedGeometry.java b/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/StitchedGeometry.java new file mode 100644 index 0000000..2ee4f48 --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/StitchedGeometry.java @@ -0,0 +1,5 @@ +package org.geysermc.rainbow.mapping.geometry; + +import org.geysermc.rainbow.pack.geometry.BedrockGeometry; + +public record StitchedGeometry(BedrockGeometry geometry, TextureHolder stitchedTextures) {} diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/StitchedTextures.java b/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/StitchedTextures.java new file mode 100644 index 0000000..1ce8e85 --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/StitchedTextures.java @@ -0,0 +1,78 @@ +package org.geysermc.rainbow.mapping.geometry; + +import com.mojang.blaze3d.platform.NativeImage; +import net.minecraft.Util; +import net.minecraft.client.renderer.block.model.TextureSlots; +import net.minecraft.client.renderer.texture.SpriteContents; +import net.minecraft.client.renderer.texture.SpriteLoader; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.resources.metadata.animation.FrameSize; +import net.minecraft.client.resources.model.Material; +import net.minecraft.data.AtlasIds; +import net.minecraft.resources.ResourceLocation; +import org.geysermc.rainbow.Rainbow; +import org.geysermc.rainbow.RainbowIO; +import org.geysermc.rainbow.mapping.PackContext; +import org.geysermc.rainbow.mixin.SpriteContentsAccessor; +import org.geysermc.rainbow.mixin.SpriteLoaderAccessor; +import org.geysermc.rainbow.mixin.TextureSlotsAccessor; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.stream.Stream; + +public record StitchedTextures(Map sprites, Supplier stitched, int width, int height) { + + public Optional getSprite(String key) { + if (TextureSlotsAccessor.invokeIsTextureReference(key)) { + key = key.substring(1); + } + return Optional.ofNullable(sprites.get(key)); + } + + public static StitchedTextures stitchModelTextures(TextureSlots textures, PackContext context) { + Map materials = ((TextureSlotsAccessor) textures).getResolvedValues(); + SpriteLoader.Preparations preparations = prepareStitching(materials.values().stream().map(Material::texture), context); + + Map sprites = new HashMap<>(); + for (Map.Entry material : materials.entrySet()) { + sprites.put(material.getKey(), preparations.getSprite(material.getValue().texture())); + } + return new StitchedTextures(Map.copyOf(sprites), () -> stitchTextureAtlas(preparations), preparations.width(), preparations.height()); + } + + private static SpriteLoader.Preparations prepareStitching(Stream textures, PackContext context) { + // Atlas ID doesn't matter much here, but BLOCKS is the most appropriate + // Not sure if 1024 should be the max supported texture size, but it seems to work + SpriteLoader spriteLoader = new SpriteLoader(AtlasIds.BLOCKS, 1024, 16, 16); + List sprites = textures.distinct() + .map(texture -> readSpriteContents(texture, context)) + .mapMulti(Optional::ifPresent) + .toList(); + return ((SpriteLoaderAccessor) spriteLoader).invokeStitch(sprites, 0, Util.backgroundExecutor()); + } + + private static Optional readSpriteContents(ResourceLocation location, PackContext context) { + return RainbowIO.safeIO(() -> { + try (InputStream textureStream = context.assetResolver().openAsset(Rainbow.decorateTextureLocation(location))) { + NativeImage texture = NativeImage.read(textureStream); + return new SpriteContents(location, new FrameSize(texture.getWidth(), texture.getHeight()), texture); + } + }); + } + + private static NativeImage stitchTextureAtlas(SpriteLoader.Preparations preparations) { + NativeImage stitched = new NativeImage(preparations.width(), preparations.height(), true); + for (TextureAtlasSprite sprite : preparations.regions().values()) { + try (SpriteContents contents = sprite.contents()) { + ((SpriteContentsAccessor) contents).getOriginalImage().copyRect(stitched, 0, 0, + sprite.getX(), sprite.getY(), contents.width(), contents.height(), false, false); + } + } + return stitched; + } +} diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/TextureHolder.java b/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/TextureHolder.java new file mode 100644 index 0000000..c209741 --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/mapping/geometry/TextureHolder.java @@ -0,0 +1,42 @@ +package org.geysermc.rainbow.mapping.geometry; + +import com.mojang.blaze3d.platform.NativeImage; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.ProblemReporter; +import org.geysermc.rainbow.Rainbow; +import org.geysermc.rainbow.RainbowIO; +import org.geysermc.rainbow.image.NativeImageUtil; +import org.geysermc.rainbow.mapping.AssetResolver; + +import java.io.InputStream; +import java.util.Optional; +import java.util.function.Supplier; + +public record TextureHolder(ResourceLocation location, Optional> supplier, boolean existsInResources) { + + public Optional load(AssetResolver assetResolver, ProblemReporter reporter) { + if (existsInResources) { + return RainbowIO.safeIO(() -> { + try (InputStream texture = assetResolver.openAsset(Rainbow.decorateTextureLocation(location))) { + return texture.readAllBytes(); + } + }); + } else if (supplier.isPresent()) { + return RainbowIO.safeIO(() -> NativeImageUtil.writeToByteArray(supplier.get().get())); + } + reporter.report(() -> "missing texture for " + location + "; please provide it manually"); + return Optional.empty(); + } + + public static TextureHolder createProvided(ResourceLocation location, Supplier supplier) { + return new TextureHolder(location, Optional.of(supplier), false); + } + + public static TextureHolder createFromResources(ResourceLocation location) { + return new TextureHolder(location, Optional.empty(), true); + } + + public static TextureHolder createNonExistent(ResourceLocation location) { + return new TextureHolder(location, Optional.empty(), false); + } +} diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mixin/FaceBakeryAccessor.java b/rainbow/src/main/java/org/geysermc/rainbow/mixin/FaceBakeryAccessor.java new file mode 100644 index 0000000..2fa94a4 --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/mixin/FaceBakeryAccessor.java @@ -0,0 +1,17 @@ +package org.geysermc.rainbow.mixin; + +import net.minecraft.client.renderer.block.model.BlockElementFace; +import net.minecraft.client.renderer.block.model.FaceBakery; +import net.minecraft.core.Direction; +import org.joml.Vector3fc; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(FaceBakery.class) +public interface FaceBakeryAccessor { + + @Invoker + static BlockElementFace.UVs invokeDefaultFaceUV(Vector3fc posFrom, Vector3fc posTo, Direction facing) { + throw new AssertionError(); + } +} diff --git a/src/main/java/org/geysermc/rainbow/mixin/LateBoundIdMapperAccessor.java b/rainbow/src/main/java/org/geysermc/rainbow/mixin/LateBoundIdMapperAccessor.java similarity index 100% rename from src/main/java/org/geysermc/rainbow/mixin/LateBoundIdMapperAccessor.java rename to rainbow/src/main/java/org/geysermc/rainbow/mixin/LateBoundIdMapperAccessor.java diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mixin/NativeImageAccessor.java b/rainbow/src/main/java/org/geysermc/rainbow/mixin/NativeImageAccessor.java new file mode 100644 index 0000000..6840312 --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/mixin/NativeImageAccessor.java @@ -0,0 +1,18 @@ +package org.geysermc.rainbow.mixin; + +import com.mojang.blaze3d.platform.NativeImage; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +import java.io.IOException; +import java.nio.channels.WritableByteChannel; + +@Mixin(NativeImage.class) +public interface NativeImageAccessor { + + @Invoker + void invokeCheckAllocated(); + + @Invoker + boolean invokeWriteToChannel(WritableByteChannel channel) throws IOException; +} diff --git a/src/main/java/org/geysermc/rainbow/mixin/RangeSelectItemModelAccessor.java b/rainbow/src/main/java/org/geysermc/rainbow/mixin/RangeSelectItemModelAccessor.java similarity index 100% rename from src/main/java/org/geysermc/rainbow/mixin/RangeSelectItemModelAccessor.java rename to rainbow/src/main/java/org/geysermc/rainbow/mixin/RangeSelectItemModelAccessor.java diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mixin/SpriteContentsAccessor.java b/rainbow/src/main/java/org/geysermc/rainbow/mixin/SpriteContentsAccessor.java new file mode 100644 index 0000000..e2793de --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/mixin/SpriteContentsAccessor.java @@ -0,0 +1,13 @@ +package org.geysermc.rainbow.mixin; + +import com.mojang.blaze3d.platform.NativeImage; +import net.minecraft.client.renderer.texture.SpriteContents; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(SpriteContents.class) +public interface SpriteContentsAccessor { + + @Accessor + NativeImage getOriginalImage(); +} diff --git a/rainbow/src/main/java/org/geysermc/rainbow/mixin/SpriteLoaderAccessor.java b/rainbow/src/main/java/org/geysermc/rainbow/mixin/SpriteLoaderAccessor.java new file mode 100644 index 0000000..4401cac --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/mixin/SpriteLoaderAccessor.java @@ -0,0 +1,16 @@ +package org.geysermc.rainbow.mixin; + +import net.minecraft.client.renderer.texture.SpriteContents; +import net.minecraft.client.renderer.texture.SpriteLoader; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +import java.util.List; +import java.util.concurrent.Executor; + +@Mixin(SpriteLoader.class) +public interface SpriteLoaderAccessor { + + @Invoker + SpriteLoader.Preparations invokeStitch(List contents, int mipLevel, Executor executor); +} diff --git a/src/main/java/org/geysermc/rainbow/mixin/TextureSlotsAccessor.java b/rainbow/src/main/java/org/geysermc/rainbow/mixin/TextureSlotsAccessor.java similarity index 71% rename from src/main/java/org/geysermc/rainbow/mixin/TextureSlotsAccessor.java rename to rainbow/src/main/java/org/geysermc/rainbow/mixin/TextureSlotsAccessor.java index c957971..6c5af2f 100644 --- a/src/main/java/org/geysermc/rainbow/mixin/TextureSlotsAccessor.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/mixin/TextureSlotsAccessor.java @@ -4,6 +4,7 @@ import net.minecraft.client.renderer.block.model.TextureSlots; import net.minecraft.client.resources.model.Material; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; import java.util.Map; @@ -12,4 +13,9 @@ public interface TextureSlotsAccessor { @Accessor Map getResolvedValues(); + + @Invoker + static boolean invokeIsTextureReference(String name) { + throw new AssertionError(); + } } diff --git a/rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockItem.java b/rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockItem.java new file mode 100644 index 0000000..5481ac0 --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockItem.java @@ -0,0 +1,44 @@ +package org.geysermc.rainbow.pack; + +import net.minecraft.resources.ResourceLocation; +import org.geysermc.rainbow.Rainbow; +import org.geysermc.rainbow.mapping.PackSerializer; +import org.geysermc.rainbow.mapping.attachable.AttachableMapper; +import org.geysermc.rainbow.mapping.geometry.BedrockGeometryContext; +import org.geysermc.rainbow.mapping.geometry.StitchedGeometry; +import org.geysermc.rainbow.mapping.geometry.TextureHolder; +import org.geysermc.rainbow.pack.attachable.BedrockAttachable; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.function.Supplier; + +public record BedrockItem(ResourceLocation identifier, String textureName, BedrockGeometryContext geometryContext, AttachableMapper.AttachableCreator attachableCreator) { + + public CompletableFuture save(PackSerializer serializer, Path attachableDirectory, Path geometryDirectory, Path animationDirectory, + Function> textureSaver) { + return CompletableFuture.allOf( + textureSaver.apply(geometryContext.icon()), + CompletableFuture.supplyAsync(() -> geometryContext.geometry().map(Supplier::get)) + .thenCompose(stitchedGeometry -> { + List attachableTextures = new ArrayList<>(); + Optional createdAttachable = attachableCreator.create(identifier, stitchedGeometry, attachableTextures::add); + return CompletableFuture.allOf( + createdAttachable.map(attachable -> attachable.save(serializer, attachableDirectory)).orElse(noop()), + CompletableFuture.allOf(attachableTextures.stream().map(textureSaver).toArray(CompletableFuture[]::new)), + stitchedGeometry.map(StitchedGeometry::geometry).map(geometry -> geometry.save(serializer, geometryDirectory)).orElse(noop()), + stitchedGeometry.map(StitchedGeometry::stitchedTextures).map(textureSaver).orElse(noop()), + geometryContext.animation().map(context -> context.animation().save(serializer, animationDirectory, Rainbow.safeResourceLocation(identifier))).orElse(noop()) + ); + }) + ); + } + + private static CompletableFuture noop() { + return CompletableFuture.completedFuture(null); + } +} diff --git a/rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockPack.java b/rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockPack.java new file mode 100644 index 0000000..9a38d69 --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockPack.java @@ -0,0 +1,297 @@ +package org.geysermc.rainbow.pack; + +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; +import net.minecraft.core.Holder; +import net.minecraft.core.component.DataComponentPatch; +import net.minecraft.core.component.DataComponents; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.ProblemReporter; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.CustomModelData; +import org.geysermc.rainbow.CodecUtil; +import org.geysermc.rainbow.PackConstants; +import org.geysermc.rainbow.Rainbow; +import org.geysermc.rainbow.RainbowIO; +import org.geysermc.rainbow.mapping.AssetResolver; +import org.geysermc.rainbow.mapping.BedrockItemMapper; +import org.geysermc.rainbow.mapping.PackContext; +import org.geysermc.rainbow.mapping.PackSerializer; +import org.geysermc.rainbow.mapping.geometry.GeometryRenderer; +import org.geysermc.rainbow.definition.GeyserMappings; +import org.geysermc.rainbow.mapping.geometry.TextureHolder; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +public class BedrockPack { + private final String name; + private final PackManifest manifest; + private final PackPaths paths; + private final PackSerializer serializer; + + private final BedrockTextures.Builder itemTextures = BedrockTextures.builder(); + private final Set bedrockItems = new HashSet<>(); + private final Set modelsMapped = new HashSet<>(); + private final IntSet customModelDataMapped = new IntOpenHashSet(); + + private final PackContext context; + private final ProblemReporter reporter; + + public BedrockPack(String name, PackManifest manifest, PackPaths paths, PackSerializer serializer, AssetResolver assetResolver, + Optional geometryRenderer, ProblemReporter reporter, + boolean reportSuccesses) { + this.name = name; + this.manifest = manifest; + this.paths = paths; + this.serializer = serializer; + + // Not reading existing item mappings/texture atlas for now since that doesn't work all that well yet + this.context = new PackContext(new GeyserMappings(), paths, item -> { + itemTextures.withItemTexture(item); + bedrockItems.add(item); + }, assetResolver, geometryRenderer, reportSuccesses); + this.reporter = reporter; + } + + public String name() { + return name; + } + + public MappingResult map(ItemStack stack) { + if (stack.isEmpty()) { + return MappingResult.NONE_MAPPED; + } + + AtomicBoolean problems = new AtomicBoolean(); + ProblemReporter mapReporter = new ProblemReporter() { + + @Override + public @NotNull ProblemReporter forChild(PathElement child) { + return reporter.forChild(child); + } + + @Override + public void report(Problem problem) { + problems.set(true); + reporter.report(problem); + } + }; + + Optional patchedModel = stack.getComponentsPatch().get(DataComponents.ITEM_MODEL); + //noinspection OptionalAssignedToNull - annoying Mojang + if (patchedModel == null || patchedModel.isEmpty()) { + CustomModelData customModelData = stack.get(DataComponents.CUSTOM_MODEL_DATA); + Float firstNumber; + if (customModelData == null || (firstNumber = customModelData.getFloat(0)) == null + || !customModelDataMapped.add((firstNumber.intValue()))) { + return MappingResult.NONE_MAPPED; + } + + BedrockItemMapper.tryMapStack(stack, firstNumber.intValue(), mapReporter, context); + } else { + ResourceLocation model = patchedModel.get(); + if (!modelsMapped.add(model)) { + return MappingResult.NONE_MAPPED; + } + + BedrockItemMapper.tryMapStack(stack, model, mapReporter, context); + } + + return problems.get() ? MappingResult.PROBLEMS_OCCURRED : MappingResult.MAPPED_SUCCESSFULLY; + } + + public MappingResult map(Holder item, DataComponentPatch patch) { + ItemStack stack = new ItemStack(item); + stack.applyComponents(patch); + return map(stack); + } + + public CompletableFuture save() { + List> futures = new ArrayList<>(); + + futures.add(serializer.saveJson(GeyserMappings.CODEC, context.mappings(), paths.mappings())); + futures.add(serializer.saveJson(PackManifest.CODEC, manifest, paths.manifest())); + futures.add(serializer.saveJson(BedrockTextureAtlas.CODEC, BedrockTextureAtlas.itemAtlas(name, itemTextures), paths.itemAtlas())); + + Function> textureSaver = texture -> { + ResourceLocation textureLocation = Rainbow.decorateTextureLocation(texture.location()); + return texture.load(context.assetResolver(), reporter) + .map(bytes -> serializer.saveTexture(bytes, paths.packRoot().resolve(textureLocation.getPath()))) + .orElse(CompletableFuture.completedFuture(null)); + }; + + for (BedrockItem item : bedrockItems) { + futures.add(item.save(serializer, paths.attachables(), paths.geometry(), paths.animation(), textureSaver)); + } + + if (paths.zipOutput().isPresent()) { + RainbowIO.safeIO(() -> CodecUtil.tryZipDirectory(paths.packRoot(), paths.zipOutput().get())); + } + + if (reporter instanceof AutoCloseable closeable) { + try { + closeable.close(); + } catch (Exception ignored) {} + } + + return CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)); + } + + public int getMappings() { + return context.mappings().size(); + } + + public Set getBedrockItems() { + return Set.copyOf(bedrockItems); + } + + public int getItemTextureAtlasSize() { + return itemTextures.build().size(); + } + + public ProblemReporter getReporter() { + return reporter; + } + + public static Builder builder(String name, Path mappingsPath, Path packRootPath, PackSerializer packSerializer, AssetResolver assetResolver) { + return new Builder(name, mappingsPath, packRootPath, packSerializer, assetResolver); + } + + public static class Builder { + private static final Path ATTACHABLES_DIRECTORY = Path.of("attachables"); + private static final Path GEOMETRY_DIRECTORY = Path.of("models/entity"); + private static final Path ANIMATION_DIRECTORY = Path.of("animations"); + + private static final Path MANIFEST_FILE = Path.of("manifest.json"); + private static final Path ITEM_ATLAS_FILE = Path.of("textures/item_texture.json"); + + private final String name; + private final Path mappingsPath; + private final Path packRootPath; + private final PackSerializer packSerializer; + private final AssetResolver assetResolver; + private PackManifest manifest; + private UnaryOperator attachablesPath = resolve(ATTACHABLES_DIRECTORY); + private UnaryOperator geometryPath = resolve(GEOMETRY_DIRECTORY); + private UnaryOperator animationPath = resolve(ANIMATION_DIRECTORY); + private UnaryOperator manifestPath = resolve(MANIFEST_FILE); + private UnaryOperator itemAtlasPath = resolve(ITEM_ATLAS_FILE); + private Path packZipFile = null; + private GeometryRenderer geometryRenderer = null; + private Function reporter; + private boolean reportSuccesses = false; + + public Builder(String name, Path mappingsPath, Path packRootPath, PackSerializer packSerializer, AssetResolver assetResolver) { + this.name = name; + this.mappingsPath = mappingsPath; + this.packRootPath = packRootPath; + this.reporter = ProblemReporter.Collector::new; + this.packSerializer = packSerializer; + this.assetResolver = assetResolver; + manifest = defaultManifest(name); + } + + public Builder withManifest(PackManifest manifest) { + this.manifest = manifest; + return this; + } + + public Builder withAttachablesPath(Path absolute) { + return withAttachablesPath(path -> absolute); + } + + public Builder withAttachablesPath(UnaryOperator path) { + attachablesPath = path; + return this; + } + + public Builder withGeometryPath(Path absolute) { + return withGeometryPath(path -> absolute); + } + + public Builder withGeometryPath(UnaryOperator path) { + geometryPath = path; + return this; + } + + public Builder withAnimationPath(Path absolute) { + return withAnimationPath(path -> absolute); + } + + public Builder withAnimationPath(UnaryOperator path) { + animationPath = path; + return this; + } + + public Builder withManifestPath(Path absolute) { + return withManifestPath(path -> absolute); + } + + public Builder withManifestPath(UnaryOperator path) { + manifestPath = path; + return this; + } + + public Builder withItemAtlasPath(Path absolute) { + return withItemAtlasPath(path -> absolute); + } + + public Builder withItemAtlasPath(UnaryOperator path) { + itemAtlasPath = path; + return this; + } + + public Builder withPackZipFile(Path absolute) { + packZipFile = absolute; + return this; + } + + public Builder withGeometryRenderer(GeometryRenderer renderer) { + geometryRenderer = renderer; + return this; + } + + public Builder withReporter(Function reporter) { + this.reporter = reporter; + return this; + } + + public Builder reportSuccesses() { + this.reportSuccesses = true; + return this; + } + + public BedrockPack build() { + PackPaths paths = new PackPaths(mappingsPath, packRootPath, attachablesPath.apply(packRootPath), + geometryPath.apply(packRootPath), animationPath.apply(packRootPath), manifestPath.apply(packRootPath), + itemAtlasPath.apply(packRootPath), Optional.ofNullable(packZipFile)); + return new BedrockPack(name, manifest, paths, packSerializer, assetResolver, Optional.ofNullable(geometryRenderer), reporter.apply(() -> "Bedrock pack " + name + " "), reportSuccesses); + } + + private static UnaryOperator resolve(Path child) { + return root -> root.resolve(child); + } + + private static PackManifest defaultManifest(String name) { + return PackManifest.create(name, PackConstants.DEFAULT_PACK_DESCRIPTION, UUID.randomUUID(), BedrockVersion.of(0)); + } + } + + public enum MappingResult { + NONE_MAPPED, + MAPPED_SUCCESSFULLY, + PROBLEMS_OCCURRED + } +} diff --git a/src/main/java/org/geysermc/rainbow/pack/BedrockTextureAtlas.java b/rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockTextureAtlas.java similarity index 100% rename from src/main/java/org/geysermc/rainbow/pack/BedrockTextureAtlas.java rename to rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockTextureAtlas.java diff --git a/src/main/java/org/geysermc/rainbow/pack/BedrockTextures.java b/rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockTextures.java similarity index 97% rename from src/main/java/org/geysermc/rainbow/pack/BedrockTextures.java rename to rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockTextures.java index 030da51..2b2345f 100644 --- a/src/main/java/org/geysermc/rainbow/pack/BedrockTextures.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockTextures.java @@ -33,7 +33,7 @@ public record BedrockTextures(Map textures) { private final Map textures = new HashMap<>(); public Builder withItemTexture(BedrockItem item) { - return withTexture(item.textureName(), TEXTURES_FOLDER + item.texture().getPath()); + return withTexture(item.textureName(), TEXTURES_FOLDER + item.geometryContext().icon().location().getPath()); } public Builder withTexture(String name, String texture) { diff --git a/src/main/java/org/geysermc/rainbow/pack/BedrockVersion.java b/rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockVersion.java similarity index 100% rename from src/main/java/org/geysermc/rainbow/pack/BedrockVersion.java rename to rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockVersion.java diff --git a/src/main/java/org/geysermc/rainbow/pack/PackManifest.java b/rainbow/src/main/java/org/geysermc/rainbow/pack/PackManifest.java similarity index 88% rename from src/main/java/org/geysermc/rainbow/pack/PackManifest.java rename to rainbow/src/main/java/org/geysermc/rainbow/pack/PackManifest.java index 456dc52..3611faf 100644 --- a/src/main/java/org/geysermc/rainbow/pack/PackManifest.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/pack/PackManifest.java @@ -5,6 +5,7 @@ import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; import net.minecraft.core.UUIDUtil; import org.geysermc.rainbow.CodecUtil; +import org.geysermc.rainbow.PackConstants; import java.util.List; import java.util.UUID; @@ -56,5 +57,10 @@ public record PackManifest(Header header, List modules) { return new Module(name, description, uuid, version.increment()); } } + + public static PackManifest create(String name, String description, UUID uuid, BedrockVersion version) { + return new PackManifest(new PackManifest.Header(name, description, uuid, version, PackConstants.ENGINE_VERSION), + List.of(new PackManifest.Module(name, description, uuid, version))); + } } diff --git a/rainbow/src/main/java/org/geysermc/rainbow/pack/PackPaths.java b/rainbow/src/main/java/org/geysermc/rainbow/pack/PackPaths.java new file mode 100644 index 0000000..16f101f --- /dev/null +++ b/rainbow/src/main/java/org/geysermc/rainbow/pack/PackPaths.java @@ -0,0 +1,8 @@ +package org.geysermc.rainbow.pack; + +import java.nio.file.Path; +import java.util.Optional; + +public record PackPaths(Path mappings, Path packRoot, Path attachables, Path geometry, Path animation, + Path manifest, Path itemAtlas, Optional zipOutput) { +} diff --git a/src/main/java/org/geysermc/rainbow/pack/animation/BedrockAnimation.java b/rainbow/src/main/java/org/geysermc/rainbow/pack/animation/BedrockAnimation.java similarity index 95% rename from src/main/java/org/geysermc/rainbow/pack/animation/BedrockAnimation.java rename to rainbow/src/main/java/org/geysermc/rainbow/pack/animation/BedrockAnimation.java index dbdf1fc..263813e 100644 --- a/src/main/java/org/geysermc/rainbow/pack/animation/BedrockAnimation.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/pack/animation/BedrockAnimation.java @@ -5,14 +5,15 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import com.mojang.serialization.codecs.RecordCodecBuilder; import org.geysermc.rainbow.CodecUtil; +import org.geysermc.rainbow.mapping.PackSerializer; import org.geysermc.rainbow.pack.BedrockVersion; import org.joml.Vector3fc; -import java.io.IOException; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.concurrent.CompletableFuture; public record BedrockAnimation(BedrockVersion formatVersion, Map definitions) { public static final BedrockVersion FORMAT_VERSION = BedrockVersion.of(1, 8, 0); @@ -24,8 +25,8 @@ public record BedrockAnimation(BedrockVersion formatVersion, Map save(PackSerializer serializer, Path animationDirectory, String identifier) { + return serializer.saveJson(CODEC, this, animationDirectory.resolve(identifier + ".animation.json")); } public static Builder builder() { diff --git a/src/main/java/org/geysermc/rainbow/pack/attachable/BedrockAttachable.java b/rainbow/src/main/java/org/geysermc/rainbow/pack/attachable/BedrockAttachable.java similarity index 96% rename from src/main/java/org/geysermc/rainbow/pack/attachable/BedrockAttachable.java rename to rainbow/src/main/java/org/geysermc/rainbow/pack/attachable/BedrockAttachable.java index a6e8b25..1178962 100644 --- a/src/main/java/org/geysermc/rainbow/pack/attachable/BedrockAttachable.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/pack/attachable/BedrockAttachable.java @@ -9,15 +9,14 @@ import net.minecraft.resources.ResourceLocation; import net.minecraft.util.ExtraCodecs; import net.minecraft.util.StringRepresentable; import net.minecraft.world.entity.EquipmentSlot; -import org.geysermc.rainbow.CodecUtil; import org.geysermc.rainbow.PackConstants; import org.geysermc.rainbow.Rainbow; +import org.geysermc.rainbow.mapping.PackSerializer; import org.geysermc.rainbow.pack.BedrockTextures; import org.geysermc.rainbow.pack.BedrockVersion; import org.geysermc.rainbow.pack.geometry.BedrockGeometry; import org.jetbrains.annotations.NotNull; -import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.EnumMap; @@ -25,6 +24,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.CompletableFuture; import java.util.stream.Stream; public record BedrockAttachable(BedrockVersion formatVersion, AttachableInfo info) { @@ -35,9 +35,9 @@ public record BedrockAttachable(BedrockVersion formatVersion, AttachableInfo inf ).apply(instance, BedrockAttachable::new) ); - public void save(Path attachablesDirectory) throws IOException { + public CompletableFuture save(PackSerializer serializer, Path attachablesDirectory) { // Get a safe attachable path by using Geyser's way of getting icons - CodecUtil.trySaveJson(CODEC, this, attachablesDirectory.resolve(Rainbow.fileSafeResourceLocation(info.identifier) + ".json")); + return serializer.saveJson(CODEC, this, attachablesDirectory.resolve(Rainbow.safeResourceLocation(info.identifier) + ".json")); } public static Builder builder(ResourceLocation identifier) { diff --git a/src/main/java/org/geysermc/rainbow/pack/attachable/VanillaGeometries.java b/rainbow/src/main/java/org/geysermc/rainbow/pack/attachable/VanillaGeometries.java similarity index 100% rename from src/main/java/org/geysermc/rainbow/pack/attachable/VanillaGeometries.java rename to rainbow/src/main/java/org/geysermc/rainbow/pack/attachable/VanillaGeometries.java diff --git a/src/main/java/org/geysermc/rainbow/pack/attachable/VanillaMaterials.java b/rainbow/src/main/java/org/geysermc/rainbow/pack/attachable/VanillaMaterials.java similarity index 100% rename from src/main/java/org/geysermc/rainbow/pack/attachable/VanillaMaterials.java rename to rainbow/src/main/java/org/geysermc/rainbow/pack/attachable/VanillaMaterials.java diff --git a/src/main/java/org/geysermc/rainbow/pack/attachable/VanillaRenderControllers.java b/rainbow/src/main/java/org/geysermc/rainbow/pack/attachable/VanillaRenderControllers.java similarity index 100% rename from src/main/java/org/geysermc/rainbow/pack/attachable/VanillaRenderControllers.java rename to rainbow/src/main/java/org/geysermc/rainbow/pack/attachable/VanillaRenderControllers.java diff --git a/src/main/java/org/geysermc/rainbow/pack/attachable/VanillaTextures.java b/rainbow/src/main/java/org/geysermc/rainbow/pack/attachable/VanillaTextures.java similarity index 100% rename from src/main/java/org/geysermc/rainbow/pack/attachable/VanillaTextures.java rename to rainbow/src/main/java/org/geysermc/rainbow/pack/attachable/VanillaTextures.java diff --git a/src/main/java/org/geysermc/rainbow/pack/geometry/BedrockGeometry.java b/rainbow/src/main/java/org/geysermc/rainbow/pack/geometry/BedrockGeometry.java similarity index 96% rename from src/main/java/org/geysermc/rainbow/pack/geometry/BedrockGeometry.java rename to rainbow/src/main/java/org/geysermc/rainbow/pack/geometry/BedrockGeometry.java index 993abe0..e63a430 100644 --- a/src/main/java/org/geysermc/rainbow/pack/geometry/BedrockGeometry.java +++ b/rainbow/src/main/java/org/geysermc/rainbow/pack/geometry/BedrockGeometry.java @@ -5,12 +5,12 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; import net.minecraft.core.Direction; import org.geysermc.rainbow.CodecUtil; +import org.geysermc.rainbow.mapping.PackSerializer; import org.geysermc.rainbow.pack.BedrockVersion; import org.joml.Vector2fc; import org.joml.Vector3f; import org.joml.Vector3fc; -import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; @@ -18,6 +18,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.CompletableFuture; public record BedrockGeometry(BedrockVersion formatVersion, List definitions) { public static final BedrockVersion FORMAT_VERSION = BedrockVersion.of(1, 21, 0); @@ -31,8 +32,10 @@ public record BedrockGeometry(BedrockVersion formatVersion, List save(PackSerializer serializer, Path geometryDirectory) { + return serializer.saveJson(CODEC, this, geometryDirectory.resolve(definitions.getFirst().info.identifier + ".geo.json")); } public static BedrockGeometry of(GeometryDefinition... definitions) { diff --git a/rainbow/src/main/resources/fabric.mod.json b/rainbow/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..a672300 --- /dev/null +++ b/rainbow/src/main/resources/fabric.mod.json @@ -0,0 +1,32 @@ +{ + "schemaVersion": 1, + "id": "rainbow", + "version": "${version}", + "name": "Rainbow", + "description": "Rainbow is a mod to generate Geyser item mappings and bedrock resourcepacks for use with Geyser's custom item API (v2)", + "authors": [ + "GeyserMC contributors" + ], + "contact": { + "homepage": "https://github.com/GeyserMC/rainbow", + "issues": "https://github.com/GeyserMC/rainbow/issues", + "sources": "https://github.com/GeyserMC/rainbow" + }, + "license": "MIT", + "environment": "client", + "mixins": [ + "rainbow.mixins.json" + ], + "depends": { + "fabricloader": ">=${loader_version}", + "fabric-api": "*", + "minecraft": "${supported_versions}" + }, + "custom": { + "modmenu": { + "links": { + "modmenu.discord": "https://discord.gg/GeyserMC" + } + } + } +} diff --git a/rainbow/src/main/resources/rainbow.mixins.json b/rainbow/src/main/resources/rainbow.mixins.json new file mode 100644 index 0000000..43398d5 --- /dev/null +++ b/rainbow/src/main/resources/rainbow.mixins.json @@ -0,0 +1,18 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "org.geysermc.rainbow.mixin", + "compatibilityLevel": "JAVA_21", + "client": [ + "FaceBakeryAccessor", + "LateBoundIdMapperAccessor", + "NativeImageAccessor", + "RangeSelectItemModelAccessor", + "SpriteContentsAccessor", + "SpriteLoaderAccessor", + "TextureSlotsAccessor" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index f91a4fe..0000000 --- a/settings.gradle +++ /dev/null @@ -1,9 +0,0 @@ -pluginManagement { - repositories { - maven { - name = 'Fabric' - url = 'https://maven.fabricmc.net/' - } - gradlePluginPortal() - } -} diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..f44dcdb --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + maven { + name = "Fabric" + url = uri("https://maven.fabricmc.net/") + } + gradlePluginPortal() + } + includeBuild("build-logic") +} + +include(":rainbow") +include(":client") +include(":datagen") + +rootProject.name = "rainbow-parent" diff --git a/src/main/java/org/geysermc/rainbow/PackManager.java b/src/main/java/org/geysermc/rainbow/PackManager.java deleted file mode 100644 index 8148d5d..0000000 --- a/src/main/java/org/geysermc/rainbow/PackManager.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.geysermc.rainbow; - -import org.geysermc.rainbow.pack.BedrockPack; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Optional; -import java.util.function.Consumer; -import java.util.function.Function; - -public final class PackManager { - - private Optional currentPack = Optional.empty(); - - public void startPack(String name) throws IOException { - if (currentPack.isPresent()) { - throw new IllegalStateException("Already started a pack (" + currentPack.get().name() + ")"); - } - - currentPack = Optional.of(new BedrockPack(name)); - } - - public void run(Consumer consumer) { - currentPack.ifPresent(consumer); - } - - public void runOrElse(Consumer consumer, Runnable runnable) { - currentPack.ifPresentOrElse(consumer, runnable); - } - - public Optional getExportPath() { - return currentPack.map(BedrockPack::getExportPath); - } - - public Optional finish() { - Optional success = currentPack.map(BedrockPack::save); - currentPack = Optional.empty(); - return success; - } -} diff --git a/src/main/java/org/geysermc/rainbow/mapping/PackContext.java b/src/main/java/org/geysermc/rainbow/mapping/PackContext.java deleted file mode 100644 index b8a788a..0000000 --- a/src/main/java/org/geysermc/rainbow/mapping/PackContext.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.geysermc.rainbow.mapping; - -import net.minecraft.resources.ResourceLocation; -import org.geysermc.rainbow.mapping.geyser.GeyserMappings; - -import java.nio.file.Path; -import java.util.function.Consumer; - -public record PackContext(GeyserMappings mappings, Path packPath, BedrockItemConsumer itemConsumer, Consumer additionalTextureConsumer) { -} diff --git a/src/main/java/org/geysermc/rainbow/mapping/geometry/BedrockGeometryContext.java b/src/main/java/org/geysermc/rainbow/mapping/geometry/BedrockGeometryContext.java deleted file mode 100644 index 2e26ac2..0000000 --- a/src/main/java/org/geysermc/rainbow/mapping/geometry/BedrockGeometryContext.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.geysermc.rainbow.mapping.geometry; - -import net.minecraft.resources.ResourceLocation; -import org.geysermc.rainbow.pack.geometry.BedrockGeometry; - -public record BedrockGeometryContext(BedrockGeometry geometry, ResourceLocation texture) {} diff --git a/src/main/java/org/geysermc/rainbow/mapping/geyser/GeyserItemDefinition.java b/src/main/java/org/geysermc/rainbow/mapping/geyser/GeyserItemDefinition.java deleted file mode 100644 index ee7ef16..0000000 --- a/src/main/java/org/geysermc/rainbow/mapping/geyser/GeyserItemDefinition.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.geysermc.rainbow.mapping.geyser; - -import net.minecraft.resources.ResourceLocation; - -import java.util.Optional; - -public interface GeyserItemDefinition extends GeyserMapping { - - GeyserBaseDefinition base(); - - boolean conflictsWith(Optional parentModel, GeyserItemDefinition other); -} diff --git a/src/main/java/org/geysermc/rainbow/pack/BedrockItem.java b/src/main/java/org/geysermc/rainbow/pack/BedrockItem.java deleted file mode 100644 index f2403cc..0000000 --- a/src/main/java/org/geysermc/rainbow/pack/BedrockItem.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.geysermc.rainbow.pack; - -import net.minecraft.resources.ResourceLocation; -import org.geysermc.rainbow.Rainbow; -import org.geysermc.rainbow.pack.animation.BedrockAnimation; -import org.geysermc.rainbow.pack.attachable.BedrockAttachable; -import org.geysermc.rainbow.pack.geometry.BedrockGeometry; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Optional; - -public record BedrockItem(ResourceLocation identifier, String textureName, ResourceLocation texture, boolean exportTexture, Optional attachable, - Optional geometry, Optional animation) { - - public void save(Path attachableDirectory, Path geometryDirectory, Path animationDirectory) throws IOException { - if (attachable.isPresent()) { - attachable.get().save(attachableDirectory); - } - if (geometry.isPresent()) { - geometry.get().save(geometryDirectory); - } - if (animation.isPresent()) { - animation.get().save(animationDirectory, Rainbow.fileSafeResourceLocation(identifier)); - } - } -} diff --git a/src/main/java/org/geysermc/rainbow/pack/BedrockPack.java b/src/main/java/org/geysermc/rainbow/pack/BedrockPack.java deleted file mode 100644 index 6aeade7..0000000 --- a/src/main/java/org/geysermc/rainbow/pack/BedrockPack.java +++ /dev/null @@ -1,259 +0,0 @@ -package org.geysermc.rainbow.pack; - -import com.mojang.serialization.JsonOps; -import it.unimi.dsi.fastutil.ints.IntOpenHashSet; -import it.unimi.dsi.fastutil.ints.IntSet; -import net.fabricmc.loader.api.FabricLoader; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.components.SplashRenderer; -import net.minecraft.core.component.DataComponents; -import net.minecraft.resources.RegistryOps; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.util.ProblemReporter; -import net.minecraft.util.RandomSource; -import net.minecraft.util.StringUtil; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.component.CustomModelData; -import org.apache.commons.io.IOUtils; -import org.geysermc.rainbow.CodecUtil; -import org.geysermc.rainbow.PackConstants; -import org.geysermc.rainbow.Rainbow; -import org.geysermc.rainbow.mapping.BedrockItemMapper; -import org.geysermc.rainbow.mapping.PackContext; -import org.geysermc.rainbow.mapping.geyser.GeyserMappings; -import org.geysermc.rainbow.mixin.SplashRendererAccessor; -import org.jetbrains.annotations.NotNull; - -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicBoolean; - -public class BedrockPack { - private static final List PACK_SUMMARY_COMMENTS = List.of("Use the custom item API v2 build!", "bugrock moment", "RORY", - "use !!plshelp", "rm -rf --no-preserve-root /*", "welcome to the internet!", "beep beep. boop boop?", "FROG", "it is frog day", "it is cat day!", - "eclipse will hear about this.", "you must now say the word 'frog' in the #general channel", "You Just Lost The Game", "you are now breathing manually", - "you are now blinking manually", "you're eligible for a free hug token! <3", "don't mind me!", "hissss", "Gayser and Floodgayte, my favourite plugins.", - "meow", "we'll be done here soon™", "got anything else to say?", "we're done now!", "this will be fixed by v6053", "expect it to be done within 180 business days!", - "any colour you like", "someone tell Mojang about this", "you can't unbake baked models, so we'll store the unbaked models", "soon fully datagen ready", - "packconverter when", "codecs ftw"); - private static final RandomSource RANDOM = RandomSource.create(); - - private static final Path EXPORT_DIRECTORY = FabricLoader.getInstance().getGameDir().resolve(Rainbow.MOD_ID); - private static final Path PACK_DIRECTORY = Path.of("pack"); - private static final Path ATTACHABLES_DIRECTORY = Path.of("attachables"); - private static final Path GEOMETRY_DIRECTORY = Path.of("models/entity"); - private static final Path ANIMATION_DIRECTORY = Path.of("animations"); - - private static final Path MAPPINGS_FILE = Path.of("geyser_mappings.json"); - private static final Path MANIFEST_FILE = Path.of("manifest.json"); - private static final Path ITEM_ATLAS_FILE = Path.of("textures/item_texture.json"); - - private static final Path PACK_ZIP_FILE = Path.of("pack.zip"); - private static final Path REPORT_FILE = Path.of("report.txt"); - - private final String name; - private final Path exportPath; - private final Path packPath; - private final PackManifest manifest; - private final GeyserMappings mappings; - private final BedrockTextures.Builder itemTextures; - - private final Set bedrockItems = new HashSet<>(); - private final Set texturesToExport = new HashSet<>(); - private final Set modelsMapped = new HashSet<>(); - private final IntSet customModelDataMapped = new IntOpenHashSet(); - - private final ProblemReporter.Collector reporter; - - public BedrockPack(String name) throws IOException { - this.name = name; - - // Not reading existing item mappings/texture atlas for now since that doesn't work all that well yet - exportPath = createPackDirectory(name); - packPath = exportPath.resolve(PACK_DIRECTORY); - //mappings = CodecUtil.readOrCompute(GeyserMappings.CODEC, exportPath.resolve(MAPPINGS_FILE), GeyserMappings::new); - mappings = new GeyserMappings(); - manifest = CodecUtil.readOrCompute(PackManifest.CODEC, packPath.resolve(MANIFEST_FILE), () -> defaultManifest(name)).increment(); - /*itemTextures = CodecUtil.readOrCompute(BedrockTextureAtlas.ITEM_ATLAS_CODEC, packPath.resolve(ITEM_ATLAS_FILE), - () -> BedrockTextureAtlas.itemAtlas(name, BedrockTextures.builder())).textures().toBuilder();*/ - itemTextures = BedrockTextures.builder(); - - reporter = new ProblemReporter.Collector(() -> "Bedrock pack " + name + " "); - } - - public String name() { - return name; - } - - public MappingResult map(ItemStack stack) { - if (stack.isEmpty()) { - return MappingResult.NONE_MAPPED; - } - - AtomicBoolean problems = new AtomicBoolean(); - ProblemReporter mapReporter = new ProblemReporter() { - - @Override - public @NotNull ProblemReporter forChild(PathElement child) { - return reporter.forChild(child); - } - - @Override - public void report(Problem problem) { - problems.set(true); - reporter.report(problem); - } - }; - PackContext context = new PackContext(mappings, packPath, item -> { - itemTextures.withItemTexture(item); - if (item.exportTexture()) { - texturesToExport.add(item.texture()); - } - bedrockItems.add(item); - }, texturesToExport::add); - - Optional patchedModel = stack.getComponentsPatch().get(DataComponents.ITEM_MODEL); - //noinspection OptionalAssignedToNull - annoying Mojang - if (patchedModel == null || patchedModel.isEmpty()) { - CustomModelData customModelData = stack.get(DataComponents.CUSTOM_MODEL_DATA); - Float firstNumber; - if (customModelData == null || (firstNumber = customModelData.getFloat(0)) == null - || !customModelDataMapped.add((firstNumber.intValue()))) { - return MappingResult.NONE_MAPPED; - } - - BedrockItemMapper.tryMapStack(stack, firstNumber.intValue(), mapReporter, context); - } else { - ResourceLocation model = patchedModel.get(); - if (!modelsMapped.add(model)) { - return MappingResult.NONE_MAPPED; - } - - BedrockItemMapper.tryMapStack(stack, model, mapReporter, context); - } - - return problems.get() ? MappingResult.PROBLEMS_OCCURRED : MappingResult.MAPPED_SUCCESSFULLY; - } - - public boolean save() { - boolean success = true; - - try { - CodecUtil.trySaveJson(GeyserMappings.CODEC, mappings, exportPath.resolve(MAPPINGS_FILE), RegistryOps.create(JsonOps.INSTANCE, Minecraft.getInstance().level.registryAccess())); - CodecUtil.trySaveJson(PackManifest.CODEC, manifest, packPath.resolve(MANIFEST_FILE)); - CodecUtil.trySaveJson(BedrockTextureAtlas.CODEC, BedrockTextureAtlas.itemAtlas(name, itemTextures), packPath.resolve(ITEM_ATLAS_FILE)); - } catch (IOException | NullPointerException exception) { - reporter.forChild(() -> "saving Geyser mappings, pack manifest, and texture atlas ").report(() -> "failed to save to pack: " + exception); - success = false; - } - for (BedrockItem item : bedrockItems) { - try { - item.save(packPath.resolve(ATTACHABLES_DIRECTORY), packPath.resolve(GEOMETRY_DIRECTORY), packPath.resolve(ANIMATION_DIRECTORY)); - } catch (IOException exception) { - reporter.forChild(() -> "files for bedrock item " + item.identifier() + " ").report(() -> "failed to save to pack: " + exception); - success = false; - } - } - - for (ResourceLocation texture : texturesToExport) { - texture = texture.withPath(path -> "textures/" + path + ".png"); - try (InputStream inputTexture = Minecraft.getInstance().getResourceManager().open(texture)) { - Path texturePath = packPath.resolve(texture.getPath()); - CodecUtil.ensureDirectoryExists(texturePath.getParent()); - try (OutputStream outputTexture = new FileOutputStream(texturePath.toFile())) { - IOUtils.copy(inputTexture, outputTexture); - } - } catch (IOException exception) { - ResourceLocation finalTexture = texture; - reporter.forChild(() -> "texture " + finalTexture + " ").report(() -> "failed to save to pack: " + exception); - success = false; - } - } - - try { - CodecUtil.tryZipDirectory(packPath, exportPath.resolve(PACK_ZIP_FILE)); - } catch (IOException exception) { - success = false; - } - - try { - Files.writeString(exportPath.resolve(REPORT_FILE), createPackSummary()); - } catch (IOException exception) { - // TODO log - } - return success; - } - - public Path getExportPath() { - return exportPath; - } - - private String createPackSummary() { - String problems = reporter.getTreeReport(); - if (StringUtil.isBlank(problems)) { - problems = "Well that's odd... there's nothing here!"; - } - - long attachables = bedrockItems.stream().filter(item -> item.attachable().isPresent()).count(); - long geometries = bedrockItems.stream().filter(item -> item.geometry().isPresent()).count(); - long animations = bedrockItems.stream().filter(item -> item.animation().isPresent()).count(); - - return """ --- PACK GENERATION REPORT -- -// %s - -Generated pack: %s -Mappings written: %d -Item texture atlas size: %d -Attachables tried to export: %d -Geometry files tried to export: %d -Animations tried to export: %d -Textures tried to export: %d - --- MAPPING TREE REPORT -- -%s -""".formatted(randomSummaryComment(), name, mappings.size(), itemTextures.build().size(), - attachables, geometries, animations, texturesToExport.size(), problems); - } - - private static String randomSummaryComment() { - if (RANDOM.nextDouble() < 0.6) { - SplashRenderer splash = Minecraft.getInstance().getSplashManager().getSplash(); - if (splash == null) { - return "Undefined Undefined :("; - } - return ((SplashRendererAccessor) splash).getSplash(); - } - return randomBuiltinSummaryComment(); - } - - private static String randomBuiltinSummaryComment() { - return PACK_SUMMARY_COMMENTS.get(RANDOM.nextInt(PACK_SUMMARY_COMMENTS.size())); - } - - private static Path createPackDirectory(String name) throws IOException { - Path path = EXPORT_DIRECTORY.resolve(name); - CodecUtil.ensureDirectoryExists(path); - return path; - } - - private static PackManifest defaultManifest(String name) { - return new PackManifest(new PackManifest.Header(name, PackConstants.DEFAULT_PACK_DESCRIPTION, UUID.randomUUID(), BedrockVersion.of(0), PackConstants.ENGINE_VERSION), - List.of(new PackManifest.Module(name, PackConstants.DEFAULT_PACK_DESCRIPTION, UUID.randomUUID(), BedrockVersion.of(0)))); - } - - public enum MappingResult { - NONE_MAPPED, - MAPPED_SUCCESSFULLY, - PROBLEMS_OCCURRED - } -}