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

Create build-logic, main rainbow and client module

This commit is contained in:
Eclipse
2025-10-14 08:03:42 +00:00
parent b9ea28ee8f
commit 16db0fe788
73 changed files with 44 additions and 2 deletions

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

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

View File

@@ -0,0 +1,113 @@
package org.geysermc.rainbow;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.mojang.serialization.DynamicOps;
import com.mojang.serialization.JsonOps;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.util.ExtraCodecs;
import org.joml.Vector2f;
import org.joml.Vector2fc;
import org.joml.Vector3f;
import org.joml.Vector3fc;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Stream;
public class CodecUtil {
// It's fine to cast to mutable here since codecs won't change the data
public static final Codec<Vector2fc> VECTOR2F_CODEC = ExtraCodecs.VECTOR2F.xmap(vector -> vector, vector -> (Vector2f) vector);
public static final Codec<Vector3fc> VECTOR3F_CODEC = ExtraCodecs.VECTOR3F.xmap(vector -> vector, vector -> (Vector3f) vector);
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
public static <O, T> RecordCodecBuilder<O, T> unitVerifyCodec(Codec<T> codec, String field, T value) {
return codec.validate(read -> {
if (!read.equals(value)) {
return DataResult.error(() -> field + " must equal " + value + ", was " + read);
}
return DataResult.success(read);
}).fieldOf(field).forGetter(object -> value);
}
public static <T> T readOrCompute(Codec<T> codec, Path path, Supplier<T> supplier) throws IOException {
if (Files.exists(path)) {
return tryReadJson(codec, path);
}
return supplier.get();
}
public static <T> T tryReadJson(Codec<T> codec, Path path) throws IOException {
return tryReadJson(codec, path, JsonOps.INSTANCE);
}
public static <T> T tryReadJson(Codec<T> codec, Path path, DynamicOps<JsonElement> ops) throws IOException {
try {
String raw = Files.readString(path);
JsonElement json = GSON.fromJson(raw, JsonElement.class);
return codec.parse(ops, json).getOrThrow();
} catch (IOException exception) {
Rainbow.LOGGER.warn("Failed to read JSON file {}!", path, exception);
throw exception;
}
}
public static <T> void trySaveJson(Codec<T> codec, T object, Path path) throws IOException {
trySaveJson(codec, object, path, JsonOps.INSTANCE);
}
public static <T> void trySaveJson(Codec<T> codec, T object, Path path, DynamicOps<JsonElement> ops) throws IOException {
JsonElement json = codec.encodeStart(ops, object).getOrThrow();
try {
ensureDirectoryExists(path.getParent());
Files.writeString(path, GSON.toJson(json));
} catch (IOException exception) {
Rainbow.LOGGER.warn("Failed to write file {}!", path, exception);
throw exception;
}
}
public static void tryZipDirectory(Path directory, Path output) throws IOException {
try (FileSystem zip = FileSystems.newFileSystem(output, Map.of("create", "true"))) {
try (Stream<Path> paths = Files.walk(directory)) {
paths.forEach(path -> {
try {
Path inZip = zip.getPath(String.valueOf(directory.relativize(path)));
if (Files.isDirectory(path)) {
Files.createDirectories(inZip);
} else {
Files.copy(path, inZip, StandardCopyOption.REPLACE_EXISTING);
}
} catch (IOException exception) {
Rainbow.LOGGER.warn("Failed to copy contents from {} to ZIP-file {}!", path, output, exception);
}
});
}
} catch (IOException exception) {
Rainbow.LOGGER.warn("Failed to write ZIP-file {}!", output, exception);
throw exception;
}
}
public static void ensureDirectoryExists(Path directory) throws IOException {
if (!Files.isDirectory(directory)) {
try {
Files.createDirectories(directory);
} catch (IOException exception) {
Rainbow.LOGGER.warn("Failed to create directory!", exception);
throw exception;
}
}
}
}

View File

@@ -0,0 +1,16 @@
package org.geysermc.rainbow;
import net.kyori.adventure.key.Key;
import net.minecraft.resources.ResourceLocation;
@SuppressWarnings("PatternValidation")
public interface KeyUtil {
static Key resourceLocationToKey(ResourceLocation location) {
return Key.key(location.getNamespace(), location.getPath());
}
static ResourceLocation keyToResourceLocation(Key key) {
return ResourceLocation.fromNamespaceAndPath(key.namespace(), key.value());
}
}

View File

@@ -0,0 +1,8 @@
package org.geysermc.rainbow;
import org.geysermc.rainbow.pack.BedrockVersion;
public class PackConstants {
public static final String DEFAULT_PACK_DESCRIPTION = "A resourcepack generated by " + Rainbow.MOD_NAME;
public static final BedrockVersion ENGINE_VERSION = BedrockVersion.of(1, 21, 0);
}

View File

@@ -0,0 +1,40 @@
package org.geysermc.rainbow;
import org.geysermc.rainbow.pack.BedrockPack;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
public final class PackManager {
private Optional<BedrockPack> currentPack = Optional.empty();
public void startPack(String name) throws IOException {
if (currentPack.isPresent()) {
throw new IllegalStateException("Already started a pack (" + currentPack.get().name() + ")");
}
currentPack = Optional.of(new BedrockPack(name));
}
public void run(Consumer<BedrockPack> consumer) {
currentPack.ifPresent(consumer);
}
public void runOrElse(Consumer<BedrockPack> consumer, Runnable runnable) {
currentPack.ifPresentOrElse(consumer, runnable);
}
public Optional<Path> getExportPath() {
return currentPack.map(BedrockPack::getExportPath);
}
public Optional<Boolean> finish() {
Optional<Boolean> success = currentPack.map(BedrockPack::save);
currentPack = Optional.empty();
return success;
}
}

View File

@@ -0,0 +1,116 @@
package org.geysermc.rainbow;
import com.mojang.logging.LogUtils;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
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.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.Minecraft;
import net.minecraft.commands.synchronization.SingletonArgumentInfo;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import org.geysermc.pack.bedrock.resource.BedrockResourcePack;
import org.geysermc.pack.converter.PackConversionContext;
import org.geysermc.pack.converter.PackConverter;
import org.geysermc.pack.converter.converter.ActionListener;
import org.geysermc.pack.converter.converter.Converter;
import org.geysermc.pack.converter.converter.base.PackManifestConverter;
import org.geysermc.pack.converter.converter.lang.LangConverter;
import org.geysermc.pack.converter.converter.misc.SplashTextConverter;
import org.geysermc.pack.converter.converter.model.ModelConverter;
import org.geysermc.pack.converter.data.ConversionData;
import org.geysermc.pack.converter.util.DefaultLogListener;
import org.geysermc.pack.converter.util.LogListener;
import org.geysermc.rainbow.command.CommandSuggestionsArgumentType;
import org.geysermc.rainbow.command.PackGeneratorCommand;
import org.geysermc.rainbow.creative.MinecraftCreativeResourcePack;
import org.geysermc.rainbow.mapper.PackMapper;
import org.slf4j.Logger;
import javax.imageio.ImageIO;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
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();
private final PackManager packManager = new PackManager();
private final PackMapper packMapper = new PackMapper(packManager);
// TODO export language overrides
@Override
public void onInitializeClient() {
ClientCommandRegistrationCallback.EVENT.register((dispatcher, buildContext) -> PackGeneratorCommand.register(dispatcher, packManager, packMapper));
ClientTickEvents.START_CLIENT_TICK.register(packMapper::tick);
ArgumentTypeRegistry.registerArgumentType(getModdedLocation("command_suggestions"),
CommandSuggestionsArgumentType.class, SingletonArgumentInfo.contextFree(CommandSuggestionsArgumentType::new));
ClientCommandRegistrationCallback.EVENT.register((dispatcher, buildContext) -> {
dispatcher.register(ClientCommandManager.literal("debugtest")
.executes(context -> {
PackConverter packConverter = new PackConverter()
.packName("RAINBOW-TEST")
.output(FabricLoader.getInstance().getGameDir().resolve("pack-convert-out"));
Path tmpDir = FabricLoader.getInstance().getGameDir().resolve("pack-convert-temp");
ImageIO.scanForPlugins();
MinecraftCreativeResourcePack resourcePack = new MinecraftCreativeResourcePack(Minecraft.getInstance().getResourceManager());
BedrockResourcePack bedrockResourcePack = new BedrockResourcePack(tmpDir);
LogListener logListener = new DefaultLogListener();
final Converter.ConversionDataCreationContext conversionDataCreationContext = new Converter.ConversionDataCreationContext(
packConverter, logListener, null, tmpDir, resourcePack, resourcePack
);
List<Converter<?>> converters = new ArrayList<>();
converters.add(new PackManifestConverter());
converters.add(new LangConverter());
converters.add(new ModelConverter());
int errors = 0;
for (Converter converter : converters) {
ConversionData data = converter.createConversionData(conversionDataCreationContext);
PackConversionContext<?> conversionContext = new PackConversionContext<>(data, packConverter, resourcePack, bedrockResourcePack, logListener);
List<ActionListener<?>> actionListeners = List.of();
try {
actionListeners.forEach(actionListener -> actionListener.preConvert((PackConversionContext) conversionContext));
converter.convert(conversionContext);
actionListeners.forEach(actionListener -> actionListener.postConvert((PackConversionContext) conversionContext));
} catch (Throwable t) {
logListener.error("Error converting pack!", t);
errors++;
}
}
try {
bedrockResourcePack.export();
} catch (IOException e) {
throw new RuntimeException(e);
}
context.getSource().sendFeedback(Component.literal("exporting, " + errors + " errors"));
return 0;
})
);
});
}
public static ResourceLocation getModdedLocation(String path) {
return ResourceLocation.fromNamespaceAndPath(MOD_ID, path);
}
public static String fileSafeResourceLocation(ResourceLocation location) {
return location.toString().replace(':', '.').replace('/', '_');
}
}

View File

@@ -0,0 +1,16 @@
package org.geysermc.rainbow.accessor;
import net.minecraft.client.renderer.item.ClientItem;
import net.minecraft.client.resources.model.ResolvedModel;
import net.minecraft.resources.ResourceLocation;
import java.util.Optional;
// Implemented on ModelManager, since this class doesn't keep the resolved models or unbaked client items after baking, we have to store them manually.
// This comes with some extra memory usage, but Rainbow should only be used to convert packs, so it should be fine
public interface ResolvedModelAccessor {
Optional<ResolvedModel> rainbow$getResolvedModel(ResourceLocation location);
Optional<ClientItem> rainbow$getClientItem(ResourceLocation location);
}

View File

@@ -0,0 +1,54 @@
package org.geysermc.rainbow.command;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.context.StringRange;
import com.mojang.brigadier.suggestion.Suggestion;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import com.mojang.datafixers.util.Pair;
import net.minecraft.client.Minecraft;
import java.util.concurrent.CompletableFuture;
public class CommandSuggestionsArgumentType implements ArgumentType<Pair<String, CompletableFuture<Suggestions>>> {
public static final CommandSuggestionsArgumentType TYPE = new CommandSuggestionsArgumentType();
@Override
public Pair<String, CompletableFuture<Suggestions>> parse(StringReader reader) {
String command = reader.getRemaining();
reader.setCursor(reader.getTotalLength());
return Pair.of(command, Minecraft.getInstance().getConnection().getSuggestionsProvider().customSuggestion(createCommandSuggestionsContext(command)));
}
@Override
public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
int offset = builder.getStart();
return Minecraft.getInstance().getConnection().getSuggestionsProvider().customSuggestion(createCommandSuggestionsContext(builder.getRemaining()))
.thenApply(suggestions -> addOffset(suggestions, offset));
}
public static Pair<String, CompletableFuture<Suggestions>> getSuggestions(CommandContext<?> context, String argument) {
return context.getArgument(argument, Pair.class);
}
private Suggestions addOffset(Suggestions suggestions, int offset) {
StringRange offsetRange = addOffset(suggestions.getRange(), offset);
return new Suggestions(offsetRange, suggestions.getList().stream()
.map(suggestion -> new Suggestion(addOffset(suggestion.getRange(), offset), suggestion.getText(), suggestion.getTooltip()))
.toList());
}
private StringRange addOffset(StringRange range, int offset) {
return new StringRange(range.getStart() + offset, range.getEnd() + offset);
}
private static CommandContext<?> createCommandSuggestionsContext(String string) {
// hack
return new CommandContext<>(null,
string,
null, null, null, null, null, null, null, false);
}
}

View File

@@ -0,0 +1,137 @@
package org.geysermc.rainbow.command;
import com.mojang.brigadier.Command;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.StringArgumentType;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
import net.minecraft.ChatFormatting;
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.pack.BedrockPack;
import java.nio.file.Path;
import java.util.Optional;
import java.util.function.BiConsumer;
public class PackGeneratorCommand {
private static final Component NO_PACK_CREATED = Component.translatable("commands.rainbow.no_pack", Component.literal("/rainbow create <name>")
.withStyle(style -> style.withColor(ChatFormatting.BLUE).withUnderlined(true)
.withClickEvent(new ClickEvent.SuggestCommand("/rainbow create "))));
public static void register(CommandDispatcher<FabricClientCommandSource> dispatcher, PackManager packManager, PackMapper packMapper) {
dispatcher.register(ClientCommandManager.literal("rainbow")
.then(ClientCommandManager.literal("create")
.then(ClientCommandManager.argument("name", StringArgumentType.word())
.executes(context -> {
String name = StringArgumentType.getString(context, "name");
try {
packManager.startPack(name);
} catch (Exception exception) {
context.getSource().sendError(Component.translatable("commands.rainbow.create_pack_failed"));
throw new RuntimeException(exception);
}
context.getSource().sendFeedback(Component.translatable("commands.rainbow.pack_created", name));
return 0;
})
)
)
.then(ClientCommandManager.literal("map")
.executes(runWithPack(packManager, (source, pack) -> {
ItemStack heldItem = source.getPlayer().getMainHandItem();
switch (pack.map(heldItem)) {
case NONE_MAPPED -> source.sendError(Component.translatable("commands.rainbow.no_item_mapped"));
case PROBLEMS_OCCURRED -> source.sendFeedback(Component.translatable("commands.rainbow.mapped_held_item_problems"));
case MAPPED_SUCCESSFULLY -> source.sendFeedback(Component.translatable("commands.rainbow.mapped_held_item"));
}
}))
)
.then(ClientCommandManager.literal("mapinventory")
.executes(runWithPack(packManager, (source, pack) -> {
int mapped = 0;
boolean errors = false;
Inventory inventory = source.getPlayer().getInventory();
for (ItemStack stack : inventory) {
BedrockPack.MappingResult result = pack.map(stack);
if (result != BedrockPack.MappingResult.NONE_MAPPED) {
mapped++;
if (result == BedrockPack.MappingResult.PROBLEMS_OCCURRED) {
errors = true;
}
}
}
if (mapped > 0) {
source.sendFeedback(Component.translatable("commands.rainbow.mapped_items_from_inventory", mapped));
if (errors) {
source.sendFeedback(Component.translatable("commands.rainbow.mapped_items_problems"));
}
} else {
source.sendError(Component.translatable("commands.rainbow.no_items_mapped"));
}
}))
)
.then(ClientCommandManager.literal("auto")
/* This is disabled for now.
.then(ClientCommandManager.literal("command")
.then(ClientCommandManager.argument("suggestions", CommandSuggestionsArgumentType.TYPE)
.executes(context -> {
Pair<String, CompletableFuture<Suggestions>> suggestions = CommandSuggestionsArgumentType.getSuggestions(context, "suggestions");
String baseCommand = suggestions.getFirst();
suggestions.getSecond().thenAccept(completed -> {
ItemSuggestionProvider provider = new ItemSuggestionProvider(completed.getList().stream()
.map(suggestion -> baseCommand.substring(0, suggestion.getRange().getStart()) + suggestion.getText())
.toList());
packMapper.setItemProvider(provider);
context.getSource().sendFeedback(Component.literal("Running " + provider.queueSize() + " commands to obtain custom items to map"));
});
return 0;
})
)
)
*/
.then(ClientCommandManager.literal("inventory")
.executes(runWithPack(packManager, (source, pack) -> {
packMapper.setItemProvider(InventoryMapper.INSTANCE);
source.sendFeedback(Component.translatable("commands.rainbow.automatic_inventory_mapping"));
}))
)
.then(ClientCommandManager.literal("stop")
.executes(runWithPack(packManager, (source, pack) -> {
packMapper.setItemProvider(null);
source.sendFeedback(Component.translatable("commands.rainbow.stopped_automatic_mapping"));
}))
)
)
.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));
return 0;
})
)
);
}
private static Command<FabricClientCommandSource> runWithPack(PackManager manager, BiConsumer<FabricClientCommandSource, BedrockPack> executor) {
return context -> {
manager.runOrElse(pack -> executor.accept(context.getSource(), pack),
() -> context.getSource().sendError(NO_PACK_CREATED));
return 0;
};
}
}

