1
0
mirror of https://github.com/GeyserMC/Rainbow.git synced 2025-12-19 14:59:16 +00:00

Split the mod up in 2 subprojects, add datagen subproject, improve 3D model generation/texture stitching (#8)

This commit is contained in:
Eclipse
2025-10-16 15:28:57 +00:00
committed by GitHub
113 changed files with 1967 additions and 707 deletions

View File

@@ -33,12 +33,19 @@ jobs:
env: env:
BUILD_NUMBER: ${{ steps.release-info.outputs.curentRelease }} 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 - name: Archive Artifacts
uses: GeyserMC/actions/upload-multi-artifact@master uses: GeyserMC/actions/upload-multi-artifact@master
if: success() if: success()
with: with:
artifacts: | artifacts: |
rainbow:build/libs/Rainbow.jar rainbow:client/build/libs/Rainbow.jar
- name: Get Version - name: Get Version
if: ${{ (success() || failure()) && github.repository == 'GeyserMC/Rainbow' }} if: ${{ (success() || failure()) && github.repository == 'GeyserMC/Rainbow' }}
@@ -69,7 +76,7 @@ jobs:
privateKey: ${{ secrets.DOWNLOADS_PRIVATE_KEY }} privateKey: ${{ secrets.DOWNLOADS_PRIVATE_KEY }}
host: ${{ secrets.DOWNLOADS_SERVER_IP }} host: ${{ secrets.DOWNLOADS_SERVER_IP }}
files: | files: |
build/libs/Rainbow.jar client/build/libs/Rainbow.jar
changelog: ${{ steps.metadata.outputs.body }} changelog: ${{ steps.metadata.outputs.body }}
# - name: Publish to Modrinth # - name: Publish to Modrinth
@@ -87,4 +94,4 @@ jobs:
discordWebhook: ${{ secrets.DISCORD_WEBHOOK }} discordWebhook: ${{ secrets.DISCORD_WEBHOOK }}
status: ${{ job.status }} status: ${{ job.status }}
body: ${{ steps.metadata.outputs.body }} body: ${{ steps.metadata.outputs.body }}
includeDownloads: ${{ success() && github.repository == 'GeyserMC/Rainbow' && github.ref_name == 'master' }} includeDownloads: ${{ github.ref_name == 'master' }}

View File

@@ -4,7 +4,7 @@
[![Discord](https://img.shields.io/discord/613163671870242838.svg?color=%237289da&label=discord)](https://discord.gg/geysermc) [![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 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: Rainbow is currently experimental and capable of the following:

View File

@@ -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)
}

View File

@@ -0,0 +1,7 @@
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}

View File

@@ -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()

View File

@@ -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<JavaCompile>().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"
}
}
}

View File

@@ -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"])
}
}
}

View File

@@ -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"
}
}
}

31
client/build.gradle.kts Normal file
View File

@@ -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<Copy>("copyRainbowClientJar") {
group = "build"
val remapJarTask = getByName<RemapJarTask>("remapJar")
dependsOn(remapJarTask)
from(remapJarTask.archiveFile)
rename {
"Rainbow.jar"
}
into(project.layout.buildDirectory.file("libs"))
}
named("build") {
dependsOn(copyJarTask)
}
}

1
client/gradle.properties Normal file
View File

@@ -0,0 +1 @@
archives_base_name=rainbow-client

View File

@@ -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<ResolvedModel> getResolvedModel(ResourceLocation location) {
return ((ResolvedModelAccessor) modelManager).rainbow$getResolvedModel(location);
}
@Override
public Optional<ClientItem> getClientItem(ResourceLocation location) {
return ((ResolvedModelAccessor) modelManager).rainbow$getClientItem(location);
}
@Override
public Optional<EquipmentClientInfo> getEquipmentInfo(ResourceKey<EquipmentAsset> key) {
return Optional.of(equipmentAssetManager.get(key));
}
@Override
public InputStream openAsset(ResourceLocation location) throws IOException {
return resourceManager.open(location);
}
}

View File

@@ -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 <T> CompletableFuture<?> saveJson(Codec<T> codec, T object, Path path) {
DynamicOps<JsonElement> 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"));
}
}

View File

@@ -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<String> 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<BedrockPack> 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<BedrockPack> consumer) {
currentPack.ifPresent(consumer);
}
public void runOrElse(Consumer<BedrockPack> consumer, Runnable runnable) {
currentPack.ifPresentOrElse(consumer, runnable);
}
public Optional<Path> 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<BedrockItem> 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;
}
}

View File

@@ -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.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.command.v2.ArgumentTypeRegistry; import net.fabricmc.fabric.api.command.v2.ArgumentTypeRegistry;
import net.minecraft.commands.synchronization.SingletonArgumentInfo; import net.minecraft.commands.synchronization.SingletonArgumentInfo;
import net.minecraft.resources.ResourceLocation; import org.geysermc.rainbow.Rainbow;
import org.geysermc.rainbow.command.CommandSuggestionsArgumentType; import org.geysermc.rainbow.RainbowIO;
import org.geysermc.rainbow.command.PackGeneratorCommand; import org.geysermc.rainbow.client.command.CommandSuggestionsArgumentType;
import org.geysermc.rainbow.mapper.PackMapper; import org.geysermc.rainbow.client.command.PackGeneratorCommand;
import org.slf4j.Logger; import org.geysermc.rainbow.client.mapper.PackMapper;
public class Rainbow implements ClientModInitializer { public class RainbowClient implements ClientModInitializer {
public static final String MOD_ID = "rainbow";
public static final String MOD_NAME = "Rainbow";
public static final Logger LOGGER = LogUtils.getLogger();
private final PackManager packManager = new PackManager(); private final PackManager packManager = new PackManager();
private final PackMapper packMapper = new PackMapper(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)); ClientCommandRegistrationCallback.EVENT.register((dispatcher, buildContext) -> PackGeneratorCommand.register(dispatcher, packManager, packMapper));
ClientTickEvents.START_CLIENT_TICK.register(packMapper::tick); ClientTickEvents.START_CLIENT_TICK.register(packMapper::tick);
ArgumentTypeRegistry.registerArgumentType(getModdedLocation("command_suggestions"), ArgumentTypeRegistry.registerArgumentType(Rainbow.getModdedLocation("command_suggestions"),
CommandSuggestionsArgumentType.class, SingletonArgumentInfo.contextFree(CommandSuggestionsArgumentType::new)); CommandSuggestionsArgumentType.class, SingletonArgumentInfo.contextFree(CommandSuggestionsArgumentType::new));
}
public static ResourceLocation getModdedLocation(String path) { RainbowIO.registerExceptionListener(new RainbowClientIOHandler());
return ResourceLocation.fromNamespaceAndPath(MOD_ID, path);
}
public static String fileSafeResourceLocation(ResourceLocation location) {
return location.toString().replace(':', '.').replace('/', '_');
} }
} }

View File

@@ -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")));
}
}

View File

@@ -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.renderer.item.ClientItem;
import net.minecraft.client.resources.model.ResolvedModel; import net.minecraft.client.resources.model.ResolvedModel;

View File

@@ -1,4 +1,4 @@
package org.geysermc.rainbow.command; package org.geysermc.rainbow.client.command;
import com.mojang.brigadier.StringReader; import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.arguments.ArgumentType; import com.mojang.brigadier.arguments.ArgumentType;

View File

