diff --git a/.gitignore b/.gitignore index d5f737e..ec8202f 100644 --- a/.gitignore +++ b/.gitignore @@ -114,6 +114,7 @@ gradle-app.setting # Common working directory run/ runs/ +run-server/ # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) !gradle-wrapper.jar diff --git a/build.gradle b/build.gradle index 3927aa5..e1e20ae 100644 --- a/build.gradle +++ b/build.gradle @@ -74,3 +74,11 @@ publishing { repositories {} } + +loom { + runs { + named("server") { + runDir = "run-server" + } + } +} diff --git a/src/main/java/org/geysermc/packgenerator/CodecUtil.java b/src/main/java/org/geysermc/packgenerator/CodecUtil.java index fd4c95a..eca1855 100644 --- a/src/main/java/org/geysermc/packgenerator/CodecUtil.java +++ b/src/main/java/org/geysermc/packgenerator/CodecUtil.java @@ -11,6 +11,7 @@ import com.mojang.serialization.codecs.RecordCodecBuilder; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.function.Supplier; public class CodecUtil { @@ -19,12 +20,19 @@ public class CodecUtil { public static RecordCodecBuilder unitVerifyCodec(Codec codec, String field, T value) { return codec.validate(read -> { if (!read.equals(value)) { - return DataResult.error(() -> field + " must equal " + value); + return DataResult.error(() -> field + " must equal " + value + ", was " + read); } return DataResult.success(read); }).fieldOf(field).forGetter(object -> value); } + public static T readOrCompute(Codec codec, Path path, Supplier supplier) throws IOException { + if (Files.exists(path)) { + return tryReadJson(codec, path); + } + return supplier.get(); + } + public static T tryReadJson(Codec codec, Path path) throws IOException { try { String raw = Files.readString(path); diff --git a/src/main/java/org/geysermc/packgenerator/GeyserMappingsGenerator.java b/src/main/java/org/geysermc/packgenerator/GeyserMappingsGenerator.java index 5ae887f..587dc2c 100644 --- a/src/main/java/org/geysermc/packgenerator/GeyserMappingsGenerator.java +++ b/src/main/java/org/geysermc/packgenerator/GeyserMappingsGenerator.java @@ -15,7 +15,7 @@ import java.io.IOException; public class GeyserMappingsGenerator implements ClientModInitializer { public static final String MOD_ID = "geyser-mappings-generator"; - public static final String NAME = "Geyser Mappings Generator"; + public static final String MOD_NAME = "Geyser Mappings Generator"; public static final Logger LOGGER = LogUtils.getLogger(); @Override diff --git a/src/main/java/org/geysermc/packgenerator/PackConstants.java b/src/main/java/org/geysermc/packgenerator/PackConstants.java index ec94516..4d9643a 100644 --- a/src/main/java/org/geysermc/packgenerator/PackConstants.java +++ b/src/main/java/org/geysermc/packgenerator/PackConstants.java @@ -3,6 +3,6 @@ package org.geysermc.packgenerator; import org.geysermc.packgenerator.pack.BedrockVersion; public class PackConstants { - public static final String DEFAULT_PACK_DESCRIPTION = "A resourcepack generated by " + GeyserMappingsGenerator.NAME; + public static final String DEFAULT_PACK_DESCRIPTION = "A resourcepack generated by " + GeyserMappingsGenerator.MOD_NAME; public static final BedrockVersion ENGINE_VERSION = BedrockVersion.of(1, 21, 0); } diff --git a/src/main/java/org/geysermc/packgenerator/PackManager.java b/src/main/java/org/geysermc/packgenerator/PackManager.java index 63fe476..7ea8143 100644 --- a/src/main/java/org/geysermc/packgenerator/PackManager.java +++ b/src/main/java/org/geysermc/packgenerator/PackManager.java @@ -2,53 +2,33 @@ package org.geysermc.packgenerator; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; -import net.fabricmc.loader.api.FabricLoader; import net.minecraft.network.chat.Component; import net.minecraft.world.item.ItemStack; -import org.geysermc.packgenerator.mappings.GeyserMappings; -import org.geysermc.packgenerator.pack.BedrockVersion; -import org.geysermc.packgenerator.pack.PackManifest; +import org.geysermc.packgenerator.pack.BedrockPack; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.UUID; public final class PackManager { - private static final Path EXPORT_DIRECTORY = FabricLoader.getInstance().getGameDir().resolve("geyser"); - private static final Path PACK_DIRECTORY = Path.of("pack"); - - private static final Path MAPPINGS_FILE = Path.of("geyser_mappings.json"); - private static final Path MANIFEST_FILE = Path.of("manifest.json"); private static final PackManager INSTANCE = new PackManager(); - private String currentPackName; - private Path exportPath; - private Path packPath; - private GeyserMappings mappings; - private PackManifest manifest; + private BedrockPack currentPack; private PackManager() {} public void startPack(String name) throws CommandSyntaxException, IOException { - if (currentPackName != null) { - throw new SimpleCommandExceptionType(Component.literal("Already started a pack (" + currentPackName + ")")).create(); + if (currentPack != null) { + throw new SimpleCommandExceptionType(Component.literal("Already started a pack (" + currentPack.name() + ")")).create(); } - exportPath = createPackDirectory(name); - packPath = exportPath.resolve(PACK_DIRECTORY); - mappings = readOrCreateMappings(exportPath.resolve(MAPPINGS_FILE)); - manifest = readOrCreateManifest(name, packPath.resolve(MANIFEST_FILE)); - currentPackName = name; + currentPack = new BedrockPack(name); } public boolean map(ItemStack stack, boolean throwOnModelMissing) throws CommandSyntaxException { ensurePackIsCreated(); try { - mappings.map(stack); + currentPack.map(stack); return true; } catch (IllegalArgumentException exception) { if (throwOnModelMissing) { @@ -61,41 +41,16 @@ public final class PackManager { public void finish() throws CommandSyntaxException, IOException { ensurePackIsCreated(); - - CodecUtil.trySaveJson(GeyserMappings.CODEC, mappings, exportPath.resolve(MAPPINGS_FILE)); - CodecUtil.trySaveJson(PackManifest.CODEC, manifest, packPath.resolve(MANIFEST_FILE)); - - currentPackName = null; + currentPack.save(); + currentPack = null; } private void ensurePackIsCreated() throws CommandSyntaxException { - if (currentPackName == null) { + if (currentPack == null) { throw new SimpleCommandExceptionType(Component.literal("Create a new pack first!")).create(); } } - private static Path createPackDirectory(String name) throws IOException { - Path path = EXPORT_DIRECTORY.resolve(name); - CodecUtil.ensureDirectoryExists(path); - return path; - } - - private static GeyserMappings readOrCreateMappings(Path path) throws IOException { - if (Files.exists(path)) { - return CodecUtil.tryReadJson(GeyserMappings.CODEC, path); - } - return new GeyserMappings(); - } - - private static PackManifest readOrCreateManifest(String name, Path path) throws IOException { - if (Files.exists(path)) { - return CodecUtil.tryReadJson(PackManifest.CODEC, path).increment(); - } - return new PackManifest( - new PackManifest.Header(name, PackConstants.DEFAULT_PACK_DESCRIPTION, UUID.randomUUID(), BedrockVersion.of(1), PackConstants.ENGINE_VERSION), - List.of(new PackManifest.Module(name, PackConstants.DEFAULT_PACK_DESCRIPTION, UUID.randomUUID(), BedrockVersion.of(1)))); - } - public static PackManager getInstance() { return INSTANCE; } diff --git a/src/main/java/org/geysermc/packgenerator/mappings/GeyserMapping.java b/src/main/java/org/geysermc/packgenerator/mappings/GeyserMapping.java index 156f814..a91d036 100644 --- a/src/main/java/org/geysermc/packgenerator/mappings/GeyserMapping.java +++ b/src/main/java/org/geysermc/packgenerator/mappings/GeyserMapping.java @@ -45,16 +45,12 @@ public record GeyserMapping(ResourceLocation model, ResourceLocation bedrockIden -> new GeyserMapping(model, bedrockIdentifier, displayName, predicates, 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.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) - ).apply(instance, BedrockOptions::new) - ); - public static final BedrockOptions DEFAULT = new BedrockOptions(Optional.empty(), true, false, 0); + public String textureName() { + return bedrockOptions.icon.orElse(iconFromResourceLocation(bedrockIdentifier)); + } + + private static String iconFromResourceLocation(ResourceLocation location) { + return location.toString().replace(':', '.').replace('/', '_'); } public boolean conflictsWith(GeyserMapping other) { @@ -72,4 +68,16 @@ public record GeyserMapping(ResourceLocation model, ResourceLocation bedrockIden } return false; } + + 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.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) + ).apply(instance, BedrockOptions::new) + ); + public static final BedrockOptions DEFAULT = new BedrockOptions(Optional.empty(), true, false, 0); + } } diff --git a/src/main/java/org/geysermc/packgenerator/mappings/GeyserMappings.java b/src/main/java/org/geysermc/packgenerator/mappings/GeyserMappings.java index 07d93f1..caf0b2c 100644 --- a/src/main/java/org/geysermc/packgenerator/mappings/GeyserMappings.java +++ b/src/main/java/org/geysermc/packgenerator/mappings/GeyserMappings.java @@ -15,6 +15,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Map; import java.util.Optional; +import java.util.function.Consumer; import java.util.function.Function; // TODO group definitions @@ -47,7 +48,7 @@ public class GeyserMappings { mappings.put(item, mapping); } - public void map(ItemStack stack) { + public void map(ItemStack stack, Consumer mappingConsumer) { Optional patchedModel = stack.getComponentsPatch().get(DataComponents.ITEM_MODEL); //noinspection OptionalAssignedToNull - annoying Mojang if (patchedModel == null || patchedModel.isEmpty()) { @@ -59,7 +60,10 @@ public class GeyserMappings { int protectionValue = 0; // TODO check the attributes GeyserItemMapper.mapItem(model, displayName, protectionValue, stack.getComponentsPatch()) - .forEach(mapping -> map(stack.getItemHolder(), mapping)); + .forEach(mapping -> { + map(stack.getItemHolder(), mapping); + mappingConsumer.accept(mapping); + }); } public Map, Collection> mappings() { diff --git a/src/main/java/org/geysermc/packgenerator/pack/BedrockPack.java b/src/main/java/org/geysermc/packgenerator/pack/BedrockPack.java new file mode 100644 index 0000000..5351057 --- /dev/null +++ b/src/main/java/org/geysermc/packgenerator/pack/BedrockPack.java @@ -0,0 +1,63 @@ +package org.geysermc.packgenerator.pack; + +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.world.item.ItemStack; +import org.geysermc.packgenerator.CodecUtil; +import org.geysermc.packgenerator.PackConstants; +import org.geysermc.packgenerator.mappings.GeyserMappings; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.UUID; + +public class BedrockPack { + private static final Path EXPORT_DIRECTORY = FabricLoader.getInstance().getGameDir().resolve("geyser"); + private static final Path PACK_DIRECTORY = Path.of("pack"); + + 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 final String name; + private final Path exportPath; + private final Path packPath; + private final PackManifest manifest; + private final GeyserMappings mappings; + private final BedrockTextures.Builder itemTextures; + + public BedrockPack(String name) throws IOException { + this.name = name; + exportPath = createPackDirectory(name); + packPath = exportPath.resolve(PACK_DIRECTORY); + mappings = CodecUtil.readOrCompute(GeyserMappings.CODEC, exportPath.resolve(MAPPINGS_FILE), GeyserMappings::new); + 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(); + } + + public String name() { + return name; + } + + public void map(ItemStack stack) { + mappings.map(stack, mapping -> itemTextures.withItemTexture(mapping, mapping.bedrockIdentifier().getPath())); + } + + public void save() throws IOException { + CodecUtil.trySaveJson(GeyserMappings.CODEC, mappings, exportPath.resolve(MAPPINGS_FILE)); + CodecUtil.trySaveJson(PackManifest.CODEC, manifest, packPath.resolve(MANIFEST_FILE)); + CodecUtil.trySaveJson(BedrockTextureAtlas.CODEC, BedrockTextureAtlas.itemAtlas(name, itemTextures), packPath.resolve(ITEM_ATLAS_FILE)); + } + + 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)))); + } +} diff --git a/src/main/java/org/geysermc/packgenerator/pack/BedrockTextureAtlas.java b/src/main/java/org/geysermc/packgenerator/pack/BedrockTextureAtlas.java new file mode 100644 index 0000000..686a18f --- /dev/null +++ b/src/main/java/org/geysermc/packgenerator/pack/BedrockTextureAtlas.java @@ -0,0 +1,26 @@ +package org.geysermc.packgenerator.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 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 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()); + } +} diff --git a/src/main/java/org/geysermc/packgenerator/pack/BedrockTextures.java b/src/main/java/org/geysermc/packgenerator/pack/BedrockTextures.java new file mode 100644 index 0000000..3e14945 --- /dev/null +++ b/src/main/java/org/geysermc/packgenerator/pack/BedrockTextures.java @@ -0,0 +1,55 @@ +package org.geysermc.packgenerator.pack; + +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.Codec; +import org.geysermc.packgenerator.mappings.GeyserMapping; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public record BedrockTextures(Map textures) { + public static final Codec 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 Builder toBuilder() { + Builder builder = builder(); + builder.textures.putAll(textures); + return builder; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private static final String TEXTURES_FOLDER = "textures/"; + private final Map textures = new HashMap<>(); + + public Builder withItemTexture(GeyserMapping mapping, String texturePath) { + return withTexture(mapping.textureName(), TEXTURES_FOLDER + texturePath); + } + + 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)); + } + } +}