View File

@@ -0,0 +1,42 @@
package org.geysermc.rainbow.creative;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.key.Keyed;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import team.unnamed.creative.part.ResourcePackPart;
import team.unnamed.creative.serialize.minecraft.ResourceCategory;
import team.unnamed.creative.sound.SoundRegistry;
import team.unnamed.creative.texture.Texture;
@SuppressWarnings("UnstableApiUsage")
public abstract class CachingStreamResourceContainer implements StreamResourceContainer {
private final Reference2ObjectMap<ResourceCategory<?>, Object2ObjectMap<Key, Object>> cache = new Reference2ObjectOpenHashMap<>();
private final Object2ObjectMap<String, SoundRegistry> soundRegistryCache = new Object2ObjectOpenHashMap<>();
private final Object2ObjectMap<Key, Texture> textureCache = new Object2ObjectOpenHashMap<>();
@SuppressWarnings("unchecked")
private <T extends Keyed & ResourcePackPart> T cacheOrDeserialize(ResourceCategory<T> deserializer, Key key) {
Object2ObjectMap<Key, Object> deserializerCache = cache.computeIfAbsent(deserializer, cacheKey -> new Object2ObjectOpenHashMap<>());
return (T) deserializerCache.computeIfAbsent(key, cacheKey -> StreamResourceContainer.super.deserialize(deserializer, key));
}
@Override
public <T extends Keyed & ResourcePackPart> @Nullable T deserialize(ResourceCategory<T> deserializer, Key key) {
return cacheOrDeserialize(deserializer, key);
}
@Override
public @Nullable SoundRegistry soundRegistry(@NotNull String namespace) {
return soundRegistryCache.computeIfAbsent(namespace, cacheNamespace -> StreamResourceContainer.super.soundRegistry(namespace));
}
@Override
public @Nullable Texture texture(@NotNull Key key) {
return textureCache.computeIfAbsent(key, cacheKey -> StreamResourceContainer.super.texture(key));
}
}

View File

@@ -0,0 +1,140 @@
package org.geysermc.rainbow.creative;
import net.kyori.adventure.key.Key;
import org.jetbrains.annotations.NotNull;
import team.unnamed.creative.atlas.Atlas;
import team.unnamed.creative.base.Writable;
import team.unnamed.creative.blockstate.BlockState;
import team.unnamed.creative.equipment.Equipment;
import team.unnamed.creative.font.Font;
import team.unnamed.creative.item.Item;
import team.unnamed.creative.lang.Language;
import team.unnamed.creative.model.Model;
import team.unnamed.creative.overlay.ResourceContainer;
import team.unnamed.creative.resources.MergeStrategy;
import team.unnamed.creative.sound.Sound;
import team.unnamed.creative.sound.SoundRegistry;
import team.unnamed.creative.texture.Texture;
@SuppressWarnings("NonExtendableApiUsage")
public interface ImmutableResourceContainer extends ResourceContainer {
private static <T> T thr() {
throw new UnsupportedOperationException("ResourceContainer is immutable");
}
@Override
default void atlas(@NotNull Atlas atlas) {
thr();
}
@Override
default boolean removeAtlas(@NotNull Key key) {
return thr();
}
@Override
default void blockState(@NotNull BlockState state) {
thr();
}
@Override
default boolean removeBlockState(@NotNull Key key) {
return thr();
}
@Override
default void equipment(@NotNull Equipment equipment) {
thr();
}
@Override
default boolean removeEquipment(@NotNull Key key) {
return thr();
}
@Override
default void font(@NotNull Font font) {
thr();
}
@Override
default boolean removeFont(@NotNull Key key) {
return thr();
}
@Override
default void item(@NotNull Item item) {
thr();
}
@Override
default boolean removeItem(@NotNull Key key) {
return thr();
}
@Override
default void language(@NotNull Language language) {
thr();
}
@Override
default boolean removeLanguage(@NotNull Key key) {
return thr();
}
@Override
default void model(@NotNull Model model) {
thr();
}
@Override
default boolean removeModel(@NotNull Key key) {
return thr();
}
@Override
default void soundRegistry(@NotNull SoundRegistry soundRegistry) {
thr();
}
@Override
default boolean removeSoundRegistry(@NotNull String namespace) {
return thr();
}
@Override
default void sound(@NotNull Sound sound) {
thr();
}
@Override
default boolean removeSound(@NotNull Key key) {
return thr();
}
@Override
default void texture(@NotNull Texture texture) {
thr();
}
@Override
default boolean removeTexture(@NotNull Key key) {
return thr();
}
@Override
default void unknownFile(@NotNull String path, @NotNull Writable data) {
thr();
}
@Override
default boolean removeUnknownFile(@NotNull String path) {
return thr();
}
@Override
default void merge(@NotNull ResourceContainer other, @NotNull MergeStrategy strategy) {
thr();
}
}

View File

@@ -0,0 +1,37 @@
package org.geysermc.rainbow.creative;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import team.unnamed.creative.ResourcePack;
import team.unnamed.creative.base.Writable;
import team.unnamed.creative.metadata.Metadata;
import team.unnamed.creative.metadata.sodium.SodiumMeta;
import team.unnamed.creative.overlay.Overlay;
@SuppressWarnings("NonExtendableApiUsage")
public interface ImmutableResourcePack extends ResourcePack, ImmutableResourceContainer {
private static void thr() {
throw new UnsupportedOperationException("ResourcePack is immutable");
}
@Override
default void icon(@Nullable Writable icon) {
thr();
}
@Override
default void metadata(@NotNull Metadata metadata) {
thr();
}
@Override
default void overlay(@NotNull Overlay overlay) {
thr();
}
@Override
default void sodiumMeta(@NotNull SodiumMeta sodiumMeta) {
thr();
}
}

View File

@@ -0,0 +1,71 @@
package org.geysermc.rainbow.creative;
import net.kyori.adventure.key.Key;
import net.minecraft.server.packs.resources.Resource;
import net.minecraft.server.packs.resources.ResourceManager;
import org.geysermc.rainbow.KeyUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import team.unnamed.creative.base.Writable;
import team.unnamed.creative.metadata.Metadata;
import team.unnamed.creative.overlay.Overlay;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
public class MinecraftCreativeResourcePack extends CachingStreamResourceContainer implements ImmutableResourcePack {
// Matches a path in 3 groups: the first directory, the rest of the path, and the file extension (e.g. .json)
private static final Pattern PATH_SANITIZE_REGEX = Pattern.compile("(^\\w+/)(.*)(\\.\\w+$)");
private final ResourceManager resourceManager;
public MinecraftCreativeResourcePack(ResourceManager resourceManager) {
this.resourceManager = resourceManager;
}
@Override
public @Nullable InputStream open(Key key) throws IOException {
Optional<Resource> resource = resourceManager.getResource(KeyUtil.keyToResourceLocation(key));
if (resource.isPresent()) {
return resource.get().open();
}
return null;
}
@Override
public List<Key> assets(String category) {
return resourceManager.listResources(category, resource -> true).keySet().stream()
.map(location -> location.withPath(path -> PATH_SANITIZE_REGEX.matcher(path).replaceAll("$2")))
.map(KeyUtil::resourceLocationToKey)
.toList();
}
@Override
public Collection<String> namespaces() {
return resourceManager.getNamespaces();
}
@Override
public @Nullable Writable icon() {
return null;
}
@Override
public @NotNull Metadata metadata() {
return Metadata.empty();
}
@Override
public @Nullable Overlay overlay(@NotNull String directory) {
return null;
}
@Override
public @NotNull Collection<Overlay> overlays() {
return List.of();
}
}

View File

@@ -0,0 +1,254 @@
package org.geysermc.rainbow.creative;
import com.google.gson.JsonElement;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.key.Keyed;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import team.unnamed.creative.atlas.Atlas;
import team.unnamed.creative.base.Writable;
import team.unnamed.creative.blockstate.BlockState;
import team.unnamed.creative.equipment.Equipment;
import team.unnamed.creative.font.Font;
import team.unnamed.creative.item.Item;
import team.unnamed.creative.lang.Language;
import team.unnamed.creative.metadata.Metadata;
import team.unnamed.creative.model.Model;
import team.unnamed.creative.part.ResourcePackPart;
import team.unnamed.creative.serialize.minecraft.GsonUtil;
import team.unnamed.creative.serialize.minecraft.MinecraftResourcePackStructure;
import team.unnamed.creative.serialize.minecraft.ResourceCategory;
import team.unnamed.creative.serialize.minecraft.atlas.AtlasSerializer;
import team.unnamed.creative.serialize.minecraft.blockstate.BlockStateSerializer;
import team.unnamed.creative.serialize.minecraft.equipment.EquipmentCategory;
import team.unnamed.creative.serialize.minecraft.font.FontSerializer;
import team.unnamed.creative.serialize.minecraft.item.ItemSerializer;
import team.unnamed.creative.serialize.minecraft.language.LanguageSerializer;
import team.unnamed.creative.serialize.minecraft.metadata.MetadataSerializer;
import team.unnamed.creative.serialize.minecraft.model.ModelSerializer;
import team.unnamed.creative.serialize.minecraft.sound.SoundRegistrySerializer;
import team.unnamed.creative.serialize.minecraft.sound.SoundSerializer;
import team.unnamed.creative.sound.Sound;
import team.unnamed.creative.sound.SoundRegistry;
import team.unnamed.creative.texture.Texture;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
@SuppressWarnings({"UnstableApiUsage", "PatternValidation"})
public interface StreamResourceContainer extends ImmutableResourceContainer {
@Nullable
InputStream open(Key key) throws IOException;
List<Key> assets(String category);
Collection<String> namespaces();
String METADATA_EXTENSION = MinecraftResourcePackStructure.METADATA_EXTENSION;
String TEXTURE_EXTENSION = MinecraftResourcePackStructure.TEXTURE_EXTENSION;
String TEXTURE_CATEGORY = MinecraftResourcePackStructure.TEXTURES_FOLDER;
String SOUNDS_FILE = MinecraftResourcePackStructure.SOUNDS_FILE;
private static Key addExtensionAndCategory(Key key, String category, String extension) {
return Key.key(key.namespace(), category + "/" + key.value() + extension);
}
private static JsonElement parseJson(InputStream input) throws IOException {
try (Reader reader = new InputStreamReader(input, StandardCharsets.UTF_8)) {
return GsonUtil.parseReader(reader);
}
}
@Nullable
private static Writable writable(Key key, String category, String extension, ResourceOpener opener) throws IOException {
Key withExtension = addExtensionAndCategory(key, category, extension);
try (InputStream input = opener.open(withExtension)) {
if (input != null) {
return Writable.inputStream(() -> opener.open(withExtension));
}
}
return null;
}
private static <T extends Keyed & ResourcePackPart> List<T> collect(ResourceCategory<T> category, Function<Key, T> deserializer,
Function<String, List<Key>> assetLookup) {
return assetLookup.apply(category.folder(-1)).stream()
.map(deserializer)
.filter(Objects::nonNull)
.toList();
}
@Nullable
default <T extends Keyed & ResourcePackPart> T deserialize(ResourceCategory<T> category, Key key) {
Key withExtensionAndCategory = addExtensionAndCategory(key, category.folder(-1), category.extension(-1));
try (InputStream input = open(withExtensionAndCategory)) {
if (input != null) {
return category.deserializer().deserialize(input, key);
}
} catch (IOException exception) {
throw new RuntimeException(exception);
}
return null;
}
@Override
default @Nullable Atlas atlas(@NotNull Key key) {
return deserialize(AtlasSerializer.CATEGORY, key);
}
@Override
default @NotNull Collection<Atlas> atlases() {
return collect(AtlasSerializer.CATEGORY, this::atlas, this::assets);
}
@Override
default @Nullable BlockState blockState(@NotNull Key key) {
return deserialize(BlockStateSerializer.CATEGORY, key);
}
@Override
@NotNull
default Collection<BlockState> blockStates() {
return collect(BlockStateSerializer.CATEGORY, this::blockState, this::assets);
}
@Override
default @Nullable Equipment equipment(@NotNull Key key) {
return deserialize(EquipmentCategory.INSTANCE, key);
}
@Override
@NotNull
default Collection<Equipment> equipment() {
return collect(EquipmentCategory.INSTANCE, this::equipment, this::assets);
}
@Override
default @Nullable Font font(@NotNull Key key) {
return deserialize(FontSerializer.CATEGORY, key);
}
@Override
@NotNull
default Collection<Font> fonts() {
return collect(FontSerializer.CATEGORY, this::font, this::assets);
}
@Override
default @Nullable Item item(@NotNull Key key) {
return deserialize(ItemSerializer.CATEGORY, key);
}
@Override
@NotNull
default Collection<Item> items() {
return collect(ItemSerializer.CATEGORY, this::item, this::assets);
}
@Override
default @Nullable Language language(@NotNull Key key) {
return deserialize(LanguageSerializer.CATEGORY, key);
}
@Override
@NotNull
default Collection<Language> languages() {
return collect(LanguageSerializer.CATEGORY, this::language, this::assets);
}
@Override
default @Nullable Model model(@NotNull Key key) {
return deserialize(ModelSerializer.CATEGORY, key);
}
@Override
@NotNull
default Collection<Model> models() {
return collect(ModelSerializer.CATEGORY, this::model, this::assets);
}
@Override
default @Nullable SoundRegistry soundRegistry(@NotNull String namespace) {
try (InputStream input = open(Key.key(namespace, SOUNDS_FILE))) {
if (input != null) {
return SoundRegistrySerializer.INSTANCE.readFromTree(parseJson(input), namespace);
}
} catch (IOException exception) {
throw new RuntimeException(exception);
}
return null;
}
@Override
@NotNull
default Collection<SoundRegistry> soundRegistries() {
return namespaces().stream()
.map(this::soundRegistry)
.filter(Objects::nonNull)
.toList();
}
@Override
default @Nullable Sound sound(@NotNull Key key) {
return deserialize(SoundSerializer.CATEGORY, key);
}
@Override
@NotNull
default Collection<Sound> sounds() {
return collect(SoundSerializer.CATEGORY, this::sound, this::assets);
}
@Override
default @Nullable Texture texture(@NotNull Key key) {
try {
Writable texture = writable(key, TEXTURE_CATEGORY, TEXTURE_EXTENSION, this::open);
if (texture != null) {
Metadata metadata = Metadata.empty();
try (InputStream metadataStream = open(addExtensionAndCategory(key, TEXTURE_CATEGORY, METADATA_EXTENSION))) {
if (metadataStream != null) {
metadata = MetadataSerializer.INSTANCE.readFromTree(parseJson(metadataStream));
}
}
return Texture.texture(key, texture, metadata);
}
} catch (IOException exception) {
throw new RuntimeException(exception);
}
return null;
}
@Override
@NotNull
default Collection<Texture> textures() {
return assets(TEXTURE_CATEGORY).stream()
.map(this::texture)
.filter(Objects::nonNull)
.toList();
}
@Override
default @Nullable Writable unknownFile(@NotNull String path) {
throw new UnsupportedOperationException("Unsupported by StreamResourceContainer");
}
@Override
default @NotNull Map<String, Writable> unknownFiles() {
return Map.of();
}
@FunctionalInterface
interface ResourceOpener {
InputStream open(Key key) throws IOException;
}
}

