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

Support writing legacy definitions

This commit is contained in:
Eclipse
2025-07-19 10:06:18 +00:00
parent e6a79258eb
commit 983ae8c973
11 changed files with 174 additions and 103 deletions

View File

@@ -34,4 +34,8 @@ public class Rainbow implements ClientModInitializer {
public static ResourceLocation getModdedLocation(String path) {
return ResourceLocation.fromNamespaceAndPath(MOD_ID, path);
}
public static String fileSafeResourceLocation(ResourceLocation location) {
return location.toString().replace(':', '.').replace('/', '_');
}
}

View File

@@ -42,6 +42,7 @@ 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.GeyserMappings;
import org.geysermc.rainbow.mapping.geyser.GeyserSingleDefinition;
import org.geysermc.rainbow.mapping.geyser.predicate.GeyserConditionPredicate;
@@ -78,14 +79,17 @@ public class BedrockItemMapper {
}
mapItem(model, displayName, protectionValue, stack.getComponentsPatch(), reporter,
mapping -> mappings.map(stack.getItemHolder(), mapping), packPath, itemConsumer, additionalTextureConsumer);
packPath, mapping -> mappings.map(stack.getItemHolder(), new GeyserSingleDefinition(mapping, Optional.of(model))),
itemConsumer, additionalTextureConsumer);
}
public static void mapItem(ResourceLocation modelLocation, String displayName, int protectionValue, DataComponentPatch componentPatch, ProblemReporter reporter,
Consumer<GeyserSingleDefinition> mappingConsumer, Path packPath, BedrockItemConsumer itemConsumer, Consumer<ResourceLocation> additionalTextureConsumer) {
Path packPath, Consumer<GeyserBaseDefinition> mappingConsumer, BedrockItemConsumer itemConsumer,
Consumer<ResourceLocation> additionalTextureConsumer) {
ItemModel model = Minecraft.getInstance().getModelManager().getItemModel(modelLocation);
MappingContext context = new MappingContext(List.of(), modelLocation, displayName, protectionValue, componentPatch,
reporter.forChild(() -> "client item definition " + modelLocation + " "), mappingConsumer, itemConsumer, packPath, additionalTextureConsumer);
MappingContext context = new MappingContext(List.of(), displayName, protectionValue, componentPatch,
reporter.forChild(() -> "client item definition " + modelLocation + " "),
packPath, mappingConsumer, itemConsumer, additionalTextureConsumer);
mapItem(model, context);
}
@@ -153,8 +157,8 @@ public class BedrockItemMapper {
mapItem(onFalse, context.with(new GeyserConditionPredicate(predicateProperty, false), "condition on false "));
}
@SuppressWarnings("unchecked")
private static <T> void mapSelectModel(SelectItemModel<T> model, MappingContext context) {
//noinspection unchecked
SelectItemModelProperty<T> property = ((SelectItemModelAccessor<T>) model).getProperty();
Function<T, GeyserMatchPredicate.MatchPredicateData> dataConstructor = switch (property) {
case Charge ignored -> chargeType -> new GeyserMatchPredicate.ChargeType((CrossbowItem.ChargeType) chargeType);
@@ -165,7 +169,6 @@ public class BedrockItemMapper {
default -> null;
};
//noinspection unchecked
Object2ObjectMap<T, ItemModel> cases = ((SelectItemModelCasesAccessor<T>) model).rainbow$getCases();
if (dataConstructor == null) {
@@ -186,24 +189,24 @@ public class BedrockItemMapper {
mapItem(cases.defaultReturnValue(), context.child("select fallback case "));
}
private record MappingContext(List<GeyserPredicate> predicateStack, ResourceLocation model, String displayName, int protectionValue, DataComponentPatch componentPatch, ProblemReporter reporter,
Consumer<GeyserSingleDefinition> mappingConsumer, BedrockItemConsumer itemConsumer, Path packPath,
private record MappingContext(List<GeyserPredicate> predicateStack, String displayName, int protectionValue, DataComponentPatch componentPatch, ProblemReporter reporter,
Path packPath, Consumer<GeyserBaseDefinition> mappingConsumer, BedrockItemConsumer itemConsumer,
Consumer<ResourceLocation> additionalTextureConsumer) {
public MappingContext with(GeyserPredicate predicate, String childName) {
return new MappingContext(Stream.concat(predicateStack.stream(), Stream.of(predicate)).toList(), model, displayName, protectionValue, componentPatch,
reporter.forChild(() -> childName), mappingConsumer, itemConsumer, packPath, additionalTextureConsumer);
return new MappingContext(Stream.concat(predicateStack.stream(), Stream.of(predicate)).toList(), displayName, protectionValue, componentPatch,
reporter.forChild(() -> childName), packPath, mappingConsumer, itemConsumer, additionalTextureConsumer);
}
public MappingContext child(String childName) {
return new MappingContext(predicateStack, model, displayName, protectionValue, componentPatch, reporter.forChild(() -> childName),
mappingConsumer, itemConsumer, packPath, additionalTextureConsumer);
return new MappingContext(predicateStack, displayName, protectionValue, componentPatch, reporter.forChild(() -> childName),
packPath, mappingConsumer, itemConsumer, additionalTextureConsumer);
}
public void create(ResourceLocation bedrockIdentifier, ResourceLocation texture, boolean displayHandheld,
Optional<ResolvedModel> customModel) {
GeyserSingleDefinition definition = new GeyserSingleDefinition(Optional.of(model), bedrockIdentifier, Optional.of(displayName), predicateStack,
new GeyserSingleDefinition.BedrockOptions(Optional.empty(), true, displayHandheld, protectionValue), componentPatch);
GeyserBaseDefinition definition = new GeyserBaseDefinition(bedrockIdentifier, Optional.of(displayName), predicateStack,
new GeyserBaseDefinition.BedrockOptions(Optional.empty(), true, displayHandheld, protectionValue), componentPatch);
try {
mappingConsumer.accept(definition);
} catch (Exception exception) {
@@ -221,7 +224,7 @@ public class BedrockItemMapper {
boolean exportTexture = true;
if (customModel.isPresent()) {
ItemStack fakeItem = new ItemStack(Items.FLINT);
fakeItem.set(DataComponents.ITEM_MODEL, model);
//fakeItem.set(DataComponents.ITEM_MODEL, model); TODO
texture = texture.withPath(path -> path + "_icon");
GeometryRenderer.render(fakeItem, packPath.resolve(BedrockTextures.TEXTURES_FOLDER + texture.getPath() + ".png"));

View File

@@ -0,0 +1,78 @@
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) {
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

@@ -21,16 +21,16 @@ public record GeyserGroupDefinition(Optional<ResourceLocation> model, List<Geyse
return new GeyserGroupDefinition(model, Stream.concat(definitions.stream(), Stream.of(mapping)).toList());
}
public boolean isFor(ResourceLocation model) {
return this.model.isPresent() && this.model.get().equals(model);
public boolean isFor(Optional<ResourceLocation> model) {
return this.model.isPresent() && model.isPresent() && this.model.get().equals(model.get());
}
public boolean conflictsWith(Optional<ResourceLocation> parentModel, GeyserSingleDefinition other) {
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 GeyserSingleDefinition single && single.conflictsWith(thisModel, other)) {
} else if (definition instanceof GeyserItemDefinition item && item.conflictsWith(thisModel, other)) {
return true;
}
}

View File

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

View File

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

View File

@@ -21,6 +21,7 @@ public interface GeyserMapping {
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);

View File

@@ -36,8 +36,8 @@ public class GeyserMappings {
}
}
public void map(Holder<Item> item, GeyserSingleDefinition mapping) {
ResourceLocation model = mapping.model().orElseThrow();
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));
@@ -48,19 +48,22 @@ public class GeyserMappings {
}
modelGroup = Optional.of(existingGroup);
break;
} else if (existing instanceof GeyserSingleDefinition single) {
if (single.conflictsWith(Optional.empty(), mapping)) {
throw new IllegalArgumentException("Mapping conflicts with existing single mapping");
} else if (model.equals(single.model().orElseThrow())) {
mappings.remove(item, single);
modelGroup = Optional.of(new GeyserGroupDefinition(Optional.of(model), List.of(single.withoutModel())));
} 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());
mappings.put(item, modelGroup.get().with(mapping.withoutModel()));
// 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);
}

View File

@@ -1,96 +1,35 @@
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.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 GeyserSingleDefinition(Optional<ResourceLocation> model, ResourceLocation bedrockIdentifier, Optional<String> displayName,
List<GeyserPredicate> predicates, BedrockOptions bedrockOptions, DataComponentPatch components) implements GeyserMapping {
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 record GeyserSingleDefinition(GeyserBaseDefinition base, Optional<ResourceLocation> model) implements GeyserItemDefinition {
public static final MapCodec<GeyserSingleDefinition> CODEC = RecordCodecBuilder.mapCodec(instance ->
instance.group(
ResourceLocation.CODEC.optionalFieldOf("model").forGetter(GeyserSingleDefinition::model),
ResourceLocation.CODEC.fieldOf("bedrock_identifier").forGetter(GeyserSingleDefinition::bedrockIdentifier),
Codec.STRING.optionalFieldOf("display_name").forGetter(GeyserSingleDefinition::displayName),
GeyserPredicate.LIST_CODEC.optionalFieldOf("predicate", List.of()).forGetter(GeyserSingleDefinition::predicates),
BedrockOptions.CODEC.optionalFieldOf("bedrock_options", BedrockOptions.DEFAULT).forGetter(GeyserSingleDefinition::bedrockOptions),
FILTERED_COMPONENT_MAP_CODEC.optionalFieldOf("components", DataComponentPatch.EMPTY).forGetter(GeyserSingleDefinition::components)
GeyserBaseDefinition.MAP_CODEC.forGetter(GeyserSingleDefinition::base),
ResourceLocation.CODEC.optionalFieldOf("model").forGetter(GeyserSingleDefinition::model)
).apply(instance, GeyserSingleDefinition::new)
);
public String textureName() {
return bedrockOptions.icon.orElse(iconFromResourceLocation(bedrockIdentifier));
}
public boolean conflictsWith(Optional<ResourceLocation> parentModel, GeyserSingleDefinition other) {
@Override
public boolean conflictsWith(Optional<ResourceLocation> parentModel, GeyserItemDefinition other) {
if (other instanceof GeyserSingleDefinition otherSingle) {
ResourceLocation thisModel = model.or(() -> parentModel).orElseThrow();
ResourceLocation otherModel = other.model.or(() -> parentModel).orElseThrow();
if (!thisModel.equals(otherModel)) {
return false;
} else if (predicates.size() == other.predicates.size()) {
boolean predicatesAreEqual = true;
for (GeyserPredicate predicate : predicates) {
if (!other.predicates.contains(predicate)) {
predicatesAreEqual = false;
break;
}
}
return predicatesAreEqual;
ResourceLocation otherModel = otherSingle.model.or(() -> parentModel).orElseThrow();
return thisModel.equals(otherModel) && base.conflictsWith(other.base());
}
return false;
}
public GeyserSingleDefinition withoutModel() {
return new GeyserSingleDefinition(Optional.empty(), bedrockIdentifier, displayName, predicates, bedrockOptions, components);
return new GeyserSingleDefinition(base, Optional.empty());
}
@Override
public Type type() {
return Type.SINGLE;
}
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);
}
// TODO this method is used in other places too now, maybe move it
public static String iconFromResourceLocation(ResourceLocation location) {
return location.toString().replace(':', '.').replace('/', '_');
}
}

View File

@@ -1,7 +1,7 @@
package org.geysermc.rainbow.pack;
import net.minecraft.resources.ResourceLocation;
import org.geysermc.rainbow.mapping.geyser.GeyserSingleDefinition;
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;
@@ -21,7 +21,7 @@ public record BedrockItem(ResourceLocation identifier, String textureName, Resou
geometry.get().save(geometryDirectory);
}
if (animation.isPresent()) {
animation.get().save(animationDirectory, GeyserSingleDefinition.iconFromResourceLocation(identifier));
animation.get().save(animationDirectory, Rainbow.fileSafeResourceLocation(identifier));
}
}
}

View File

@@ -11,7 +11,7 @@ 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.mapping.geyser.GeyserSingleDefinition;
import org.geysermc.rainbow.Rainbow;
import org.geysermc.rainbow.pack.BedrockTextures;
import org.geysermc.rainbow.pack.BedrockVersion;
import org.geysermc.rainbow.pack.geometry.BedrockGeometry;
@@ -37,7 +37,7 @@ public record BedrockAttachable(BedrockVersion formatVersion, AttachableInfo inf
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(GeyserSingleDefinition.iconFromResourceLocation(info.identifier) + ".json"));
CodecUtil.trySaveJson(CODEC, this, attachablesDirectory.resolve(Rainbow.fileSafeResourceLocation(info.identifier) + ".json"));
}
public static Builder builder(ResourceLocation identifier) {