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

Sort Geyser mappings, async texture stitching, fix datagen

This commit is contained in:
Eclipse
2025-10-16 07:38:59 +00:00
parent 0c4a877220
commit 7f67a40c13
17 changed files with 145 additions and 75 deletions

View File

@@ -87,9 +87,9 @@ public final class PackManager {
} }
Set<BedrockItem> bedrockItems = pack.getBedrockItems(); Set<BedrockItem> bedrockItems = pack.getBedrockItems();
long attachables = bedrockItems.stream().filter(item -> item.attachable().isPresent()).count(); //long attachables = bedrockItems.stream().filter(item -> item.attachableCreator().isPresent()).count();
long geometries = bedrockItems.stream().filter(item -> item.geometry().geometry().isPresent()).count(); long geometries = bedrockItems.stream().filter(item -> item.geometryContext().geometry().isPresent()).count();
long animations = bedrockItems.stream().filter(item -> item.geometry().animation().isPresent()).count(); long animations = bedrockItems.stream().filter(item -> item.geometryContext().animation().isPresent()).count();
return """ return """
-- PACK GENERATION REPORT -- -- PACK GENERATION REPORT --
@@ -98,15 +98,15 @@ public final class PackManager {
Generated pack: %s Generated pack: %s
Mappings written: %d Mappings written: %d
Item texture atlas size: %d Item texture atlas size: %d
Attachables tried to export: %d Attachables tried to export: FIXME
Geometry files tried to export: %d Geometry files tried to export: %d
Animations tried to export: %d Animations tried to export: %d
Textures tried to export: %d Textures tried to export: FIXME
-- MAPPING TREE REPORT -- -- MAPPING TREE REPORT --
%s %s
""".formatted(randomSummaryComment(), pack.name(), pack.getMappings(), pack.getItemTextureAtlasSize(), """.formatted(randomSummaryComment(), pack.name(), pack.getMappings(), pack.getItemTextureAtlasSize(),
attachables, geometries, animations, pack.getAdditionalExportedTextures(), problems); geometries, animations, problems);
} }
private static String randomSummaryComment() { private static String randomSummaryComment() {

View File

@@ -70,7 +70,7 @@ public abstract class RainbowModelProvider extends FabricModelProvider {
CompletableFuture<BedrockPack> bedrockPack = ClientPackLoader.openClientResources() CompletableFuture<BedrockPack> bedrockPack = ClientPackLoader.openClientResources()
.thenCompose(resourceManager -> registries.thenApply(registries -> { .thenCompose(resourceManager -> registries.thenApply(registries -> {
try (resourceManager) { try (resourceManager) {
BedrockPack pack = createBedrockPack(outputRoot, new Serializer(output, resourceManager, registries), BedrockPack pack = createBedrockPack(outputRoot, new Serializer(output, registries),
new DatagenResolver(resourceManager, equipmentInfos, itemInfos, models)).build(); new DatagenResolver(resourceManager, equipmentInfos, itemInfos, models)).build();
for (Item item : itemInfos.keySet()) { for (Item item : itemInfos.keySet()) {
@@ -105,7 +105,7 @@ public abstract class RainbowModelProvider extends FabricModelProvider {
this.models = models; this.models = models;
} }
private record Serializer(CachedOutput output, ResourceManager resourceManager, HolderLookup.Provider registries) implements PackSerializer { private record Serializer(CachedOutput output, HolderLookup.Provider registries) implements PackSerializer {
@Override @Override
public <T> CompletableFuture<?> saveJson(Codec<T> codec, T object, Path path) { public <T> CompletableFuture<?> saveJson(Codec<T> codec, T object, Path path) {
@@ -113,12 +113,10 @@ public abstract class RainbowModelProvider extends FabricModelProvider {
} }
@Override @Override
public CompletableFuture<?> saveTexture(ResourceLocation texture, Path path) { public CompletableFuture<?> saveTexture(byte[] texture, Path path) {
return CompletableFuture.runAsync(() -> { return CompletableFuture.runAsync(() -> {
ResourceLocation texturePath = texture.withPath(p -> "textures/" + p + ".png"); try {
try (InputStream inputTexture = resourceManager.open(texturePath)) { output.writeIfNeeded(path, texture, HashCode.fromBytes(texture));
byte[] textureBytes = inputTexture.readAllBytes();
output.writeIfNeeded(path, textureBytes, HashCode.fromBytes(textureBytes));
} catch (IOException exception) { } catch (IOException exception) {
LOGGER.error("Failed to save file to {}", path, exception); LOGGER.error("Failed to save file to {}", path, exception);
} }
@@ -181,5 +179,10 @@ public abstract class RainbowModelProvider extends FabricModelProvider {
public Optional<EquipmentClientInfo> getEquipmentInfo(ResourceKey<EquipmentAsset> key) { public Optional<EquipmentClientInfo> getEquipmentInfo(ResourceKey<EquipmentAsset> key) {
return Optional.ofNullable(equipmentInfos.get(key)); return Optional.ofNullable(equipmentInfos.get(key));
} }
@Override
public InputStream openAsset(ResourceLocation location) throws IOException {
return resourceManager.open(location);
}
} }
} }

View File

@@ -3,9 +3,12 @@ package org.geysermc.rainbow.definition;
import com.mojang.serialization.MapCodec; import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder; import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import org.jetbrains.annotations.NotNull;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream; import java.util.stream.Stream;
public record GeyserGroupDefinition(Optional<ResourceLocation> model, List<GeyserMapping> definitions) implements GeyserMapping { public record GeyserGroupDefinition(Optional<ResourceLocation> model, List<GeyserMapping> definitions) implements GeyserMapping {
@@ -18,7 +21,9 @@ public record GeyserGroupDefinition(Optional<ResourceLocation> model, List<Geyse
); );
public GeyserGroupDefinition with(GeyserMapping mapping) { public GeyserGroupDefinition with(GeyserMapping mapping) {
return new GeyserGroupDefinition(model, Stream.concat(definitions.stream(), Stream.of(mapping)).toList()); return new GeyserGroupDefinition(model, Stream.concat(definitions.stream(), Stream.of(mapping))
.sorted(Comparator.comparing(Function.identity()))
.toList());
} }
public boolean isFor(Optional<ResourceLocation> model) { public boolean isFor(Optional<ResourceLocation> model) {
@@ -53,4 +58,26 @@ public record GeyserGroupDefinition(Optional<ResourceLocation> model, List<Geyse
public Type type() { public Type type() {
return Type.GROUP; return Type.GROUP;
} }
@Override
public int compareTo(@NotNull GeyserMapping other) {
if (other instanceof GeyserGroupDefinition(Optional<ResourceLocation> otherModel, List<GeyserMapping> otherDefinitions)) {
if (model.isPresent() && otherModel.isPresent()) {
return model.get().compareTo(otherModel.get());
} else if (model.isPresent()) {
return 1; // Groups with models are always greater than groups without
} else if (otherModel.isPresent()) {
return -1;
} else if (definitions.isEmpty() && otherDefinitions.isEmpty()) {
return 0;
} else if (definitions.isEmpty()) {
return -1; // Groups with definitions are always greater than groups without
} else if (otherDefinitions.isEmpty()) {
return 1;
}
// Compare the first definition as a last resort
return definitions.getFirst().compareTo(otherDefinitions.getFirst());
}
return 1; // Groups are always greater than individual mappings
}
} }

View File

@@ -1,6 +1,7 @@
package org.geysermc.rainbow.definition; package org.geysermc.rainbow.definition;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import org.jetbrains.annotations.NotNull;
import java.util.Optional; import java.util.Optional;
@@ -9,4 +10,12 @@ public interface GeyserItemDefinition extends GeyserMapping {
GeyserBaseDefinition base(); GeyserBaseDefinition base();
boolean conflictsWith(Optional<ResourceLocation> parentModel, GeyserItemDefinition other); boolean conflictsWith(Optional<ResourceLocation> parentModel, GeyserItemDefinition other);
@Override
default int compareTo(@NotNull GeyserMapping other) {
if (other instanceof GeyserItemDefinition itemDefinition) {
return base().bedrockIdentifier().compareTo(itemDefinition.base().bedrockIdentifier());
}
return -1; // Groups are always greater than individual mappings
}
} }

View File

@@ -6,7 +6,7 @@ import com.mojang.serialization.MapCodec;
import net.minecraft.util.StringRepresentable; import net.minecraft.util.StringRepresentable;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public interface GeyserMapping { public interface GeyserMapping extends Comparable<GeyserMapping> {
Codec<GeyserMapping> CODEC = Codec.lazyInitialized(() -> Type.CODEC.dispatch(GeyserMapping::type, Type::codec)); 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 // Not perfect since we're not checking single definitions in groups without a model... but good enough

View File

@@ -11,6 +11,7 @@ import org.geysermc.rainbow.CodecUtil;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@@ -26,7 +27,10 @@ public class GeyserMappings {
).apply(instance, (format, mappings) -> new GeyserMappings(mappings)) ).apply(instance, (format, mappings) -> new GeyserMappings(mappings))
); );
private final Multimap<Holder<Item>, GeyserMapping> mappings = MultimapBuilder.hashKeys().hashSetValues().build(); private final Multimap<Holder<Item>, GeyserMapping> mappings = MultimapBuilder
.hashKeys()
.<GeyserMapping>treeSetValues(Comparator.comparing(mapping -> mapping))
.build();
public GeyserMappings() {} public GeyserMappings() {}

View File

@@ -123,7 +123,7 @@ public class BedrockItemMapper {
bedrockIdentifier = itemModelLocation; bedrockIdentifier = itemModelLocation;
} }
BedrockGeometryContext geometry = BedrockGeometryContext.create(itemModel); BedrockGeometryContext geometry = BedrockGeometryContext.create(bedrockIdentifier, itemModel, context.packContext);
if (context.packContext.reportSuccesses()) { if (context.packContext.reportSuccesses()) {
// Not a problem, but just report to get the model printed in the report file // Not a problem, but just report to get the model printed in the report file
context.report("creating mapping for block model " + itemModelLocation); context.report("creating mapping for block model " + itemModelLocation);
@@ -238,9 +238,8 @@ public class BedrockItemMapper {
return; return;
} }
// TODO move attachable mapping somewhere else for cleaner code?
packContext.itemConsumer().accept(new BedrockItem(bedrockIdentifier, base.textureName(), geometry, packContext.itemConsumer().accept(new BedrockItem(bedrockIdentifier, base.textureName(), geometry,
AttachableMapper.mapItem(stack.getComponentsPatch(), bedrockIdentifier, geometry, packContext.assetResolver(), packContext.additionalTextureConsumer()))); AttachableMapper.mapItem(packContext.assetResolver(), geometry, stack.getComponentsPatch())));
} }
public void report(String problem) { public void report(String problem) {

View File

@@ -2,12 +2,7 @@ package org.geysermc.rainbow.mapping;
import org.geysermc.rainbow.mapping.geometry.GeometryRenderer; import org.geysermc.rainbow.mapping.geometry.GeometryRenderer;
import org.geysermc.rainbow.definition.GeyserMappings; import org.geysermc.rainbow.definition.GeyserMappings;
import org.geysermc.rainbow.mapping.geometry.TextureHolder;
import org.geysermc.rainbow.pack.PackPaths; import org.geysermc.rainbow.pack.PackPaths;
import java.util.function.Consumer;
public record PackContext(GeyserMappings mappings, PackPaths paths, BedrockItemConsumer itemConsumer, AssetResolver assetResolver, public record PackContext(GeyserMappings mappings, PackPaths paths, BedrockItemConsumer itemConsumer, AssetResolver assetResolver,
GeometryRenderer geometryRenderer, Consumer<TextureHolder> additionalTextureConsumer, GeometryRenderer geometryRenderer, boolean reportSuccesses) {}
boolean reportSuccesses) {
}

View File

@@ -19,12 +19,12 @@ import java.util.function.Consumer;
public class AttachableMapper { public class AttachableMapper {
public static Optional<BedrockAttachable> mapItem(DataComponentPatch components, ResourceLocation bedrockIdentifier, BedrockGeometryContext geometryContext, public static AttachableCreator mapItem(AssetResolver assetResolver, BedrockGeometryContext geometryContext, DataComponentPatch components) {
AssetResolver assetResolver, Consumer<TextureHolder> textureConsumer) {
// Crazy optional statement // Crazy optional statement
// Unfortunately we can't have both equippables and custom models, so we prefer the latter :( // Unfortunately we can't have both equippables and custom models, so we prefer the latter :(
return geometryContext.geometry() return (bedrockIdentifier, stitchedGeometry, textureConsumer) -> stitchedGeometry
.map(geometry -> BedrockAttachable.geometry(bedrockIdentifier, geometry.definitions().getFirst(), geometryContext.texture().location().getPath())) .map(BedrockGeometryContext.StitchedGeometry::geometry)
.map(geometry -> BedrockAttachable.geometry(bedrockIdentifier, geometry.definitions().getFirst(), geometryContext.icon().location().getPath()))
.or(() -> Optional.ofNullable(components.get(DataComponents.EQUIPPABLE)) .or(() -> Optional.ofNullable(components.get(DataComponents.EQUIPPABLE))
.flatMap(optional -> (Optional<Equippable>) optional) .flatMap(optional -> (Optional<Equippable>) optional)
.flatMap(equippable -> equippable.assetId().flatMap(assetResolver::getEquipmentInfo).map(info -> Pair.of(equippable.slot(), info))) .flatMap(equippable -> equippable.assetId().flatMap(assetResolver::getEquipmentInfo).map(info -> Pair.of(equippable.slot(), info)))
@@ -55,4 +55,10 @@ public class AttachableMapper {
private static ResourceLocation getTexture(List<EquipmentClientInfo.Layer> info, EquipmentClientInfo.LayerType layer) { private static ResourceLocation getTexture(List<EquipmentClientInfo.Layer> info, EquipmentClientInfo.LayerType layer) {
return info.getFirst().textureId().withPath(path -> "entity/equipment/" + layer.getSerializedName() + "/" + path); return info.getFirst().textureId().withPath(path -> "entity/equipment/" + layer.getSerializedName() + "/" + path);
} }
@FunctionalInterface
public interface AttachableCreator {
Optional<BedrockAttachable> create(ResourceLocation bedrockIdentifier, Optional<BedrockGeometryContext.StitchedGeometry> geometry, Consumer<TextureHolder> textureConsumer);
}
} }

View File

@@ -1,53 +1,62 @@
package org.geysermc.rainbow.mapping.geometry; package org.geysermc.rainbow.mapping.geometry;
import com.google.common.base.Suppliers;
import net.minecraft.client.renderer.block.model.TextureSlots; import net.minecraft.client.renderer.block.model.TextureSlots;
import net.minecraft.client.resources.model.Material; import net.minecraft.client.resources.model.Material;
import net.minecraft.client.resources.model.ResolvedModel; import net.minecraft.client.resources.model.ResolvedModel;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import org.geysermc.rainbow.Rainbow; import org.geysermc.rainbow.Rainbow;
import org.geysermc.rainbow.mapping.PackContext;
import org.geysermc.rainbow.mapping.animation.AnimationMapper; import org.geysermc.rainbow.mapping.animation.AnimationMapper;
import org.geysermc.rainbow.mapping.animation.BedrockAnimationContext; import org.geysermc.rainbow.mapping.animation.BedrockAnimationContext;
import org.geysermc.rainbow.pack.geometry.BedrockGeometry; import org.geysermc.rainbow.pack.geometry.BedrockGeometry;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Stream; import java.util.stream.Stream;
public record BedrockGeometryContext(Optional<BedrockGeometry> geometry, Optional<BedrockAnimationContext> animation, TextureHolder texture, public record BedrockGeometryContext(Optional<Supplier<StitchedGeometry>> geometry,
Optional<BedrockAnimationContext> animation, TextureHolder icon,
boolean handheld) { boolean handheld) {
private static final List<ResourceLocation> HANDHELD_MODELS = Stream.of("item/handheld", "item/handheld_rod", "item/handheld_mace") private static final List<ResourceLocation> HANDHELD_MODELS = Stream.of("item/handheld", "item/handheld_rod", "item/handheld_mace")
.map(ResourceLocation::withDefaultNamespace) .map(ResourceLocation::withDefaultNamespace)
.toList(); .toList();
public static BedrockGeometryContext create(ResolvedModel model) { public static BedrockGeometryContext create(ResourceLocation bedrockIdentifier, ResolvedModel model, PackContext context) {
ResolvedModel parentModel = model.parent(); ResolvedModel parentModel = model.parent();
// debugName() returns the resource location of the model as a string // debugName() returns the resource location of the model as a string
boolean handheld = parentModel != null && HANDHELD_MODELS.contains(ResourceLocation.parse(parentModel.debugName())); boolean handheld = parentModel != null && HANDHELD_MODELS.contains(ResourceLocation.parse(parentModel.debugName()));
TextureSlots textures = model.getTopTextureSlots(); TextureSlots textures = model.getTopTextureSlots();
Material layer0Texture = textures.getMaterial("layer0"); Material layer0Texture = textures.getMaterial("layer0");
Optional<BedrockGeometry> geometry; Optional<Supplier<StitchedGeometry>> geometry;
Optional<BedrockAnimationContext> animation; Optional<BedrockAnimationContext> animation;
TextureHolder texture; TextureHolder icon;
if (layer0Texture != null) { if (layer0Texture != null) {
geometry = Optional.empty(); geometry = Optional.empty();
animation = Optional.empty(); animation = Optional.empty();
texture = new TextureHolder(layer0Texture.texture()); icon = new TextureHolder(layer0Texture.texture());
} else { } else {
// Unknown model (doesn't use layer0), so we immediately assume the geometry is custom // Unknown model (doesn't use layer0), so we immediately assume the geometry is custom
// This check should probably be done differently (actually check if the model is 2D or 3D) // This check should probably be done differently (actually check if the model is 2D or 3D)
ResourceLocation modelLocation = ResourceLocation.parse(model.debugName()); ResourceLocation modelLocation = ResourceLocation.parse(model.debugName());
String safeIdentifier = Rainbow.fileSafeResourceLocation(modelLocation); String safeIdentifier = Rainbow.fileSafeResourceLocation(bedrockIdentifier);
StitchedTextures stitchedTextures = StitchedTextures.stitchModelTextures(textures); geometry = Optional.of(Suppliers.memoize(() -> {
geometry = GeometryMapper.mapGeometry(safeIdentifier, "bone", model, stitchedTextures); StitchedTextures stitchedTextures = StitchedTextures.stitchModelTextures(textures, context);
BedrockGeometry mappedGeometry = GeometryMapper.mapGeometry(safeIdentifier, "bone", model, stitchedTextures);
return new StitchedGeometry(mappedGeometry, new TextureHolder(modelLocation.withSuffix("_stitched"), stitchedTextures.stitched()));
}));
animation = Optional.of(AnimationMapper.mapAnimation(safeIdentifier, "bone", model.getTopTransforms())); animation = Optional.of(AnimationMapper.mapAnimation(safeIdentifier, "bone", model.getTopTransforms()));
texture = new TextureHolder(modelLocation.withSuffix("_stitched"), Optional.of(stitchedTextures.stitched())); icon = new TextureHolder(modelLocation); // TODO
// TODO geometry rendering
} }
return new BedrockGeometryContext(geometry, animation, texture, handheld); return new BedrockGeometryContext(geometry, animation, icon, handheld);
} }
public record StitchedGeometry(BedrockGeometry geometry, TextureHolder stitchedTextures) {}
} }

View File

@@ -14,15 +14,14 @@ import org.joml.Vector3f;
import org.joml.Vector3fc; import org.joml.Vector3fc;
import java.util.Map; import java.util.Map;
import java.util.Optional;
public class GeometryMapper { public class GeometryMapper {
private static final Vector3fc CENTRE_OFFSET = new Vector3f(8.0F, 0.0F, 8.0F); private static final Vector3fc CENTRE_OFFSET = new Vector3f(8.0F, 0.0F, 8.0F);
public static Optional<BedrockGeometry> mapGeometry(String identifier, String boneName, ResolvedModel model, StitchedTextures textures) { public static BedrockGeometry mapGeometry(String identifier, String boneName, ResolvedModel model, StitchedTextures textures) {
UnbakedGeometry top = model.getTopGeometry(); UnbakedGeometry top = model.getTopGeometry();
if (top == UnbakedGeometry.EMPTY) { if (top == UnbakedGeometry.EMPTY) {
return Optional.empty(); return BedrockGeometry.EMPTY;
} }
BedrockGeometry.Builder builder = BedrockGeometry.builder(identifier); BedrockGeometry.Builder builder = BedrockGeometry.builder(identifier);
@@ -54,7 +53,7 @@ public class GeometryMapper {
// Bind to the bone of the current item slot // Bind to the bone of the current item slot
bone.withBinding("q.item_slot_to_bone_name(context.item_slot)"); bone.withBinding("q.item_slot_to_bone_name(context.item_slot)");
return Optional.of(builder.withBone(bone).build()); return builder.withBone(bone).build();
} }
private static BedrockGeometry.Cube.Builder mapBlockElement(BlockElement element, StitchedTextures textures) { private static BedrockGeometry.Cube.Builder mapBlockElement(BlockElement element, StitchedTextures textures) {

View File

@@ -11,6 +11,8 @@ import net.minecraft.client.resources.metadata.animation.FrameSize;
import net.minecraft.client.resources.model.Material; import net.minecraft.client.resources.model.Material;
import net.minecraft.data.AtlasIds; import net.minecraft.data.AtlasIds;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import org.geysermc.rainbow.Rainbow;
import org.geysermc.rainbow.mapping.PackContext;
import org.geysermc.rainbow.mixin.SpriteContentsAccessor; import org.geysermc.rainbow.mixin.SpriteContentsAccessor;
import org.geysermc.rainbow.mixin.SpriteLoaderAccessor; import org.geysermc.rainbow.mixin.SpriteLoaderAccessor;
import org.geysermc.rainbow.mixin.TextureSlotsAccessor; import org.geysermc.rainbow.mixin.TextureSlotsAccessor;
@@ -33,9 +35,9 @@ public record StitchedTextures(Map<String, TextureAtlasSprite> sprites, Supplier
return Optional.ofNullable(sprites.get(key)); return Optional.ofNullable(sprites.get(key));
} }
public static StitchedTextures stitchModelTextures(TextureSlots textures) { public static StitchedTextures stitchModelTextures(TextureSlots textures, PackContext context) {
Map<String, Material> materials = ((TextureSlotsAccessor) textures).getResolvedValues(); Map<String, Material> materials = ((TextureSlotsAccessor) textures).getResolvedValues();
SpriteLoader.Preparations preparations = prepareStitching(materials.values().stream().map(Material::texture)); SpriteLoader.Preparations preparations = prepareStitching(materials.values().stream().map(Material::texture), context);
Map<String, TextureAtlasSprite> sprites = new HashMap<>(); Map<String, TextureAtlasSprite> sprites = new HashMap<>();
for (Map.Entry<String, Material> material : materials.entrySet()) { for (Map.Entry<String, Material> material : materials.entrySet()) {
@@ -44,19 +46,19 @@ public record StitchedTextures(Map<String, TextureAtlasSprite> sprites, Supplier
return new StitchedTextures(Map.copyOf(sprites), () -> stitchTextureAtlas(preparations), preparations.width(), preparations.height()); return new StitchedTextures(Map.copyOf(sprites), () -> stitchTextureAtlas(preparations), preparations.width(), preparations.height());
} }
private static SpriteLoader.Preparations prepareStitching(Stream<ResourceLocation> textures) { private static SpriteLoader.Preparations prepareStitching(Stream<ResourceLocation> textures, PackContext context) {
// Atlas ID doesn't matter much here, but BLOCKS is the most appropriate // Atlas ID doesn't matter much here, but BLOCKS is the most appropriate
// Not sure if 1024 should be the max supported texture size, but it seems to work // Not sure if 1024 should be the max supported texture size, but it seems to work
SpriteLoader spriteLoader = new SpriteLoader(AtlasIds.BLOCKS, 1024, 16, 16); SpriteLoader spriteLoader = new SpriteLoader(AtlasIds.BLOCKS, 1024, 16, 16);
List<SpriteContents> sprites = textures.distinct().map(StitchedTextures::readSpriteContents).toList(); List<SpriteContents> sprites = textures.distinct().map(texture -> readSpriteContents(texture, context)).toList();
return ((SpriteLoaderAccessor) spriteLoader).invokeStitch(sprites, 0, Util.backgroundExecutor()); return ((SpriteLoaderAccessor) spriteLoader).invokeStitch(sprites, 0, Util.backgroundExecutor());
} }
private static SpriteContents readSpriteContents(ResourceLocation location) { private static SpriteContents readSpriteContents(ResourceLocation location, PackContext context) {
// TODO decorate path util // TODO decorate path util
// TODO don't use ResourceManager // TODO don't use ResourceManager
// TODO IO is on main thread here? // TODO IO is on main thread here?
try (InputStream textureStream = Minecraft.getInstance().getResourceManager().open(location.withPath(path -> "textures/" + path + ".png"))) { try (InputStream textureStream = context.assetResolver().openAsset(Rainbow.decorateTextureLocation(location))) {
NativeImage texture = NativeImage.read(textureStream); NativeImage texture = NativeImage.read(textureStream);
return new SpriteContents(location, new FrameSize(texture.getWidth(), texture.getHeight()), texture); return new SpriteContents(location, new FrameSize(texture.getWidth(), texture.getHeight()), texture);
} catch (IOException exception) { } catch (IOException exception) {

View File

@@ -8,6 +8,10 @@ import java.util.function.Supplier;
public record TextureHolder(ResourceLocation location, Optional<Supplier<NativeImage>> supplier) { public record TextureHolder(ResourceLocation location, Optional<Supplier<NativeImage>> supplier) {
public TextureHolder(ResourceLocation location, Supplier<NativeImage> supplier) {
this(location, Optional.of(supplier));
}
public TextureHolder(ResourceLocation location) { public TextureHolder(ResourceLocation location) {
this(location, Optional.empty()); this(location, Optional.empty());
} }

View File

@@ -3,24 +3,39 @@ package org.geysermc.rainbow.pack;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import org.geysermc.rainbow.Rainbow; import org.geysermc.rainbow.Rainbow;
import org.geysermc.rainbow.mapping.PackSerializer; import org.geysermc.rainbow.mapping.PackSerializer;
import org.geysermc.rainbow.mapping.attachable.AttachableMapper;
import org.geysermc.rainbow.mapping.geometry.BedrockGeometryContext; import org.geysermc.rainbow.mapping.geometry.BedrockGeometryContext;
import org.geysermc.rainbow.mapping.geometry.TextureHolder;
import org.geysermc.rainbow.pack.attachable.BedrockAttachable; import org.geysermc.rainbow.pack.attachable.BedrockAttachable;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream; import java.util.function.Function;
import java.util.function.Supplier;
public record BedrockItem(ResourceLocation identifier, String textureName, BedrockGeometryContext geometry, Optional<BedrockAttachable> attachable) { public record BedrockItem(ResourceLocation identifier, String textureName, BedrockGeometryContext geometryContext, AttachableMapper.AttachableCreator attachableCreator) {
public List<CompletableFuture<?>> save(PackSerializer serializer, Path attachableDirectory, Path geometryDirectory, Path animationDirectory) { public CompletableFuture<?> save(PackSerializer serializer, Path attachableDirectory, Path geometryDirectory, Path animationDirectory,
return Stream.concat( Function<TextureHolder, CompletableFuture<?>> textureSaver) {
attachable.stream().map(present -> present.save(serializer, attachableDirectory)), return CompletableFuture.supplyAsync(() -> geometryContext.geometry().map(Supplier::get))
Stream.concat( .thenCompose(stitchedGeometry -> {
geometry.geometry().stream().map(present -> present.save(serializer, geometryDirectory)), List<TextureHolder> attachableTextures = new ArrayList<>();
geometry.animation().stream().map(context -> context.animation().save(serializer, animationDirectory, Rainbow.fileSafeResourceLocation(identifier))) Optional<BedrockAttachable> createdAttachable = attachableCreator.create(identifier, stitchedGeometry, attachableTextures::add);
) return CompletableFuture.allOf(
).toList(); textureSaver.apply(geometryContext.icon()),
createdAttachable.map(attachable -> attachable.save(serializer, attachableDirectory)).orElse(noop()),
CompletableFuture.allOf(attachableTextures.stream().map(textureSaver).toArray(CompletableFuture[]::new)),
stitchedGeometry.map(BedrockGeometryContext.StitchedGeometry::geometry).map(geometry -> geometry.save(serializer, geometryDirectory)).orElse(noop()),
stitchedGeometry.map(BedrockGeometryContext.StitchedGeometry::stitchedTextures).map(textureSaver).orElse(noop()),
geometryContext.animation().map(context -> context.animation().save(serializer, animationDirectory, Rainbow.fileSafeResourceLocation(identifier))).orElse(noop())
);
});
}
private static <T> CompletableFuture<T> noop() {
return CompletableFuture.completedFuture(null);
} }
} }

View File

@@ -35,6 +35,7 @@ import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.function.UnaryOperator; import java.util.function.UnaryOperator;
public class BedrockPack { public class BedrockPack {
@@ -45,7 +46,6 @@ public class BedrockPack {
private final BedrockTextures.Builder itemTextures = BedrockTextures.builder(); private final BedrockTextures.Builder itemTextures = BedrockTextures.builder();
private final Set<BedrockItem> bedrockItems = new HashSet<>(); private final Set<BedrockItem> bedrockItems = new HashSet<>();
private final Set<TextureHolder> texturesToExport = new HashSet<>();
private final Set<ResourceLocation> modelsMapped = new HashSet<>(); private final Set<ResourceLocation> modelsMapped = new HashSet<>();
private final IntSet customModelDataMapped = new IntOpenHashSet(); private final IntSet customModelDataMapped = new IntOpenHashSet();
@@ -63,9 +63,8 @@ public class BedrockPack {
// Not reading existing item mappings/texture atlas for now since that doesn't work all that well yet // Not reading existing item mappings/texture atlas for now since that doesn't work all that well yet
this.context = new PackContext(new GeyserMappings(), paths, item -> { this.context = new PackContext(new GeyserMappings(), paths, item -> {
itemTextures.withItemTexture(item); itemTextures.withItemTexture(item);
texturesToExport.add(item.geometry().texture());
bedrockItems.add(item); bedrockItems.add(item);
}, assetResolver, geometryRenderer, texturesToExport::add, reportSuccesses); }, assetResolver, geometryRenderer, reportSuccesses);
this.reporter = reporter; this.reporter = reporter;
} }
@@ -128,13 +127,10 @@ public class BedrockPack {
futures.add(serializer.saveJson(GeyserMappings.CODEC, context.mappings(), paths.mappings())); futures.add(serializer.saveJson(GeyserMappings.CODEC, context.mappings(), paths.mappings()));
futures.add(serializer.saveJson(PackManifest.CODEC, manifest, paths.manifest())); futures.add(serializer.saveJson(PackManifest.CODEC, manifest, paths.manifest()));
futures.add(serializer.saveJson(BedrockTextureAtlas.CODEC, BedrockTextureAtlas.itemAtlas(name, itemTextures), paths.itemAtlas())); futures.add(serializer.saveJson(BedrockTextureAtlas.CODEC, BedrockTextureAtlas.itemAtlas(name, itemTextures), paths.itemAtlas()));
for (BedrockItem item : bedrockItems) {
futures.addAll(item.save(serializer, paths.attachables(), paths.geometry(), paths.animation()));
}
for (TextureHolder texture : texturesToExport) { Function<TextureHolder, CompletableFuture<?>> textureSaver = texture -> {
ResourceLocation textureLocation = Rainbow.decorateTextureLocation(texture.location()); ResourceLocation textureLocation = Rainbow.decorateTextureLocation(texture.location());
texture.supplier() return texture.supplier()
.flatMap(image -> { .flatMap(image -> {
try { try {
return Optional.of(NativeImageUtil.writeToByteArray(image.get())); return Optional.of(NativeImageUtil.writeToByteArray(image.get()));
@@ -152,7 +148,11 @@ public class BedrockPack {
} }
}) })
.map(bytes -> serializer.saveTexture(bytes, paths.packRoot().resolve(textureLocation.getPath()))) .map(bytes -> serializer.saveTexture(bytes, paths.packRoot().resolve(textureLocation.getPath())))
.ifPresent(futures::add); .orElse(CompletableFuture.completedFuture(null));
};
for (BedrockItem item : bedrockItems) {
futures.add(item.save(serializer, paths.attachables(), paths.geometry(), paths.animation(), textureSaver));
} }
if (paths.zipOutput().isPresent()) { if (paths.zipOutput().isPresent()) {
@@ -178,10 +178,6 @@ public class BedrockPack {
return itemTextures.build().size(); return itemTextures.build().size();
} }
public int getAdditionalExportedTextures() {
return texturesToExport.size();
}
public ProblemReporter.Collector getReporter() { public ProblemReporter.Collector getReporter() {
return reporter; return reporter;
} }

View File

@@ -33,7 +33,7 @@ public record BedrockTextures(Map<String, String> textures) {
private final Map<String, String> textures = new HashMap<>(); private final Map<String, String> textures = new HashMap<>();
public Builder withItemTexture(BedrockItem item) { public Builder withItemTexture(BedrockItem item) {
return withTexture(item.textureName(), TEXTURES_FOLDER + item.geometry().texture().location().getPath()); return withTexture(item.textureName(), TEXTURES_FOLDER + item.geometryContext().icon().location().getPath());
} }
public Builder withTexture(String name, String texture) { public Builder withTexture(String name, String texture) {

View File

@@ -32,6 +32,8 @@ public record BedrockGeometry(BedrockVersion formatVersion, List<GeometryDefinit
).apply(instance, BedrockGeometry::new) ).apply(instance, BedrockGeometry::new)
); );
public static BedrockGeometry EMPTY = new BedrockGeometry(FORMAT_VERSION, List.of());
public CompletableFuture<?> save(PackSerializer serializer, Path geometryDirectory) { public CompletableFuture<?> save(PackSerializer serializer, Path geometryDirectory) {
return serializer.saveJson(CODEC, this, geometryDirectory.resolve(definitions.getFirst().info.identifier + ".geo.json")); return serializer.saveJson(CODEC, this, geometryDirectory.resolve(definitions.getFirst().info.identifier + ".geo.json"));
} }