@@ -1,4 +1,4 @@
package org.geysermc.rainbow.command; package org.geysermc.rainbow.client.command;
import com.mojang.brigadier.Command; import com.mojang.brigadier.Command;
import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.CommandDispatcher;
@@ -10,9 +10,9 @@ import net.minecraft.network.chat.ClickEvent;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import org.geysermc.rainbow.PackManager; import org.geysermc.rainbow.client.PackManager;
import org.geysermc.rainbow.mapper.InventoryMapper; import org.geysermc.rainbow.client.mapper.InventoryMapper;
import org.geysermc.rainbow.mapper.PackMapper; import org.geysermc.rainbow.client.mapper.PackMapper;
import org.geysermc.rainbow.pack.BedrockPack; import org.geysermc.rainbow.pack.BedrockPack;
import java.nio.file.Path; import java.nio.file.Path;
@@ -113,14 +113,13 @@ public class PackGeneratorCommand {
.then(ClientCommandManager.literal("finish") .then(ClientCommandManager.literal("finish")
.executes(context -> { .executes(context -> {
Optional<Path> exportPath = packManager.getExportPath(); Optional<Path> exportPath = packManager.getExportPath();
packManager.finish().ifPresentOrElse(success -> { if (packManager.finish()) {
if (!success) { // TODO error when exporting fails
context.getSource().sendError(Component.translatable("commands.rainbow.pack_finished_error")); context.getSource().sendFeedback(Component.translatable("commands.rainbow.pack_finished_successfully")
} else { .withStyle(style -> style.withUnderlined(true).withClickEvent(new ClickEvent.OpenFile(exportPath.orElseThrow()))));
context.getSource().sendFeedback(Component.translatable("commands.rainbow.pack_finished_successfully") } else {
.withStyle(style -> style.withUnderlined(true).withClickEvent(new ClickEvent.OpenFile(exportPath.orElseThrow())))); context.getSource().sendError(NO_PACK_CREATED);
} }
}, () -> context.getSource().sendError(NO_PACK_CREATED));
return 0; return 0;
}) })
) )

View File

@@ -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.multiplayer.ClientPacketListener;
import net.minecraft.client.player.LocalPlayer; import net.minecraft.client.player.LocalPlayer;

View File

@@ -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.multiplayer.ClientPacketListener;
import net.minecraft.client.player.LocalPlayer; import net.minecraft.client.player.LocalPlayer;

View File

@@ -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.multiplayer.ClientPacketListener;
import net.minecraft.client.player.LocalPlayer; import net.minecraft.client.player.LocalPlayer;

View File

@@ -1,14 +1,13 @@
package org.geysermc.rainbow.mapper; package org.geysermc.rainbow.client.mapper;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientPacketListener; import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.client.player.LocalPlayer; import net.minecraft.client.player.LocalPlayer;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import org.geysermc.rainbow.PackManager; import org.geysermc.rainbow.client.PackManager;
import org.geysermc.rainbow.pack.BedrockPack; import org.geysermc.rainbow.pack.BedrockPack;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
public class PackMapper { public class PackMapper {
private final PackManager packManager; private final PackManager packManager;

View File

@@ -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.renderer.entity.EntityRenderDispatcher;
import net.minecraft.client.resources.model.EquipmentAssetManager; import net.minecraft.client.resources.model.EquipmentAssetManager;

View File

@@ -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.GuiItemRenderState;
import net.minecraft.client.gui.render.state.ScreenArea; import net.minecraft.client.gui.render.state.ScreenArea;

View File

@@ -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.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; 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.client.resources.model.ResolvedModel;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.PreparableReloadListener; 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.Mixin;
import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;

View File

@@ -1,4 +1,4 @@
package org.geysermc.rainbow.mixin; package org.geysermc.rainbow.client.mixin;
import com.mojang.blaze3d.textures.GpuTexture; import com.mojang.blaze3d.textures.GpuTexture;
import net.minecraft.client.gui.render.pip.PictureInPictureRenderer; import net.minecraft.client.gui.render.pip.PictureInPictureRenderer;

View File

@@ -1,8 +1,8 @@
package org.geysermc.rainbow.mixin; package org.geysermc.rainbow.client.mixin;
import com.mojang.blaze3d.textures.GpuTexture; import com.mojang.blaze3d.textures.GpuTexture;
import net.minecraft.client.gui.render.pip.PictureInPictureRenderer; 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.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Shadow;

View File

@@ -1,4 +1,4 @@
package org.geysermc.rainbow.mixin; package org.geysermc.rainbow.client.mixin;
import net.minecraft.client.gui.components.SplashRenderer; import net.minecraft.client.gui.components.SplashRenderer;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;

View File

@@ -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.buffers.GpuBuffer;
import com.mojang.blaze3d.platform.NativeImage; 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.client.renderer.item.TrackingItemStackRenderState;
import net.minecraft.world.item.ItemDisplayContext; import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import org.geysermc.rainbow.CodecUtil; import org.geysermc.rainbow.mapping.geometry.GeometryRenderer;
import org.geysermc.rainbow.mixin.PictureInPictureRendererAccessor; import org.geysermc.rainbow.client.mixin.PictureInPictureRendererAccessor;
import org.geysermc.rainbow.render.PictureInPictureCopyRenderer;
import org.joml.Matrix3x2fStack; import org.joml.Matrix3x2fStack;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Objects; 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 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 // 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(); TrackingItemStackRenderState itemRenderState = new TrackingItemStackRenderState();
Minecraft.getInstance().getItemModelResolver().updateForTopItem(itemRenderState, stack, ItemDisplayContext.GUI, null, null, 0); Minecraft.getInstance().getItemModelResolver().updateForTopItem(itemRenderState, stack, ItemDisplayContext.GUI, null, null, 0);
itemRenderState.setOversizedInGui(true); itemRenderState.setOversizedInGui(true);
@@ -41,12 +40,12 @@ public class GeometryRenderer {
//noinspection DataFlowIssue //noinspection DataFlowIssue
((PictureInPictureCopyRenderer) itemRenderer).rainbow$allowTextureCopy(); ((PictureInPictureCopyRenderer) itemRenderer).rainbow$allowTextureCopy();
itemRenderer.prepare(oversizedRenderState, new GuiRenderState(), 4); 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 // Simplified TextureUtil#writeAsPNG with some modifications to just write to a NativeImage, flip the image and just generate it at full size
private static void writeAsPNG(Path path, GpuTexture texture) { private static NativeImage writeToImage(GpuTexture texture) {
RenderSystem.assertOnRenderThread(); RenderSystem.assertOnRenderThread();
int width = texture.getWidth(0); int width = texture.getWidth(0);
int height = texture.getHeight(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); GpuBuffer buffer = RenderSystem.getDevice().createBuffer(() -> "Texture output buffer", GpuBuffer.USAGE_COPY_DST | GpuBuffer.USAGE_MAP_READ, bufferSize);
CommandEncoder commandEncoder = RenderSystem.getDevice().createCommandEncoder(); CommandEncoder commandEncoder = RenderSystem.getDevice().createCommandEncoder();
NativeImage image = new NativeImage(width, height, false);
Runnable writer = () -> { Runnable writer = () -> {
try (GpuBuffer.MappedView mappedView = commandEncoder.mapBuffer(buffer, true, false)) { 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 y = 0; y < height; y++) { for (int x = 0; x < width; x++) {
for (int x = 0; x < width; x++) { int colour = mappedView.data().getInt((x + y * width) * texture.getFormat().pixelSize());
int colour = mappedView.data().getInt((x + y * width) * texture.getFormat().pixelSize()); image.setPixelABGR(x, height - y - 1, colour);
nativeImage.setPixelABGR(x, height - y - 1, colour);
}
} }
CodecUtil.ensureDirectoryExists(path.getParent());
nativeImage.writeToFile(path);
} catch (IOException var19) {
// TODO
} }
} }
buffer.close(); buffer.close();
}; };
commandEncoder.copyTextureToBuffer(texture, buffer, 0, writer, 0); commandEncoder.copyTextureToBuffer(texture, buffer, 0, writer, 0);
return image;
} }
} }

View File

@@ -1,4 +1,4 @@
package org.geysermc.rainbow.render; package org.geysermc.rainbow.client.render;
public interface PictureInPictureCopyRenderer { public interface PictureInPictureCopyRenderer {

View File

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 282 B

View File

@@ -13,5 +13,7 @@
"commands.rainbow.pack_created": "Created pack with name %s", "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_error": "Errors occurred whilst writing the pack to disk!",
"commands.rainbow.pack_finished_successfully": "Wrote 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!"
} }