View File

@@ -0,0 +1,14 @@
package org.geysermc.rainbow.mapper;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.world.item.ItemStack;
import java.util.stream.Stream;
public interface CustomItemProvider {
Stream<ItemStack> nextItems(LocalPlayer player, ClientPacketListener connection);
boolean isDone();
}

View File

@@ -0,0 +1,23 @@
package org.geysermc.rainbow.mapper;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.world.item.ItemStack;
import java.util.stream.Stream;
public class InventoryMapper implements CustomItemProvider {
public static final InventoryMapper INSTANCE = new InventoryMapper();
private InventoryMapper() {}
@Override
public Stream<ItemStack> nextItems(LocalPlayer player, ClientPacketListener connection) {
return player.containerMenu.getItems().stream();
}
@Override
public boolean isDone() {
return false;
}
}

View File

@@ -0,0 +1,53 @@
package org.geysermc.rainbow.mapper;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.network.protocol.game.ServerboundChatCommandPacket;
import net.minecraft.world.item.ItemStack;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
// TODO safety
public class ItemSuggestionProvider implements CustomItemProvider {
private final List<String> remainingCommands;
private boolean waitingOnItem = false;
private boolean waitingOnClear = false;
public ItemSuggestionProvider(List<String> commands) {
remainingCommands = new ArrayList<>(commands);
}
public Stream<ItemStack> nextItems(LocalPlayer player, ClientPacketListener connection) {
if (!remainingCommands.isEmpty() || waitingOnItem) {
if (waitingOnClear && player.getInventory().isEmpty()) {
waitingOnClear = false;
} else if (!waitingOnItem) {
connection.send(new ServerboundChatCommandPacket(remainingCommands.removeFirst()));
waitingOnItem = true;
} else {
if (!player.getInventory().isEmpty()) {
Stream<ItemStack> items = player.getInventory().getNonEquipmentItems().stream();
connection.send(new ServerboundChatCommandPacket("clear"));
waitingOnItem = false;
if (!remainingCommands.isEmpty()) {
waitingOnClear = true;
}
return items;
}
}
}
return Stream.empty();
}
public int queueSize() {
return remainingCommands.size();
}
@Override
public boolean isDone() {
return remainingCommands.isEmpty() && !waitingOnItem && !waitingOnClear;
}
}

View File

@@ -0,0 +1,46 @@
package org.geysermc.rainbow.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.pack.BedrockPack;
import java.util.Objects;
import java.util.Optional;
public class PackMapper {
private final PackManager packManager;
private CustomItemProvider itemProvider;
public PackMapper(PackManager packManager) {
this.packManager = packManager;
}
public void setItemProvider(CustomItemProvider itemProvider) {
this.itemProvider = itemProvider;
}
public void tick(Minecraft minecraft) {
if (itemProvider != null) {
LocalPlayer player = Objects.requireNonNull(minecraft.player);
ClientPacketListener connection = Objects.requireNonNull(minecraft.getConnection());
packManager.runOrElse(pack -> {
// TODO maybe report problems here... probably better to do so in pack class though
long mapped = itemProvider.nextItems(player, connection)
.map(pack::map)
.filter(result -> result != BedrockPack.MappingResult.NONE_MAPPED)
.count();
if (mapped != 0) {
player.displayClientMessage(Component.translatable("chat.rainbow.mapped_items", mapped), false);
}
if (itemProvider.isDone()) {
player.displayClientMessage(Component.translatable("chat.rainbow.automatic_mapping_finished"), false);
itemProvider = null;
}
}, () -> itemProvider = null);
}
}
}

View File

@@ -0,0 +1,9 @@
package org.geysermc.rainbow.mapping;
import org.geysermc.rainbow.pack.BedrockItem;
@FunctionalInterface
public interface BedrockItemConsumer {
void accept(BedrockItem item);
}

View File

@@ -0,0 +1,314 @@
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;
import net.minecraft.client.renderer.item.ItemModel;
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.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.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.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.ProblemReporter;
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.item.CrossbowItem;
import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.item.ItemStack;
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.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;
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) {
//noinspection unchecked
return ((LateBoundIdMapperAccessor<ResourceLocation, ?>) ItemModels.ID_MAPPER).getIdToValue().inverse().get(model.type());
}
public static void tryMapStack(ItemStack stack, ResourceLocation modelLocation, ProblemReporter reporter, PackContext context) {
getModels().rainbow$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 + " ");
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)) {
if (index == 0) {
float scaledCustomModelData = customModelData * scale;
float[] thresholds = ArrayUtils.toPrimitive(entries.stream()
.map(RangeSelectItemModel.Entry::threshold)
.toArray(Float[]::new));
int modelIndex = RangeSelectItemModelAccessor.invokeLastIndexLessOrEqual(thresholds, scaledCustomModelData);
Optional<ItemModel.Unbaked> model = modelIndex == -1 ? fallback : Optional.of(entries.get(modelIndex).model());
model.ifPresentOrElse(present -> mapItem(present, stack, childReporter, base -> new GeyserLegacyDefinition(base, customModelData), context),
() -> childReporter.report(() -> "custom model data index lookup returned -1, and no fallback is present"));
} else {
childReporter.report(() -> "range_dispatch custom model data property index is not zero, unable to apply custom model data");
}
return;
}
}
childReporter.report(() -> "item model is not range_dispatch, unable to apply custom model data");
}
public static void mapItem(ItemModel.Unbaked model, ItemStack stack, ProblemReporter reporter,
Function<GeyserBaseDefinition, GeyserItemDefinition> definitionCreator, PackContext packContext) {
mapItem(model, new MappingContext(List.of(), stack, reporter, definitionCreator, packContext));
}
private static void mapItem(ItemModel.Unbaked model, MappingContext context) {
switch (model) {
case BlockModelWrapper.Unbaked modelWrapper -> mapBlockModelWrapper(modelWrapper, context.child("plain model " + modelWrapper.model()));
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));
}
}
private static void mapBlockModelWrapper(BlockModelWrapper.Unbaked model, MappingContext context) {
ResourceLocation itemModelLocation = model.model();
getModels().rainbow$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());
} else {
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 -> {
// 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));
}
private static void mapConditionalModel(ConditionalItemModel.Unbaked model, MappingContext context) {
ItemModelPropertyTest property = model.property();
GeyserConditionPredicate.Property predicateProperty = switch (property) {
case Broken ignored -> GeyserConditionPredicate.BROKEN;
case Damaged ignored -> GeyserConditionPredicate.DAMAGED;
case CustomModelDataProperty customModelData -> new GeyserConditionPredicate.CustomModelData(customModelData.index());
case HasComponent hasComponent -> new GeyserConditionPredicate.HasComponent(hasComponent.componentType()); // ignoreDefault property not a thing, we should look into that in Geyser! TODO
case FishingRodCast ignored -> GeyserConditionPredicate.FISHING_ROD_CAST;
default -> null;
};
ItemModel.Unbaked onTrue = model.onTrue();
ItemModel.Unbaked onFalse = model.onFalse();
if (predicateProperty == null) {
context.reporter.report(() -> "unsupported conditional model property " + property + ", only mapping on_false");
mapItem(onFalse, context.child("condition on_false (unsupported property)"));
return;
}
mapItem(onTrue, context.with(new GeyserConditionPredicate(predicateProperty, true), "condition on true "));
mapItem(onFalse, context.with(new GeyserConditionPredicate(predicateProperty, false), "condition on false "));
}
private static void mapRangeSelectModel(RangeSelectItemModel.Unbaked model, MappingContext context) {
RangeSelectItemModelProperty property = model.property();
GeyserRangeDispatchPredicate.Property predicateProperty = switch (property) {
case BundleFullness ignored -> GeyserRangeDispatchPredicate.BUNDLE_FULLNESS;
case Count count -> new GeyserRangeDispatchPredicate.Count(count.normalize());
// Mojang, why? :(
case net.minecraft.client.renderer.item.properties.numeric.CustomModelDataProperty customModelData -> new GeyserRangeDispatchPredicate.CustomModelData(customModelData.index());
case Damage damage -> new GeyserRangeDispatchPredicate.Damage(damage.normalize());
default -> null;
};
if (predicateProperty == null) {
context.reporter.report(() -> "unsupported range dispatch model property " + property + ", 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()));
}
}
model.fallback().ifPresent(fallback -> mapItem(fallback, context.child("range dispatch fallback")));
}
@SuppressWarnings("unchecked")
private static void mapSelectModel(SelectItemModel.Unbaked model, MappingContext context) {
SelectItemModel.UnbakedSwitch<?, ?> unbakedSwitch = model.unbakedSwitch();
Function<Object, GeyserMatchPredicate.MatchPredicateData> dataConstructor = switch (unbakedSwitch.property()) {
case Charge ignored -> chargeType -> new GeyserMatchPredicate.ChargeType((CrossbowItem.ChargeType) chargeType);
case TrimMaterialProperty ignored -> material -> new GeyserMatchPredicate.TrimMaterialData((ResourceKey<TrimMaterial>) material);
case ContextDimension ignored -> dimension -> new GeyserMatchPredicate.ContextDimension((ResourceKey<Level>) dimension);
// Why, Mojang?
case net.minecraft.client.renderer.item.properties.select.CustomModelDataProperty customModelData -> string -> new GeyserMatchPredicate.CustomModelData((String) string, customModelData.index());
default -> null;
};
List<? extends SelectItemModel.SwitchCase<?>> cases = unbakedSwitch.cases();
if (dataConstructor == null) {
if (unbakedSwitch.property() instanceof DisplayContext) {
context.reporter.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) "));
return;
}
}
}
context.reporter.report(() -> "unsupported select model property " + unbakedSwitch.property() + ", only mapping fallback, if present");
model.fallback().ifPresent(fallback -> mapItem(fallback, context.child("select fallback case (unsupported property) ")));
return;
}
cases.forEach(switchCase -> {
switchCase.values().forEach(value -> {
mapItem(switchCase.model(), context.with(new GeyserMatchPredicate(dataConstructor.apply(value)), "select case " + value + " "));
});
});
model.fallback().ifPresent(fallback -> mapItem(fallback, context.child("select fallback case ")));
}
private record MappingContext(List<GeyserPredicate> predicateStack, ItemStack stack, ProblemReporter reporter,
Function<GeyserBaseDefinition, GeyserItemDefinition> definitionCreator, PackContext packContext) {
public MappingContext with(GeyserPredicate predicate, String childName) {
return new MappingContext(Stream.concat(predicateStack.stream(), Stream.of(predicate)).toList(), stack, reporter.forChild(() -> childName), definitionCreator, packContext);
}
public MappingContext child(String childName) {
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();
}
GeyserBaseDefinition base = new GeyserBaseDefinition(bedrockIdentifier, Optional.of(stack.getHoverName().getString()), predicateStack,
new GeyserBaseDefinition.BedrockOptions(Optional.empty(), true, displayHandheld, calculateProtectionValue(stack), tags),
stack.getComponentsPatch());
try {
packContext.mappings().map(stack.getItemHolder(), definitionCreator.apply(base));
} catch (Exception exception) {
reporter.forChild(() -> "mapping with bedrock identifier " + bedrockIdentifier + " ").report(() -> "failed to pass mapping: " + exception.getMessage());
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()));
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)));
}
private static int calculateProtectionValue(ItemStack stack) {
ItemAttributeModifiers modifiers = stack.get(DataComponents.ATTRIBUTE_MODIFIERS);
if (modifiers != null) {
return modifiers.modifiers().stream()
.filter(modifier -> modifier.attribute() == Attributes.ARMOR && modifier.modifier().operation() == AttributeModifier.Operation.ADD_VALUE)
.mapToInt(entry -> (int) entry.modifier().amount())
.sum();
}
return 0;
}
}
}

View File

@@ -0,0 +1,10 @@
package org.geysermc.rainbow.mapping;
import net.minecraft.resources.ResourceLocation;
import org.geysermc.rainbow.mapping.geyser.GeyserMappings;
import java.nio.file.Path;
import java.util.function.Consumer;
public record PackContext(GeyserMappings mappings, Path packPath, BedrockItemConsumer itemConsumer, Consumer<ResourceLocation> additionalTextureConsumer) {
}

View File

@@ -0,0 +1,39 @@
package org.geysermc.rainbow.mapping.animation;
import net.minecraft.client.renderer.block.model.ItemTransform;
import net.minecraft.client.renderer.block.model.ItemTransforms;
import org.geysermc.rainbow.pack.animation.BedrockAnimation;
import org.joml.Vector3f;
import org.joml.Vector3fc;
// TODO these offset values are completely wrong, 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);
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();
Vector3f firstPersonPosition = FIRST_PERSON_POSITION_OFFSET.add(firstPerson.translation(), new Vector3f());
Vector3f firstPersonRotation = FIRST_PERSON_ROTATION_OFFSET.add(firstPerson.rotation(), new Vector3f());
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());
Vector3f thirdPersonScale = new Vector3f(thirdPerson.scale());
return new BedrockAnimationContext(BedrockAnimation.builder()
.withAnimation(identifier + ".hold_first_person", BedrockAnimation.animation()
.withLoopMode(BedrockAnimation.LoopMode.LOOP)
.withBone(bone, firstPersonPosition, firstPersonRotation, firstPersonScale))
.withAnimation(identifier + ".hold_third_person", BedrockAnimation.animation()
.withLoopMode(BedrockAnimation.LoopMode.LOOP)
.withBone(bone, thirdPersonPosition, thirdPersonRotation, thirdPersonScale))
.build(), "animation." + identifier + ".hold_first_person", "animation." + identifier + ".hold_third_person");
}
}

View File

@@ -0,0 +1,6 @@
package org.geysermc.rainbow.mapping.animation;
import org.geysermc.rainbow.pack.animation.BedrockAnimation;
public record BedrockAnimationContext(BedrockAnimation animation, String firstPerson, String thirdPerson) {
}

View File

@@ -0,0 +1,62 @@
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;
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.geometry.BedrockGeometryContext;
import org.geysermc.rainbow.mixin.EntityRenderDispatcherAccessor;
import org.geysermc.rainbow.pack.attachable.BedrockAttachable;
import java.util.List;
import java.util.Optional;
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) {
// 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()))
.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)));
})
.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());
}))
.map(attachable -> {
customAnimation.ifPresent(context -> {
attachable.withAnimation("first_person", context.firstPerson());
attachable.withAnimation("third_person", context.thirdPerson());
attachable.withScript("animate", "first_person", "context.is_first_person == 1.0");
attachable.withScript("animate", "third_person", "context.is_first_person == 0.0");
});
return attachable.build();
});
}
private static EquipmentClientInfo.LayerType getLayer(EquipmentSlot slot) {
return slot == EquipmentSlot.LEGS ? EquipmentClientInfo.LayerType.HUMANOID_LEGGINGS : EquipmentClientInfo.LayerType.HUMANOID;
}
private static ResourceLocation getTexture(List<EquipmentClientInfo.Layer> info, EquipmentClientInfo.LayerType layer) {
return info.getFirst().textureId().withPath(path -> "entity/equipment/" + layer.getSerializedName() + "/" + path);
}
}

View File

@@ -0,0 +1,6 @@
package org.geysermc.rainbow.mapping.geometry;
import net.minecraft.resources.ResourceLocation;
import org.geysermc.rainbow.pack.geometry.BedrockGeometry;
public record BedrockGeometryContext(BedrockGeometry geometry, ResourceLocation texture) {}

View File

