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:
13
.github/workflows/build.yml
vendored
13
.github/workflows/build.yml
vendored
@@ -33,12 +33,19 @@ jobs:
|
||||
env:
|
||||
BUILD_NUMBER: ${{ steps.release-info.outputs.curentRelease }}
|
||||
|
||||
- name: Publish to Maven Repository
|
||||
if: ${{ success() && github.repository == 'GeyserMC/Rainbow' && github.ref_name == 'master' }}
|
||||
run: ./gradlew publish
|
||||
env:
|
||||
ORG_GRADLE_PROJECT_geysermcUsername: ${{ vars.DEPLOY_USER }}
|
||||
ORG_GRADLE_PROJECT_geysermcPassword: ${{ secrets.DEPLOY_PASS }}
|
||||
|
||||
- name: Archive Artifacts
|
||||
uses: GeyserMC/actions/upload-multi-artifact@master
|
||||
if: success()
|
||||
with:
|
||||
artifacts: |
|
||||
rainbow:build/libs/Rainbow.jar
|
||||
rainbow:client/build/libs/Rainbow.jar
|
||||
|
||||
- name: Get Version
|
||||
if: ${{ (success() || failure()) && github.repository == 'GeyserMC/Rainbow' }}
|
||||
@@ -69,7 +76,7 @@ jobs:
|
||||
privateKey: ${{ secrets.DOWNLOADS_PRIVATE_KEY }}
|
||||
host: ${{ secrets.DOWNLOADS_SERVER_IP }}
|
||||
files: |
|
||||
build/libs/Rainbow.jar
|
||||
client/build/libs/Rainbow.jar
|
||||
changelog: ${{ steps.metadata.outputs.body }}
|
||||
|
||||
# - name: Publish to Modrinth
|
||||
@@ -87,4 +94,4 @@ jobs:
|
||||
discordWebhook: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
status: ${{ job.status }}
|
||||
body: ${{ steps.metadata.outputs.body }}
|
||||
includeDownloads: ${{ success() && github.repository == 'GeyserMC/Rainbow' && github.ref_name == 'master' }}
|
||||
includeDownloads: ${{ github.ref_name == 'master' }}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
[](https://discord.gg/geysermc)
|
||||
|
||||
Rainbow is a client-side Minecraft mod for the Fabric modloader to generate Geyser item mappings and bedrock resourcepacks
|
||||
for use with Geyser's [custom item API (v2)](https://github.com/geyserMC/geyser/pull/5189). Rainbow is available for Minecraft 1.21.7 and 1.21.8.
|
||||
for use with Geyser's [custom item API (v2)](https://github.com/geyserMC/geyser/pull/5189). Rainbow is available for Minecraft 1.21.9 and 1.21.10.
|
||||
|
||||
Rainbow is currently experimental and capable of the following:
|
||||
|
||||
|
||||
18
build-logic/build.gradle.kts
Normal file
18
build-logic/build.gradle.kts
Normal 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)
|
||||
}
|
||||
7
build-logic/settings.gradle.kts
Normal file
7
build-logic/settings.gradle.kts
Normal file
@@ -0,0 +1,7 @@
|
||||
dependencyResolutionManagement {
|
||||
versionCatalogs {
|
||||
create("libs") {
|
||||
from(files("../gradle/libs.versions.toml"))
|
||||
}
|
||||
}
|
||||
}
|
||||
6
build-logic/src/main/kotlin/libs.kt
Normal file
6
build-logic/src/main/kotlin/libs.kt
Normal 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()
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"])
|
||||
}
|
||||
}
|
||||
}
|
||||
76
build.gradle
76
build.gradle
@@ -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
31
client/build.gradle.kts
Normal 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
1
client/gradle.properties
Normal file
@@ -0,0 +1 @@
|
||||
archives_base_name=rainbow-client
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,17 @@
|
||||
package org.geysermc.rainbow;
|
||||
package org.geysermc.rainbow.client;
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.command.v2.ArgumentTypeRegistry;
|
||||
import net.minecraft.commands.synchronization.SingletonArgumentInfo;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.geysermc.rainbow.command.CommandSuggestionsArgumentType;
|
||||
import org.geysermc.rainbow.command.PackGeneratorCommand;
|
||||
import org.geysermc.rainbow.mapper.PackMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.geysermc.rainbow.Rainbow;
|
||||
import org.geysermc.rainbow.RainbowIO;
|
||||
import org.geysermc.rainbow.client.command.CommandSuggestionsArgumentType;
|
||||
import org.geysermc.rainbow.client.command.PackGeneratorCommand;
|
||||
import org.geysermc.rainbow.client.mapper.PackMapper;
|
||||
|
||||
public class Rainbow implements ClientModInitializer {
|
||||
|
||||
public static final String MOD_ID = "rainbow";
|
||||
public static final String MOD_NAME = "Rainbow";
|
||||
public static final Logger LOGGER = LogUtils.getLogger();
|
||||
public class RainbowClient implements ClientModInitializer {
|
||||
|
||||
private final PackManager packManager = new PackManager();
|
||||
private final PackMapper packMapper = new PackMapper(packManager);
|
||||
@@ -27,15 +22,9 @@ public class Rainbow implements ClientModInitializer {
|
||||
ClientCommandRegistrationCallback.EVENT.register((dispatcher, buildContext) -> PackGeneratorCommand.register(dispatcher, packManager, packMapper));
|
||||
ClientTickEvents.START_CLIENT_TICK.register(packMapper::tick);
|
||||
|
||||
ArgumentTypeRegistry.registerArgumentType(getModdedLocation("command_suggestions"),
|
||||
ArgumentTypeRegistry.registerArgumentType(Rainbow.getModdedLocation("command_suggestions"),
|
||||
CommandSuggestionsArgumentType.class, SingletonArgumentInfo.contextFree(CommandSuggestionsArgumentType::new));
|
||||
}
|
||||
|
||||
public static ResourceLocation getModdedLocation(String path) {
|
||||
return ResourceLocation.fromNamespaceAndPath(MOD_ID, path);
|
||||
}
|
||||
|
||||
public static String fileSafeResourceLocation(ResourceLocation location) {
|
||||
return location.toString().replace(':', '.').replace('/', '_');
|
||||
RainbowIO.registerExceptionListener(new RainbowClientIOHandler());
|
||||
}
|
||||
}
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.geysermc.rainbow.accessor;
|
||||
package org.geysermc.rainbow.client.accessor;
|
||||
|
||||
import net.minecraft.client.renderer.item.ClientItem;
|
||||
import net.minecraft.client.resources.model.ResolvedModel;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.geysermc.rainbow.command;
|
||||
package org.geysermc.rainbow.client.command;
|
||||
|
||||
import com.mojang.brigadier.StringReader;
|
||||
import com.mojang.brigadier.arguments.ArgumentType;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.geysermc.rainbow.command;
|
||||
package org.geysermc.rainbow.client.command;
|
||||
|
||||
import com.mojang.brigadier.Command;
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
@@ -10,9 +10,9 @@ import net.minecraft.network.chat.ClickEvent;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.geysermc.rainbow.PackManager;
|
||||
import org.geysermc.rainbow.mapper.InventoryMapper;
|
||||
import org.geysermc.rainbow.mapper.PackMapper;
|
||||
import org.geysermc.rainbow.client.PackManager;
|
||||
import org.geysermc.rainbow.client.mapper.InventoryMapper;
|
||||
import org.geysermc.rainbow.client.mapper.PackMapper;
|
||||
import org.geysermc.rainbow.pack.BedrockPack;
|
||||
|
||||
import java.nio.file.Path;
|
||||
@@ -113,14 +113,13 @@ public class PackGeneratorCommand {
|
||||
.then(ClientCommandManager.literal("finish")
|
||||
.executes(context -> {
|
||||
Optional<Path> exportPath = packManager.getExportPath();
|
||||
packManager.finish().ifPresentOrElse(success -> {
|
||||
if (!success) {
|
||||
context.getSource().sendError(Component.translatable("commands.rainbow.pack_finished_error"));
|
||||
} else {
|
||||
context.getSource().sendFeedback(Component.translatable("commands.rainbow.pack_finished_successfully")
|
||||
.withStyle(style -> style.withUnderlined(true).withClickEvent(new ClickEvent.OpenFile(exportPath.orElseThrow()))));
|
||||
}
|
||||
}, () -> context.getSource().sendError(NO_PACK_CREATED));
|
||||
if (packManager.finish()) {
|
||||
// TODO error when exporting fails
|
||||
context.getSource().sendFeedback(Component.translatable("commands.rainbow.pack_finished_successfully")
|
||||
.withStyle(style -> style.withUnderlined(true).withClickEvent(new ClickEvent.OpenFile(exportPath.orElseThrow()))));
|
||||
} else {
|
||||
context.getSource().sendError(NO_PACK_CREATED);
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.geysermc.rainbow.mapper;
|
||||
package org.geysermc.rainbow.client.mapper;
|
||||
|
||||
import net.minecraft.client.multiplayer.ClientPacketListener;
|
||||
import net.minecraft.client.player.LocalPlayer;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.geysermc.rainbow.mapper;
|
||||
package org.geysermc.rainbow.client.mapper;
|
||||
|
||||
import net.minecraft.client.multiplayer.ClientPacketListener;
|
||||
import net.minecraft.client.player.LocalPlayer;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.geysermc.rainbow.mapper;
|
||||
package org.geysermc.rainbow.client.mapper;
|
||||
|
||||
import net.minecraft.client.multiplayer.ClientPacketListener;
|
||||
import net.minecraft.client.player.LocalPlayer;
|
||||
@@ -1,14 +1,13 @@
|
||||
package org.geysermc.rainbow.mapper;
|
||||
package org.geysermc.rainbow.client.mapper;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientPacketListener;
|
||||
import net.minecraft.client.player.LocalPlayer;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import org.geysermc.rainbow.PackManager;
|
||||
import org.geysermc.rainbow.client.PackManager;
|
||||
import org.geysermc.rainbow.pack.BedrockPack;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public class PackMapper {
|
||||
private final PackManager packManager;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.geysermc.rainbow.mixin;
|
||||
package org.geysermc.rainbow.client.mixin;
|
||||
|
||||
import net.minecraft.client.renderer.entity.EntityRenderDispatcher;
|
||||
import net.minecraft.client.resources.model.EquipmentAssetManager;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.geysermc.rainbow.mixin;
|
||||
package org.geysermc.rainbow.client.mixin;
|
||||
|
||||
import net.minecraft.client.gui.render.state.GuiItemRenderState;
|
||||
import net.minecraft.client.gui.render.state.ScreenArea;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.geysermc.rainbow.mixin;
|
||||
package org.geysermc.rainbow.client.mixin;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
@@ -9,7 +9,7 @@ import net.minecraft.client.resources.model.ModelManager;
|
||||
import net.minecraft.client.resources.model.ResolvedModel;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.packs.resources.PreparableReloadListener;
|
||||
import org.geysermc.rainbow.accessor.ResolvedModelAccessor;
|
||||
import org.geysermc.rainbow.client.accessor.ResolvedModelAccessor;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.geysermc.rainbow.mixin;
|
||||
package org.geysermc.rainbow.client.mixin;
|
||||
|
||||
import com.mojang.blaze3d.textures.GpuTexture;
|
||||
import net.minecraft.client.gui.render.pip.PictureInPictureRenderer;
|
||||
@@ -1,8 +1,8 @@
|
||||
package org.geysermc.rainbow.mixin;
|
||||
package org.geysermc.rainbow.client.mixin;
|
||||
|
||||
import com.mojang.blaze3d.textures.GpuTexture;
|
||||
import net.minecraft.client.gui.render.pip.PictureInPictureRenderer;
|
||||
import org.geysermc.rainbow.render.PictureInPictureCopyRenderer;
|
||||
import org.geysermc.rainbow.client.render.PictureInPictureCopyRenderer;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.geysermc.rainbow.mixin;
|
||||
package org.geysermc.rainbow.client.mixin;
|
||||
|
||||
import net.minecraft.client.gui.components.SplashRenderer;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.geysermc.rainbow.mapping.geometry;
|
||||
package org.geysermc.rainbow.client.render;
|
||||
|
||||
import com.mojang.blaze3d.buffers.GpuBuffer;
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
@@ -14,20 +14,19 @@ import net.minecraft.client.gui.render.state.pip.OversizedItemRenderState;
|
||||
import net.minecraft.client.renderer.item.TrackingItemStackRenderState;
|
||||
import net.minecraft.world.item.ItemDisplayContext;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.geysermc.rainbow.CodecUtil;
|
||||
import org.geysermc.rainbow.mixin.PictureInPictureRendererAccessor;
|
||||
import org.geysermc.rainbow.render.PictureInPictureCopyRenderer;
|
||||
import org.geysermc.rainbow.mapping.geometry.GeometryRenderer;
|
||||
import org.geysermc.rainbow.client.mixin.PictureInPictureRendererAccessor;
|
||||
import org.joml.Matrix3x2fStack;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
// TODO maybe just use this even for normal 2D items, not sure, could be useful for composite models and stuff
|
||||
// TODO output in a size bedrock likes
|
||||
public class GeometryRenderer {
|
||||
public class MinecraftGeometryRenderer implements GeometryRenderer {
|
||||
public static final MinecraftGeometryRenderer INSTANCE = new MinecraftGeometryRenderer();
|
||||
|
||||
public static void render(ItemStack stack, Path path) {
|
||||
@Override
|
||||
public NativeImage render(ItemStack stack) {
|
||||
TrackingItemStackRenderState itemRenderState = new TrackingItemStackRenderState();
|
||||
Minecraft.getInstance().getItemModelResolver().updateForTopItem(itemRenderState, stack, ItemDisplayContext.GUI, null, null, 0);
|
||||
itemRenderState.setOversizedInGui(true);
|
||||
@@ -41,12 +40,12 @@ public class GeometryRenderer {
|
||||
//noinspection DataFlowIssue
|
||||
((PictureInPictureCopyRenderer) itemRenderer).rainbow$allowTextureCopy();
|
||||
itemRenderer.prepare(oversizedRenderState, new GuiRenderState(), 4);
|
||||
writeAsPNG(path, ((PictureInPictureRendererAccessor) itemRenderer).getTexture());
|
||||
return writeToImage(((PictureInPictureRendererAccessor) itemRenderer).getTexture());
|
||||
}
|
||||
}
|
||||
|
||||
// Simplified TextureUtil#writeAsPNG with some modifications to flip the image and just generate it at full size
|
||||
private static void writeAsPNG(Path path, GpuTexture texture) {
|
||||
// Simplified TextureUtil#writeAsPNG with some modifications to just write to a NativeImage, flip the image and just generate it at full size
|
||||
private static NativeImage writeToImage(GpuTexture texture) {
|
||||
RenderSystem.assertOnRenderThread();
|
||||
int width = texture.getWidth(0);
|
||||
int height = texture.getHeight(0);
|
||||
@@ -55,25 +54,21 @@ public class GeometryRenderer {
|
||||
GpuBuffer buffer = RenderSystem.getDevice().createBuffer(() -> "Texture output buffer", GpuBuffer.USAGE_COPY_DST | GpuBuffer.USAGE_MAP_READ, bufferSize);
|
||||
CommandEncoder commandEncoder = RenderSystem.getDevice().createCommandEncoder();
|
||||
|
||||
NativeImage image = new NativeImage(width, height, false);
|
||||
Runnable writer = () -> {
|
||||
try (GpuBuffer.MappedView mappedView = commandEncoder.mapBuffer(buffer, true, false)) {
|
||||
try (NativeImage nativeImage = new NativeImage(width, height, false)) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
int colour = mappedView.data().getInt((x + y * width) * texture.getFormat().pixelSize());
|
||||
nativeImage.setPixelABGR(x, height - y - 1, colour);
|
||||
}
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
int colour = mappedView.data().getInt((x + y * width) * texture.getFormat().pixelSize());
|
||||
image.setPixelABGR(x, height - y - 1, colour);
|
||||
}
|
||||
|
||||
CodecUtil.ensureDirectoryExists(path.getParent());
|
||||
nativeImage.writeToFile(path);
|
||||
} catch (IOException var19) {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
buffer.close();
|
||||
};
|
||||
commandEncoder.copyTextureToBuffer(texture, buffer, 0, writer, 0);
|
||||
|
||||
return image;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.geysermc.rainbow.render;
|
||||
package org.geysermc.rainbow.client.render;
|
||||
|
||||
public interface PictureInPictureCopyRenderer {
|
||||
|
||||
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 282 B |
@@ -13,5 +13,7 @@
|
||||
"commands.rainbow.pack_created": "Created pack with name %s",
|
||||
"commands.rainbow.pack_finished_error": "Errors occurred whilst writing the pack to disk!",
|
||||
"commands.rainbow.pack_finished_successfully": "Wrote pack to disk",
|
||||
"commands.rainbow.stopped_automatic_mapping": "Stopped automatic mapping of custom items"
|
||||
"commands.rainbow.stopped_automatic_mapping": "Stopped automatic mapping of custom items",
|
||||
"toast.rainbow.io_exception.description": "Please check your game logs for more information",
|
||||
"toast.rainbow.io_exception.title": "A filesystem error occurred in Rainbow!"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "rainbow",
|
||||
"id": "rainbow-client",
|
||||
"version": "${version}",
|
||||
"name": "Rainbow",
|
||||
"description": "Rainbow is a mod to generate Geyser item mappings and bedrock resourcepacks for use with Geyser's custom item API (v2)",
|
||||
@@ -17,11 +17,11 @@
|
||||
"environment": "client",
|
||||
"entrypoints": {
|
||||
"client": [
|
||||
"org.geysermc.rainbow.Rainbow"
|
||||
"org.geysermc.rainbow.client.RainbowClient"
|
||||
]
|
||||
},
|
||||
"mixins": [
|
||||
"rainbow.mixins.json"
|
||||
"rainbow-client.mixins.json"
|
||||
],
|
||||
"depends": {
|
||||
"fabricloader": ">=${loader_version}",
|
||||
@@ -1,19 +1,15 @@
|
||||
{
|
||||
"required": true,
|
||||
"minVersion": "0.8",
|
||||
"package": "org.geysermc.rainbow.mixin",
|
||||
"package": "org.geysermc.rainbow.client.mixin",
|
||||
"compatibilityLevel": "JAVA_21",
|
||||
"mixins": [],
|
||||
"client": [
|
||||
"EntityRenderDispatcherAccessor",
|
||||
"GuiItemRenderStateMixin",
|
||||
"LateBoundIdMapperAccessor",
|
||||
"ModelManagerMixin",
|
||||
"PictureInPictureRendererAccessor",
|
||||
"PictureInPictureRendererMixin",
|
||||
"RangeSelectItemModelAccessor",
|
||||
"SplashRendererAccessor",
|
||||
"TextureSlotsAccessor"
|
||||
"SplashRendererAccessor"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
12
datagen/build.gradle.kts
Normal file
12
datagen/build.gradle.kts
Normal 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")
|
||||
}
|
||||
1
datagen/gradle.properties
Normal file
1
datagen/gradle.properties
Normal file
@@ -0,0 +1 @@
|
||||
archives_base_name=rainbow-datagen
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
33
datagen/src/main/resources/fabric.mod.json
Normal file
33
datagen/src/main/resources/fabric.mod.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
datagen/src/main/resources/rainbow-datagen.accesswidener
Normal file
3
datagen/src/main/resources/rainbow-datagen.accesswidener
Normal 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
|
||||
14
datagen/src/main/resources/rainbow-datagen.mixins.json
Normal file
14
datagen/src/main/resources/rainbow-datagen.mixins.json
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,5 @@
|
||||
org.gradle.jvmargs=-Xmx1G
|
||||
|
||||
# Fabric Properties
|
||||
minecraft_version=1.21.10
|
||||
parchment_version=1.21.10:2025.10.12
|
||||
loader_version=0.17.3
|
||||
|
||||
# Mod Properties
|
||||
mod_version=0.1.0-1.21.10
|
||||
supported_versions=>=1.21.9 <=1.21.10
|
||||
maven_group=org.geysermc
|
||||
archives_base_name=rainbow
|
||||
|
||||
# Dependencies
|
||||
fabric_version=0.135.0+1.21.10
|
||||
mod_version=0.2.0-1.21.10-SNAPSHOT
|
||||
maven_group=org.geysermc.rainbow
|
||||
|
||||
26
gradle/libs.versions.toml
Normal file
26
gradle/libs.versions.toml
Normal 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"}
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
5
gradlew
vendored
5
gradlew
vendored
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -114,7 +114,6 @@ case "$( uname )" in #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
@@ -172,7 +171,6 @@ fi
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
@@ -212,7 +210,6 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
|
||||
3
gradlew.bat
vendored
3
gradlew.bat
vendored
@@ -70,11 +70,10 @@ goto fail
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
||||
4
rainbow/build.gradle.kts
Normal file
4
rainbow/build.gradle.kts
Normal file
@@ -0,0 +1,4 @@
|
||||
plugins {
|
||||
id("rainbow.base-conventions")
|
||||
id("rainbow.publish-conventions")
|
||||
}
|
||||
1
rainbow/gradle.properties
Normal file
1
rainbow/gradle.properties
Normal file
@@ -0,0 +1 @@
|
||||
archives_base_name=rainbow-core
|
||||
28
rainbow/src/main/java/org/geysermc/rainbow/Rainbow.java
Normal file
28
rainbow/src/main/java/org/geysermc/rainbow/Rainbow.java
Normal 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");
|
||||
}
|
||||
}
|
||||
59
rainbow/src/main/java/org/geysermc/rainbow/RainbowIO.java
Normal file
59
rainbow/src/main/java/org/geysermc/rainbow/RainbowIO.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.geysermc.rainbow.mapping.geyser;
|
||||
package org.geysermc.rainbow.definition;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
@@ -8,7 +8,7 @@ import net.minecraft.core.component.DataComponentType;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.geysermc.rainbow.Rainbow;
|
||||
import org.geysermc.rainbow.mapping.geyser.predicate.GeyserPredicate;
|
||||
import org.geysermc.rainbow.definition.predicate.GeyserPredicate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@@ -61,7 +61,7 @@ public record GeyserBaseDefinition(ResourceLocation bedrockIdentifier, Optional<
|
||||
}
|
||||
|
||||
public String textureName() {
|
||||
return bedrockOptions.icon.orElse(Rainbow.fileSafeResourceLocation(bedrockIdentifier));
|
||||
return bedrockOptions.icon.orElse(Rainbow.safeResourceLocation(bedrockIdentifier));
|
||||
}
|
||||
|
||||
public record BedrockOptions(Optional<String> icon, boolean allowOffhand, boolean displayHandheld, int protectionValue, List<ResourceLocation> tags) {
|
||||
@@ -1,11 +1,14 @@
|
||||
package org.geysermc.rainbow.mapping.geyser;
|
||||
package org.geysermc.rainbow.definition;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public record GeyserGroupDefinition(Optional<ResourceLocation> model, List<GeyserMapping> definitions) implements GeyserMapping {
|
||||
@@ -18,7 +21,9 @@ public record GeyserGroupDefinition(Optional<ResourceLocation> model, List<Geyse
|
||||
);
|
||||
|
||||
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) {
|
||||
@@ -53,4 +58,26 @@ public record GeyserGroupDefinition(Optional<ResourceLocation> model, List<Geyse
|
||||
public Type type() {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.geysermc.rainbow.mapping.geyser;
|
||||
package org.geysermc.rainbow.definition;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.geysermc.rainbow.mapping.geyser;
|
||||
package org.geysermc.rainbow.definition;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.DataResult;
|
||||
@@ -6,7 +6,7 @@ import com.mojang.serialization.MapCodec;
|
||||
import net.minecraft.util.StringRepresentable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public interface GeyserMapping {
|
||||
public interface GeyserMapping extends Comparable<GeyserMapping> {
|
||||
|
||||
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
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.geysermc.rainbow.mapping.geyser;
|
||||
package org.geysermc.rainbow.definition;
|
||||
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.common.collect.MultimapBuilder;
|
||||
@@ -11,6 +11,7 @@ import org.geysermc.rainbow.CodecUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -26,7 +27,10 @@ public class GeyserMappings {
|
||||
).apply(instance, (format, mappings) -> new GeyserMappings(mappings))
|
||||
);
|
||||
|
||||
private final Multimap<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() {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.geysermc.rainbow.mapping.geyser;
|
||||
package org.geysermc.rainbow.definition;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.geysermc.rainbow.mapping.geyser.predicate;
|
||||
package org.geysermc.rainbow.definition.predicate;
|
||||
|
||||
import com.google.common.base.Suppliers;
|
||||
import com.mojang.serialization.Codec;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.geysermc.rainbow.mapping.geyser.predicate;
|
||||
package org.geysermc.rainbow.definition.predicate;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.geysermc.rainbow.mapping.geyser.predicate;
|
||||
package org.geysermc.rainbow.definition.predicate;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.geysermc.rainbow.mapping.geyser.predicate;
|
||||
package org.geysermc.rainbow.definition.predicate;
|
||||
|
||||
import com.google.common.base.Suppliers;
|
||||
import com.mojang.serialization.Codec;
|
||||
@@ -24,7 +24,7 @@ public record GeyserRangeDispatchPredicate(Property property, float threshold, f
|
||||
|
||||
@Override
|
||||
public Type type() {
|
||||
return null;
|
||||
return Type.RANGE_DISPATCH;
|
||||
}
|
||||
|
||||
public interface Property {
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.geysermc.rainbow.mapping;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.item.BlockModelWrapper;
|
||||
import net.minecraft.client.renderer.item.ClientItem;
|
||||
import net.minecraft.client.renderer.item.ConditionalItemModel;
|
||||
@@ -9,25 +8,27 @@ import net.minecraft.client.renderer.item.ItemModels;
|
||||
import net.minecraft.client.renderer.item.RangeSelectItemModel;
|
||||
import net.minecraft.client.renderer.item.SelectItemModel;
|
||||
import net.minecraft.client.renderer.item.properties.conditional.Broken;
|
||||
import net.minecraft.client.renderer.item.properties.conditional.ConditionalItemModelProperties;
|
||||
import net.minecraft.client.renderer.item.properties.conditional.ConditionalItemModelProperty;
|
||||
import net.minecraft.client.renderer.item.properties.conditional.CustomModelDataProperty;
|
||||
import net.minecraft.client.renderer.item.properties.conditional.Damaged;
|
||||
import net.minecraft.client.renderer.item.properties.conditional.FishingRodCast;
|
||||
import net.minecraft.client.renderer.item.properties.conditional.HasComponent;
|
||||
import net.minecraft.client.renderer.item.properties.conditional.ItemModelPropertyTest;
|
||||
import net.minecraft.client.renderer.item.properties.numeric.BundleFullness;
|
||||
import net.minecraft.client.renderer.item.properties.numeric.Count;
|
||||
import net.minecraft.client.renderer.item.properties.numeric.Damage;
|
||||
import net.minecraft.client.renderer.item.properties.numeric.RangeSelectItemModelProperties;
|
||||
import net.minecraft.client.renderer.item.properties.numeric.RangeSelectItemModelProperty;
|
||||
import net.minecraft.client.renderer.item.properties.select.Charge;
|
||||
import net.minecraft.client.renderer.item.properties.select.ContextDimension;
|
||||
import net.minecraft.client.renderer.item.properties.select.DisplayContext;
|
||||
import net.minecraft.client.renderer.item.properties.select.SelectItemModelProperties;
|
||||
import net.minecraft.client.renderer.item.properties.select.TrimMaterialProperty;
|
||||
import net.minecraft.client.resources.model.Material;
|
||||
import net.minecraft.client.resources.model.ResolvedModel;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.tags.ItemTags;
|
||||
import net.minecraft.util.ExtraCodecs;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
@@ -38,26 +39,19 @@ import net.minecraft.world.item.component.ItemAttributeModifiers;
|
||||
import net.minecraft.world.item.equipment.trim.TrimMaterial;
|
||||
import net.minecraft.world.level.Level;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.geysermc.rainbow.accessor.ResolvedModelAccessor;
|
||||
import org.geysermc.rainbow.mapping.animation.AnimationMapper;
|
||||
import org.geysermc.rainbow.mapping.animation.BedrockAnimationContext;
|
||||
import org.geysermc.rainbow.mapping.attachable.AttachableMapper;
|
||||
import org.geysermc.rainbow.mapping.geometry.BedrockGeometryContext;
|
||||
import org.geysermc.rainbow.mapping.geometry.GeometryMapper;
|
||||
import org.geysermc.rainbow.mapping.geometry.GeometryRenderer;
|
||||
import org.geysermc.rainbow.mapping.geyser.GeyserBaseDefinition;
|
||||
import org.geysermc.rainbow.mapping.geyser.GeyserItemDefinition;
|
||||
import org.geysermc.rainbow.mapping.geyser.GeyserLegacyDefinition;
|
||||
import org.geysermc.rainbow.mapping.geyser.GeyserSingleDefinition;
|
||||
import org.geysermc.rainbow.mapping.geyser.predicate.GeyserConditionPredicate;
|
||||
import org.geysermc.rainbow.mapping.geyser.predicate.GeyserMatchPredicate;
|
||||
import org.geysermc.rainbow.mapping.geyser.predicate.GeyserPredicate;
|
||||
import org.geysermc.rainbow.mapping.geyser.predicate.GeyserRangeDispatchPredicate;
|
||||
import org.geysermc.rainbow.definition.GeyserBaseDefinition;
|
||||
import org.geysermc.rainbow.definition.GeyserItemDefinition;
|
||||
import org.geysermc.rainbow.definition.GeyserLegacyDefinition;
|
||||
import org.geysermc.rainbow.definition.GeyserSingleDefinition;
|
||||
import org.geysermc.rainbow.definition.predicate.GeyserConditionPredicate;
|
||||
import org.geysermc.rainbow.definition.predicate.GeyserMatchPredicate;
|
||||
import org.geysermc.rainbow.definition.predicate.GeyserPredicate;
|
||||
import org.geysermc.rainbow.definition.predicate.GeyserRangeDispatchPredicate;
|
||||
import org.geysermc.rainbow.mixin.LateBoundIdMapperAccessor;
|
||||
import org.geysermc.rainbow.mixin.RangeSelectItemModelAccessor;
|
||||
import org.geysermc.rainbow.mixin.TextureSlotsAccessor;
|
||||
import org.geysermc.rainbow.pack.BedrockItem;
|
||||
import org.geysermc.rainbow.pack.BedrockTextures;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@@ -65,31 +59,26 @@ import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class BedrockItemMapper {
|
||||
private static final List<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")
|
||||
.map(ResourceLocation::withDefaultNamespace)
|
||||
.toList();
|
||||
|
||||
private static ResolvedModelAccessor getModels() {
|
||||
return (ResolvedModelAccessor) Minecraft.getInstance().getModelManager();
|
||||
}
|
||||
|
||||
private static ResourceLocation getModelId(ItemModel.Unbaked model) {
|
||||
private static <T> ResourceLocation getId(ExtraCodecs.LateBoundIdMapper<ResourceLocation, T> mapper,
|
||||
T type) {
|
||||
//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) {
|
||||
getModels().rainbow$getClientItem(modelLocation).map(ClientItem::model)
|
||||
context.assetResolver().getClientItem(modelLocation).map(ClientItem::model)
|
||||
.ifPresentOrElse(model -> mapItem(model, stack, reporter.forChild(() -> "client item definition " + modelLocation + " "), base -> new GeyserSingleDefinition(base, Optional.of(modelLocation)), context),
|
||||
() -> reporter.report(() -> "missing client item definition " + modelLocation));
|
||||
}
|
||||
|
||||
public static void tryMapStack(ItemStack stack, int customModelData, ProblemReporter reporter, PackContext context) {
|
||||
ItemModel.Unbaked vanillaModel = getModels().rainbow$getClientItem(stack.get(DataComponents.ITEM_MODEL)).map(ClientItem::model).orElseThrow();
|
||||
ProblemReporter childReporter = reporter.forChild(() -> "item model " + vanillaModel + " with custom model data " + customModelData + " ");
|
||||
ResourceLocation itemModel = stack.get(DataComponents.ITEM_MODEL);
|
||||
ItemModel.Unbaked vanillaModel = context.assetResolver().getClientItem(itemModel).map(ClientItem::model).orElseThrow();
|
||||
ProblemReporter childReporter = reporter.forChild(() -> "item model " + itemModel + " with custom model data " + customModelData + " ");
|
||||
if (vanillaModel instanceof RangeSelectItemModel.Unbaked(RangeSelectItemModelProperty property, float scale, List<RangeSelectItemModel.Entry> entries, Optional<ItemModel.Unbaked> fallback)) {
|
||||
// WHY, Mojang?
|
||||
if (property instanceof net.minecraft.client.renderer.item.properties.numeric.CustomModelDataProperty(int index)) {
|
||||
@@ -123,19 +112,15 @@ public class BedrockItemMapper {
|
||||
case ConditionalItemModel.Unbaked conditional -> mapConditionalModel(conditional, context.child("condition model "));
|
||||
case RangeSelectItemModel.Unbaked rangeSelect -> mapRangeSelectModel(rangeSelect, context.child("range select model "));
|
||||
case SelectItemModel.Unbaked select -> mapSelectModel(select, context.child("select model "));
|
||||
default -> context.reporter.report(() -> "unsupported item model " + getModelId(model));
|
||||
default -> context.report("unsupported item model " + getId(ItemModels.ID_MAPPER, model.type()));
|
||||
}
|
||||
}
|
||||
|
||||
private static void mapBlockModelWrapper(BlockModelWrapper.Unbaked model, MappingContext context) {
|
||||
ResourceLocation itemModelLocation = model.model();
|
||||
|
||||
getModels().rainbow$getResolvedModel(itemModelLocation)
|
||||
context.packContext().assetResolver().getResolvedModel(itemModelLocation)
|
||||
.ifPresentOrElse(itemModel -> {
|
||||
ResolvedModel parentModel = itemModel.parent();
|
||||
// debugName() returns the resource location of the model as a string
|
||||
boolean handheld = parentModel != null && HANDHELD_MODELS.contains(ResourceLocation.parse(parentModel.debugName()));
|
||||
|
||||
ResourceLocation bedrockIdentifier;
|
||||
if (itemModelLocation.getNamespace().equals(ResourceLocation.DEFAULT_NAMESPACE)) {
|
||||
bedrockIdentifier = ResourceLocation.fromNamespaceAndPath("geyser_mc", itemModelLocation.getPath());
|
||||
@@ -143,33 +128,17 @@ public class BedrockItemMapper {
|
||||
bedrockIdentifier = itemModelLocation;
|
||||
}
|
||||
|
||||
Material layer0Texture = itemModel.getTopTextureSlots().getMaterial("layer0");
|
||||
Optional<ResourceLocation> texture;
|
||||
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 -> {
|
||||
BedrockGeometryContext geometry = BedrockGeometryContext.create(bedrockIdentifier, context.stack, itemModel, context.packContext);
|
||||
if (context.packContext.reportSuccesses()) {
|
||||
// Not a problem, but just report to get the model printed in the report file
|
||||
context.reporter.report(() -> "creating mapping for block model " + itemModelLocation);
|
||||
context.create(bedrockIdentifier, itemTexture, handheld, customGeometry);
|
||||
}, () -> context.reporter.report(() -> "not mapping block model " + itemModelLocation + " because it has no texture"));
|
||||
}, () -> context.reporter.report(() -> "missing block model " + itemModelLocation));
|
||||
context.report("creating mapping for block model " + itemModelLocation);
|
||||
}
|
||||
context.create(bedrockIdentifier, geometry);
|
||||
}, () -> context.report("missing block model " + itemModelLocation));
|
||||
}
|
||||
|
||||
private static void mapConditionalModel(ConditionalItemModel.Unbaked model, MappingContext context) {
|
||||
ItemModelPropertyTest property = model.property();
|
||||
ConditionalItemModelProperty property = model.property();
|
||||
GeyserConditionPredicate.Property predicateProperty = switch (property) {
|
||||
case Broken ignored -> GeyserConditionPredicate.BROKEN;
|
||||
case Damaged ignored -> GeyserConditionPredicate.DAMAGED;
|
||||
@@ -182,7 +151,7 @@ public class BedrockItemMapper {
|
||||
ItemModel.Unbaked onFalse = model.onFalse();
|
||||
|
||||
if (predicateProperty == null) {
|
||||
context.reporter.report(() -> "unsupported conditional model property " + property + ", only mapping on_false");
|
||||
context.report("unsupported conditional model property " + getId(ConditionalItemModelProperties.ID_MAPPER, property.type()) + ", only mapping on_false");
|
||||
mapItem(onFalse, context.child("condition on_false (unsupported property)"));
|
||||
return;
|
||||
}
|
||||
@@ -203,7 +172,7 @@ public class BedrockItemMapper {
|
||||
};
|
||||
|
||||
if (predicateProperty == null) {
|
||||
context.reporter.report(() -> "unsupported range dispatch model property " + property + ", only mapping fallback, if it is present");
|
||||
context.report("unsupported range dispatch model property " + getId(RangeSelectItemModelProperties.ID_MAPPER, property.type()) + ", only mapping fallback, if it is present");
|
||||
} else {
|
||||
for (RangeSelectItemModel.Entry entry : model.entries()) {
|
||||
mapItem(entry.model(), context.with(new GeyserRangeDispatchPredicate(predicateProperty, entry.threshold(), model.scale()), "threshold " + entry.threshold()));
|
||||
@@ -229,7 +198,7 @@ public class BedrockItemMapper {
|
||||
|
||||
if (dataConstructor == null) {
|
||||
if (unbakedSwitch.property() instanceof DisplayContext) {
|
||||
context.reporter.report(() -> "unsupported select model property display_context, only mapping \"gui\" case, if it exists");
|
||||
context.report("unsupported select model property display_context, only mapping \"gui\" case, if it exists");
|
||||
for (SelectItemModel.SwitchCase<?> switchCase : cases) {
|
||||
if (switchCase.values().contains(ItemDisplayContext.GUI)) {
|
||||
mapItem(switchCase.model(), context.child("select GUI display_context case (unsupported property) "));
|
||||
@@ -237,7 +206,7 @@ public class BedrockItemMapper {
|
||||
}
|
||||
}
|
||||
}
|
||||
context.reporter.report(() -> "unsupported select model property " + unbakedSwitch.property() + ", only mapping fallback, if present");
|
||||
context.report("unsupported select model property " + getId(SelectItemModelProperties.ID_MAPPER, unbakedSwitch.property().type()) + ", only mapping fallback, if present");
|
||||
model.fallback().ifPresent(fallback -> mapItem(fallback, context.child("select fallback case (unsupported property) ")));
|
||||
return;
|
||||
}
|
||||
@@ -261,17 +230,11 @@ public class BedrockItemMapper {
|
||||
return new MappingContext(predicateStack, stack, reporter.forChild(() -> childName), definitionCreator, packContext);
|
||||
}
|
||||
|
||||
public void create(ResourceLocation bedrockIdentifier, ResourceLocation texture, boolean displayHandheld,
|
||||
Optional<ResolvedModel> customModel) {
|
||||
List<ResourceLocation> tags;
|
||||
if (stack.is(ItemTags.TRIMMABLE_ARMOR)) {
|
||||
tags = TRIMMABLE_ARMOR_TAGS;
|
||||
} else {
|
||||
tags = List.of();
|
||||
}
|
||||
public void create(ResourceLocation bedrockIdentifier, BedrockGeometryContext geometry) {
|
||||
List<ResourceLocation> tags = stack.is(ItemTags.TRIMMABLE_ARMOR) ? TRIMMABLE_ARMOR_TAGS : List.of();
|
||||
|
||||
GeyserBaseDefinition base = new GeyserBaseDefinition(bedrockIdentifier, Optional.of(stack.getHoverName().getString()), predicateStack,
|
||||
new GeyserBaseDefinition.BedrockOptions(Optional.empty(), true, displayHandheld, calculateProtectionValue(stack), tags),
|
||||
GeyserBaseDefinition base = new GeyserBaseDefinition(bedrockIdentifier, Optional.ofNullable(stack.getHoverName().tryCollapseToString()), predicateStack,
|
||||
new GeyserBaseDefinition.BedrockOptions(Optional.empty(), true, geometry.handheld(), calculateProtectionValue(stack), tags),
|
||||
stack.getComponentsPatch());
|
||||
try {
|
||||
packContext.mappings().map(stack.getItemHolder(), definitionCreator.apply(base));
|
||||
@@ -280,24 +243,12 @@ public class BedrockItemMapper {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO Should probably get a better way to get geometry texture
|
||||
String safeIdentifier = base.textureName();
|
||||
String bone = "bone";
|
||||
ResourceLocation geometryTexture = texture;
|
||||
Optional<BedrockGeometryContext> bedrockGeometry = customModel.map(model -> GeometryMapper.mapGeometry(safeIdentifier, bone, model, geometryTexture));
|
||||
Optional<BedrockAnimationContext> bedrockAnimation = customModel.map(model -> AnimationMapper.mapAnimation(safeIdentifier, bone, model.getTopTransforms()));
|
||||
packContext.itemConsumer().accept(new BedrockItem(bedrockIdentifier, base.textureName(), geometry,
|
||||
AttachableMapper.mapItem(packContext.assetResolver(), geometry, stack.getComponentsPatch())));
|
||||
}
|
||||
|
||||
boolean exportTexture = true;
|
||||
if (customModel.isPresent()) {
|
||||
texture = texture.withPath(path -> path + "_icon");
|
||||
GeometryRenderer.render(stack, packContext.packPath().resolve(BedrockTextures.TEXTURES_FOLDER + texture.getPath() + ".png"));
|
||||
exportTexture = false;
|
||||
packContext.additionalTextureConsumer().accept(geometryTexture);
|
||||
}
|
||||
|
||||
packContext.itemConsumer().accept(new BedrockItem(bedrockIdentifier, base.textureName(), texture, exportTexture,
|
||||
AttachableMapper.mapItem(stack.getComponentsPatch(), bedrockIdentifier, bedrockGeometry, bedrockAnimation, packContext.additionalTextureConsumer()),
|
||||
bedrockGeometry.map(BedrockGeometryContext::geometry), bedrockAnimation.map(BedrockAnimationContext::animation)));
|
||||
public void report(String problem) {
|
||||
reporter.report(() -> problem);
|
||||
}
|
||||
|
||||
private static int calculateProtectionValue(ItemStack stack) {
|
||||
@@ -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) {}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -6,15 +6,12 @@ import org.geysermc.rainbow.pack.animation.BedrockAnimation;
|
||||
import org.joml.Vector3f;
|
||||
import org.joml.Vector3fc;
|
||||
|
||||
// TODO these offset values are completely wrong, I think
|
||||
// TODO these offset values are still not entirely right, I think
|
||||
public class AnimationMapper {
|
||||
// These aren't perfect... but I spent over 1.5 hours trying to get these. It's good enough for me.
|
||||
private static final Vector3fc FIRST_PERSON_POSITION_OFFSET = new Vector3f(-7.0F, 22.5F, -7.0F);
|
||||
private static final Vector3fc FIRST_PERSON_ROTATION_OFFSET = new Vector3f(-22.5F, 50.0F, -32.5F);
|
||||
|
||||
private static final Vector3fc THIRD_PERSON_POSITION_OFFSET = new Vector3f(0.0F, 13.0F, -3.0F);
|
||||
private static final Vector3fc THIRD_PERSON_ROTATION_OFFSET = new Vector3f(90.0F, -90.0F, 0.0F);
|
||||
|
||||
// These transformations perfect... but I spent over 3 hours trying to get these. It's good enough for me.
|
||||
public static BedrockAnimationContext mapAnimation(String identifier, String bone, ItemTransforms transforms) {
|
||||
// I don't think it's possible to display separate animations for left- and right hands
|
||||
ItemTransform firstPerson = transforms.firstPersonRightHand();
|
||||
@@ -23,8 +20,10 @@ public class AnimationMapper {
|
||||
Vector3f firstPersonScale = new Vector3f(firstPerson.scale());
|
||||
|
||||
ItemTransform thirdPerson = transforms.thirdPersonRightHand();
|
||||
Vector3f thirdPersonPosition = THIRD_PERSON_POSITION_OFFSET.add(thirdPerson.translation(), new Vector3f());
|
||||
Vector3f thirdPersonRotation = THIRD_PERSON_ROTATION_OFFSET.add(-thirdPerson.rotation().x(), thirdPerson.rotation().y(), thirdPerson.rotation().z(), new Vector3f());
|
||||
// Translation Y/Z axes are swapped on bedrock, bedrock displays the model lower than Java does, and the X/Y axes (Java) is inverted on bedrock
|
||||
Vector3f thirdPersonPosition = new Vector3f(-thirdPerson.translation().x(), 10.0F + thirdPerson.translation().z(), -thirdPerson.translation().y());
|
||||
// Rotation X/Y axes are inverted on bedrock, bedrock needs a +90-degree rotation on the X axis, and I couldn't figure out how the Z axis works
|
||||
Vector3f thirdPersonRotation = new Vector3f(-thirdPerson.rotation().x() + 90.0F, -thirdPerson.rotation().y(), 0.0F);
|
||||
Vector3f thirdPersonScale = new Vector3f(thirdPerson.scale());
|
||||
|
||||
return new BedrockAnimationContext(BedrockAnimation.builder()
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.geysermc.rainbow.mapping.attachable;
|
||||
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.resources.model.EquipmentAssetManager;
|
||||
import net.minecraft.client.resources.model.EquipmentClientInfo;
|
||||
import net.minecraft.core.component.DataComponentPatch;
|
||||
@@ -9,9 +8,10 @@ import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.EquipmentSlot;
|
||||
import net.minecraft.world.item.equipment.Equippable;
|
||||
import org.geysermc.rainbow.mapping.animation.BedrockAnimationContext;
|
||||
import org.geysermc.rainbow.mapping.AssetResolver;
|
||||
import org.geysermc.rainbow.mapping.geometry.BedrockGeometryContext;
|
||||
import org.geysermc.rainbow.mixin.EntityRenderDispatcherAccessor;
|
||||
import org.geysermc.rainbow.mapping.geometry.StitchedGeometry;
|
||||
import org.geysermc.rainbow.mapping.geometry.TextureHolder;
|
||||
import org.geysermc.rainbow.pack.attachable.BedrockAttachable;
|
||||
|
||||
import java.util.List;
|
||||
@@ -20,31 +20,27 @@ import java.util.function.Consumer;
|
||||
|
||||
public class AttachableMapper {
|
||||
|
||||
public static Optional<BedrockAttachable> mapItem(DataComponentPatch components, ResourceLocation bedrockIdentifier, Optional<BedrockGeometryContext> customGeometry,
|
||||
Optional<BedrockAnimationContext> customAnimation, Consumer<ResourceLocation> textureConsumer) {
|
||||
public static AttachableCreator mapItem(AssetResolver assetResolver, BedrockGeometryContext geometryContext, DataComponentPatch components) {
|
||||
// Crazy optional statement
|
||||
// Unfortunately we can't have both equippables and custom models, so we prefer the latter :(
|
||||
return customGeometry
|
||||
.map(geometry -> BedrockAttachable.geometry(bedrockIdentifier, geometry.geometry().definitions().getFirst(), geometry.texture().getPath()))
|
||||
return (bedrockIdentifier, stitchedGeometry, textureConsumer) -> stitchedGeometry
|
||||
.map(stitched -> BedrockAttachable.geometry(bedrockIdentifier, stitched.geometry().definitions().getFirst(), stitched.stitchedTextures().location().getPath()))
|
||||
.or(() -> Optional.ofNullable(components.get(DataComponents.EQUIPPABLE))
|
||||
.flatMap(optional -> (Optional<Equippable>) optional)
|
||||
.flatMap(equippable -> {
|
||||
EquipmentAssetManager equipmentAssets = ((EntityRenderDispatcherAccessor) Minecraft.getInstance().getEntityRenderDispatcher()).getEquipmentAssets();
|
||||
return equippable.assetId().map(asset -> Pair.of(equippable.slot(), equipmentAssets.get(asset)));
|
||||
})
|
||||
.flatMap(equippable -> equippable.assetId().flatMap(assetResolver::getEquipmentInfo).map(info -> Pair.of(equippable.slot(), info)))
|
||||
.filter(assetInfo -> assetInfo.getSecond() != EquipmentAssetManager.MISSING)
|
||||
.map(assetInfo -> assetInfo
|
||||
.mapSecond(info -> info.getLayers(getLayer(assetInfo.getFirst()))))
|
||||
.filter(assetInfo -> !assetInfo.getSecond().isEmpty())
|
||||
.map(assetInfo -> {
|
||||
ResourceLocation texture = getTexture(assetInfo.getSecond(), getLayer(assetInfo.getFirst()));
|
||||
textureConsumer.accept(texture);
|
||||
return BedrockAttachable.equipment(bedrockIdentifier, assetInfo.getFirst(), texture.getPath());
|
||||
ResourceLocation equipmentTexture = getTexture(assetInfo.getSecond(), getLayer(assetInfo.getFirst()));
|
||||
textureConsumer.accept(TextureHolder.createFromResources(equipmentTexture));
|
||||
return BedrockAttachable.equipment(bedrockIdentifier, assetInfo.getFirst(), equipmentTexture.getPath());
|
||||
}))
|
||||
.map(attachable -> {
|
||||
customAnimation.ifPresent(context -> {
|
||||
attachable.withAnimation("first_person", context.firstPerson());
|
||||
attachable.withAnimation("third_person", context.thirdPerson());
|
||||
geometryContext.animation().ifPresent(animation -> {
|
||||
attachable.withAnimation("first_person", animation.firstPerson());
|
||||
attachable.withAnimation("third_person", animation.thirdPerson());
|
||||
attachable.withScript("animate", "first_person", "context.is_first_person == 1.0");
|
||||
attachable.withScript("animate", "third_person", "context.is_first_person == 0.0");
|
||||
});
|
||||
@@ -59,4 +55,10 @@ public class AttachableMapper {
|
||||
private static ResourceLocation getTexture(List<EquipmentClientInfo.Layer> info, EquipmentClientInfo.LayerType layer) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,9 @@ import net.minecraft.client.renderer.block.model.BlockElementFace;
|
||||
import net.minecraft.client.renderer.block.model.BlockElementRotation;
|
||||
import net.minecraft.client.renderer.block.model.SimpleUnbakedGeometry;
|
||||
import net.minecraft.client.resources.model.ResolvedModel;
|
||||
import net.minecraft.client.resources.model.UnbakedGeometry;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.geysermc.rainbow.mixin.FaceBakeryAccessor;
|
||||
import org.geysermc.rainbow.pack.geometry.BedrockGeometry;
|
||||
import org.joml.Vector2f;
|
||||
import org.joml.Vector3f;
|
||||
@@ -17,26 +18,30 @@ import java.util.Map;
|
||||
public class GeometryMapper {
|
||||
private static final Vector3fc CENTRE_OFFSET = new Vector3f(8.0F, 0.0F, 8.0F);
|
||||
|
||||
public static BedrockGeometryContext mapGeometry(String identifier, String boneName, ResolvedModel model, ResourceLocation texture) {
|
||||
public static BedrockGeometry mapGeometry(String identifier, String boneName, ResolvedModel model, StitchedTextures textures) {
|
||||
UnbakedGeometry top = model.getTopGeometry();
|
||||
if (top == UnbakedGeometry.EMPTY) {
|
||||
return BedrockGeometry.EMPTY;
|
||||
}
|
||||
|
||||
BedrockGeometry.Builder builder = BedrockGeometry.builder(identifier);
|
||||
// Blockbench seems to always use these values TODO that's wrong
|
||||
builder.withVisibleBoundsWidth(4.0F);
|
||||
builder.withVisibleBoundsHeight(4.0F);
|
||||
builder.withVisibleBoundsOffset(new Vector3f(0.0F, 0.75F, 0.0F));
|
||||
|
||||
// TODO proper texture size
|
||||
builder.withTextureWidth(16);
|
||||
builder.withTextureHeight(16);
|
||||
builder.withTextureWidth(textures.width());
|
||||
builder.withTextureHeight(textures.height());
|
||||
|
||||
BedrockGeometry.Bone.Builder bone = BedrockGeometry.bone(boneName);
|
||||
|
||||
Vector3f min = new Vector3f(Float.MAX_VALUE);
|
||||
Vector3f max = new Vector3f(Float.MIN_VALUE);
|
||||
|
||||
SimpleUnbakedGeometry geometry = (SimpleUnbakedGeometry) model.getTopGeometry();
|
||||
SimpleUnbakedGeometry geometry = (SimpleUnbakedGeometry) top;
|
||||
for (BlockElement element : geometry.elements()) {
|
||||
// TODO the origin here is wrong, some models seem to be mirrored weirdly in blockbench
|
||||
BedrockGeometry.Cube cube = mapBlockElement(element).build();
|
||||
BedrockGeometry.Cube cube = mapBlockElement(element, textures).build();
|
||||
bone.withCube(cube);
|
||||
min.min(cube.origin());
|
||||
max.max(cube.origin().add(cube.size(), new Vector3f()));
|
||||
@@ -48,35 +53,43 @@ public class GeometryMapper {
|
||||
|
||||
// Bind to the bone of the current item slot
|
||||
bone.withBinding("q.item_slot_to_bone_name(context.item_slot)");
|
||||
return new BedrockGeometryContext(builder.withBone(bone).build(), texture);
|
||||
return builder.withBone(bone).build();
|
||||
}
|
||||
|
||||
private static BedrockGeometry.Cube.Builder mapBlockElement(BlockElement element) {
|
||||
private static BedrockGeometry.Cube.Builder mapBlockElement(BlockElement element, StitchedTextures textures) {
|
||||
// The centre of the model is back by 8 in the X and Z direction on Java, so move the origin of the cube and the pivot like that
|
||||
BedrockGeometry.Cube.Builder builder = BedrockGeometry.cube(element.from().sub(CENTRE_OFFSET, new Vector3f()), element.to().sub(element.from(), new Vector3f()));
|
||||
|
||||
for (Map.Entry<Direction, BlockElementFace> faceEntry : element.faces().entrySet()) {
|
||||
// TODO texture key
|
||||
Direction direction = faceEntry.getKey();
|
||||
BlockElementFace face = faceEntry.getValue();
|
||||
|
||||
Vector2f uvOrigin;
|
||||
Vector2f uvSize;
|
||||
BlockElementFace.UVs uvs = face.uvs();
|
||||
if (uvs != null) {
|
||||
// Up and down faces are special
|
||||
if (direction.getAxis() == Direction.Axis.Y) {
|
||||
uvOrigin = new Vector2f(uvs.maxU(), uvs.maxV());
|
||||
uvSize = new Vector2f(uvs.minU() - uvs.maxU(), uvs.minV() - uvs.maxV());
|
||||
} else {
|
||||
uvOrigin = new Vector2f(uvs.minU(), uvs.minV());
|
||||
uvSize = new Vector2f(uvs.maxU() - uvs.minU(), uvs.maxV() - uvs.minV());
|
||||
}
|
||||
} else {
|
||||
uvOrigin = new Vector2f();
|
||||
uvSize = new Vector2f();
|
||||
if (uvs == null) {
|
||||
// Java defaults to a set of UV values determined by the position of the face if no UV values were specified
|
||||
uvs = FaceBakeryAccessor.invokeDefaultFaceUV(element.from(), element.to(), direction);
|
||||
}
|
||||
|
||||
// Up and down faces are special and have their UVs flipped
|
||||
if (direction.getAxis() == Direction.Axis.Y) {
|
||||
uvOrigin = new Vector2f(uvs.maxU(), uvs.maxV());
|
||||
uvSize = new Vector2f(uvs.minU() - uvs.maxU(), uvs.minV() - uvs.maxV());
|
||||
} else {
|
||||
uvOrigin = new Vector2f(uvs.minU(), uvs.minV());
|
||||
uvSize = new Vector2f(uvs.maxU() - uvs.minU(), uvs.maxV() - uvs.minV());
|
||||
}
|
||||
|
||||
// If the texture was stitched (which it should have been, unless it doesn't exist), s UV values on Java are always in the [0;16] range, adjust the values properly to the texture size,
|
||||
// and offset the UVs by the texture's starting UV
|
||||
textures.getSprite(face.texture()).ifPresent(sprite -> {
|
||||
float widthMultiplier = sprite.contents().width() / 16.0F;
|
||||
float heightMultiplier = sprite.contents().height() / 16.0F;
|
||||
uvOrigin.mul(widthMultiplier, heightMultiplier);
|
||||
uvSize.mul(widthMultiplier, heightMultiplier);
|
||||
uvOrigin.add(sprite.getX(), sprite.getY());
|
||||
});
|
||||
builder.withFace(direction, uvOrigin, uvSize, face.rotation());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.geysermc.rainbow.mapping.geometry;
|
||||
|
||||
import org.geysermc.rainbow.pack.geometry.BedrockGeometry;
|
||||
|
||||
public record StitchedGeometry(BedrockGeometry geometry, TextureHolder stitchedTextures) {}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import net.minecraft.client.renderer.block.model.TextureSlots;
|
||||
import net.minecraft.client.resources.model.Material;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
import org.spongepowered.asm.mixin.gen.Invoker;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@@ -12,4 +13,9 @@ public interface TextureSlotsAccessor {
|
||||
|
||||
@Accessor
|
||||
Map<String, Material> getResolvedValues();
|
||||
|
||||
@Invoker
|
||||
static boolean invokeIsTextureReference(String name) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
297
rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockPack.java
Normal file
297
rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockPack.java
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ public record BedrockTextures(Map<String, String> textures) {
|
||||
private final Map<String, String> textures = new HashMap<>();
|
||||
|
||||
public Builder withItemTexture(BedrockItem item) {
|
||||
return withTexture(item.textureName(), TEXTURES_FOLDER + item.texture().getPath());
|
||||
return withTexture(item.textureName(), TEXTURES_FOLDER + item.geometryContext().icon().location().getPath());
|
||||
}
|
||||
|
||||
public Builder withTexture(String name, String texture) {
|
||||
@@ -5,6 +5,7 @@ import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import net.minecraft.core.UUIDUtil;
|
||||
import org.geysermc.rainbow.CodecUtil;
|
||||
import org.geysermc.rainbow.PackConstants;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
@@ -56,5 +57,10 @@ public record PackManifest(Header header, List<Module> modules) {
|
||||
return new Module(name, description, uuid, version.increment());
|
||||
}
|
||||
}
|
||||
|
||||
public static PackManifest create(String name, String description, UUID uuid, BedrockVersion version) {
|
||||
return new PackManifest(new PackManifest.Header(name, description, uuid, version, PackConstants.ENGINE_VERSION),
|
||||
List.of(new PackManifest.Module(name, description, uuid, version)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -5,14 +5,15 @@ import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.DataResult;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import org.geysermc.rainbow.CodecUtil;
|
||||
import org.geysermc.rainbow.mapping.PackSerializer;
|
||||
import org.geysermc.rainbow.pack.BedrockVersion;
|
||||
import org.joml.Vector3fc;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public record BedrockAnimation(BedrockVersion formatVersion, Map<String, AnimationDefinition> definitions) {
|
||||
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)
|
||||
);
|
||||
|
||||
public void save(Path animationDirectory, String identifier) throws IOException {
|
||||
CodecUtil.trySaveJson(CODEC, this, animationDirectory.resolve(identifier + ".animation.json"));
|
||||
public CompletableFuture<?> save(PackSerializer serializer, Path animationDirectory, String identifier) {
|
||||
return serializer.saveJson(CODEC, this, animationDirectory.resolve(identifier + ".animation.json"));
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
@@ -9,15 +9,14 @@ import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.ExtraCodecs;
|
||||
import net.minecraft.util.StringRepresentable;
|
||||
import net.minecraft.world.entity.EquipmentSlot;
|
||||
import org.geysermc.rainbow.CodecUtil;
|
||||
import org.geysermc.rainbow.PackConstants;
|
||||
import org.geysermc.rainbow.Rainbow;
|
||||
import org.geysermc.rainbow.mapping.PackSerializer;
|
||||
import org.geysermc.rainbow.pack.BedrockTextures;
|
||||
import org.geysermc.rainbow.pack.BedrockVersion;
|
||||
import org.geysermc.rainbow.pack.geometry.BedrockGeometry;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
@@ -25,6 +24,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public record BedrockAttachable(BedrockVersion formatVersion, AttachableInfo info) {
|
||||
@@ -35,9 +35,9 @@ public record BedrockAttachable(BedrockVersion formatVersion, AttachableInfo inf
|
||||
).apply(instance, BedrockAttachable::new)
|
||||
);
|
||||
|
||||
public void save(Path attachablesDirectory) throws IOException {
|
||||
public CompletableFuture<?> save(PackSerializer serializer, Path attachablesDirectory) {
|
||||
// Get a safe attachable path by using Geyser's way of getting icons
|
||||
CodecUtil.trySaveJson(CODEC, this, attachablesDirectory.resolve(Rainbow.fileSafeResourceLocation(info.identifier) + ".json"));
|
||||
return serializer.saveJson(CODEC, this, attachablesDirectory.resolve(Rainbow.safeResourceLocation(info.identifier) + ".json"));
|
||||
}
|
||||
|
||||
public static Builder builder(ResourceLocation identifier) {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user