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

Texture atlas generation, refactor pack managing

This commit is contained in:
Eclipse
2025-06-29 13:30:47 +00:00
parent 25c9ef6616
commit de4470d5f2
11 changed files with 197 additions and 69 deletions

1
.gitignore vendored
View File

@@ -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

View File

@@ -74,3 +74,11 @@ publishing {
repositories {}
}
loom {
runs {
named("server") {
runDir = "run-server"
}
}
}

View File

@@ -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 <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);
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 {
try {
String raw = Files.readString(path);

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -45,16 +45,12 @@ public record GeyserMapping(ResourceLocation model, ResourceLocation bedrockIden
-> new GeyserMapping(model, bedrockIdentifier, displayName, predicates, bedrockOptions, components))
);
public record BedrockOptions(Optional<String> icon, boolean allowOffhand, boolean displayHandheld, int protectionValue) {
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)
).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<String> icon, boolean allowOffhand, boolean displayHandheld, int protectionValue) {
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)
).apply(instance, BedrockOptions::new)
);
public static final BedrockOptions DEFAULT = new BedrockOptions(Optional.empty(), true, false, 0);
}
}

View File

@@ -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<GeyserMapping> mappingConsumer) {
Optional<? extends ResourceLocation> 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<Holder<Item>, Collection<GeyserMapping>> mappings() {

View File

@@ -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))));
}
}

View File

@@ -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<BedrockTextureAtlas> CODEC = RecordCodecBuilder.create(instance ->
instance.group(
Codec.STRING.fieldOf("resource_pack_name").forGetter(BedrockTextureAtlas::resourcePackName),
Codec.STRING.fieldOf("texture_name").forGetter(BedrockTextureAtlas::atlasName),
BedrockTextures.CODEC.fieldOf("texture_data").forGetter(BedrockTextureAtlas::textures)
).apply(instance, BedrockTextureAtlas::new)
);
public static final Codec<BedrockTextureAtlas> ITEM_ATLAS_CODEC = CODEC.validate(atlas -> {
if (!ITEM_ATLAS.equals(atlas.atlasName)) {
return DataResult.error(() -> "Expected atlas to be " + ITEM_ATLAS + ", got " + atlas.atlasName);
}
return DataResult.success(atlas);
});
public static BedrockTextureAtlas itemAtlas(String resourcePackName, BedrockTextures.Builder textures) {
return new BedrockTextureAtlas(resourcePackName, ITEM_ATLAS, textures.build());
}
}

View File

@@ -0,0 +1,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<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 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<String, String> 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));
}
}
}