@@ -0,0 +1,98 @@
package org.geysermc.rainbow.mapping.geometry;
import net.minecraft.client.renderer.block.model.BlockElement;
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.core.Direction;
import net.minecraft.resources.ResourceLocation;
import org.geysermc.rainbow.pack.geometry.BedrockGeometry;
import org.joml.Vector2f;
import org.joml.Vector3f;
import org.joml.Vector3fc;
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) {
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);
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();
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();
bone.withCube(cube);
min.min(cube.origin());
max.max(cube.origin().add(cube.size(), new Vector3f()));
}
// Calculate the pivot to be at the centre of the bone
// This is important for animations later, display animations rotate around the centre on Java
bone.withPivot(min.add(max.sub(min).div(2.0F)));
// 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);
}
private static BedrockGeometry.Cube.Builder mapBlockElement(BlockElement element) {
// 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();
}
builder.withFace(direction, uvOrigin, uvSize, face.rotation());
}
BlockElementRotation rotation = element.rotation();
if (rotation != null) {
// MC multiplies model origin by 0.0625 when loading rotation origin
builder.withPivot(rotation.origin().div(0.0625F, new Vector3f()).sub(CENTRE_OFFSET));
Vector3f bedrockRotation = switch (rotation.axis()) {
case X -> new Vector3f(rotation.angle(), 0.0F, 0.0F);
case Y -> new Vector3f(0.0F, rotation.angle(), 0.0F);
case Z -> new Vector3f(0.0F, 0.0F, rotation.angle());
};
builder.withRotation(bedrockRotation);
}
return builder;
}
}

View File

@@ -0,0 +1,79 @@
package org.geysermc.rainbow.mapping.geometry;
import com.mojang.blaze3d.buffers.GpuBuffer;
import com.mojang.blaze3d.platform.NativeImage;
import com.mojang.blaze3d.systems.CommandEncoder;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.textures.GpuTexture;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.navigation.ScreenRectangle;
import net.minecraft.client.gui.render.pip.OversizedItemRenderer;
import net.minecraft.client.gui.render.state.GuiItemRenderState;
import net.minecraft.client.gui.render.state.GuiRenderState;
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.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 static void render(ItemStack stack, Path path) {
TrackingItemStackRenderState itemRenderState = new TrackingItemStackRenderState();
Minecraft.getInstance().getItemModelResolver().updateForTopItem(itemRenderState, stack, ItemDisplayContext.GUI, null, null, 0);
itemRenderState.setOversizedInGui(true);
GuiItemRenderState guiItemRenderState = new GuiItemRenderState("geometry_render", new Matrix3x2fStack(16), itemRenderState, 0, 0, null);
ScreenRectangle sizeBounds = guiItemRenderState.oversizedItemBounds();
Objects.requireNonNull(sizeBounds);
OversizedItemRenderState oversizedRenderState = new OversizedItemRenderState(guiItemRenderState, sizeBounds.left(), sizeBounds.top(), sizeBounds.right() + 4, sizeBounds.bottom() + 4);
try (OversizedItemRenderer itemRenderer = new OversizedItemRenderer(Minecraft.getInstance().renderBuffers().bufferSource())) {
//noinspection DataFlowIssue
((PictureInPictureCopyRenderer) itemRenderer).rainbow$allowTextureCopy();
itemRenderer.prepare(oversizedRenderState, new GuiRenderState(), 4);
writeAsPNG(path, ((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) {
RenderSystem.assertOnRenderThread();
int width = texture.getWidth(0);
int height = texture.getHeight(0);
int bufferSize = texture.getFormat().pixelSize() * width * height;
GpuBuffer buffer = RenderSystem.getDevice().createBuffer(() -> "Texture output buffer", GpuBuffer.USAGE_COPY_DST | GpuBuffer.USAGE_MAP_READ, bufferSize);
CommandEncoder commandEncoder = RenderSystem.getDevice().createCommandEncoder();
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);
}
}
CodecUtil.ensureDirectoryExists(path.getParent());
nativeImage.writeToFile(path);
} catch (IOException var19) {
// TODO
}
}
buffer.close();
};
commandEncoder.copyTextureToBuffer(texture, buffer, 0, writer, 0);
}
}

View File

@@ -0,0 +1,79 @@
package org.geysermc.rainbow.mapping.geyser;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.core.component.DataComponentPatch;
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 java.util.List;
import java.util.Optional;
import java.util.function.Function;
// TODO other keys, etc.
// TODO sometimes still includes components key when patch before filtering is not empty but after is
// TODO display name can be a component
public record GeyserBaseDefinition(ResourceLocation bedrockIdentifier, Optional<String> displayName,
List<GeyserPredicate> predicates, BedrockOptions bedrockOptions, DataComponentPatch components) {
private static final List<DataComponentType<?>> SUPPORTED_COMPONENTS = List.of(DataComponents.CONSUMABLE, DataComponents.EQUIPPABLE, DataComponents.FOOD,
DataComponents.MAX_DAMAGE, DataComponents.MAX_STACK_SIZE, DataComponents.USE_COOLDOWN, DataComponents.ENCHANTABLE, DataComponents.ENCHANTMENT_GLINT_OVERRIDE);
private static final Codec<DataComponentPatch> FILTERED_COMPONENT_MAP_CODEC = DataComponentPatch.CODEC.xmap(Function.identity(), patch -> {
DataComponentPatch.Builder filtered = DataComponentPatch.builder();
patch.entrySet().stream()
.filter(entry -> entry.getValue().isEmpty() || SUPPORTED_COMPONENTS.contains(entry.getKey()))
.forEach(entry -> {
if (entry.getValue().isPresent()) {
filtered.set((DataComponentType) entry.getKey(), entry.getValue().orElseThrow());
} else {
filtered.remove(entry.getKey());
}
});
return filtered.build();
});
public static final MapCodec<GeyserBaseDefinition> MAP_CODEC = RecordCodecBuilder.mapCodec(instance ->
instance.group(
ResourceLocation.CODEC.fieldOf("bedrock_identifier").forGetter(GeyserBaseDefinition::bedrockIdentifier),
Codec.STRING.optionalFieldOf("display_name").forGetter(GeyserBaseDefinition::displayName),
GeyserPredicate.LIST_CODEC.optionalFieldOf("predicate", List.of()).forGetter(GeyserBaseDefinition::predicates),
BedrockOptions.CODEC.optionalFieldOf("bedrock_options", BedrockOptions.DEFAULT).forGetter(GeyserBaseDefinition::bedrockOptions),
FILTERED_COMPONENT_MAP_CODEC.optionalFieldOf("components", DataComponentPatch.EMPTY).forGetter(GeyserBaseDefinition::components)
).apply(instance, GeyserBaseDefinition::new)
);
public boolean conflictsWith(GeyserBaseDefinition other) {
if (predicates.size() == other.predicates.size()) {
boolean predicatesAreEqual = true;
for (GeyserPredicate predicate : predicates) {
if (!other.predicates.contains(predicate)) {
predicatesAreEqual = false;
break;
}
}
return predicatesAreEqual;
}
return false;
}
public String textureName() {
return bedrockOptions.icon.orElse(Rainbow.fileSafeResourceLocation(bedrockIdentifier));
}
public record BedrockOptions(Optional<String> icon, boolean allowOffhand, boolean displayHandheld, int protectionValue, List<ResourceLocation> tags) {
public static final Codec<BedrockOptions> CODEC = RecordCodecBuilder.create(instance ->
instance.group(
Codec.STRING.optionalFieldOf("icon").forGetter(BedrockOptions::icon),
Codec.BOOL.optionalFieldOf("allow_offhand", true).forGetter(BedrockOptions::allowOffhand),
Codec.BOOL.optionalFieldOf("display_handheld", false).forGetter(BedrockOptions::displayHandheld),
Codec.INT.optionalFieldOf("protection_value", 0).forGetter(BedrockOptions::protectionValue),
ResourceLocation.CODEC.listOf().optionalFieldOf("tags", List.of()).forGetter(BedrockOptions::tags)
).apply(instance, BedrockOptions::new)
);
public static final BedrockOptions DEFAULT = new BedrockOptions(Optional.empty(), true, false, 0, List.of());
}
}

View File

@@ -0,0 +1,56 @@
package org.geysermc.rainbow.mapping.geyser;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.resources.ResourceLocation;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
public record GeyserGroupDefinition(Optional<ResourceLocation> model, List<GeyserMapping> definitions) implements GeyserMapping {
public static final MapCodec<GeyserGroupDefinition> CODEC = RecordCodecBuilder.mapCodec(instance ->
instance.group(
ResourceLocation.CODEC.optionalFieldOf("model").forGetter(GeyserGroupDefinition::model),
GeyserMapping.CODEC.listOf().fieldOf("definitions").forGetter(GeyserGroupDefinition::definitions)
).apply(instance, GeyserGroupDefinition::new)
);
public GeyserGroupDefinition with(GeyserMapping mapping) {
return new GeyserGroupDefinition(model, Stream.concat(definitions.stream(), Stream.of(mapping)).toList());
}
public boolean isFor(Optional<ResourceLocation> model) {
return this.model.isPresent() && model.isPresent() && this.model.get().equals(model.get());
}
public boolean conflictsWith(Optional<ResourceLocation> parentModel, GeyserItemDefinition other) {
Optional<ResourceLocation> thisModel = model.or(() -> parentModel);
for (GeyserMapping definition : definitions) {
if (definition instanceof GeyserGroupDefinition group && group.conflictsWith(thisModel, other)) {
return true;
} else if (definition instanceof GeyserItemDefinition item && item.conflictsWith(thisModel, other)) {
return true;
}
}
return false;
}
public int size() {
int totalSize = 0;
for (GeyserMapping definition : definitions) {
if (definition instanceof GeyserGroupDefinition group) {
totalSize += group.size();
} else {
totalSize++;
}
}
return totalSize;
}
@Override
public Type type() {
return Type.GROUP;
}
}

View File

@@ -0,0 +1,12 @@
package org.geysermc.rainbow.mapping.geyser;
import net.minecraft.resources.ResourceLocation;
import java.util.Optional;
public interface GeyserItemDefinition extends GeyserMapping {
GeyserBaseDefinition base();
boolean conflictsWith(Optional<ResourceLocation> parentModel, GeyserItemDefinition other);
}

View File

@@ -0,0 +1,31 @@
package org.geysermc.rainbow.mapping.geyser;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.resources.ResourceLocation;
import java.util.Optional;
public record GeyserLegacyDefinition(GeyserBaseDefinition base, int customModelData) implements GeyserItemDefinition {
public static final MapCodec<GeyserLegacyDefinition> CODEC = RecordCodecBuilder.mapCodec(instance ->
instance.group(
GeyserBaseDefinition.MAP_CODEC.forGetter(GeyserLegacyDefinition::base),
Codec.INT.fieldOf("custom_model_data").forGetter(GeyserLegacyDefinition::customModelData)
).apply(instance, GeyserLegacyDefinition::new)
);
@Override
public boolean conflictsWith(Optional<ResourceLocation> parentModel, GeyserItemDefinition other) {
if (other instanceof GeyserLegacyDefinition otherLegacy) {
return customModelData == otherLegacy.customModelData && base.conflictsWith(otherLegacy.base);
}
return false;
}
@Override
public Type type() {
return Type.LEGACY;
}
}

View File

@@ -0,0 +1,46 @@
package org.geysermc.rainbow.mapping.geyser;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.mojang.serialization.MapCodec;
import net.minecraft.util.StringRepresentable;
import org.jetbrains.annotations.NotNull;
public interface 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
Codec<GeyserMapping> MODEL_SAFE_CODEC = CODEC.validate(mapping -> {
if (mapping instanceof GeyserSingleDefinition single && single.model().isEmpty()) {
return DataResult.error(() -> "Top level single definition must have a model");
}
return DataResult.success(mapping);
});
Type type();
enum Type implements StringRepresentable {
SINGLE("definition", GeyserSingleDefinition.CODEC),
LEGACY("legacy", GeyserLegacyDefinition.CODEC),
GROUP("group", GeyserGroupDefinition.CODEC);
public static final Codec<Type> CODEC = StringRepresentable.fromEnum(Type::values);
private final String name;
private final MapCodec<? extends GeyserMapping> codec;
Type(String name, MapCodec<? extends GeyserMapping> codec) {
this.name = name;
this.codec = codec;
}
public MapCodec<? extends GeyserMapping> codec() {
return codec;
}
@Override
public @NotNull String getSerializedName() {
return name;
}
}
}

View File

@@ -0,0 +1,87 @@
package org.geysermc.rainbow.mapping.geyser;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.core.Holder;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item;
import org.geysermc.rainbow.CodecUtil;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
public class GeyserMappings {
private static final Codec<Map<Holder<Item>, Collection<GeyserMapping>>> MAPPINGS_CODEC = Codec.unboundedMap(Item.CODEC, GeyserMapping.MODEL_SAFE_CODEC.listOf().xmap(Function.identity(), ArrayList::new));
public static final Codec<GeyserMappings> CODEC = RecordCodecBuilder.create(instance ->
instance.group(
CodecUtil.unitVerifyCodec(Codec.INT, "format_version", 2),
MAPPINGS_CODEC.fieldOf("items").forGetter(GeyserMappings::mappings)
).apply(instance, (format, mappings) -> new GeyserMappings(mappings))
);
private final Multimap<Holder<Item>, GeyserMapping> mappings = MultimapBuilder.hashKeys().hashSetValues().build();
public GeyserMappings() {}
private GeyserMappings(Map<Holder<Item>, Collection<GeyserMapping>> mappings) {
for (Holder<Item> item : mappings.keySet()) {
this.mappings.putAll(item, mappings.get(item));
}
}
public void map(Holder<Item> item, GeyserItemDefinition mapping) {
Optional<ResourceLocation> model = mapping instanceof GeyserSingleDefinition single ? Optional.of(single.model().orElseThrow()) : Optional.empty();
Optional<GeyserGroupDefinition> modelGroup = Optional.empty();
Collection<GeyserMapping> existingMappings = new ArrayList<>(mappings.get(item));
for (GeyserMapping existing : existingMappings) {
if (existing instanceof GeyserGroupDefinition existingGroup && existingGroup.isFor(model)) {
if (existingGroup.conflictsWith(Optional.empty(), mapping)) {
throw new IllegalArgumentException("Mapping conflicts with existing group mapping");
}
modelGroup = Optional.of(existingGroup);
break;
} else if (existing instanceof GeyserItemDefinition itemDefinition) {
if (itemDefinition.conflictsWith(Optional.empty(), mapping)) {
throw new IllegalArgumentException("Mapping conflicts with existing item mapping");
} else if (model.isPresent() && itemDefinition instanceof GeyserSingleDefinition single && model.get().equals(single.model().orElseThrow())) {
mappings.remove(item, itemDefinition);
modelGroup = Optional.of(new GeyserGroupDefinition(model, List.of(single.withoutModel())));
}
}
}
if (modelGroup.isPresent()) {
mappings.remove(item, modelGroup.get());
// We're only putting mappings in groups when they're single definitions - legacy mappings always go ungrouped
assert mapping instanceof GeyserSingleDefinition;
mappings.put(item, modelGroup.get().with(((GeyserSingleDefinition) mapping).withoutModel()));
} else {
mappings.put(item, mapping);
}
}
public int size() {
int totalSize = 0;
for (GeyserMapping mapping : mappings.values()) {
if (mapping instanceof GeyserGroupDefinition group) {
totalSize += group.size();
} else {
totalSize++;
}
}
return totalSize;
}
public Map<Holder<Item>, Collection<GeyserMapping>> mappings() {
return mappings.asMap();
}
}

View File

