diff --git a/gradle.properties b/gradle.properties index 50453c9..f564519 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,14 +1,14 @@ org.gradle.jvmargs=-Xmx1G # Fabric Properties -minecraft_version=1.21.4 -parchment_version=1.21.4:2025.02.16 -loader_version=0.16.10 +minecraft_version=1.21.6 +parchment_version=1.21.5:2025.06.15 +loader_version=0.16.14 # Mod Properties -mod_version=0.0.1-1.21.4 +mod_version=0.0.1-1.21.6 maven_group=xyz.eclipseisoffline archives_base_name=geyser-mappings-generator # Dependencies -fabric_version=0.118.5+1.21.4 +fabric_version=0.128.1+1.21.6 diff --git a/src/main/java/xyz/eclipseisoffline/geyser/GeyserMapping.java b/src/main/java/xyz/eclipseisoffline/geyser/GeyserMapping.java new file mode 100644 index 0000000..6bd612b --- /dev/null +++ b/src/main/java/xyz/eclipseisoffline/geyser/GeyserMapping.java @@ -0,0 +1,32 @@ +package xyz.eclipseisoffline.geyser; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.component.DataComponentMap; +import net.minecraft.resources.ResourceLocation; + +import java.util.Optional; + +public record GeyserMapping(ResourceLocation model, ResourceLocation bedrockIdentifier, BedrockOptions bedrockOptions, DataComponentMap components) { + public static final Codec CODEC = RecordCodecBuilder.create(instance -> + instance.group( + Codec.STRING.fieldOf("type").forGetter(mapping -> "definition"), + ResourceLocation.CODEC.fieldOf("model").forGetter(GeyserMapping::model), + ResourceLocation.CODEC.fieldOf("bedrock_identifier").forGetter(GeyserMapping::bedrockIdentifier), + BedrockOptions.CODEC.fieldOf("bedrock_options").forGetter(GeyserMapping::bedrockOptions), + DataComponentMap.CODEC.fieldOf("components").forGetter(GeyserMapping::components) + ).apply(instance, (type, model, bedrockIdentifier, bedrockOptions, components) + -> new GeyserMapping(model, bedrockIdentifier, bedrockOptions, components)) + ); + + public record BedrockOptions(Optional icon, boolean allowOffhand, boolean displayHandheld, int protectionValue) { + public static final Codec CODEC = RecordCodecBuilder.create(instance -> + instance.group( + Codec.STRING.optionalFieldOf("icon").forGetter(BedrockOptions::icon), + Codec.BOOL.fieldOf("allow_offhand").forGetter(BedrockOptions::allowOffhand), + Codec.BOOL.fieldOf("display_handheld").forGetter(BedrockOptions::displayHandheld), + Codec.INT.fieldOf("protection_value").forGetter(BedrockOptions::protectionValue) + ).apply(instance, BedrockOptions::new) + ); + } +} diff --git a/src/main/java/xyz/eclipseisoffline/geyser/GeyserMappings.java b/src/main/java/xyz/eclipseisoffline/geyser/GeyserMappings.java new file mode 100644 index 0000000..08427e1 --- /dev/null +++ b/src/main/java/xyz/eclipseisoffline/geyser/GeyserMappings.java @@ -0,0 +1,43 @@ +package xyz.eclipseisoffline.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.world.item.Item; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; +import java.util.function.Function; + +public class GeyserMappings { + private static final Codec, Collection>> MAPPINGS_CODEC = Codec.unboundedMap(Item.CODEC, GeyserMapping.CODEC.listOf().xmap(Function.identity(), ArrayList::new)); + + public static final Codec CODEC = RecordCodecBuilder.create(instance -> + instance.group( + Codec.INT.fieldOf("format_version").forGetter(mappings -> 2), + MAPPINGS_CODEC.fieldOf("items").forGetter(GeyserMappings::mappings) + ).apply(instance, (format, mappings) -> new GeyserMappings(mappings)) + ); + + private final Multimap, GeyserMapping> mappings = MultimapBuilder.hashKeys().hashSetValues().build(); + + public GeyserMappings() {} + + private GeyserMappings(Map, Collection> mappings) { + for (Holder item : mappings.keySet()) { + this.mappings.putAll(item, mappings.get(item)); + } + } + + public void map(Holder item, GeyserMapping mapping) { + // TODO conflict detection + mappings.put(item, mapping); + } + + public Map, Collection> mappings() { + return mappings.asMap(); + } +} diff --git a/src/main/java/xyz/eclipseisoffline/geyser/GeyserMappingsGenerator.java b/src/main/java/xyz/eclipseisoffline/geyser/GeyserMappingsGenerator.java index baa3e9b..03b93a0 100644 --- a/src/main/java/xyz/eclipseisoffline/geyser/GeyserMappingsGenerator.java +++ b/src/main/java/xyz/eclipseisoffline/geyser/GeyserMappingsGenerator.java @@ -1,11 +1,48 @@ package xyz.eclipseisoffline.geyser; +import com.mojang.brigadier.arguments.StringArgumentType; +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.minecraft.network.chat.Component; +import net.minecraft.world.item.ItemStack; +import org.slf4j.Logger; public class GeyserMappingsGenerator implements ClientModInitializer { + public static final Logger LOGGER = LogUtils.getLogger(); + @Override public void onInitializeClient() { - + ClientCommandRegistrationCallback.EVENT.register((dispatcher, buildContext) -> { + dispatcher.register(ClientCommandManager.literal("geyser") + .then(ClientCommandManager.literal("create") + .then(ClientCommandManager.argument("name", StringArgumentType.word()) + .executes(context -> { + String name = StringArgumentType.getString(context, "name"); + PackManager.getInstance().startPack(name); + context.getSource().sendFeedback(Component.literal("Created pack with name " + name)); + return 0; + }) + ) + ) + .then(ClientCommandManager.literal("map") + .executes(context -> { + ItemStack heldItem = context.getSource().getPlayer().getMainHandItem(); + PackManager.getInstance().map(heldItem); + context.getSource().sendFeedback(Component.literal("Added held item to Geyser mappings")); + return 0; + }) + ) + .then(ClientCommandManager.literal("finish") + .executes(context -> { + PackManager.getInstance().finish(); + context.getSource().sendFeedback(Component.literal("Wrote pack to disk")); + return 0; + }) + ) + ); + }); } } diff --git a/src/main/java/xyz/eclipseisoffline/geyser/PackManager.java b/src/main/java/xyz/eclipseisoffline/geyser/PackManager.java new file mode 100644 index 0000000..027ef49 --- /dev/null +++ b/src/main/java/xyz/eclipseisoffline/geyser/PackManager.java @@ -0,0 +1,98 @@ +package xyz.eclipseisoffline.geyser; + +import com.google.gson.FormattingStyle; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.stream.JsonWriter; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import com.mojang.serialization.JsonOps; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.core.component.DataComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; + +public final class PackManager { + public static final Path EXPORT_DIRECTORY = FabricLoader.getInstance().getGameDir() + .resolve("geyser"); + + private static final PackManager INSTANCE = new PackManager(); + + private String currentPackName; + private Path exportPath; + private GeyserMappings mappings; + + private PackManager() {} + + public void startPack(String name) throws CommandSyntaxException { + if (currentPackName != null) { + throw new SimpleCommandExceptionType(Component.literal("Already started a pack with name " + currentPackName)).create(); + } + currentPackName = name; + exportPath = createPackDirectory(currentPackName); + mappings = new GeyserMappings(); + } + + public void map(ItemStack stack) throws CommandSyntaxException { + ensurePackIsCreated(); + + Optional patchedModel = stack.getComponentsPatch().get(DataComponents.ITEM_MODEL); + //noinspection OptionalAssignedToNull - annoying Mojang + if (patchedModel == null || patchedModel.isEmpty()) { + throw new SimpleCommandExceptionType(Component.literal("Item stack does not have a custom model")).create(); + } + + ResourceLocation model = patchedModel.get(); + GeyserMapping mapping = new GeyserMapping(model, model, + new GeyserMapping.BedrockOptions(Optional.empty(), true, false, 0), + stack.getComponentsPatch().split().added()); // TODO removed components + mappings.map(stack.getItemHolder(), mapping); + } + + public void finish() throws CommandSyntaxException { + ensurePackIsCreated(); + + JsonElement savedMappings = GeyserMappings.CODEC.encodeStart(JsonOps.INSTANCE, mappings) + .getOrThrow(error -> new SimpleCommandExceptionType(Component.literal("Failed to encode Geyser mappings! " + error)).create()); + + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + try { + Files.writeString(exportPath.resolve("geyser_mappings.json"), gson.toJson(savedMappings)); + } catch (IOException exception) { + GeyserMappingsGenerator.LOGGER.warn("Failed to write Geyser mappings to pack!", exception); + throw new SimpleCommandExceptionType(Component.literal("Failed to write Geyser mappings to pack!")).create(); + } + } + + private void ensurePackIsCreated() throws CommandSyntaxException { + if (currentPackName == null) { + throw new SimpleCommandExceptionType(Component.literal("Create a new pack first!")).create(); + } + } + + private static Path createPackDirectory(String name) throws CommandSyntaxException { + Path path = EXPORT_DIRECTORY.resolve(name); + if (!Files.isDirectory(path)) { + try { + Files.createDirectories(path); + } catch (IOException exception) { + GeyserMappingsGenerator.LOGGER.warn("Failed to create pack export directory!", exception); + throw new SimpleCommandExceptionType(Component.literal("Failed to create pack export directory for pack " + name)).create(); + } + } + return path; + } + + public static PackManager getInstance() { + return INSTANCE; + } +}