View File

@@ -1,6 +1,6 @@
{ {
"schemaVersion": 1, "schemaVersion": 1,
"id": "rainbow", "id": "rainbow-client",
"version": "${version}", "version": "${version}",
"name": "Rainbow", "name": "Rainbow",
"description": "Rainbow is a mod to generate Geyser item mappings and bedrock resourcepacks for use with Geyser's custom item API (v2)", "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", "environment": "client",
"entrypoints": { "entrypoints": {
"client": [ "client": [
"org.geysermc.rainbow.Rainbow" "org.geysermc.rainbow.client.RainbowClient"
] ]
}, },
"mixins": [ "mixins": [
"rainbow.mixins.json" "rainbow-client.mixins.json"
], ],
"depends": { "depends": {
"fabricloader": ">=${loader_version}", "fabricloader": ">=${loader_version}",

View File

@@ -1,19 +1,15 @@
{ {
"required": true, "required": true,
"minVersion": "0.8", "minVersion": "0.8",
"package": "org.geysermc.rainbow.mixin", "package": "org.geysermc.rainbow.client.mixin",
"compatibilityLevel": "JAVA_21", "compatibilityLevel": "JAVA_21",
"mixins": [],
"client": [ "client": [
"EntityRenderDispatcherAccessor", "EntityRenderDispatcherAccessor",
"GuiItemRenderStateMixin", "GuiItemRenderStateMixin",
"LateBoundIdMapperAccessor",
"ModelManagerMixin", "ModelManagerMixin",
"PictureInPictureRendererAccessor", "PictureInPictureRendererAccessor",
"PictureInPictureRendererMixin", "PictureInPictureRendererMixin",
"RangeSelectItemModelAccessor", "SplashRendererAccessor"
"SplashRendererAccessor",
"TextureSlotsAccessor"
], ],
"injectors": { "injectors": {
"defaultRequire": 1 "defaultRequire": 1

12
datagen/build.gradle.kts Normal file
View File

@@ -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")
}

View File

@@ -0,0 +1 @@
archives_base_name=rainbow-datagen

View File

@@ -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<File> assetsDirSpec = parser.accepts("assetsDir").withRequiredArg().ofType(File.class);
OptionSpec<String> 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<CloseableResourceManager> 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> T parseArgument(OptionSet set, OptionSpec<T> spec) {
try {
return set.valueOf(spec);
} catch (Throwable exception) {
if (spec instanceof ArgumentAcceptingOptionSpec<T> argumentAccepting) {
List<T> list = argumentAccepting.defaultValues();
if (!list.isEmpty()) {
return list.getFirst();
}
}
throw exception;
}
}
}

View File

@@ -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<HolderLookup.Provider> registries;
private final Map<ResourceKey<EquipmentAsset>, EquipmentClientInfo> equipmentInfos;
private final Path outputRoot;
private Map<Item, ClientItem> itemInfos;
private Map<ResourceLocation, ModelInstance> models;
protected RainbowModelProvider(FabricDataOutput output, CompletableFuture<HolderLookup.Provider> registries,
Map<ResourceKey<EquipmentAsset>, 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<HolderLookup.Provider> registries,
Map<ResourceKey<EquipmentAsset>, EquipmentClientInfo> equipmentInfos) {
this(output, registries, equipmentInfos, ResourceLocation.withDefaultNamespace("bedrock"));
}
protected RainbowModelProvider(FabricDataOutput output, CompletableFuture<HolderLookup.Provider> registries) {
this(output, registries, Map.of());
}
@Override
public @NotNull CompletableFuture<?> run(CachedOutput output) {
CompletableFuture<?> vanillaModels = super.run(output);
CompletableFuture<BedrockPack> 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<Item, ClientItem> itemInfos) {
this.itemInfos = itemInfos;
}
@ApiStatus.Internal
public void setModels(Map<ResourceLocation, ModelInstance> models) {
this.models = models;
}
private record Serializer(CachedOutput output, HolderLookup.Provider registries) implements PackSerializer {
@Override
public <T> CompletableFuture<?> saveJson(Codec<T> 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<ResourceKey<EquipmentAsset>, EquipmentClientInfo> equipmentInfos;
private final Map<ResourceLocation, ClientItem> itemInfos;
private final Map<ResourceLocation, ModelInstance> models;
private final Map<ResourceLocation, Optional<ResolvedModel>> resolvedModelCache = new HashMap<>();
private DatagenResolver(ResourceManager resourceManager, Map<ResourceKey<EquipmentAsset>, EquipmentClientInfo> equipmentInfos,
Map<Item, ClientItem> itemInfos, Map<ResourceLocation, ModelInstance> models) {
this.resourceManager = resourceManager;
this.equipmentInfos = equipmentInfos;
this.itemInfos = new HashMap<>();
for (Map.Entry<Item, ClientItem> entry : itemInfos.entrySet()) {
this.itemInfos.put(entry.getKey().builtInRegistryHolder().key().location(), entry.getValue());
}
this.models = models;
}
@Override
public Optional<ResolvedModel> getResolvedModel(ResourceLocation location) {
return resolvedModelCache.computeIfAbsent(location, key -> Optional.ofNullable(models.get(location))
.<UnbakedModel>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<ClientItem> getClientItem(ResourceLocation location) {
return Optional.ofNullable(itemInfos.get(location));
}
@Override
public Optional<EquipmentClientInfo> getEquipmentInfo(ResourceKey<EquipmentAsset> key) {
return Optional.ofNullable(equipmentInfos.get(key));
}
@Override
public InputStream openAsset(ResourceLocation location) throws IOException {
return resourceManager.open(location);
}
}
}

View File

@@ -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<Item, ClientItem> getItemInfos();
}

View File

@@ -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<CompletableFuture<?>> 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());
}
}
}

View File

@@ -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<ResourceLocation, ModelInstance> getModels();
}

View File

@@ -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"
}
}
}
}

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -1,15 +1,5 @@
org.gradle.jvmargs=-Xmx1G 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 Properties
mod_version=0.1.0-1.21.10 mod_version=0.2.0-1.21.10-SNAPSHOT
supported_versions=>=1.21.9 <=1.21.10 maven_group=org.geysermc.rainbow
maven_group=org.geysermc
archives_base_name=rainbow
# Dependencies
fabric_version=0.135.0+1.21.10

26
gradle/libs.versions.toml Normal file
View File

@@ -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"}

Binary file not shown.

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

5
gradlew vendored
View File

@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# #
# Copyright © 2015-2021 the original authors. # Copyright © 2015 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -114,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;; NONSTOP* ) nonstop=true ;;
esac esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM. # 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 # For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" ) JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -212,7 +210,6 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@" "$@"

3
gradlew.bat vendored
View File

@@ -70,11 +70,10 @@ goto fail
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=
@rem Execute Gradle @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 :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell

4
rainbow/build.gradle.kts Normal file
View File

@@ -0,0 +1,4 @@
plugins {
id("rainbow.base-conventions")
id("rainbow.publish-conventions")
}

View File

@@ -0,0 +1 @@
archives_base_name=rainbow-core

View File

@@ -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");
}
}

View File