@@ -0,0 +1,35 @@
package org.geysermc.rainbow.mapping.geyser;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.resources.ResourceLocation;
import java.util.Optional;
public record GeyserSingleDefinition(GeyserBaseDefinition base, Optional<ResourceLocation> model) implements GeyserItemDefinition {
public static final MapCodec<GeyserSingleDefinition> CODEC = RecordCodecBuilder.mapCodec(instance ->
instance.group(
GeyserBaseDefinition.MAP_CODEC.forGetter(GeyserSingleDefinition::base),
ResourceLocation.CODEC.optionalFieldOf("model").forGetter(GeyserSingleDefinition::model)
).apply(instance, GeyserSingleDefinition::new)
);
@Override
public boolean conflictsWith(Optional<ResourceLocation> parentModel, GeyserItemDefinition other) {
if (other instanceof GeyserSingleDefinition otherSingle) {
ResourceLocation thisModel = model.or(() -> parentModel).orElseThrow();
ResourceLocation otherModel = otherSingle.model.or(() -> parentModel).orElseThrow();
return thisModel.equals(otherModel) && base.conflictsWith(other.base());
}
return false;
}
public GeyserSingleDefinition withoutModel() {
return new GeyserSingleDefinition(base, Optional.empty());
}
@Override
public Type type() {
return Type.SINGLE;
}
}

View File

@@ -0,0 +1,87 @@
package org.geysermc.rainbow.mapping.geyser.predicate;
import com.google.common.base.Suppliers;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.util.ExtraCodecs;
import net.minecraft.util.StringRepresentable;
import org.jetbrains.annotations.NotNull;
import java.util.function.Supplier;
public record GeyserConditionPredicate(Property property, boolean expected) implements GeyserPredicate {
public static final MapCodec<GeyserConditionPredicate> CODEC = RecordCodecBuilder.mapCodec(instance ->
instance.group(
Property.CODEC.forGetter(GeyserConditionPredicate::property),
Codec.BOOL.optionalFieldOf("expected", true).forGetter(GeyserConditionPredicate::expected)
).apply(instance, GeyserConditionPredicate::new)
);
public static final Property BROKEN = unit(Property.Type.BROKEN);
public static final Property DAMAGED = unit(Property.Type.DAMAGED);
public static final Property FISHING_ROD_CAST = unit(Property.Type.FISHING_ROD_CAST);
@Override
public Type type() {
return Type.CONDITION;
}
public interface Property {
MapCodec<Property> CODEC = Type.CODEC.dispatchMap("property", Property::type, Type::codec);
Type type();
enum Type implements StringRepresentable {
BROKEN("broken", () -> MapCodec.unit(GeyserConditionPredicate.BROKEN)),
DAMAGED("damaged", () -> MapCodec.unit(GeyserConditionPredicate.DAMAGED)),
CUSTOM_MODEL_DATA("custom_model_data", () -> CustomModelData.CODEC),
HAS_COMPONENT("has_component", () -> HasComponent.CODEC),
FISHING_ROD_CAST("fishing_rod_cast", () -> MapCodec.unit(GeyserConditionPredicate.FISHING_ROD_CAST));
public static final Codec<Type> CODEC = StringRepresentable.fromEnum(Type::values);
private final String name;
private final Supplier<MapCodec<? extends Property>> codec;
Type(String name, Supplier<MapCodec<? extends Property>> codec) {
this.name = name;
this.codec = Suppliers.memoize(codec::get);
}
public MapCodec<? extends Property> codec() {
return codec.get();
}
@Override
public @NotNull String getSerializedName() {
return name;
}
}
}
public record CustomModelData(int index) implements Property {
public static final MapCodec<CustomModelData> CODEC = ExtraCodecs.NON_NEGATIVE_INT.optionalFieldOf("index", 0).xmap(CustomModelData::new, CustomModelData::index);
@Override
public Type type() {
return Type.CUSTOM_MODEL_DATA;
}
}
public record HasComponent(DataComponentType<?> component) implements Property {
public static final MapCodec<HasComponent> CODEC = DataComponentType.CODEC.fieldOf("component").xmap(HasComponent::new, HasComponent::component);
@Override
public Type type() {
return Type.HAS_COMPONENT;
}
}
private static Property unit(Property.Type type) {
return () -> type;
}
}

View File

@@ -0,0 +1,103 @@
package org.geysermc.rainbow.mapping.geyser.predicate;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.util.ExtraCodecs;
import net.minecraft.util.StringRepresentable;
import net.minecraft.world.item.CrossbowItem;
import net.minecraft.world.item.equipment.trim.TrimMaterial;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.NotNull;
import java.util.function.Function;
public record GeyserMatchPredicate(MatchPredicateData data) implements GeyserPredicate {
public static final MapCodec<GeyserMatchPredicate> CODEC = MatchPredicateData.CODEC.xmap(GeyserMatchPredicate::new, GeyserMatchPredicate::data);
@Override
public Type type() {
return Type.MATCH;
}
public interface MatchPredicateData {
MapCodec<MatchPredicateData> CODEC = Type.CODEC.dispatchMap("property", MatchPredicateData::type, Type::codec);
Type type();
enum Type implements StringRepresentable {
CHARGE_TYPE("charge_type", ChargeType.CODEC),
TRIM_MATERIAL("trim_material", TrimMaterialData.CODEC),
CONTEXT_DIMENSION("context_dimension", ContextDimension.CODEC),
CUSTOM_MODEL_DATA("custom_model_data", CustomModelData.CODEC);
public static final Codec<Type> CODEC = StringRepresentable.fromEnum(Type::values);
private final String name;
private final MapCodec<? extends MatchPredicateData> codec;
Type(String name, MapCodec<? extends MatchPredicateData> codec) {
this.name = name;
this.codec = codec;
}
public MapCodec<? extends MatchPredicateData> codec() {
return codec;
}
@Override
public @NotNull String getSerializedName() {
return name;
}
}
}
public record ChargeType(CrossbowItem.ChargeType chargeType) implements MatchPredicateData {
public static final MapCodec<ChargeType> CODEC = simpleCodec(CrossbowItem.ChargeType.CODEC, ChargeType::chargeType, ChargeType::new);
@Override
public Type type() {
return Type.CHARGE_TYPE;
}
}
public record TrimMaterialData(ResourceKey<TrimMaterial> material) implements MatchPredicateData {
public static final MapCodec<TrimMaterialData> CODEC = simpleCodec(ResourceKey.codec(Registries.TRIM_MATERIAL), TrimMaterialData::material, TrimMaterialData::new);
@Override
public Type type() {
return Type.TRIM_MATERIAL;
}
}
public record ContextDimension(ResourceKey<Level> level) implements MatchPredicateData {
public static final MapCodec<ContextDimension> CODEC = simpleCodec(ResourceKey.codec(Registries.DIMENSION), ContextDimension::level, ContextDimension::new);
@Override
public Type type() {
return Type.CONTEXT_DIMENSION;
}
}
public record CustomModelData(String string, int index) implements MatchPredicateData {
public static final MapCodec<CustomModelData> CODEC = RecordCodecBuilder.mapCodec(instance ->
instance.group(
Codec.STRING.fieldOf("value").forGetter(CustomModelData::string),
ExtraCodecs.NON_NEGATIVE_INT.optionalFieldOf("index", 0).forGetter(CustomModelData::index)
).apply(instance, CustomModelData::new)
);
@Override
public Type type() {
return Type.CUSTOM_MODEL_DATA;
}
}
private static <P, T> MapCodec<P> simpleCodec(Codec<T> codec, Function<P, T> getter, Function<T, P> constructor) {
return codec.fieldOf("value").xmap(constructor, getter);
}
}

View File

@@ -0,0 +1,42 @@
package org.geysermc.rainbow.mapping.geyser.predicate;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import net.minecraft.util.ExtraCodecs;
import net.minecraft.util.StringRepresentable;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public interface GeyserPredicate {
Codec<GeyserPredicate> CODEC = Type.CODEC.dispatch(GeyserPredicate::type, Type::codec);
Codec<List<GeyserPredicate>> LIST_CODEC = ExtraCodecs.compactListCodec(CODEC);
Type type();
enum Type implements StringRepresentable {
CONDITION("condition", GeyserConditionPredicate.CODEC),
MATCH("match", GeyserMatchPredicate.CODEC),
RANGE_DISPATCH("range_dispatch", GeyserRangeDispatchPredicate.CODEC);
public static final Codec<Type> CODEC = StringRepresentable.fromEnum(Type::values);
private final String name;
private final MapCodec<? extends GeyserPredicate> codec;
Type(String name, MapCodec<? extends GeyserPredicate> codec) {
this.name = name;
this.codec = codec;
}
public MapCodec<? extends GeyserPredicate> codec() {
return codec;
}
@Override
public @NotNull String getSerializedName() {
return name;
}
}
}

View File

@@ -0,0 +1,93 @@
package org.geysermc.rainbow.mapping.geyser.predicate;
import com.google.common.base.Suppliers;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.util.ExtraCodecs;
import net.minecraft.util.StringRepresentable;
import org.jetbrains.annotations.NotNull;
import java.util.function.Supplier;
public record GeyserRangeDispatchPredicate(Property property, float threshold, float scale) implements GeyserPredicate {
public static final MapCodec<GeyserRangeDispatchPredicate> CODEC = RecordCodecBuilder.mapCodec(instance ->
instance.group(
Property.CODEC.forGetter(GeyserRangeDispatchPredicate::property),
Codec.FLOAT.fieldOf("threshold").forGetter(GeyserRangeDispatchPredicate::threshold),
Codec.FLOAT.fieldOf("scale").forGetter(GeyserRangeDispatchPredicate::scale)
).apply(instance, GeyserRangeDispatchPredicate::new)
);
public static final Property BUNDLE_FULLNESS = unit(Property.Type.BUNDLE_FULLNESS);
@Override
public Type type() {
return null;
}
public interface Property {
MapCodec<Property> CODEC = Type.CODEC.dispatchMap("property", Property::type, Type::codec);
Type type();
enum Type implements StringRepresentable {
BUNDLE_FULLNESS("bundle_fullness", () -> MapCodec.unit(GeyserRangeDispatchPredicate.BUNDLE_FULLNESS)),
DAMAGE("damage", () -> Damage.CODEC),
COUNT("count", () -> Count.CODEC),
CUSTOM_MODEL_DATA("custom_model_data", () -> CustomModelData.CODEC);
public static final Codec<Type> CODEC = StringRepresentable.fromEnum(Type::values);
private final String name;
private final Supplier<MapCodec<? extends Property>> codec;
Type(String name, Supplier<MapCodec<? extends Property>> codec) {
this.name = name;
this.codec = Suppliers.memoize(codec::get);
}
public MapCodec<? extends Property> codec() {
return codec.get();
}
@Override
public @NotNull String getSerializedName() {
return name;
}
}
}
public record Damage(boolean normalize) implements Property {
public static final MapCodec<Damage> CODEC = Codec.BOOL.fieldOf("normalize").xmap(Damage::new, Damage::normalize);
@Override
public Type type() {
return Type.DAMAGE;
}
}
public record Count(boolean normalize) implements Property {
public static final MapCodec<Count> CODEC = Codec.BOOL.fieldOf("normalize").xmap(Count::new, Count::normalize);
@Override
public Type type() {
return Type.COUNT;
}
}
public record CustomModelData(int index) implements Property {
public static final MapCodec<CustomModelData> CODEC = ExtraCodecs.NON_NEGATIVE_INT.optionalFieldOf("index", 0).xmap(CustomModelData::new, CustomModelData::index);
@Override
public Type type() {
return Type.CUSTOM_MODEL_DATA;
}
}
private static Property unit(Property.Type type) {
return () -> type;
}
}

View File

@@ -0,0 +1,13 @@
package org.geysermc.rainbow.mixin;
import net.minecraft.client.renderer.entity.EntityRenderDispatcher;
import net.minecraft.client.resources.model.EquipmentAssetManager;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(EntityRenderDispatcher.class)
public interface EntityRenderDispatcherAccessor {
@Accessor
EquipmentAssetManager getEquipmentAssets();
}

View File

@@ -0,0 +1,16 @@
package org.geysermc.rainbow.mixin;
import net.minecraft.client.gui.render.state.GuiItemRenderState;
import net.minecraft.client.gui.render.state.ScreenArea;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.Constant;
import org.spongepowered.asm.mixin.injection.ModifyConstant;
@Mixin(GuiItemRenderState.class)
public abstract class GuiItemRenderStateMixin implements ScreenArea {
@ModifyConstant(method = "calculateOversizedItemBounds", constant = @Constant(intValue = 16))
public int neverReturnNull(int i) {
return -1;
}
}

View File

@@ -0,0 +1,13 @@
package org.geysermc.rainbow.mixin;
import com.google.common.collect.BiMap;
import net.minecraft.util.ExtraCodecs;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(ExtraCodecs.LateBoundIdMapper.class)
public interface LateBoundIdMapperAccessor<I, V> {
@Accessor
BiMap<I, V> getIdToValue();
}

View File

