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:
3
rainbow/build.gradle.kts
Normal file
3
rainbow/build.gradle.kts
Normal file
@@ -0,0 +1,3 @@
|
||||
plugins {
|
||||
id("rainbow.base-conventions")
|
||||
}
|
||||
113
rainbow/src/main/java/org/geysermc/rainbow/CodecUtil.java
Normal file
113
rainbow/src/main/java/org/geysermc/rainbow/CodecUtil.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
rainbow/src/main/java/org/geysermc/rainbow/KeyUtil.java
Normal file
16
rainbow/src/main/java/org/geysermc/rainbow/KeyUtil.java
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
40
rainbow/src/main/java/org/geysermc/rainbow/PackManager.java
Normal file
40
rainbow/src/main/java/org/geysermc/rainbow/PackManager.java
Normal 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;
|
||||
}
|
||||
}
|
||||
116
rainbow/src/main/java/org/geysermc/rainbow/Rainbow.java
Normal file
116
rainbow/src/main/java/org/geysermc/rainbow/Rainbow.java
Normal 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('/', '_');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.geysermc.rainbow.mapping;
|
||||
|
||||
import org.geysermc.rainbow.pack.BedrockItem;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface BedrockItemConsumer {
|
||||
|
||||
void accept(BedrockItem item);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
259
rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockPack.java
Normal file
259
rainbow/src/main/java/org/geysermc/rainbow/pack/BedrockPack.java
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.geysermc.rainbow.render;
|
||||
|
||||
public interface PictureInPictureCopyRenderer {
|
||||
|
||||
void rainbow$allowTextureCopy();
|
||||
}
|
||||
BIN
rainbow/src/main/resources/assets/rainbow/icon.png
Normal file
BIN
rainbow/src/main/resources/assets/rainbow/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 282 B |
17
rainbow/src/main/resources/assets/rainbow/lang/en_us.json
Normal file
17
rainbow/src/main/resources/assets/rainbow/lang/en_us.json
Normal 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"
|
||||
}
|
||||
38
rainbow/src/main/resources/fabric.mod.json
Normal file
38
rainbow/src/main/resources/fabric.mod.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
rainbow/src/main/resources/rainbow.mixins.json
Normal file
21
rainbow/src/main/resources/rainbow.mixins.json
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user