@@ -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<IOExceptionListener> listeners = new ArrayList<>();
private RainbowIO() {}
public static <T> Optional<T> safeIO(IOSupplier<T> 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> T safeIO(IOSupplier<T> 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> {
T get() throws IOException;
}
@FunctionalInterface
public interface IORunnable {
void run() throws IOException;
}
@FunctionalInterface
public interface IOExceptionListener {
void error(IOException exception);
}
}

View File

@@ -1,4 +1,4 @@
package org.geysermc.rainbow.mapping.geyser; package org.geysermc.rainbow.definition;
import com.mojang.serialization.Codec; import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec; import com.mojang.serialization.MapCodec;
@@ -8,7 +8,7 @@ import net.minecraft.core.component.DataComponentType;
import net.minecraft.core.component.DataComponents; import net.minecraft.core.component.DataComponents;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import org.geysermc.rainbow.Rainbow; 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.List;
import java.util.Optional; import java.util.Optional;
@@ -61,7 +61,7 @@ public record GeyserBaseDefinition(ResourceLocation bedrockIdentifier, Optional<
} }
public String textureName() { public String textureName() {
return bedrockOptions.icon.orElse(Rainbow.fileSafeResourceLocation(bedrockIdentifier)); return bedrockOptions.icon.orElse(Rainbow.safeResourceLocation(bedrockIdentifier));
} }
public record BedrockOptions(Optional<String> icon, boolean allowOffhand, boolean displayHandheld, int protectionValue, List<ResourceLocation> tags) { public record BedrockOptions(Optional<String> icon, boolean allowOffhand, boolean displayHandheld, int protectionValue, List<ResourceLocation> tags) {

View File

@@ -1,11 +1,14 @@
package org.geysermc.rainbow.mapping.geyser; package org.geysermc.rainbow.definition;
import com.mojang.serialization.MapCodec; import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder; import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import org.jetbrains.annotations.NotNull;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream; import java.util.stream.Stream;
public record GeyserGroupDefinition(Optional<ResourceLocation> model, List<GeyserMapping> definitions) implements GeyserMapping { public record GeyserGroupDefinition(Optional<ResourceLocation> model, List<GeyserMapping> definitions) implements GeyserMapping {
@@ -18,7 +21,9 @@ public record GeyserGroupDefinition(Optional<ResourceLocation> model, List<Geyse
); );
public GeyserGroupDefinition with(GeyserMapping mapping) { public GeyserGroupDefinition with(GeyserMapping mapping) {
return new GeyserGroupDefinition(model, Stream.concat(definitions.stream(), Stream.of(mapping)).toList()); return new GeyserGroupDefinition(model, Stream.concat(definitions.stream(), Stream.of(mapping))
.sorted(Comparator.comparing(Function.identity()))
.toList());
} }
public boolean isFor(Optional<ResourceLocation> model) { public boolean isFor(Optional<ResourceLocation> model) {
@@ -53,4 +58,26 @@ public record GeyserGroupDefinition(Optional<ResourceLocation> model, List<Geyse
public Type type() { public Type type() {
return Type.GROUP; return Type.GROUP;
} }
@Override
public int compareTo(@NotNull GeyserMapping other) {
if (other instanceof GeyserGroupDefinition(Optional<ResourceLocation> otherModel, List<GeyserMapping> 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
}
} }

View File

@@ -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<ResourceLocation> 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
}
}

View File

@@ -1,4 +1,4 @@
package org.geysermc.rainbow.mapping.geyser; package org.geysermc.rainbow.definition;
import com.mojang.serialization.Codec; import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec; import com.mojang.serialization.MapCodec;

View File

@@ -1,4 +1,4 @@
package org.geysermc.rainbow.mapping.geyser; package org.geysermc.rainbow.definition;
import com.mojang.serialization.Codec; import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult; import com.mojang.serialization.DataResult;
@@ -6,7 +6,7 @@ import com.mojang.serialization.MapCodec;
import net.minecraft.util.StringRepresentable; import net.minecraft.util.StringRepresentable;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public interface GeyserMapping { public interface GeyserMapping extends Comparable<GeyserMapping> {
Codec<GeyserMapping> CODEC = Codec.lazyInitialized(() -> Type.CODEC.dispatch(GeyserMapping::type, Type::codec)); Codec<GeyserMapping> 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 // Not perfect since we're not checking single definitions in groups without a model... but good enough

View File

@@ -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.Multimap;
import com.google.common.collect.MultimapBuilder; import com.google.common.collect.MultimapBuilder;
@@ -11,6 +11,7 @@ import org.geysermc.rainbow.CodecUtil;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@@ -26,7 +27,10 @@ public class GeyserMappings {
).apply(instance, (format, mappings) -> new GeyserMappings(mappings)) ).apply(instance, (format, mappings) -> new GeyserMappings(mappings))
); );
private final Multimap<Holder<Item>, GeyserMapping> mappings = MultimapBuilder.hashKeys().hashSetValues().build(); private final Multimap<Holder<Item>, GeyserMapping> mappings = MultimapBuilder
.hashKeys()
.<GeyserMapping>treeSetValues(Comparator.comparing(mapping -> mapping))
.build();
public GeyserMappings() {} public GeyserMappings() {}

View File

@@ -1,4 +1,4 @@
package org.geysermc.rainbow.mapping.geyser; package org.geysermc.rainbow.definition;
import com.mojang.serialization.MapCodec; import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder; import com.mojang.serialization.codecs.RecordCodecBuilder;

View File

@@ -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.google.common.base.Suppliers;
import com.mojang.serialization.Codec; import com.mojang.serialization.Codec;

View File

@@ -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.Codec;
import com.mojang.serialization.MapCodec; import com.mojang.serialization.MapCodec;

View File

@@ -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.Codec;
import com.mojang.serialization.MapCodec; import com.mojang.serialization.MapCodec;

View File

@@ -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.google.common.base.Suppliers;
import com.mojang.serialization.Codec; import com.mojang.serialization.Codec;
@@ -24,7 +24,7 @@ public record GeyserRangeDispatchPredicate(Property property, float threshold, f
@Override @Override
public Type type() { public Type type() {
return null; return Type.RANGE_DISPATCH;
} }
public interface Property { public interface Property {

View File

@@ -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();
}
}
}
}

View File

@@ -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<ResolvedModel> getResolvedModel(ResourceLocation location);
Optional<ClientItem> getClientItem(ResourceLocation location);
Optional<EquipmentClientInfo> getEquipmentInfo(ResourceKey<EquipmentAsset> key);
InputStream openAsset(ResourceLocation location) throws IOException;
}

View File

@@ -1,6 +1,5 @@
package org.geysermc.rainbow.mapping; package org.geysermc.rainbow.mapping;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.item.BlockModelWrapper; import net.minecraft.client.renderer.item.BlockModelWrapper;
import net.minecraft.client.renderer.item.ClientItem; import net.minecraft.client.renderer.item.ClientItem;
import net.minecraft.client.renderer.item.ConditionalItemModel; 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.RangeSelectItemModel;
import net.minecraft.client.renderer.item.SelectItemModel; import net.minecraft.client.renderer.item.SelectItemModel;
import net.minecraft.client.renderer.item.properties.conditional.Broken; 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.CustomModelDataProperty;
import net.minecraft.client.renderer.item.properties.conditional.Damaged; 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.FishingRodCast;
import net.minecraft.client.renderer.item.properties.conditional.HasComponent; 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.BundleFullness;
import net.minecraft.client.renderer.item.properties.numeric.Count; 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.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.numeric.RangeSelectItemModelProperty;
import net.minecraft.client.renderer.item.properties.select.Charge; 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.ContextDimension;
import net.minecraft.client.renderer.item.properties.select.DisplayContext; 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.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.core.component.DataComponents;
import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.tags.ItemTags; import net.minecraft.tags.ItemTags;
import net.minecraft.util.ExtraCodecs;
import net.minecraft.util.ProblemReporter; import net.minecraft.util.ProblemReporter;
import net.minecraft.world.entity.ai.attributes.AttributeModifier; import net.minecraft.world.entity.ai.attributes.AttributeModifier;
import net.minecraft.world.entity.ai.attributes.Attributes; 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.item.equipment.trim.TrimMaterial;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import org.apache.commons.lang3.ArrayUtils; 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.attachable.AttachableMapper;
import org.geysermc.rainbow.mapping.geometry.BedrockGeometryContext; import org.geysermc.rainbow.mapping.geometry.BedrockGeometryContext;
import org.geysermc.rainbow.mapping.geometry.GeometryMapper; import org.geysermc.rainbow.definition.GeyserBaseDefinition;
import org.geysermc.rainbow.mapping.geometry.GeometryRenderer; import org.geysermc.rainbow.definition.GeyserItemDefinition;
import org.geysermc.rainbow.mapping.geyser.GeyserBaseDefinition; import org.geysermc.rainbow.definition.GeyserLegacyDefinition;
import org.geysermc.rainbow.mapping.geyser.GeyserItemDefinition; import org.geysermc.rainbow.definition.GeyserSingleDefinition;
import org.geysermc.rainbow.mapping.geyser.GeyserLegacyDefinition; import org.geysermc.rainbow.definition.predicate.GeyserConditionPredicate;
import org.geysermc.rainbow.mapping.geyser.GeyserSingleDefinition; import org.geysermc.rainbow.definition.predicate.GeyserMatchPredicate;
import org.geysermc.rainbow.mapping.geyser.predicate.GeyserConditionPredicate; import org.geysermc.rainbow.definition.predicate.GeyserPredicate;
import org.geysermc.rainbow.mapping.geyser.predicate.GeyserMatchPredicate; import org.geysermc.rainbow.definition.predicate.GeyserRangeDispatchPredicate;
import org.geysermc.rainbow.mapping.geyser.predicate.GeyserPredicate;
import org.geysermc.rainbow.mapping.geyser.predicate.GeyserRangeDispatchPredicate;
import org.geysermc.rainbow.mixin.LateBoundIdMapperAccessor; import org.geysermc.rainbow.mixin.LateBoundIdMapperAccessor;
import org.geysermc.rainbow.mixin.RangeSelectItemModelAccessor; import org.geysermc.rainbow.mixin.RangeSelectItemModelAccessor;
import org.geysermc.rainbow.mixin.TextureSlotsAccessor;
import org.geysermc.rainbow.pack.BedrockItem; import org.geysermc.rainbow.pack.BedrockItem;
import org.geysermc.rainbow.pack.BedrockTextures;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -65,31 +59,26 @@ import java.util.function.Function;
import java.util.stream.Stream; import java.util.stream.Stream;
public class BedrockItemMapper { public class BedrockItemMapper {
private static final List<ResourceLocation> HANDHELD_MODELS = Stream.of("item/handheld", "item/handheld_rod", "item/handheld_mace")
.map(ResourceLocation::withDefaultNamespace)
.toList();
private static final List<ResourceLocation> TRIMMABLE_ARMOR_TAGS = Stream.of("is_armor", "trimmable_armors") private static final List<ResourceLocation> TRIMMABLE_ARMOR_TAGS = Stream.of("is_armor", "trimmable_armors")
.map(ResourceLocation::withDefaultNamespace) .map(ResourceLocation::withDefaultNamespace)
.toList(); .toList();
private static ResolvedModelAccessor getModels() { private static <T> ResourceLocation getId(ExtraCodecs.LateBoundIdMapper<ResourceLocation, T> mapper,
return (ResolvedModelAccessor) Minecraft.getInstance().getModelManager(); T type) {
}
private static ResourceLocation getModelId(ItemModel.Unbaked model) {
//noinspection unchecked //noinspection unchecked
return ((LateBoundIdMapperAccessor<ResourceLocation, ?>) ItemModels.ID_MAPPER).getIdToValue().inverse().get(model.type()); return ((LateBoundIdMapperAccessor<ResourceLocation, ?>) mapper).getIdToValue().inverse().get(type);
} }
public static void tryMapStack(ItemStack stack, ResourceLocation modelLocation, ProblemReporter reporter, PackContext context) { 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), .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)); () -> reporter.report(() -> "missing client item definition " + modelLocation));
} }
public static void tryMapStack(ItemStack stack, int customModelData, ProblemReporter reporter, PackContext context) { 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(); ResourceLocation itemModel = stack.get(DataComponents.ITEM_MODEL);
ProblemReporter childReporter = reporter.forChild(() -> "item model " + vanillaModel + " with custom model data " + customModelData + " "); 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<RangeSelectItemModel.Entry> entries, Optional<ItemModel.Unbaked> fallback)) { if (vanillaModel instanceof RangeSelectItemModel.Unbaked(RangeSelectItemModelProperty property, float scale, List<RangeSelectItemModel.Entry> entries, Optional<ItemModel.Unbaked> fallback)) {
// WHY, Mojang? // WHY, Mojang?
if (property instanceof net.minecraft.client.renderer.item.properties.numeric.CustomModelDataProperty(int index)) { 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 ConditionalItemModel.Unbaked conditional -> mapConditionalModel(conditional, context.child("condition model "));
case RangeSelectItemModel.Unbaked rangeSelect -> mapRangeSelectModel(rangeSelect, context.child("range select model ")); case RangeSelectItemModel.Unbaked rangeSelect -> mapRangeSelectModel(rangeSelect, context.child("range select model "));
case SelectItemModel.Unbaked select -> mapSelectModel(select, context.child("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) { private static void mapBlockModelWrapper(BlockModelWrapper.Unbaked model, MappingContext context) {
ResourceLocation itemModelLocation = model.model(); ResourceLocation itemModelLocation = model.model();
getModels().rainbow$getResolvedModel(itemModelLocation) context.packContext().assetResolver().getResolvedModel(itemModelLocation)
.ifPresentOrElse(itemModel -> { .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; ResourceLocation bedrockIdentifier;
if (itemModelLocation.getNamespace().equals(ResourceLocation.DEFAULT_NAMESPACE)) { if (itemModelLocation.getNamespace().equals(ResourceLocation.DEFAULT_NAMESPACE)) {
bedrockIdentifier = ResourceLocation.fromNamespaceAndPath("geyser_mc", itemModelLocation.getPath()); bedrockIdentifier = ResourceLocation.fromNamespaceAndPath("geyser_mc", itemModelLocation.getPath());
@@ -143,33 +128,17 @@ public class BedrockItemMapper {
bedrockIdentifier = itemModelLocation; bedrockIdentifier = itemModelLocation;
} }
Material layer0Texture = itemModel.getTopTextureSlots().getMaterial("layer0"); BedrockGeometryContext geometry = BedrockGeometryContext.create(bedrockIdentifier, context.stack, itemModel, context.packContext);
Optional<ResourceLocation> texture; if (context.packContext.reportSuccesses()) {
Optional<ResolvedModel> 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 -> {
// Not a problem, but just report to get the model printed in the report file // 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.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.create(bedrockIdentifier, geometry);
}, () -> context.reporter.report(() -> "missing block model " + itemModelLocation)); }, () -> context.report("missing block model " + itemModelLocation));
} }
private static void mapConditionalModel(ConditionalItemModel.Unbaked model, MappingContext context) { private static void mapConditionalModel(ConditionalItemModel.Unbaked model, MappingContext context) {
ItemModelPropertyTest property = model.property(); ConditionalItemModelProperty property = model.property();
GeyserConditionPredicate.Property predicateProperty = switch (property) { GeyserConditionPredicate.Property predicateProperty = switch (property) {
case Broken ignored -> GeyserConditionPredicate.BROKEN; case Broken ignored -> GeyserConditionPredicate.BROKEN;
case Damaged ignored -> GeyserConditionPredicate.DAMAGED; case Damaged ignored -> GeyserConditionPredicate.DAMAGED;
@@ -182,7 +151,7 @@ public class BedrockItemMapper {
ItemModel.Unbaked onFalse = model.onFalse(); ItemModel.Unbaked onFalse = model.onFalse();
if (predicateProperty == null) { 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)")); mapItem(onFalse, context.child("condition on_false (unsupported property)"));
return; return;
} }
@@ -203,7 +172,7 @@ public class BedrockItemMapper {
}; };
if (predicateProperty == null) { 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 { } else {
for (RangeSelectItemModel.Entry entry : model.entries()) { for (RangeSelectItemModel.Entry entry : model.entries()) {
mapItem(entry.model(), context.with(new GeyserRangeDispatchPredicate(predicateProperty, entry.threshold(), model.scale()), "threshold " + entry.threshold())); 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 (dataConstructor == null) {
if (unbakedSwitch.property() instanceof DisplayContext) { 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) { for (SelectItemModel.SwitchCase<?> switchCase : cases) {
if (switchCase.values().contains(ItemDisplayContext.GUI)) { if (switchCase.values().contains(ItemDisplayContext.GUI)) {
mapItem(switchCase.model(), context.child("select GUI display_context case (unsupported property) ")); 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) "))); model.fallback().ifPresent(fallback -> mapItem(fallback, context.child("select fallback case (unsupported property) ")));
return; return;
} }
@@ -261,17 +230,11 @@ public class BedrockItemMapper {
return new MappingContext(predicateStack, stack, reporter.forChild(() -> childName), definitionCreator, packContext); return new MappingContext(predicateStack, stack, reporter.forChild(() -> childName), definitionCreator, packContext);
} }
public void create(ResourceLocation bedrockIdentifier, ResourceLocation texture, boolean displayHandheld, public void create(ResourceLocation bedrockIdentifier, BedrockGeometryContext geometry) {
Optional<ResolvedModel> customModel) { List<ResourceLocation> tags = stack.is(ItemTags.TRIMMABLE_ARMOR) ? TRIMMABLE_ARMOR_TAGS : List.of();
List<ResourceLocation> tags;
if (stack.is(ItemTags.TRIMMABLE_ARMOR)) {
tags = TRIMMABLE_ARMOR_TAGS;
} else {
tags = List.of();
}
GeyserBaseDefinition base = new GeyserBaseDefinition(bedrockIdentifier, Optional.of(stack.getHoverName().getString()), predicateStack, GeyserBaseDefinition base = new GeyserBaseDefinition(bedrockIdentifier, Optional.ofNullable(stack.getHoverName().tryCollapseToString()), predicateStack,
new GeyserBaseDefinition.BedrockOptions(Optional.empty(), true, displayHandheld, calculateProtectionValue(stack), tags), new GeyserBaseDefinition.BedrockOptions(Optional.empty(), true, geometry.handheld(), calculateProtectionValue(stack), tags),
stack.getComponentsPatch()); stack.getComponentsPatch());
try { try {
packContext.mappings().map(stack.getItemHolder(), definitionCreator.apply(base)); packContext.mappings().map(stack.getItemHolder(), definitionCreator.apply(base));
@@ -280,24 +243,12 @@ public class BedrockItemMapper {
return; return;
} }
// TODO Should probably get a better way to get geometry texture packContext.itemConsumer().accept(new BedrockItem(bedrockIdentifier, base.textureName(), geometry,
String safeIdentifier = base.textureName(); AttachableMapper.mapItem(packContext.assetResolver(), geometry, stack.getComponentsPatch())));
String bone = "bone"; }
ResourceLocation geometryTexture = texture;
Optional<BedrockGeometryContext> bedrockGeometry = customModel.map(model -> GeometryMapper.mapGeometry(safeIdentifier, bone, model, geometryTexture));
Optional<BedrockAnimationContext> bedrockAnimation = customModel.map(model -> AnimationMapper.mapAnimation(safeIdentifier, bone, model.getTopTransforms()));
boolean exportTexture = true; public void report(String problem) {
if (customModel.isPresent()) { reporter.report(() -> problem);
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)));
} }
private static int calculateProtectionValue(ItemStack stack) { private static int calculateProtectionValue(ItemStack stack) {

View File

@@ -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> geometryRenderer, boolean reportSuccesses) {}

View File

@@ -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 {
<T> CompletableFuture<?> saveJson(Codec<T> codec, T object, Path path);
CompletableFuture<?> saveTexture(byte[] texture, Path path);
}

View File

@@ -6,15 +6,12 @@ import org.geysermc.rainbow.pack.animation.BedrockAnimation;
import org.joml.Vector3f; import org.joml.Vector3f;
import org.joml.Vector3fc; 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 { 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_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 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); // These transformations perfect... but I spent over 3 hours trying to get these. It's good enough for me.
private static final Vector3fc THIRD_PERSON_ROTATION_OFFSET = new Vector3f(90.0F, -90.0F, 0.0F);
public static BedrockAnimationContext mapAnimation(String identifier, String bone, ItemTransforms transforms) { 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 // I don't think it's possible to display separate animations for left- and right hands
ItemTransform firstPerson = transforms.firstPersonRightHand(); ItemTransform firstPerson = transforms.firstPersonRightHand();
@@ -23,8 +20,10 @@ public class AnimationMapper {
Vector3f firstPersonScale = new Vector3f(firstPerson.scale()); Vector3f firstPersonScale = new Vector3f(firstPerson.scale());
ItemTransform thirdPerson = transforms.thirdPersonRightHand(); ItemTransform thirdPerson = transforms.thirdPersonRightHand();
Vector3f thirdPersonPosition = THIRD_PERSON_POSITION_OFFSET.add(thirdPerson.translation(), 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 thirdPersonRotation = THIRD_PERSON_ROTATION_OFFSET.add(-thirdPerson.rotation().x(), thirdPerson.rotation().y(), thirdPerson.rotation().z(), new Vector3f()); 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()); Vector3f thirdPersonScale = new Vector3f(thirdPerson.scale());
return new BedrockAnimationContext(BedrockAnimation.builder() return new BedrockAnimationContext(BedrockAnimation.builder()

View File

@@ -1,7 +1,6 @@
package org.geysermc.rainbow.mapping.attachable; package org.geysermc.rainbow.mapping.attachable;
import com.mojang.datafixers.util.Pair; import com.mojang.datafixers.util.Pair;
import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.model.EquipmentAssetManager; import net.minecraft.client.resources.model.EquipmentAssetManager;
import net.minecraft.client.resources.model.EquipmentClientInfo; import net.minecraft.client.resources.model.EquipmentClientInfo;
import net.minecraft.core.component.DataComponentPatch; import net.minecraft.core.component.DataComponentPatch;
@@ -9,9 +8,10 @@ import net.minecraft.core.component.DataComponents;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.item.equipment.Equippable; 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.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 org.geysermc.rainbow.pack.attachable.BedrockAttachable;
import java.util.List; import java.util.List;
@@ -20,31 +20,27 @@ import java.util.function.Consumer;
public class AttachableMapper { public class AttachableMapper {
public static Optional<BedrockAttachable> mapItem(DataComponentPatch components, ResourceLocation bedrockIdentifier, Optional<BedrockGeometryContext> customGeometry, public static AttachableCreator mapItem(AssetResolver assetResolver, BedrockGeometryContext geometryContext, DataComponentPatch components) {
Optional<BedrockAnimationContext> customAnimation, Consumer<ResourceLocation> textureConsumer) {
// Crazy optional statement // Crazy optional statement
// Unfortunately we can't have both equippables and custom models, so we prefer the latter :( // Unfortunately we can't have both equippables and custom models, so we prefer the latter :(
return customGeometry return (bedrockIdentifier, stitchedGeometry, textureConsumer) -> stitchedGeometry
.map(geometry -> BedrockAttachable.geometry(bedrockIdentifier, geometry.geometry().definitions().getFirst(), geometry.texture().getPath())) .map(stitched -> BedrockAttachable.geometry(bedrockIdentifier, stitched.geometry().definitions().getFirst(), stitched.stitchedTextures().location().getPath()))
.or(() -> Optional.ofNullable(components.get(DataComponents.EQUIPPABLE)) .or(() -> Optional.ofNullable(components.get(DataComponents.EQUIPPABLE))
.flatMap(optional -> (Optional<Equippable>) optional) .flatMap(optional -> (Optional<Equippable>) optional)
.flatMap(equippable -> { .flatMap(equippable -> equippable.assetId().flatMap(assetResolver::getEquipmentInfo).map(info -> Pair.of(equippable.slot(), info)))
EquipmentAssetManager equipmentAssets = ((EntityRenderDispatcherAccessor) Minecraft.getInstance().getEntityRenderDispatcher()).getEquipmentAssets();
return equippable.assetId().map(asset -> Pair.of(equippable.slot(), equipmentAssets.get(asset)));
})
.filter(assetInfo -> assetInfo.getSecond() != EquipmentAssetManager.MISSING) .filter(assetInfo -> assetInfo.getSecond() != EquipmentAssetManager.MISSING)
.map(assetInfo -> assetInfo .map(assetInfo -> assetInfo
.mapSecond(info -> info.getLayers(getLayer(assetInfo.getFirst())))) .mapSecond(info -> info.getLayers(getLayer(assetInfo.getFirst()))))
.filter(assetInfo -> !assetInfo.getSecond().isEmpty()) .filter(assetInfo -> !assetInfo.getSecond().isEmpty())
.map(assetInfo -> { .map(assetInfo -> {
ResourceLocation texture = getTexture(assetInfo.getSecond(), getLayer(assetInfo.getFirst())); ResourceLocation equipmentTexture = getTexture(assetInfo.getSecond(), getLayer(assetInfo.getFirst()));
textureConsumer.accept(texture); textureConsumer.accept(TextureHolder.createFromResources(equipmentTexture));
return BedrockAttachable.equipment(bedrockIdentifier, assetInfo.getFirst(), texture.getPath()); return BedrockAttachable.equipment(bedrockIdentifier, assetInfo.getFirst(), equipmentTexture.getPath());
})) }))
.map(attachable -> { .map(attachable -> {
customAnimation.ifPresent(context -> { geometryContext.animation().ifPresent(animation -> {
attachable.withAnimation("first_person", context.firstPerson()); attachable.withAnimation("first_person", animation.firstPerson());
attachable.withAnimation("third_person", context.thirdPerson()); attachable.withAnimation("third_person", animation.thirdPerson());
attachable.withScript("animate", "first_person", "context.is_first_person == 1.0"); attachable.withScript("animate", "first_person", "context.is_first_person == 1.0");
attachable.withScript("animate", "third_person", "context.is_first_person == 0.0"); attachable.withScript("animate", "third_person", "context.is_first_person == 0.0");
}); });
@@ -59,4 +55,10 @@ public class AttachableMapper {
private static ResourceLocation getTexture(List<EquipmentClientInfo.Layer> info, EquipmentClientInfo.LayerType layer) { private static ResourceLocation getTexture(List<EquipmentClientInfo.Layer> info, EquipmentClientInfo.LayerType layer) {
return info.getFirst().textureId().withPath(path -> "entity/equipment/" + layer.getSerializedName() + "/" + path); return info.getFirst().textureId().withPath(path -> "entity/equipment/" + layer.getSerializedName() + "/" + path);
} }
@FunctionalInterface
public interface AttachableCreator {
Optional<BedrockAttachable> create(ResourceLocation bedrockIdentifier, Optional<StitchedGeometry> geometry, Consumer<TextureHolder> textureConsumer);
}
} }

View File

@@ -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<Supplier<StitchedGeometry>> geometry,
Optional<BedrockAnimationContext> animation, TextureHolder icon,
boolean handheld) {
private static final List<ResourceLocation> 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<Supplier<StitchedGeometry>> geometry;
Optional<BedrockAnimationContext> 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);
}
}

View File

@@ -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.BlockElementRotation;
import net.minecraft.client.renderer.block.model.SimpleUnbakedGeometry; import net.minecraft.client.renderer.block.model.SimpleUnbakedGeometry;
import net.minecraft.client.resources.model.ResolvedModel; import net.minecraft.client.resources.model.ResolvedModel;
import net.minecraft.client.resources.model.UnbakedGeometry;
import net.minecraft.core.Direction; 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.geysermc.rainbow.pack.geometry.BedrockGeometry;
import org.joml.Vector2f; import org.joml.Vector2f;
import org.joml.Vector3f; import org.joml.Vector3f;
@@ -17,26 +18,30 @@ import java.util.Map;
public class GeometryMapper { public class GeometryMapper {
private static final Vector3fc CENTRE_OFFSET = new Vector3f(8.0F, 0.0F, 8.0F); 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); BedrockGeometry.Builder builder = BedrockGeometry.builder(identifier);
// Blockbench seems to always use these values TODO that's wrong // Blockbench seems to always use these values TODO that's wrong
builder.withVisibleBoundsWidth(4.0F); builder.withVisibleBoundsWidth(4.0F);
builder.withVisibleBoundsHeight(4.0F); builder.withVisibleBoundsHeight(4.0F);
builder.withVisibleBoundsOffset(new Vector3f(0.0F, 0.75F, 0.0F)); builder.withVisibleBoundsOffset(new Vector3f(0.0F, 0.75F, 0.0F));
// TODO proper texture size builder.withTextureWidth(textures.width());
builder.withTextureWidth(16); builder.withTextureHeight(textures.height());
builder.withTextureHeight(16);
BedrockGeometry.Bone.Builder bone = BedrockGeometry.bone(boneName); BedrockGeometry.Bone.Builder bone = BedrockGeometry.bone(boneName);
Vector3f min = new Vector3f(Float.MAX_VALUE); Vector3f min = new Vector3f(Float.MAX_VALUE);
Vector3f max = new Vector3f(Float.MIN_VALUE); Vector3f max = new Vector3f(Float.MIN_VALUE);
SimpleUnbakedGeometry geometry = (SimpleUnbakedGeometry) model.getTopGeometry(); SimpleUnbakedGeometry geometry = (SimpleUnbakedGeometry) top;
for (BlockElement element : geometry.elements()) { for (BlockElement element : geometry.elements()) {
// TODO the origin here is wrong, some models seem to be mirrored weirdly in blockbench // 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); bone.withCube(cube);
min.min(cube.origin()); min.min(cube.origin());
max.max(cube.origin().add(cube.size(), new Vector3f())); 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 // Bind to the bone of the current item slot
bone.withBinding("q.item_slot_to_bone_name(context.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 // 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())); BedrockGeometry.Cube.Builder builder = BedrockGeometry.cube(element.from().sub(CENTRE_OFFSET, new Vector3f()), element.to().sub(element.from(), new Vector3f()));
for (Map.Entry<Direction, BlockElementFace> faceEntry : element.faces().entrySet()) { for (Map.Entry<Direction, BlockElementFace> faceEntry : element.faces().entrySet()) {
// TODO texture key
Direction direction = faceEntry.getKey(); Direction direction = faceEntry.getKey();
BlockElementFace face = faceEntry.getValue(); BlockElementFace face = faceEntry.getValue();
Vector2f uvOrigin; Vector2f uvOrigin;
Vector2f uvSize; Vector2f uvSize;
BlockElementFace.UVs uvs = face.uvs(); BlockElementFace.UVs uvs = face.uvs();
if (uvs != null) { if (uvs == null) {
// Up and down faces are special // Java defaults to a set of UV values determined by the position of the face if no UV values were specified
if (direction.getAxis() == Direction.Axis.Y) { uvs = FaceBakeryAccessor.invokeDefaultFaceUV(element.from(), element.to(), direction);
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();
} }
// 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()); builder.withFace(direction, uvOrigin, uvSize, face.rotation());
} }

View File

@@ -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);
}

View File

@@ -0,0 +1,5 @@
package org.geysermc.rainbow.mapping.geometry;
import org.geysermc.rainbow.pack.geometry.BedrockGeometry;
public record StitchedGeometry(BedrockGeometry geometry, TextureHolder stitchedTextures) {}

View File

@@ -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<String, TextureAtlasSprite> sprites, Supplier<NativeImage> stitched, int width, int height) {
public Optional<TextureAtlasSprite> 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<String, Material> materials = ((TextureSlotsAccessor) textures).getResolvedValues();
SpriteLoader.Preparations preparations = prepareStitching(materials.values().stream().map(Material::texture), context);
Map<String, TextureAtlasSprite> sprites = new HashMap<>();
for (Map.Entry<String, Material> 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<ResourceLocation> 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<SpriteContents> sprites = textures.distinct()
.map(texture -> readSpriteContents(texture, context))
.<SpriteContents>mapMulti(Optional::ifPresent)
.toList();
return ((SpriteLoaderAccessor) spriteLoader).invokeStitch(sprites, 0, Util.backgroundExecutor());
}
private static Optional<SpriteContents> 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;
}
}

View File

@@ -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<NativeImage>> supplier, boolean existsInResources) {
public Optional<byte[]> 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<NativeImage> 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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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();
}

View File

@@ -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<SpriteContents> contents, int mipLevel, Executor executor);
}

View File

@@ -4,6 +4,7 @@ import net.minecraft.client.renderer.block.model.TextureSlots;
import net.minecraft.client.resources.model.Material; import net.minecraft.client.resources.model.Material;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor; import org.spongepowered.asm.mixin.gen.Accessor;
import org.spongepowered.asm.mixin.gen.Invoker;
import java.util.Map; import java.util.Map;
@@ -12,4 +13,9 @@ public interface TextureSlotsAccessor {
@Accessor @Accessor
Map<String, Material> getResolvedValues(); Map<String, Material> getResolvedValues();
@Invoker
static boolean invokeIsTextureReference(String name) {
throw new AssertionError();
}
} }

View File

@@ -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<TextureHolder, CompletableFuture<?>> textureSaver) {
return CompletableFuture.allOf(
textureSaver.apply(geometryContext.icon()),
CompletableFuture.supplyAsync(() -> geometryContext.geometry().map(Supplier::get))
.thenCompose(stitchedGeometry -> {
List<TextureHolder> attachableTextures = new ArrayList<>();
Optional<BedrockAttachable> 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 <T> CompletableFuture<T> noop() {
return CompletableFuture.completedFuture(null);
}
}

View File

@@ -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<BedrockItem> bedrockItems = new HashSet<>();
private final Set<ResourceLocation> 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> 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<? extends ResourceLocation> 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> item, DataComponentPatch patch) {
ItemStack stack = new ItemStack(item);
stack.applyComponents(patch);
return map(stack);
}
public CompletableFuture<?> save() {
List<CompletableFuture<?>> 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<TextureHolder, CompletableFuture<?>> 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<BedrockItem> 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<Path> attachablesPath = resolve(ATTACHABLES_DIRECTORY);
private UnaryOperator<Path> geometryPath = resolve(GEOMETRY_DIRECTORY);
private UnaryOperator<Path> animationPath = resolve(ANIMATION_DIRECTORY);
private UnaryOperator<Path> manifestPath = resolve(MANIFEST_FILE);
private UnaryOperator<Path> itemAtlasPath = resolve(ITEM_ATLAS_FILE);
private Path packZipFile = null;
private GeometryRenderer geometryRenderer = null;
private Function<ProblemReporter.PathElement, ProblemReporter> 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> path) {
attachablesPath = path;
return this;
}
public Builder withGeometryPath(Path absolute) {
return withGeometryPath(path -> absolute);
}
public Builder withGeometryPath(UnaryOperator<Path> path) {
geometryPath = path;
return this;
}
public Builder withAnimationPath(Path absolute) {
return withAnimationPath(path -> absolute);
}
public Builder withAnimationPath(UnaryOperator<Path> path) {
animationPath = path;
return this;
}
public Builder withManifestPath(Path absolute) {
return withManifestPath(path -> absolute);
}
public Builder withManifestPath(UnaryOperator<Path> path) {
manifestPath = path;
return this;
}
public Builder withItemAtlasPath(Path absolute) {
return withItemAtlasPath(path -> absolute);
}
public Builder withItemAtlasPath(UnaryOperator<Path> 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<ProblemReporter.PathElement, ProblemReporter> 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<Path> 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
}
}

View File

@@ -33,7 +33,7 @@ public record BedrockTextures(Map<String, String> textures) {
private final Map<String, String> textures = new HashMap<>(); private final Map<String, String> textures = new HashMap<>();
public Builder withItemTexture(BedrockItem item) { 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) { public Builder withTexture(String name, String texture) {

View File

@@ -5,6 +5,7 @@ import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder; import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.core.UUIDUtil; import net.minecraft.core.UUIDUtil;
import org.geysermc.rainbow.CodecUtil; import org.geysermc.rainbow.CodecUtil;
import org.geysermc.rainbow.PackConstants;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@@ -56,5 +57,10 @@ public record PackManifest(Header header, List<Module> modules) {
return new Module(name, description, uuid, version.increment()); 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)));
}
} }

View File

@@ -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<Path> zipOutput) {
}

View File

@@ -5,14 +5,15 @@ import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult; import com.mojang.serialization.DataResult;
import com.mojang.serialization.codecs.RecordCodecBuilder; import com.mojang.serialization.codecs.RecordCodecBuilder;
import org.geysermc.rainbow.CodecUtil; import org.geysermc.rainbow.CodecUtil;
import org.geysermc.rainbow.mapping.PackSerializer;
import org.geysermc.rainbow.pack.BedrockVersion; import org.geysermc.rainbow.pack.BedrockVersion;
import org.joml.Vector3fc; import org.joml.Vector3fc;
import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletableFuture;
public record BedrockAnimation(BedrockVersion formatVersion, Map<String, AnimationDefinition> definitions) { public record BedrockAnimation(BedrockVersion formatVersion, Map<String, AnimationDefinition> definitions) {
public static final BedrockVersion FORMAT_VERSION = BedrockVersion.of(1, 8, 0); public static final BedrockVersion FORMAT_VERSION = BedrockVersion.of(1, 8, 0);
@@ -24,8 +25,8 @@ public record BedrockAnimation(BedrockVersion formatVersion, Map<String, Animati
).apply(instance, BedrockAnimation::new) ).apply(instance, BedrockAnimation::new)
); );
public void save(Path animationDirectory, String identifier) throws IOException { public CompletableFuture<?> save(PackSerializer serializer, Path animationDirectory, String identifier) {
CodecUtil.trySaveJson(CODEC, this, animationDirectory.resolve(identifier + ".animation.json")); return serializer.saveJson(CODEC, this, animationDirectory.resolve(identifier + ".animation.json"));
} }
public static Builder builder() { public static Builder builder() {

View File

@@ -9,15 +9,14 @@ import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.ExtraCodecs; import net.minecraft.util.ExtraCodecs;
import net.minecraft.util.StringRepresentable; import net.minecraft.util.StringRepresentable;
import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.EquipmentSlot;
import org.geysermc.rainbow.CodecUtil;
import org.geysermc.rainbow.PackConstants; import org.geysermc.rainbow.PackConstants;
import org.geysermc.rainbow.Rainbow; import org.geysermc.rainbow.Rainbow;
import org.geysermc.rainbow.mapping.PackSerializer;
import org.geysermc.rainbow.pack.BedrockTextures; import org.geysermc.rainbow.pack.BedrockTextures;
import org.geysermc.rainbow.pack.BedrockVersion; import org.geysermc.rainbow.pack.BedrockVersion;
import org.geysermc.rainbow.pack.geometry.BedrockGeometry; import org.geysermc.rainbow.pack.geometry.BedrockGeometry;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.EnumMap; import java.util.EnumMap;
@@ -25,6 +24,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream; import java.util.stream.Stream;
public record BedrockAttachable(BedrockVersion formatVersion, AttachableInfo info) { public record BedrockAttachable(BedrockVersion formatVersion, AttachableInfo info) {
@@ -35,9 +35,9 @@ public record BedrockAttachable(BedrockVersion formatVersion, AttachableInfo inf
).apply(instance, BedrockAttachable::new) ).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 // 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) { public static Builder builder(ResourceLocation identifier) {

Some files were not shown because too many files have changed in this diff Show More