@@ -0,0 +1,59 @@
package org.geysermc.rainbow.mixin;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.item.ClientItem;
import net.minecraft.client.resources.model.ClientItemInfoLoader;
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.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
@Mixin(ModelManager.class)
public abstract class ModelManagerMixin implements PreparableReloadListener, AutoCloseable, ResolvedModelAccessor {
@Unique
private Map<ResourceLocation, ResolvedModel> unbakedResolvedModels;
@Unique
private Map<ResourceLocation, ClientItem> clientItems;
@WrapOperation(method = "method_65753", at = @At(value = "INVOKE", target = "Ljava/util/concurrent/CompletableFuture;join()Ljava/lang/Object;", ordinal = 1))
private static Object setResolvedModels(CompletableFuture<?> instance, Operation<Object> original) {
Object resolved = original.call(instance);
try {
// Couldn't be bothered setting up access wideners, this resolves the second component of the ResolvedModels record, which is called "models"
// Ideally we'd somehow use the "this" instance, but that's not possible here since the lambda we inject into is a static one
((ModelManagerMixin) (Object) Minecraft.getInstance().getModelManager()).unbakedResolvedModels = (Map<ResourceLocation, ResolvedModel>) resolved.getClass().getRecordComponents()[1].getAccessor().invoke(resolved);
} catch (IllegalAccessException | InvocationTargetException | ClassCastException exception) {
throw new RuntimeException(exception);
}
return resolved;
}
@WrapOperation(method = "method_65753", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/ClientItemInfoLoader$LoadedClientInfos;contents()Ljava/util/Map;"))
private static Map<ResourceLocation, ClientItem> setClientItems(ClientItemInfoLoader.LoadedClientInfos instance, Operation<Map<ResourceLocation, ClientItem>> original) {
// Same note as above for not using "this"
ModelManagerMixin thiz = ((ModelManagerMixin) (Object) Minecraft.getInstance().getModelManager());
thiz.clientItems = original.call(instance);
return thiz.clientItems;
}
@Override
public Optional<ResolvedModel> rainbow$getResolvedModel(ResourceLocation location) {
return unbakedResolvedModels == null ? Optional.empty() : Optional.ofNullable(unbakedResolvedModels.get(location));
}
@Override
public Optional<ClientItem> rainbow$getClientItem(ResourceLocation location) {
return clientItems == null ? Optional.empty() : Optional.ofNullable(clientItems.get(location));
}
}

View File

@@ -0,0 +1,13 @@
package org.geysermc.rainbow.mixin;
import com.mojang.blaze3d.textures.GpuTexture;
import net.minecraft.client.gui.render.pip.PictureInPictureRenderer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(PictureInPictureRenderer.class)
public interface PictureInPictureRendererAccessor {
@Accessor
GpuTexture getTexture();
}

View File

@@ -0,0 +1,34 @@
package org.geysermc.rainbow.mixin;
import com.mojang.blaze3d.textures.GpuTexture;
import net.minecraft.client.gui.render.pip.PictureInPictureRenderer;
import org.geysermc.rainbow.render.PictureInPictureCopyRenderer;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.Constant;
import org.spongepowered.asm.mixin.injection.ModifyConstant;
@Mixin(PictureInPictureRenderer.class)
public abstract class PictureInPictureRendererMixin implements AutoCloseable, PictureInPictureCopyRenderer {
@Shadow
private @Nullable GpuTexture texture;
@Unique
private boolean allowTextureCopy = false;
@Override
public void rainbow$allowTextureCopy() {
if (texture != null) {
throw new IllegalStateException("texture already created");
}
allowTextureCopy = true;
}
@ModifyConstant(method = "prepareTexturesAndProjection", constant = @Constant(intValue = 12))
public int allowUsageCopySrc(int usage) {
return allowTextureCopy ? usage | GpuTexture.USAGE_COPY_SRC : usage;
}
}

View File

@@ -0,0 +1,14 @@
package org.geysermc.rainbow.mixin;
import net.minecraft.client.renderer.item.RangeSelectItemModel;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Invoker;
@Mixin(RangeSelectItemModel.class)
public interface RangeSelectItemModelAccessor {
@Invoker
static int invokeLastIndexLessOrEqual(float[] thresholds, float value) {
throw new AssertionError();
}
}

View File

@@ -0,0 +1,12 @@
package org.geysermc.rainbow.mixin;
import net.minecraft.client.gui.components.SplashRenderer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(SplashRenderer.class)
public interface SplashRendererAccessor {
@Accessor
String getSplash();
}

View File

@@ -0,0 +1,15 @@
package org.geysermc.rainbow.mixin;
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 java.util.Map;
@Mixin(TextureSlots.class)
public interface TextureSlotsAccessor {
@Accessor
Map<String, Material> getResolvedValues();
}

View File

@@ -0,0 +1,27 @@
package org.geysermc.rainbow.pack;
import net.minecraft.resources.ResourceLocation;
import org.geysermc.rainbow.Rainbow;
import org.geysermc.rainbow.pack.animation.BedrockAnimation;
import org.geysermc.rainbow.pack.attachable.BedrockAttachable;
import org.geysermc.rainbow.pack.geometry.BedrockGeometry;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;
public record BedrockItem(ResourceLocation identifier, String textureName, ResourceLocation texture, boolean exportTexture, Optional<BedrockAttachable> attachable,
Optional<BedrockGeometry> geometry, Optional<BedrockAnimation> animation) {
public void save(Path attachableDirectory, Path geometryDirectory, Path animationDirectory) throws IOException {
if (attachable.isPresent()) {
attachable.get().save(attachableDirectory);
}
if (geometry.isPresent()) {
geometry.get().save(geometryDirectory);
}
if (animation.isPresent()) {
animation.get().save(animationDirectory, Rainbow.fileSafeResourceLocation(identifier));
}
}
}

View File

@@ -0,0 +1,259 @@
package org.geysermc.rainbow.pack;
import com.mojang.serialization.JsonOps;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.SplashRenderer;
import net.minecraft.core.component.DataComponents;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.ProblemReporter;
import net.minecraft.util.RandomSource;
import net.minecraft.util.StringUtil;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.component.CustomModelData;
import org.apache.commons.io.IOUtils;
import org.geysermc.rainbow.CodecUtil;
import org.geysermc.rainbow.PackConstants;
import org.geysermc.rainbow.Rainbow;
import org.geysermc.rainbow.mapping.BedrockItemMapper;
import org.geysermc.rainbow.mapping.PackContext;
import org.geysermc.rainbow.mapping.geyser.GeyserMappings;
import org.geysermc.rainbow.mixin.SplashRendererAccessor;
import org.jetbrains.annotations.NotNull;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
public class BedrockPack {
private static final List<String> PACK_SUMMARY_COMMENTS = List.of("Use the custom item API v2 build!", "bugrock moment", "RORY",
"use !!plshelp", "rm -rf --no-preserve-root /*", "welcome to the internet!", "beep beep. boop boop?", "FROG", "it is frog day", "it is cat day!",
"eclipse will hear about this.", "you must now say the word 'frog' in the #general channel", "You Just Lost The Game", "you are now breathing manually",
"you are now blinking manually", "you're eligible for a free hug token! <3", "don't mind me!", "hissss", "Gayser and Floodgayte, my favourite plugins.",
"meow", "we'll be done here soon™", "got anything else to say?", "we're done now!", "this will be fixed by v6053", "expect it to be done within 180 business days!",
"any colour you like", "someone tell Mojang about this", "you can't unbake baked models, so we'll store the unbaked models", "soon fully datagen ready",
"packconverter when", "codecs ftw");
private static final RandomSource RANDOM = RandomSource.create();
private static final Path EXPORT_DIRECTORY = FabricLoader.getInstance().getGameDir().resolve(Rainbow.MOD_ID);
private static final Path PACK_DIRECTORY = Path.of("pack");
private static final Path ATTACHABLES_DIRECTORY = Path.of("attachables");
private static final Path GEOMETRY_DIRECTORY = Path.of("models/entity");
private static final Path ANIMATION_DIRECTORY = Path.of("animations");
private static final Path MAPPINGS_FILE = Path.of("geyser_mappings.json");
private static final Path MANIFEST_FILE = Path.of("manifest.json");
private static final Path ITEM_ATLAS_FILE = Path.of("textures/item_texture.json");
private static final Path PACK_ZIP_FILE = Path.of("pack.zip");
private static final Path REPORT_FILE = Path.of("report.txt");
private final String name;
private final Path exportPath;
private final Path packPath;
private final PackManifest manifest;
private final GeyserMappings mappings;
private final BedrockTextures.Builder itemTextures;
private final Set<BedrockItem> bedrockItems = new HashSet<>();
private final Set<ResourceLocation> texturesToExport = new HashSet<>();
private final Set<ResourceLocation> modelsMapped = new HashSet<>();
private final IntSet customModelDataMapped = new IntOpenHashSet();
private final ProblemReporter.Collector reporter;
public BedrockPack(String name) throws IOException {
this.name = name;
// Not reading existing item mappings/texture atlas for now since that doesn't work all that well yet
exportPath = createPackDirectory(name);
packPath = exportPath.resolve(PACK_DIRECTORY);
//mappings = CodecUtil.readOrCompute(GeyserMappings.CODEC, exportPath.resolve(MAPPINGS_FILE), GeyserMappings::new);
mappings = new GeyserMappings();
manifest = CodecUtil.readOrCompute(PackManifest.CODEC, packPath.resolve(MANIFEST_FILE), () -> defaultManifest(name)).increment();
/*itemTextures = CodecUtil.readOrCompute(BedrockTextureAtlas.ITEM_ATLAS_CODEC, packPath.resolve(ITEM_ATLAS_FILE),
() -> BedrockTextureAtlas.itemAtlas(name, BedrockTextures.builder())).textures().toBuilder();*/
itemTextures = BedrockTextures.builder();
reporter = new ProblemReporter.Collector(() -> "Bedrock pack " + name + " ");
}
public String name() {
return name;
}
public MappingResult map(ItemStack stack) {
if (stack.isEmpty()) {
return MappingResult.NONE_MAPPED;
}
AtomicBoolean problems = new AtomicBoolean();
ProblemReporter mapReporter = new ProblemReporter() {
@Override
public @NotNull ProblemReporter forChild(PathElement child) {
return reporter.forChild(child);
}
@Override
public void report(Problem problem) {
problems.set(true);
reporter.report(problem);
}
};
PackContext context = new PackContext(mappings, packPath, item -> {
itemTextures.withItemTexture(item);
if (item.exportTexture()) {
texturesToExport.add(item.texture());
}
bedrockItems.add(item);
}, texturesToExport::add);
Optional<? 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 boolean save() {
boolean success = true;
try {
CodecUtil.trySaveJson(GeyserMappings.CODEC, mappings, exportPath.resolve(MAPPINGS_FILE), RegistryOps.create(JsonOps.INSTANCE, Minecraft.getInstance().level.registryAccess()));
CodecUtil.trySaveJson(PackManifest.CODEC, manifest, packPath.resolve(MANIFEST_FILE));
CodecUtil.trySaveJson(BedrockTextureAtlas.CODEC, BedrockTextureAtlas.itemAtlas(name, itemTextures), packPath.resolve(ITEM_ATLAS_FILE));
} catch (IOException | NullPointerException exception) {
reporter.forChild(() -> "saving Geyser mappings, pack manifest, and texture atlas ").report(() -> "failed to save to pack: " + exception);
success = false;
}
for (BedrockItem item : bedrockItems) {
try {
item.save(packPath.resolve(ATTACHABLES_DIRECTORY), packPath.resolve(GEOMETRY_DIRECTORY), packPath.resolve(ANIMATION_DIRECTORY));
} catch (IOException exception) {
reporter.forChild(() -> "files for bedrock item " + item.identifier() + " ").report(() -> "failed to save to pack: " + exception);
success = false;
}
}
for (ResourceLocation texture : texturesToExport) {
texture = texture.withPath(path -> "textures/" + path + ".png");
try (InputStream inputTexture = Minecraft.getInstance().getResourceManager().open(texture)) {
Path texturePath = packPath.resolve(texture.getPath());
CodecUtil.ensureDirectoryExists(texturePath.getParent());
try (OutputStream outputTexture = new FileOutputStream(texturePath.toFile())) {
IOUtils.copy(inputTexture, outputTexture);
}
} catch (IOException exception) {
ResourceLocation finalTexture = texture;
reporter.forChild(() -> "texture " + finalTexture + " ").report(() -> "failed to save to pack: " + exception);
success = false;
}
}
try {
CodecUtil.tryZipDirectory(packPath, exportPath.resolve(PACK_ZIP_FILE));
} catch (IOException exception) {
success = false;
}
try {
Files.writeString(exportPath.resolve(REPORT_FILE), createPackSummary());
} catch (IOException exception) {
// TODO log
}
return success;
}
public Path getExportPath() {
return exportPath;
}
private String createPackSummary() {
String problems = reporter.getTreeReport();
if (StringUtil.isBlank(problems)) {
problems = "Well that's odd... there's nothing here!";
}
long attachables = bedrockItems.stream().filter(item -> item.attachable().isPresent()).count();
long geometries = bedrockItems.stream().filter(item -> item.geometry().isPresent()).count();
long animations = bedrockItems.stream().filter(item -> item.animation().isPresent()).count();
return """
-- PACK GENERATION REPORT --
// %s
Generated pack: %s
Mappings written: %d
Item texture atlas size: %d
Attachables tried to export: %d
Geometry files tried to export: %d
Animations tried to export: %d
Textures tried to export: %d
-- MAPPING TREE REPORT --
%s
""".formatted(randomSummaryComment(), name, mappings.size(), itemTextures.build().size(),
attachables, geometries, animations, texturesToExport.size(), problems);
}
private static String randomSummaryComment() {
if (RANDOM.nextDouble() < 0.6) {
SplashRenderer splash = Minecraft.getInstance().getSplashManager().getSplash();
if (splash == null) {
return "Undefined Undefined :(";
}
return ((SplashRendererAccessor) splash).getSplash();
}
return randomBuiltinSummaryComment();
}
private static String randomBuiltinSummaryComment() {
return PACK_SUMMARY_COMMENTS.get(RANDOM.nextInt(PACK_SUMMARY_COMMENTS.size()));
}
private static Path createPackDirectory(String name) throws IOException {
Path path = EXPORT_DIRECTORY.resolve(name);
CodecUtil.ensureDirectoryExists(path);
return path;
}
private static PackManifest defaultManifest(String name) {
return new PackManifest(new PackManifest.Header(name, PackConstants.DEFAULT_PACK_DESCRIPTION, UUID.randomUUID(), BedrockVersion.of(0), PackConstants.ENGINE_VERSION),
List.of(new PackManifest.Module(name, PackConstants.DEFAULT_PACK_DESCRIPTION, UUID.randomUUID(), BedrockVersion.of(0))));
}
public enum MappingResult {
NONE_MAPPED,
MAPPED_SUCCESSFULLY,
PROBLEMS_OCCURRED
}
}

View File

@@ -0,0 +1,26 @@
package org.geysermc.rainbow.pack;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.mojang.serialization.codecs.RecordCodecBuilder;
public record BedrockTextureAtlas(String resourcePackName, String atlasName, BedrockTextures textures) {
public static final String ITEM_ATLAS = "atlas.items";
public static final Codec<BedrockTextureAtlas> CODEC = RecordCodecBuilder.create(instance ->
instance.group(
Codec.STRING.fieldOf("resource_pack_name").forGetter(BedrockTextureAtlas::resourcePackName),
Codec.STRING.fieldOf("texture_name").forGetter(BedrockTextureAtlas::atlasName),
BedrockTextures.CODEC.fieldOf("texture_data").forGetter(BedrockTextureAtlas::textures)
).apply(instance, BedrockTextureAtlas::new)
);
public static final Codec<BedrockTextureAtlas> ITEM_ATLAS_CODEC = CODEC.validate(atlas -> {
if (!ITEM_ATLAS.equals(atlas.atlasName)) {
return DataResult.error(() -> "Expected atlas to be " + ITEM_ATLAS + ", got " + atlas.atlasName);
}
return DataResult.success(atlas);
});
public static BedrockTextureAtlas itemAtlas(String resourcePackName, BedrockTextures.Builder textures) {
return new BedrockTextureAtlas(resourcePackName, ITEM_ATLAS, textures.build());
}
}

View File

@@ -0,0 +1,58 @@
package org.geysermc.rainbow.pack;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.Codec;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public record BedrockTextures(Map<String, String> textures) {
public static final Codec<BedrockTextures> CODEC =
Codec.compoundList(Codec.STRING, Codec.compoundList(Codec.STRING, Codec.STRING))
.xmap(pairs -> pairs.stream().map(pair -> Pair.of(pair.getFirst(), pair.getSecond().getFirst().getSecond())).collect(Pair.toMap()),
map -> map.entrySet().stream().map(entry -> Pair.of(entry.getKey(), List.of(Pair.of("textures", entry.getValue())))).toList())
.xmap(BedrockTextures::new, BedrockTextures::textures);
public static final String TEXTURES_FOLDER = "textures/";
public Builder toBuilder() {
Builder builder = builder();
builder.textures.putAll(textures);
return builder;
}
public int size() {
return textures.size();
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private final Map<String, String> textures = new HashMap<>();
public Builder withItemTexture(BedrockItem item) {
return withTexture(item.textureName(), TEXTURES_FOLDER + item.texture().getPath());
}
public Builder withTexture(String name, String texture) {
if (name.isEmpty()) {
throw new IllegalArgumentException("Name cannot be empty");
}
String currentTexture = textures.get(name);
if (currentTexture != null) {
if (!texture.equals(currentTexture)) {
throw new IllegalArgumentException("Texture conflict (name=" + name + ", existing=" + currentTexture + ", new=" + texture + ")");
}
} else {
textures.put(name, texture);
}
return this;
}
public BedrockTextures build() {
return new BedrockTextures(Map.copyOf(textures));
}
}
}

View File

@@ -0,0 +1,42 @@
package org.geysermc.rainbow.pack;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import java.util.List;
public record BedrockVersion(int major, int minor, int patch) {
public static final Codec<BedrockVersion> CODEC = Codec.INT.listOf(3, 3)
.xmap(list -> BedrockVersion.of(list.getFirst(), list.get(1), list.getLast()),
version -> List.of(version.major, version.minor, version.patch));
public static final Codec<BedrockVersion> STRING_CODEC = Codec.STRING.comapFlatMap(string -> {
String[] segments = string.split("\\.");
if (segments.length != 3) {
return DataResult.error(() -> "Semantic version must consist of 3 versions");
}
try {
int major = Integer.parseInt(segments[0]);
int minor = Integer.parseInt(segments[1]);
int patch = Integer.parseInt(segments[2]);
return DataResult.success(new BedrockVersion(major, minor, patch));
} catch (NumberFormatException exception) {
return DataResult.error(() -> "Failed to parse semantic version number");
}
}, version -> String.format("%d.%d.%d", version.major, version.minor, version.patch));
public static BedrockVersion of(int patch) {
return of(0, 0, patch);
}
public static BedrockVersion of(int minor, int patch) {
return of(0, minor, patch);
}
public static BedrockVersion of(int major, int minor, int patch) {
return new BedrockVersion(major, minor, patch);
}
public BedrockVersion increment() {
return new BedrockVersion(major, minor, patch + 1);
}
}

View File

@@ -0,0 +1,60 @@
package org.geysermc.rainbow.pack;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.core.UUIDUtil;
import org.geysermc.rainbow.CodecUtil;
import java.util.List;
import java.util.UUID;
// TODO metadata
public record PackManifest(Header header, List<Module> modules) {
public static final Codec<PackManifest> CODEC = RecordCodecBuilder.create(instance ->
instance.group(
CodecUtil.unitVerifyCodec(Codec.INT, "format_version", 2),
Header.CODEC.fieldOf("header").forGetter(PackManifest::header),
Module.CODEC.listOf().fieldOf("modules").forGetter(PackManifest::modules)
).apply(instance, (formatVersion, header, modules) -> new PackManifest(header, modules))
);
public PackManifest increment() {
return new PackManifest(header.increment(), modules.stream().map(Module::increment).toList());
}
public record Header(String name, String description, UUID uuid, BedrockVersion version, BedrockVersion minEngineVersion) {
public static final MapCodec<Header> MAP_CODEC = RecordCodecBuilder.mapCodec(instance ->
instance.group(
Codec.STRING.fieldOf("name").forGetter(Header::name),
Codec.STRING.fieldOf("description").forGetter(Header::description),
UUIDUtil.STRING_CODEC.fieldOf("uuid").forGetter(Header::uuid),
BedrockVersion.CODEC.fieldOf("version").forGetter(Header::version),
BedrockVersion.CODEC.fieldOf("min_engine_version").forGetter(Header::minEngineVersion)
).apply(instance, Header::new)
);
public static final Codec<Header> CODEC = MAP_CODEC.codec();
public Header increment() {
return new Header(name, description, uuid, version.increment(), minEngineVersion);
}
}
public record Module(String name, String description, UUID uuid, BedrockVersion version) {
public static final Codec<Module> CODEC = RecordCodecBuilder.create(instance ->
instance.group(
CodecUtil.unitVerifyCodec(Codec.STRING, "type", "resources"),
Codec.STRING.fieldOf("name").forGetter(Module::name),
Codec.STRING.fieldOf("description").forGetter(Module::description),
UUIDUtil.STRING_CODEC.fieldOf("uuid").forGetter(Module::uuid),
BedrockVersion.CODEC.fieldOf("version").forGetter(Module::version)
).apply(instance, (type, name, description, uuid, version) -> new Module(name, description, uuid, version))
);
public Module increment() {
return new Module(name, description, uuid, version.increment());
}
}
}

View File

@@ -0,0 +1,149 @@
package org.geysermc.rainbow.pack.animation;
import com.mojang.datafixers.util.Either;
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.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;
public record BedrockAnimation(BedrockVersion formatVersion, Map<String, AnimationDefinition> definitions) {
public static final BedrockVersion FORMAT_VERSION = BedrockVersion.of(1, 8, 0);
public static final Codec<BedrockAnimation> CODEC = RecordCodecBuilder.create(instance ->
instance.group(
BedrockVersion.STRING_CODEC.fieldOf("format_version").forGetter(BedrockAnimation::formatVersion),
Codec.unboundedMap(Codec.STRING, AnimationDefinition.CODEC).fieldOf("animations").forGetter(BedrockAnimation::definitions)
).apply(instance, BedrockAnimation::new)
);
public void save(Path animationDirectory, String identifier) throws IOException {
CodecUtil.trySaveJson(CODEC, this, animationDirectory.resolve(identifier + ".animation.json"));
}
public static Builder builder() {
return new Builder();
}
public static AnimationDefinition.Builder animation() {
return new AnimationDefinition.Builder();
}
public static class Builder {
private final Map<String, AnimationDefinition> animations = new HashMap<>();
public Builder withAnimation(String identifier, AnimationDefinition definition) {
animations.put("animation." + identifier, definition);
return this;
}
public Builder withAnimation(String identifier, AnimationDefinition.Builder builder) {
return withAnimation(identifier, builder.build());
}
public BedrockAnimation build() {
return new BedrockAnimation(FORMAT_VERSION, Map.copyOf(animations));
}
}
public record AnimationDefinition(LoopMode loopMode, Optional<String> startDelay, Optional<String> loopDelay, Optional<String> animationTimeUpdate,
Optional<String> blendWeight, boolean overridePreviousAnimation, Map<String, SimpleAnimation> bones) {
public static final Codec<AnimationDefinition> CODEC = RecordCodecBuilder.create(instance ->
instance.group(
LoopMode.CODEC.optionalFieldOf("loop", LoopMode.STOP).forGetter(AnimationDefinition::loopMode),
Codec.STRING.optionalFieldOf("start_delay").forGetter(AnimationDefinition::startDelay),
Codec.STRING.optionalFieldOf("loop_delay").forGetter(AnimationDefinition::loopDelay),
Codec.STRING.optionalFieldOf("anim_time_update").forGetter(AnimationDefinition::animationTimeUpdate),
Codec.STRING.optionalFieldOf("blend_weight").forGetter(AnimationDefinition::blendWeight),
Codec.BOOL.optionalFieldOf("override_previous_animation", false).forGetter(AnimationDefinition::overridePreviousAnimation),
Codec.unboundedMap(Codec.STRING, SimpleAnimation.CODEC).optionalFieldOf("bones", Map.of()).forGetter(AnimationDefinition::bones)
).apply(instance, AnimationDefinition::new)
);
public static class Builder {
private final Map<String, SimpleAnimation> bones = new HashMap<>();
private LoopMode loopMode = LoopMode.STOP;
private Optional<String> startDelay = Optional.empty();
private Optional<String> loopDelay = Optional.empty();
private Optional<String> animationTimeUpdate = Optional.empty();
private Optional<String> blendWeight = Optional.empty();
private boolean overridePreviousAnimation;
public Builder withLoopMode(LoopMode loopMode) {
this.loopMode = loopMode;
return this;
}
public Builder withStartDelay(String startDelay) {
this.startDelay = Optional.of(startDelay);
return this;
}
public Builder withLoopDelay(String loopDelay) {
this.loopDelay = Optional.of(loopDelay);
return this;
}
public Builder withAnimationTimeUpdate(String animationTimeUpdate) {
this.animationTimeUpdate = Optional.of(animationTimeUpdate);
return this;
}
public Builder withBlendWeight(String blendWeight) {
this.blendWeight = Optional.of(blendWeight);
return this;
}
public Builder overridePreviousAnimation() {
this.overridePreviousAnimation = true;
return this;
}
public Builder withBone(String bone, SimpleAnimation animation) {
bones.put(bone, animation);
return this;
}
public Builder withBone(String bone, Vector3fc position, Vector3fc rotation, Vector3fc scale) {
return withBone(bone, new SimpleAnimation(position, rotation, scale));
}
public AnimationDefinition build() {
return new AnimationDefinition(loopMode, startDelay, loopDelay, animationTimeUpdate, blendWeight, overridePreviousAnimation, Map.copyOf(bones));
}
}
}
public enum LoopMode {
STOP,
LOOP,
HOLD_ON_LAST_FRAME;
public static final Codec<LoopMode> CODEC = Codec.either(Codec.BOOL, Codec.STRING)
.comapFlatMap(either -> either.map(bool -> DataResult.success(bool ? LOOP : STOP),
string -> {
if (string.equals("hold_on_last_frame")) {
return DataResult.success(HOLD_ON_LAST_FRAME);
}
return DataResult.error(() -> "unknown loop mode");
}), mode -> mode == HOLD_ON_LAST_FRAME ? Either.right("hold_on_last_frame") : Either.left(mode == LOOP));
}
public record SimpleAnimation(Vector3fc position, Vector3fc rotation, Vector3fc scale) {
public static final Codec<SimpleAnimation> CODEC = RecordCodecBuilder.create(instance ->
instance.group(
CodecUtil.VECTOR3F_CODEC.fieldOf("position").forGetter(SimpleAnimation::position),
CodecUtil.VECTOR3F_CODEC.fieldOf("rotation").forGetter(SimpleAnimation::rotation),
CodecUtil.VECTOR3F_CODEC.fieldOf("scale").forGetter(SimpleAnimation::scale)
).apply(instance, SimpleAnimation::new)
);
}
}

View File

@@ -0,0 +1,215 @@
package org.geysermc.rainbow.pack.attachable;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.mojang.serialization.DynamicOps;
import com.mojang.serialization.codecs.RecordCodecBuilder;
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.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;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
public record BedrockAttachable(BedrockVersion formatVersion, AttachableInfo info) {
public static final Codec<BedrockAttachable> CODEC = RecordCodecBuilder.create(instance ->
instance.group(
BedrockVersion.STRING_CODEC.fieldOf("format_version").forGetter(BedrockAttachable::formatVersion),
AttachableInfo.CODEC.fieldOf("description").fieldOf("minecraft:attachable").forGetter(BedrockAttachable::info)
).apply(instance, BedrockAttachable::new)
);
public void save(Path attachablesDirectory) throws IOException {
// Get a safe attachable path by using Geyser's way of getting icons
CodecUtil.trySaveJson(CODEC, this, attachablesDirectory.resolve(Rainbow.fileSafeResourceLocation(info.identifier) + ".json"));
}
public static Builder builder(ResourceLocation identifier) {
return new Builder(identifier);
}
public static BedrockAttachable.Builder equipment(ResourceLocation identifier, EquipmentSlot slot, String texture) {
String script = switch (slot) {
case HEAD -> "v.helmet_layer_visible = 0.0;";
case CHEST -> "v.chest_layer_visible = 0.0;";
case LEGS -> "v.leg_layer_visible = 0.0;";
case FEET -> "v.boot_layer_visible = 0.0;";
default -> "";
};
return builder(identifier)
.withMaterial(DisplaySlot.DEFAULT, VanillaMaterials.ARMOR)
.withMaterial(DisplaySlot.ENCHANTED, VanillaMaterials.ARMOR_ENCHANTED)
.withTexture(DisplaySlot.DEFAULT, texture)
.withTexture(DisplaySlot.ENCHANTED, VanillaTextures.ENCHANTED_ACTOR_GLINT)
.withGeometry(DisplaySlot.DEFAULT, VanillaGeometries.fromEquipmentSlot(slot))
.withScript("parent_setup", script)
.withRenderController(VanillaRenderControllers.ARMOR);
}
public static BedrockAttachable.Builder geometry(ResourceLocation identifier, BedrockGeometry.GeometryDefinition geometry, String texture) {
return builder(identifier)
.withMaterial(DisplaySlot.DEFAULT, VanillaMaterials.ENTITY)
.withMaterial(DisplaySlot.ENCHANTED, VanillaMaterials.ENTITY_ALPHATEST_GLINT)
.withTexture(DisplaySlot.DEFAULT, texture)
.withTexture(DisplaySlot.ENCHANTED, VanillaTextures.ENCHANTED_ITEM_GLINT)
.withGeometry(DisplaySlot.DEFAULT, geometry.info().identifier())
.withRenderController(VanillaRenderControllers.ITEM_DEFAULT);
}
public static class Builder {
private final ResourceLocation identifier;
private final EnumMap<DisplaySlot, String> materials = new EnumMap<>(DisplaySlot.class);
private final EnumMap<DisplaySlot, String> textures = new EnumMap<>(DisplaySlot.class);
private final EnumMap<DisplaySlot, String> geometries = new EnumMap<>(DisplaySlot.class);
private final Map<String, String> animations = new HashMap<>();
private final Map<String, List<Script>> scripts = new HashMap<>();
private final List<String> renderControllers = new ArrayList<>();
public Builder(ResourceLocation identifier) {
this.identifier = identifier;
}
public Builder withMaterial(DisplaySlot slot, String material) {
materials.put(slot, material);
return this;
}
public Builder withTexture(DisplaySlot slot, String texture) {
textures.put(slot, BedrockTextures.TEXTURES_FOLDER + texture);
return this;
}
public Builder withGeometry(DisplaySlot slot, String geometry) {
geometries.put(slot, geometry);
return this;
}
public Builder withAnimation(String key, String animation) {
animations.put(key, animation);
return this;
}
public Builder withScript(String key, Script script) {
scripts.merge(key, List.of(script), (scripts, newScript) -> Stream.concat(scripts.stream(), newScript.stream()).toList());
return this;
}
public Builder withScript(String key, String script, String condition) {
return withScript(key, new Script(script, Optional.of(condition)));
}
public Builder withScript(String key, String script) {
return withScript(key, new Script(script, Optional.empty()));
}
public Builder withRenderController(String controller) {
renderControllers.add(controller);
return this;
}
public BedrockAttachable build() {
return new BedrockAttachable(PackConstants.ENGINE_VERSION,
new AttachableInfo(identifier, verifyDefault(materials), verifyDefault(textures), verifyDefault(geometries), Map.copyOf(animations),
new Scripts(Map.copyOf(scripts)), List.copyOf(renderControllers)));
}
private static DisplayMap verifyDefault(EnumMap<DisplaySlot, String> map) {
if (!map.containsKey(DisplaySlot.DEFAULT)) {
throw new IllegalStateException("DisplayMap must have a default key");
}
return new DisplayMap(map);
}
}
public record AttachableInfo(ResourceLocation identifier, DisplayMap materials, DisplayMap textures,
DisplayMap geometry, Map<String, String> animations, Scripts scripts,
List<String> renderControllers) {
private static final Codec<Map<String, String>> STRING_MAP_CODEC = Codec.unboundedMap(Codec.STRING, Codec.STRING);
public static final Codec<AttachableInfo> CODEC = RecordCodecBuilder.create(instance ->
instance.group(
ResourceLocation.CODEC.fieldOf("identifier").forGetter(AttachableInfo::identifier),
DisplayMap.CODEC.fieldOf("materials").forGetter(AttachableInfo::materials),
DisplayMap.CODEC.fieldOf("textures").forGetter(AttachableInfo::textures),
DisplayMap.CODEC.fieldOf("geometry").forGetter(AttachableInfo::geometry),
STRING_MAP_CODEC.optionalFieldOf("animations", Map.of()).forGetter(AttachableInfo::animations),
Scripts.CODEC.optionalFieldOf("scripts", Scripts.EMPTY).forGetter(AttachableInfo::scripts),
Codec.STRING.listOf().optionalFieldOf("render_controllers", List.of()).forGetter(AttachableInfo::renderControllers)
).apply(instance, AttachableInfo::new)
);
}
public record DisplayMap(EnumMap<DisplaySlot, String> map) {
public static final Codec<DisplayMap> CODEC = Codec.unboundedMap(DisplaySlot.CODEC, Codec.STRING)
.xmap(map -> map.isEmpty() ? new DisplayMap(new EnumMap<>(DisplaySlot.class)) : new DisplayMap(new EnumMap<>(map)), DisplayMap::map);
}
public enum DisplaySlot implements StringRepresentable {
DEFAULT("default"),
ENCHANTED("enchanted");
public static final Codec<DisplaySlot> CODEC = StringRepresentable.fromEnum(DisplaySlot::values);
private final String name;
DisplaySlot(String name) {
this.name = name;
}
@Override
public @NotNull String getSerializedName() {
return name;
}
}
public record Scripts(Map<String, List<Script>> scripts) {
public static final Codec<Scripts> CODEC = Codec.unboundedMap(Codec.STRING, ExtraCodecs.compactListCodec(Script.CODEC)).xmap(Scripts::new, Scripts::scripts);
public static final Scripts EMPTY = new Scripts(Map.of());
}
public record Script(String script, Optional<String> condition) {
private static final Codec<Script> SCRIPT_WITH_CONDITION_CODEC = Codec.unboundedMap(Codec.STRING, Codec.STRING).flatXmap(
scriptMap -> {
if (scriptMap.size() != 1) {
return DataResult.error(() -> "Script with condition must have exactly one key-value pair");
}
String script = scriptMap.keySet().iterator().next();
return DataResult.success(new Script(script, Optional.of(scriptMap.get(script))));
},
script -> script.condition.map(condition -> DataResult.success(Map.of(script.script, condition)))
.orElse(DataResult.error(() -> "Script must have a condition"))
);
public static final Codec<Script> CODEC = SCRIPT_WITH_CONDITION_CODEC.mapResult(new Codec.ResultFunction<Script>() {
@Override
public <T> DataResult<Pair<Script, T>> apply(DynamicOps<T> ops, T input, DataResult<Pair<Script, T>> decoded) {
if (decoded.isError()) {
return Codec.STRING.map(script -> new Script(script, Optional.empty())).decode(ops, input);
}
return decoded;
}
@Override
public <T> DataResult<T> coApply(DynamicOps<T> ops, Script input, DataResult<T> encoded) {
if (encoded.isError()) {
return Codec.STRING.encodeStart(ops, input.script);
}
return encoded;
}
});
}
}

View File

@@ -0,0 +1,20 @@
package org.geysermc.rainbow.pack.attachable;
import net.minecraft.world.entity.EquipmentSlot;
public class VanillaGeometries {
public static final String HELMET = "geometry.player.armor.helmet";
public static final String CHESTPLATE = "geometry.player.armor.chestplate";
public static final String LEGGINGS = "geometry.player.armor.leggings";
public static final String BOOTS = "geometry.player.armor.boots";
public static String fromEquipmentSlot(EquipmentSlot slot) {
return switch (slot) {
case FEET -> BOOTS;
case LEGS -> LEGGINGS;
case CHEST -> CHESTPLATE;
case HEAD -> HELMET;
default -> null;
};
}
}

View File

@@ -0,0 +1,8 @@
package org.geysermc.rainbow.pack.attachable;
public class VanillaMaterials {
public static final String ARMOR = "armor";
public static final String ARMOR_ENCHANTED = "armor_enchanted";
public static final String ENTITY = "entity";
public static final String ENTITY_ALPHATEST_GLINT = "entity_alphatest_glint";
}

View File

@@ -0,0 +1,6 @@
package org.geysermc.rainbow.pack.attachable;
public class VanillaRenderControllers {
public static final String ARMOR = "controller.render.armor";
public static final String ITEM_DEFAULT = "controller.render.item_default";
}

View File

@@ -0,0 +1,6 @@
package org.geysermc.rainbow.pack.attachable;
public class VanillaTextures {
public static final String ENCHANTED_ACTOR_GLINT = "misc/enchanted_actor_glint";
public static final String ENCHANTED_ITEM_GLINT = "misc/enchanted_item_glint";
}

View File

@@ -0,0 +1,286 @@
package org.geysermc.rainbow.pack.geometry;
import com.mojang.math.Quadrant;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.core.Direction;
import org.geysermc.rainbow.CodecUtil;
import org.geysermc.rainbow.pack.BedrockVersion;
import org.joml.Vector2fc;
import org.joml.Vector3f;
import org.joml.Vector3fc;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public record BedrockGeometry(BedrockVersion formatVersion, List<GeometryDefinition> definitions) {
public static final BedrockVersion FORMAT_VERSION = BedrockVersion.of(1, 21, 0);
// TODO move to util
public static final Vector3fc VECTOR3F_ZERO = new Vector3f();
public static final Codec<BedrockGeometry> CODEC = RecordCodecBuilder.create(instance ->
instance.group(
BedrockVersion.STRING_CODEC.fieldOf("format_version").forGetter(BedrockGeometry::formatVersion),
GeometryDefinition.CODEC.listOf(1, Integer.MAX_VALUE).fieldOf("minecraft:geometry").forGetter(BedrockGeometry::definitions)
).apply(instance, BedrockGeometry::new)
);
public void save(Path geometryDirectory) throws IOException {
CodecUtil.trySaveJson(CODEC, this, geometryDirectory.resolve(definitions.getFirst().info.identifier + ".geo.json"));
}
public static BedrockGeometry of(GeometryDefinition... definitions) {
return new BedrockGeometry(FORMAT_VERSION, Arrays.asList(definitions));
}
public static Builder builder(String identifier) {
return new Builder(identifier);
}
public static Bone.Builder bone(String name) {
return new Bone.Builder(name);
}
public static Cube.Builder cube(Vector3fc origin, Vector3fc size) {
return new Cube.Builder(origin, size);
}
public static class Builder {
private final String identifier;
private final List<Bone> bones = new ArrayList<>();
private Optional<Float> visibleBoundsWidth = Optional.empty();
private Optional<Float> visibleBoundsHeight = Optional.empty();
private Optional<Vector3fc> visibleBoundsOffset = Optional.empty();
private Optional<Integer> textureWidth = Optional.empty();
private Optional<Integer> textureHeight = Optional.empty();
public Builder(String identifier) {
this.identifier = "geometry." + identifier;
}
public Builder withVisibleBoundsWidth(float visibleBoundsWidth) {
this.visibleBoundsWidth = Optional.of(visibleBoundsWidth);
return this;
}
public Builder withVisibleBoundsHeight(float visibleBoundsHeight) {
this.visibleBoundsHeight = Optional.of(visibleBoundsHeight);
return this;
}
public Builder withVisibleBoundsOffset(Vector3f visibleBoundsOffset) {
this.visibleBoundsOffset = Optional.of(visibleBoundsOffset);
return this;
}
public Builder withTextureWidth(int textureWidth) {
this.textureWidth = Optional.of(textureWidth);
return this;
}
public Builder withTextureHeight(int textureHeight) {
this.textureHeight = Optional.of(textureHeight);
return this;
}
public Builder withBone(Bone bone) {
if (bones.stream().anyMatch(existing -> existing.name.equals(bone.name))) {
throw new IllegalArgumentException("Duplicate bone with name " + bone.name);
}
bones.add(bone);
return this;
}
public Builder withBone(Bone.Builder builder) {
return withBone(builder.build());
}
public BedrockGeometry build() {
return BedrockGeometry.of(new GeometryDefinition(
new GeometryInfo(identifier, visibleBoundsWidth, visibleBoundsHeight, visibleBoundsOffset, textureWidth, textureHeight), List.copyOf(bones)));
}
}
public record GeometryDefinition(GeometryInfo info, List<Bone> bones) {
public static final Codec<GeometryDefinition> CODEC = RecordCodecBuilder.create(instance ->
instance.group(
GeometryInfo.CODEC.fieldOf("description").forGetter(GeometryDefinition::info),
Bone.CODEC.listOf().optionalFieldOf("bones", List.of()).forGetter(GeometryDefinition::bones)
).apply(instance, GeometryDefinition::new)
);
}
public record GeometryInfo(String identifier, Optional<Float> visibleBoundsWidth, Optional<Float> visibleBoundsHeight, Optional<Vector3fc> visibleBoundsOffset,
Optional<Integer> textureWidth, Optional<Integer> textureHeight) {
public static final Codec<GeometryInfo> CODEC = RecordCodecBuilder.create(instance ->
instance.group(
Codec.STRING.fieldOf("identifier").forGetter(GeometryInfo::identifier),
Codec.FLOAT.optionalFieldOf("visible_bounds_width").forGetter(GeometryInfo::visibleBoundsWidth),
Codec.FLOAT.optionalFieldOf("visible_bounds_height").forGetter(GeometryInfo::visibleBoundsHeight),
CodecUtil.VECTOR3F_CODEC.optionalFieldOf("visible_bounds_offset").forGetter(GeometryInfo::visibleBoundsOffset),
Codec.INT.optionalFieldOf("texture_width").forGetter(GeometryInfo::textureWidth),
Codec.INT.optionalFieldOf("texture_height").forGetter(GeometryInfo::textureHeight)
).apply(instance, GeometryInfo::new)
);
}
public record Bone(String name, Optional<String> parent, Optional<String> binding, Vector3fc pivot, Vector3fc rotation,
boolean mirror, float inflate, List<Cube> cubes) {
public static final Codec<Bone> CODEC = RecordCodecBuilder.create(instance ->
instance.group(
Codec.STRING.fieldOf("name").forGetter(Bone::name),
Codec.STRING.optionalFieldOf("parent").forGetter(Bone::parent),
Codec.STRING.optionalFieldOf("binding").forGetter(Bone::binding),
CodecUtil.VECTOR3F_CODEC.optionalFieldOf("pivot", VECTOR3F_ZERO).forGetter(Bone::pivot),
CodecUtil.VECTOR3F_CODEC.optionalFieldOf("rotation", VECTOR3F_ZERO).forGetter(Bone::rotation),
Codec.BOOL.optionalFieldOf("mirror", false).forGetter(Bone::mirror),
Codec.FLOAT.optionalFieldOf("inflate", 0.0F).forGetter(Bone::inflate),
Cube.CODEC.listOf().optionalFieldOf("cubes", List.of()).forGetter(Bone::cubes)
).apply(instance, Bone::new)
);
public static class Builder {
private final String name;
private final List<Cube> cubes = new ArrayList<>();
private Optional<String> parent = Optional.empty();
private Optional<String> binding = Optional.empty();
private Vector3fc pivot = VECTOR3F_ZERO;
private Vector3fc rotation = VECTOR3F_ZERO;
private boolean mirror = false;
private float inflate = 0.0F;
public Builder(String name) {
this.name = name;
}
public Builder withParent(String parent) {
this.parent = Optional.of(parent);
return this;
}
public Builder withBinding(String binding) {
this.binding = Optional.of(binding);
return this;
}
public Builder withPivot(Vector3fc pivot) {
this.pivot = pivot;
return this;
}
public Builder withRotation(Vector3fc rotation) {
this.rotation = rotation;
return this;
}
public Builder mirror() {
this.mirror = true;
return this;
}
public Builder withInflate(float inflate) {
this.inflate = inflate;
return this;
}
public Builder withCube(Cube cube) {
this.cubes.add(cube);
return this;
}
public Builder withCube(Cube.Builder builder) {
return withCube(builder.build());
}
public Bone build() {
return new Bone(name, parent, binding, pivot, rotation, mirror, inflate, List.copyOf(cubes));
}
}
}
public record Cube(Vector3fc origin, Vector3fc size, Vector3fc rotation, Vector3fc pivot, float inflate, boolean mirror,
Map<Direction, Face> faces) {
private static final Codec<Map<Direction, Face>> FACE_MAP_CODEC = Codec.unboundedMap(Direction.CODEC, Face.CODEC);
public static final Codec<Cube> CODEC = RecordCodecBuilder.create(instance ->
instance.group(
CodecUtil.VECTOR3F_CODEC.fieldOf("origin").forGetter(Cube::origin),
CodecUtil.VECTOR3F_CODEC.fieldOf("size").forGetter(Cube::size),
CodecUtil.VECTOR3F_CODEC.optionalFieldOf("rotation", VECTOR3F_ZERO).forGetter(Cube::rotation),
CodecUtil.VECTOR3F_CODEC.optionalFieldOf("pivot", VECTOR3F_ZERO).forGetter(Cube::pivot),
Codec.FLOAT.optionalFieldOf("inflate", 0.0F).forGetter(Cube::inflate),
Codec.BOOL.optionalFieldOf("mirror", false).forGetter(Cube::mirror),
FACE_MAP_CODEC.optionalFieldOf("uv", Map.of()).forGetter(Cube::faces)
).apply(instance, Cube::new)
);
public static class Builder {
private final Vector3fc origin;
private final Vector3fc size;
private final Map<Direction, Face> faces = new HashMap<>();
private Vector3fc rotation = VECTOR3F_ZERO;
private Vector3fc pivot = VECTOR3F_ZERO;
private float inflate = 0.0F;
private boolean mirror = false;
public Builder(Vector3fc origin, Vector3fc size) {
this.origin = origin;
this.size = size;
}
public Builder withRotation(Vector3fc rotation) {
this.rotation = rotation;
return this;
}
public Builder withPivot(Vector3fc pivot) {
this.pivot = pivot;
return this;
}
public Builder withInflate(float inflate) {
this.inflate = inflate;
return this;
}
public Builder mirror() {
this.mirror = true;
return this;
}
public Builder withFace(Direction direction, Vector2fc uvOrigin, Vector2fc uvSize, Quadrant uvRotation) {
if (faces.containsKey(direction)) {
throw new IllegalArgumentException("Already added a face for direction " + direction);
}
faces.put(direction, new Face(uvOrigin, uvSize, uvRotation));
return this;
}
public Builder withFace(Direction direction, Vector2fc uvOrigin, Vector2fc uvSize) {
return withFace(direction, uvOrigin, uvSize, Quadrant.R0);
}
public Cube build() {
return new Cube(origin, size, rotation, pivot, inflate, mirror, Map.copyOf(faces));
}
}
}
public record Face(Vector2fc uvOrigin, Vector2fc uvSize, Quadrant uvRotation) {
public static final Codec<Face> CODEC = RecordCodecBuilder.create(instance ->
instance.group(
CodecUtil.VECTOR2F_CODEC.fieldOf("uv").forGetter(Face::uvOrigin),
CodecUtil.VECTOR2F_CODEC.fieldOf("uv_size").forGetter(Face::uvSize),
Quadrant.CODEC.optionalFieldOf("uv_rotation", Quadrant.R0).forGetter(Face::uvRotation)
).apply(instance, Face::new)
);
}
}

View File

@@ -0,0 +1,6 @@
package org.geysermc.rainbow.render;
public interface PictureInPictureCopyRenderer {
void rainbow$allowTextureCopy();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

View File

@@ -0,0 +1,17 @@
{
"chat.rainbow.automatic_mapping_finished": "Finished mapping items from provider",
"chat.rainbow.mapped_items": "Mapped %d items",
"commands.rainbow.automatic_inventory_mapping": "Now watching inventories for custom items to map",
"commands.rainbow.create_pack_failed": "Failed to create new pack!",
"commands.rainbow.mapped_held_item": "The held item was mapped",
"commands.rainbow.mapped_held_item_problems": "The held item was mapped, however problems occurred whilst doing so. Read the pack report after finishing the pack for more information",
"commands.rainbow.mapped_items_from_inventory": "Mapped %d items from your inventory",
"commands.rainbow.mapped_items_problems": "Problems occurred whilst mapping items. Read the pack report after finishing the pack for more information",
"commands.rainbow.no_item_mapped": "No item was mapped. Either no custom item was found, or it was already included in the pack",
"commands.rainbow.no_items_mapped": "No items were mapped. Either no custom items were found, or they were already included in the pack",
"commands.rainbow.no_pack": "Create a pack first: %s",
"commands.rainbow.pack_created": "Created pack with name %s",
"commands.rainbow.pack_finished_error": "Errors occurred whilst writing the pack to disk!",
"commands.rainbow.pack_finished_successfully": "Wrote pack to disk",
"commands.rainbow.stopped_automatic_mapping": "Stopped automatic mapping of custom items"
}

View File

@@ -0,0 +1,38 @@
{
"schemaVersion": 1,
"id": "rainbow",
"version": "${version}",
"name": "Rainbow",
"description": "Rainbow is a mod to generate Geyser item mappings and bedrock resourcepacks for use with Geyser's custom item API (v2)",
"authors": [
"GeyserMC contributors"
],
"contact": {
"homepage": "https://github.com/GeyserMC/rainbow",
"issues": "https://github.com/GeyserMC/rainbow/issues",
"sources": "https://github.com/GeyserMC/rainbow"
},
"license": "MIT",
"icon": "assets/rainbow/icon.png",
"environment": "client",
"entrypoints": {
"client": [
"org.geysermc.rainbow.Rainbow"
]
},
"mixins": [
"rainbow.mixins.json"
],
"depends": {
"fabricloader": ">=${loader_version}",
"fabric-api": "*",
"minecraft": "${supported_versions}"
},
"custom": {
"modmenu": {
"links": {
"modmenu.discord": "https://discord.gg/GeyserMC"
}
}
}
}

View File

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