diff --git a/src/main/java/slimeknights/mantle/Mantle.java b/src/main/java/slimeknights/mantle/Mantle.java index c90173162..ae5a3eb36 100644 --- a/src/main/java/slimeknights/mantle/Mantle.java +++ b/src/main/java/slimeknights/mantle/Mantle.java @@ -18,23 +18,18 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import slimeknights.mantle.block.entity.MantleSignBlockEntity; +import slimeknights.mantle.client.ClientEvents; import slimeknights.mantle.command.MantleCommand; import slimeknights.mantle.config.Config; import slimeknights.mantle.data.predicate.block.BlockPredicate; import slimeknights.mantle.data.predicate.block.BlockPropertiesPredicate; -import slimeknights.mantle.data.predicate.block.SetBlockPredicate; -import slimeknights.mantle.data.predicate.block.TagBlockPredicate; import slimeknights.mantle.data.predicate.damage.DamageSourcePredicate; import slimeknights.mantle.data.predicate.damage.SourceAttackerPredicate; import slimeknights.mantle.data.predicate.damage.SourceMessagePredicate; -import slimeknights.mantle.data.predicate.entity.EntitySetPredicate; import slimeknights.mantle.data.predicate.entity.HasEnchantmentEntityPredicate; import slimeknights.mantle.data.predicate.entity.LivingEntityPredicate; import slimeknights.mantle.data.predicate.entity.MobTypePredicate; -import slimeknights.mantle.data.predicate.entity.TagEntityPredicate; import slimeknights.mantle.data.predicate.item.ItemPredicate; -import slimeknights.mantle.data.predicate.item.ItemSetPredicate; -import slimeknights.mantle.data.predicate.item.ItemTagPredicate; import slimeknights.mantle.datagen.MantleFluidTagProvider; import slimeknights.mantle.datagen.MantleFluidTooltipProvider; import slimeknights.mantle.datagen.MantleTags; @@ -87,6 +82,10 @@ public void onInitialize() { this.registerBlockEntities(); MantleLoot.registerGlobalLootModifiers(); UseBlockCallback.EVENT.register(LecternBookItem::interactWithBlock); + + if (FMLEnvironment.dist == Dist.CLIENT) { + ClientEvents.onConstruct(); + } } private void registerCapabilities() { @@ -118,29 +117,15 @@ private void registerRecipeSerializers() { // predicates { // block predicates - BlockPredicate.LOADER.register(getResource("and"), BlockPredicate.AND); - BlockPredicate.LOADER.register(getResource("or"), BlockPredicate.OR); - BlockPredicate.LOADER.register(getResource("inverted"), BlockPredicate.INVERTED); - BlockPredicate.LOADER.register(getResource("any"), BlockPredicate.ANY.getLoader()); BlockPredicate.LOADER.register(getResource("requires_tool"), BlockPredicate.REQUIRES_TOOL.getLoader()); - BlockPredicate.LOADER.register(getResource("set"), SetBlockPredicate.LOADER); - BlockPredicate.LOADER.register(getResource("tag"), TagBlockPredicate.LOADER); BlockPredicate.LOADER.register(getResource("block_properties"), BlockPropertiesPredicate.LOADER); // item predicates - ItemPredicate.LOADER.register(getResource("and"), ItemPredicate.AND); - ItemPredicate.LOADER.register(getResource("or"), ItemPredicate.OR); - ItemPredicate.LOADER.register(getResource("inverted"), ItemPredicate.INVERTED); - ItemPredicate.LOADER.register(getResource("any"), ItemPredicate.ANY.getLoader()); - ItemPredicate.LOADER.register(getResource("set"), ItemSetPredicate.LOADER); - ItemPredicate.LOADER.register(getResource("tag"), ItemTagPredicate.LOADER); + // make sure the item predicate registry is loaded, nothing to register here + ItemPredicate.ANY.getLoader(); // entity predicates - LivingEntityPredicate.LOADER.register(getResource("and"), LivingEntityPredicate.AND); - LivingEntityPredicate.LOADER.register(getResource("or"), LivingEntityPredicate.OR); - LivingEntityPredicate.LOADER.register(getResource("inverted"), LivingEntityPredicate.INVERTED); // simple - LivingEntityPredicate.LOADER.register(getResource("any"), LivingEntityPredicate.ANY.getLoader()); LivingEntityPredicate.LOADER.register(getResource("fire_immune"), LivingEntityPredicate.FIRE_IMMUNE.getLoader()); LivingEntityPredicate.LOADER.register(getResource("water_sensitive"), LivingEntityPredicate.WATER_SENSITIVE.getLoader()); LivingEntityPredicate.LOADER.register(getResource("on_fire"), LivingEntityPredicate.ON_FIRE.getLoader()); @@ -151,8 +136,6 @@ private void registerRecipeSerializers() { LivingEntityPredicate.LOADER.register(getResource("underwater"), LivingEntityPredicate.UNDERWATER.getLoader()); LivingEntityPredicate.LOADER.register(getResource("raining_at"), LivingEntityPredicate.RAINING.getLoader()); // property - LivingEntityPredicate.LOADER.register(getResource("set"), EntitySetPredicate.LOADER); - LivingEntityPredicate.LOADER.register(getResource("tag"), TagEntityPredicate.LOADER); LivingEntityPredicate.LOADER.register(getResource("mob_type"), MobTypePredicate.LOADER); LivingEntityPredicate.LOADER.register(getResource("has_enchantment"), HasEnchantmentEntityPredicate.LOADER); // register mob types @@ -163,10 +146,6 @@ private void registerRecipeSerializers() { MobTypePredicate.MOB_TYPES.register(new ResourceLocation("water"), MobType.WATER); // damage predicates - DamageSourcePredicate.LOADER.register(getResource("and"), DamageSourcePredicate.AND); - DamageSourcePredicate.LOADER.register(getResource("or"), DamageSourcePredicate.OR); - DamageSourcePredicate.LOADER.register(getResource("inverted"), DamageSourcePredicate.INVERTED); - DamageSourcePredicate.LOADER.register(getResource("any"), DamageSourcePredicate.ANY.getLoader()); // vanilla properties DamageSourcePredicate.LOADER.register(getResource("projectile"), DamageSourcePredicate.PROJECTILE.getLoader()); DamageSourcePredicate.LOADER.register(getResource("explosion"), DamageSourcePredicate.EXPLOSION.getLoader()); @@ -174,6 +153,7 @@ private void registerRecipeSerializers() { DamageSourcePredicate.LOADER.register(getResource("damage_helmet"), DamageSourcePredicate.DAMAGE_HELMET.getLoader()); DamageSourcePredicate.LOADER.register(getResource("bypass_invulnerable"), DamageSourcePredicate.BYPASS_INVULNERABLE.getLoader()); DamageSourcePredicate.LOADER.register(getResource("bypass_magic"), DamageSourcePredicate.BYPASS_MAGIC.getLoader()); + DamageSourcePredicate.LOADER.register(getResource("bypass_enchantments"), DamageSourcePredicate.BYPASS_ENCHANTMENTS.getLoader()); DamageSourcePredicate.LOADER.register(getResource("fire"), DamageSourcePredicate.FIRE.getLoader()); DamageSourcePredicate.LOADER.register(getResource("magic"), DamageSourcePredicate.MAGIC.getLoader()); DamageSourcePredicate.LOADER.register(getResource("fall"), DamageSourcePredicate.FALL.getLoader()); @@ -182,7 +162,6 @@ private void registerRecipeSerializers() { DamageSourcePredicate.LOADER.register(getResource("melee"), DamageSourcePredicate.MELEE.getLoader()); DamageSourcePredicate.LOADER.register(getResource("message"), SourceMessagePredicate.LOADER); DamageSourcePredicate.LOADER.register(getResource("attacker"), SourceAttackerPredicate.LOADER); - } } diff --git a/src/main/java/slimeknights/mantle/client/ClientEvents.java b/src/main/java/slimeknights/mantle/client/ClientEvents.java index a7efd721b..3c90bc36d 100644 --- a/src/main/java/slimeknights/mantle/client/ClientEvents.java +++ b/src/main/java/slimeknights/mantle/client/ClientEvents.java @@ -52,6 +52,11 @@ public class ClientEvents implements ClientModInitializer { private static final Function COOLDOWN_TRACKER = OffhandCooldownTracker::getCooldown; + /** Called on construct to initiatlize things that need early entry */ + public static void onConstruct() { + FluidTextureManager.init(); + } + static void registerEntityRenderers() { BlockEntityRenderers.register(MantleRegistrations.SIGN, SignRenderer::new); } @@ -61,7 +66,6 @@ static void registerListeners() { ResourceManagerHelper.get(PackType.CLIENT_RESOURCES).registerReloadListener(new BookLoader()); ResourceColorManager.init(); FluidTooltipHandler.init(); - FluidTextureManager.init(event); } @Override diff --git a/src/main/java/slimeknights/mantle/client/model/inventory/ModelItem.java b/src/main/java/slimeknights/mantle/client/model/inventory/ModelItem.java index 41b53bf06..b41101d22 100644 --- a/src/main/java/slimeknights/mantle/client/model/inventory/ModelItem.java +++ b/src/main/java/slimeknights/mantle/client/model/inventory/ModelItem.java @@ -2,10 +2,9 @@ import com.google.gson.JsonObject; import com.google.gson.JsonSyntaxException; +import com.mojang.math.Vector3f; import lombok.Getter; import net.minecraft.util.GsonHelper; -import net.minecraft.world.item.ItemDisplayContext; -import org.joml.Vector3f; import slimeknights.mantle.client.model.util.ModelHelper; import slimeknights.mantle.util.JsonHelper; @@ -81,16 +80,10 @@ public boolean isHidden() { /** Parses a transform type from a string */ private static ItemDisplayContext parseTransformType(JsonObject json, String key) { String name = GsonHelper.getAsString(json, key, "none"); - switch (name) { - case "none": return ItemDisplayContext.NONE; - case "head": return ItemDisplayContext.HEAD; - case "gui": return ItemDisplayContext.GUI; - case "ground": return ItemDisplayContext.GROUND; - case "fixed": return ItemDisplayContext.FIXED; - case "thirdperson_righthand": return ItemDisplayContext.THIRD_PERSON_RIGHT_HAND; - case "thirdperson_lefthand": return ItemDisplayContext.THIRD_PERSON_LEFT_HAND; - case "firstperson_righthand": return ItemDisplayContext.FIRST_PERSON_RIGHT_HAND; - case "firstperson_lefthand": return ItemDisplayContext.FIRST_PERSON_LEFT_HAND; + for (TransformType type : TransformType.values()) { + if (name.equals(type.getSerializeName())) { + return type; + } } throw new JsonSyntaxException("Unknown transform type " + name); } diff --git a/src/main/java/slimeknights/mantle/client/model/util/ColoredBlockModel.java b/src/main/java/slimeknights/mantle/client/model/util/ColoredBlockModel.java index 90a6de3db..a7db01fa8 100644 --- a/src/main/java/slimeknights/mantle/client/model/util/ColoredBlockModel.java +++ b/src/main/java/slimeknights/mantle/client/model/util/ColoredBlockModel.java @@ -30,6 +30,7 @@ import net.minecraftforge.client.model.geometry.IGeometryBakingContext; import net.minecraftforge.client.model.geometry.IGeometryLoader; import slimeknights.mantle.Mantle; +import slimeknights.mantle.data.loadable.common.ColorLoadable; import slimeknights.mantle.util.JsonHelper; import slimeknights.mantle.util.LogicHelper; @@ -162,7 +163,7 @@ public boolean isUvLock(boolean defaultLock) { * Parses the color data from JSON */ public static ColorData fromJson(JsonObject json) { - int color = JsonHelper.parseColor(GsonHelper.getAsString(json, "color", "")); + int color = ColorLoadable.ALPHA.getOrDefault(json, "color", -1); int luminosity = GsonHelper.getAsInt(json, "luminosity", -1); Boolean uvlock = null; if (json.has("uvlock")) { diff --git a/src/main/java/slimeknights/mantle/client/model/util/MantleItemLayerModel.java b/src/main/java/slimeknights/mantle/client/model/util/MantleItemLayerModel.java index 984c4cc9b..28fe09735 100644 --- a/src/main/java/slimeknights/mantle/client/model/util/MantleItemLayerModel.java +++ b/src/main/java/slimeknights/mantle/client/model/util/MantleItemLayerModel.java @@ -30,6 +30,7 @@ import net.minecraftforge.client.model.geometry.UnbakedGeometryHelper; import net.minecraftforge.client.model.pipeline.QuadBakingVertexConsumer; import net.minecraftforge.client.model.pipeline.TransformingVertexPipeline; +import slimeknights.mantle.data.loadable.common.ColorLoadable; import slimeknights.mantle.util.ItemLayerPixels; import slimeknights.mantle.util.JsonHelper; import slimeknights.mantle.util.LogicHelper; @@ -42,6 +43,7 @@ import java.util.Collections; import java.util.EnumMap; import java.util.List; +import java.util.PrimitiveIterator.OfInt; import java.util.Set; import java.util.function.Function; @@ -161,7 +163,10 @@ public static ImmutableList getQuadsForSprite(int color, int tint, Te FaceData faceData = new FaceData(uMax, vMax); boolean translucent = false; - for(int f = 0; f < sprite.getFrameCount(); f++) { + OfInt frames = sprite.getUniqueFrames().iterator(); + boolean hasFrames = frames.hasNext(); + while (frames.hasNext()) { + int f = frames.nextInt(); boolean ptu; boolean[] ptv = new boolean[uMax]; Arrays.fill(ptv, true); @@ -308,7 +313,7 @@ else if (building) { // 3. only use the first frame // of these, 2 would give the most accurate result. However, its also the hardest to calculate // of the remaining methods, 3 is both more accurate and easier to calculate than 1, so I opted for that approach - if (sprite.getFrameCount() > 0) { + if (hasFrames) { for(int v = 0; v < vMax; v++) { for(int u = 0; u < uMax; u++) { int alpha = sprite.getPixelRGBA(0, u, vMax - v - 1) >> 24 & 0xFF; @@ -500,7 +505,7 @@ public RenderTypeGroup getRenderType(IGeometryBakingContext context, RenderTypeG * Parses the layer data from JSON */ public static LayerData fromJson(JsonObject json) { - int color = JsonHelper.parseColor(GsonHelper.getAsString(json, "color", "")); + int color = ColorLoadable.ALPHA.getOrDefault(json, "color", -1); // TODO: rename this field? int luminosity = GsonHelper.getAsInt(json, "luminosity"); boolean noTint = GsonHelper.getAsBoolean(json, "no_tint", false); diff --git a/src/main/java/slimeknights/mantle/command/DumpAllTagsCommand.java b/src/main/java/slimeknights/mantle/command/DumpAllTagsCommand.java index 429eb7fe9..5cf5cebb8 100644 --- a/src/main/java/slimeknights/mantle/command/DumpAllTagsCommand.java +++ b/src/main/java/slimeknights/mantle/command/DumpAllTagsCommand.java @@ -17,6 +17,7 @@ import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.tags.TagLoader; import net.minecraft.tags.TagManager; +import slimeknights.mantle.util.JsonHelper; import java.io.File; import java.nio.file.Path; @@ -30,7 +31,6 @@ */ public class DumpAllTagsCommand { private static final String TAG_DUMP_PATH = "./mantle_data_dump"; - private static final int EXTENSION_LENGTH = ".json".length(); /** * Registers this sub command with the root command @@ -91,8 +91,7 @@ private static int runForFolder(CommandContext context, Reso String dataPackFolder = TagManager.getTagDir(key); for (Map.Entry> entry : manager.listResourceStacks(dataPackFolder, fileName -> fileName.getPath().endsWith(".json")).entrySet()) { ResourceLocation resourcePath = entry.getKey(); - String path = resourcePath.getPath(); - ResourceLocation tagId = new ResourceLocation(resourcePath.getNamespace(), path.substring(dataPackFolder.length() + 1, path.length() - EXTENSION_LENGTH)); + ResourceLocation tagId = JsonHelper.localize(resourcePath, dataPackFolder, ".json"); DumpTagCommand.parseTag(entry.getValue(), foundTags.computeIfAbsent(resourcePath, id -> new ArrayList<>()), tagType, tagId, resourcePath); } diff --git a/src/main/java/slimeknights/mantle/data/GenericDataProvider.java b/src/main/java/slimeknights/mantle/data/GenericDataProvider.java index c8ee40b08..2106416ee 100644 --- a/src/main/java/slimeknights/mantle/data/GenericDataProvider.java +++ b/src/main/java/slimeknights/mantle/data/GenericDataProvider.java @@ -1,25 +1,33 @@ package slimeknights.mantle.data; +import com.google.common.hash.Hashing; +import com.google.common.hash.HashingOutputStream; import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.stream.JsonWriter; +import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; -import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput; import net.minecraft.data.CachedOutput; import net.minecraft.data.DataProvider; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.PackType; +import net.minecraft.util.GsonHelper; +import slimeknights.mantle.Mantle; import slimeknights.mantle.util.JsonHelper; +import javax.annotation.Nullable; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; +import java.util.Comparator; /** Generic logic to convert any serializable object into JSON. */ +@SuppressWarnings({"unused", "SameParameterValue"}) // API @RequiredArgsConstructor -@Log4j2 public abstract class GenericDataProvider implements DataProvider { protected final FabricDataOutput output; private final PackType type; @@ -30,14 +38,53 @@ public GenericDataProvider(FabricDataOutput output, PackType type, String folder this(output, type, folder, JsonHelper.DEFAULT_GSON); } + /** + * Saves the given object to JSON + * @param output Output for writing + * @param location Location relative to this data provider's root + * @param object Object to save, will be converted using this provider's GSON instance + * @param keyComparator Key comparator to use + */ + protected void saveJson(CachedOutput output, ResourceLocation location, Object object, @Nullable Comparator keyComparator) { + try { + Path path = this.generator.getOutputFolder().resolve(Paths.get(type.getDirectory(), location.getNamespace(), folder, location.getPath() + ".json")); + saveStable(output, gson.toJsonTree(object), path, keyComparator); + } catch (IOException e) { + Mantle.logger.error("Couldn't create data for {}", location, e); + } + } + /** * Saves the given object to JSON * @param output Output for writing * @param location Location relative to this data provider's root * @param object Object to save, will be converted using this provider's GSON instance */ - protected void saveJson(List> futures, CachedOutput output, ResourceLocation location, Object object) { - Path path = this.output.getOutputFolder().resolve(Paths.get(type.getDirectory(), location.getNamespace(), folder, location.getPath() + ".json")); - futures.add(DataProvider.saveStable(output, gson.toJsonTree(object), path)); + protected void saveJson(CachedOutput output, ResourceLocation location, Object object) { + saveJson(output, location, object, DataProvider.KEY_COMPARATOR); + } + + /** + * Saves the given object to JSON using a codec + * @param output Output for writing + * @param location Location relative to this data provider's root + * @param codec Codec to save the object + * @param object Object to save, will be converted using the passed codec + */ + protected void saveJson(CachedOutput output, ResourceLocation location, Codec codec, T object) { + saveJson(output, location, codec.encodeStart(JsonOps.INSTANCE, object).getOrThrow(false, Mantle.logger::error)); + } + + /** Recreation of {@link DataProvider#saveStable(CachedOutput, JsonElement, Path)} that allows swapping tke key comparator */ + @SuppressWarnings("UnstableApiUsage") + static void saveStable(CachedOutput cache, JsonElement pJson, Path pPath, @Nullable Comparator keyComparator) throws IOException { + ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(); + HashingOutputStream hashingOutput = new HashingOutputStream(Hashing.sha1(), byteOutput); + JsonWriter writer = new JsonWriter(new OutputStreamWriter(hashingOutput, StandardCharsets.UTF_8)); + writer.setSerializeNulls(false); + writer.setIndent(" "); + GsonHelper.writeValue(writer, pJson, keyComparator); + writer.close(); + cache.writeIfNeeded(pPath, byteOutput.toByteArray(), hashingOutput.hash()); } } diff --git a/src/main/java/slimeknights/mantle/data/listener/MergingJsonDataLoader.java b/src/main/java/slimeknights/mantle/data/listener/MergingJsonDataLoader.java index 50cbf8a39..cec254a5e 100644 --- a/src/main/java/slimeknights/mantle/data/listener/MergingJsonDataLoader.java +++ b/src/main/java/slimeknights/mantle/data/listener/MergingJsonDataLoader.java @@ -11,6 +11,7 @@ import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.server.packs.resources.ResourceManagerReloadListener; import net.minecraft.util.GsonHelper; +import slimeknights.mantle.util.JsonHelper; import java.io.IOException; import java.io.Reader; @@ -27,7 +28,6 @@ @RequiredArgsConstructor @Log4j2 public abstract class MergingJsonDataLoader implements ResourceManagerReloadListener { - private static final int JSON_LENGTH = ".json".length(); @VisibleForTesting protected final Gson gson; @@ -57,8 +57,7 @@ public void onResourceManagerReload(ResourceManager manager) { Map map = new HashMap<>(); for (Entry> entry : manager.listResourceStacks(folder, fileName -> fileName.getPath().endsWith(".json")).entrySet()) { ResourceLocation filePath = entry.getKey(); - String path = filePath.getPath(); - ResourceLocation id = new ResourceLocation(filePath.getNamespace(), path.substring(folder.length() + 1, path.length() - JSON_LENGTH)); + ResourceLocation id = JsonHelper.localize(filePath, folder, ".json"); for (Resource resource : entry.getValue()) { try (Reader reader = resource.openAsReader()) { diff --git a/src/main/java/slimeknights/mantle/data/loadable/ContextStreamable.java b/src/main/java/slimeknights/mantle/data/loadable/ContextStreamable.java new file mode 100644 index 000000000..db01cbb9e --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/ContextStreamable.java @@ -0,0 +1,27 @@ +package slimeknights.mantle.data.loadable; + +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.field.DirectField; +import slimeknights.mantle.util.typed.TypedMap; + +/** + * Streamable with additional context that can be passed from the parent during deserializing, notably used for recipe ID. + * It is undecided how this will be translated to {@code StreamCodecs} once they exist + */ +public interface ContextStreamable extends Streamable { + /** + * Decodes this loadable from the network + * @param buffer Buffer instance + * @param context Additional parsing context, used notably by recipe serializers to store the ID and serializer. + * Will be {@link TypedMap#EMPTY} in nested usages unless {@link DirectField} is used. + * @return Parsed object + * @throws io.netty.handler.codec.DecoderException If unable to decode + */ + T decode(FriendlyByteBuf buffer, TypedMap context); + + /** Contextless implementation of {@link #decode(FriendlyByteBuf, TypedMap)} for {@link Streamable}. */ + @Override + default T decode(FriendlyByteBuf buffer) { + return decode(buffer, TypedMap.empty()); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/ErrorFactory.java b/src/main/java/slimeknights/mantle/data/loadable/ErrorFactory.java new file mode 100644 index 000000000..b93d29688 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/ErrorFactory.java @@ -0,0 +1,47 @@ +package slimeknights.mantle.data.loadable; + +import com.google.gson.JsonSyntaxException; +import io.netty.handler.codec.DecoderException; +import io.netty.handler.codec.EncoderException; +import slimeknights.mantle.data.loadable.field.ConstantField; +import slimeknights.mantle.data.loadable.field.LoadableField; + +import java.util.function.Consumer; + +/** Simple helpers to create exceptions */ +public interface ErrorFactory extends Consumer { + /** Error factory for a json syntax error during parsing */ + ErrorFactory JSON_SYNTAX_ERROR = JsonSyntaxException::new; + /** Error factory for a decoder exception */ + ErrorFactory DECODER_EXCEPTION = DecoderException::new; + /** Error factory for a decoder exception */ + ErrorFactory ENCODER_EXCEPTION = EncoderException::new; + /** Error factory for a json during writing JSON */ + ErrorFactory RUNTIME = new ErrorFactory() { + @Override + public RuntimeException create(String error) { + return new RuntimeException(error); + } + + @Override + public RuntimeException create(RuntimeException base) { + return base; + } + }; + /** Field for constructors wishing to possibly throw */ + LoadableField FIELD = new ConstantField<>(JSON_SYNTAX_ERROR, DECODER_EXCEPTION); + + /** Throws an exception from the given error */ + @Override + default void accept(String error) { + throw create(error); + } + + /** Creates an exception with a string error */ + RuntimeException create(String error); + + /** Creates an exception wrapping the given exception message */ + default RuntimeException create(RuntimeException base) { + return create(base.getMessage()); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/IAmLoadable.java b/src/main/java/slimeknights/mantle/data/loadable/IAmLoadable.java new file mode 100644 index 000000000..d36329d4a --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/IAmLoadable.java @@ -0,0 +1,15 @@ +package slimeknights.mantle.data.loadable; + +import slimeknights.mantle.data.loadable.record.RecordLoadable; + +/** Interface for an object that has a loadable. It is expected the loadable returned works on the object itself. */ +public interface IAmLoadable { + /** Loadable instance */ + Loadable loadable(); + + /** Interface for an object with a record loadable instance */ + interface Record extends IAmLoadable { + @Override + RecordLoadable loadable(); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/Loadable.java b/src/main/java/slimeknights/mantle/data/loadable/Loadable.java new file mode 100644 index 000000000..2b57dfb36 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/Loadable.java @@ -0,0 +1,193 @@ +package slimeknights.mantle.data.loadable; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.JsonSyntaxException; +import org.jetbrains.annotations.Contract; +import slimeknights.mantle.data.loadable.field.DefaultingField; +import slimeknights.mantle.data.loadable.field.LoadableField; +import slimeknights.mantle.data.loadable.field.NullableField; +import slimeknights.mantle.data.loadable.field.RequiredField; +import slimeknights.mantle.data.loadable.field.TryDirectField; +import slimeknights.mantle.data.loadable.mapping.AnyCollectionLoadable; +import slimeknights.mantle.data.loadable.mapping.ListLoadable; +import slimeknights.mantle.data.loadable.mapping.MappedLoadable; +import slimeknights.mantle.data.loadable.mapping.SetLoadable; + +import javax.annotation.Nullable; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** Interface for a generic loadable object */ +@SuppressWarnings("unused") // API +public interface Loadable extends JsonDeserializer, JsonSerializer, Streamable { + /** + * Deserializes the object from the passed JSON element + * @param element Element of an unknown type to parse + * @param key Key that contained this element + * @return Parsed loadable value + * @throws com.google.gson.JsonSyntaxException If unable to read from JSON + */ + T convert(JsonElement element, String key); + + /** + * Writes the passed object to json + * @param object Object to serialize + * @return Serialized object + * @throws RuntimeException If unable to serialize the object + */ + JsonElement serialize(T object); + + + /* GSON methods, lets us easily use loadables with GSON adapters. Generally should not override. */ + + @Override + default T deserialize(JsonElement json, Type type, JsonDeserializationContext context) { + return convert(json, type.getTypeName()); + } + + @Override + default JsonElement serialize(T object, Type type, JsonSerializationContext context) { + return serialize(object); + } + + + /* Helpers for raw loadable use */ + + /** + * Gets then deserializes the given fieldm throwing if it is missing + * You should not override this method as we wish to leave that handling missing up to the RecordLoadable. + * Instead, consider a custom implementation of defaultField if you have a standard default. + * @param parent Parent to fetch field from + * @param key Field to get + * @return Value, or throws if missing + * @throws JsonSyntaxException If the field is missing or cannot be parsed. + */ + default T getIfPresent(JsonObject parent, String key) { + if (parent.has(key)) { + return convert(parent.get(key), key); + } + throw new JsonSyntaxException("Missing JSON field " + key + ""); + } + + /** + * Gets then deserializes the given field, or returns a default value if its missing. + * You should not override this method as we wish to leave that handling missing up to the RecordLoadable. + * Instead, consider a custom implementation of defaultField if you have a standard default. + * @param parent Parent to fetch field from + * @param key Field to get + * @param defaultValue Default value to fetch + * @return Value or default. + * @throws JsonSyntaxException If the field cannot be parsed. + */ + @Nullable + @Contract("_,_,!null->!null") + default T getOrDefault(JsonObject parent, String key, @Nullable T defaultValue) { + JsonElement element = parent.get(key); + if (element != null && !element.isJsonNull()) { + return convert(element, key); + } + return defaultValue; + } + + + /* Fields */ + + /** Creates a required field from this loadable */ + default

LoadableField requiredField(String key, Function getter) { + return new RequiredField<>(this, key, false, getter); + } + + /** Creates an optional field that falls back to null */ + default

LoadableField nullableField(String key, Function getter) { + return new NullableField<>(this, key, getter); + } + + /** Creates a defaulting field that uses a default value when missing */ + default

LoadableField defaultField(String key, T defaultValue, boolean serializeDefault, Function getter) { + return new DefaultingField<>(this, key, defaultValue, serializeDefault, getter); + } + + /** Creates a defaulting field that uses a default value when missing */ + default

LoadableField defaultField(String key, T defaultValue, Function getter) { + return defaultField(key, defaultValue, false, getter); + } + + /** Creates a field that loads this object directly into the parent JSON object if possible (it serialized to JSON), otherwise loads it into a field */ + default

LoadableField tryDirectField(String key, Function getter, String... conflicts) { + return new TryDirectField<>(this, key, getter, conflicts); + } + + + /* Collections */ + + /** Makes a list of this loadable */ + default Loadable> list(int minSize) { + return new ListLoadable<>(this, minSize); + } + + /** Makes a list of this loadable */ + default Loadable> list() { + return list(1); + } + + /** Makes a set of this loadable */ + default Loadable> set(int minSize) { + return new SetLoadable<>(this, minSize); + } + + /** Makes a set of this loadable */ + default Loadable> set() { + return set(1); + } + + /** Makes a map from this loadable with this as values using the getter to determine map keys */ + default Loadable> mapWithKeys(int minSize, Function keyGetter) { + return AnyCollectionLoadable.setBacked(this, minSize).mapWithKeys(keyGetter); + } + + /** Makes a map from this loadable with this as keys using the getter to determine map values */ + default Loadable> mapWithValues(int minSize, Function valueGetter) { + return AnyCollectionLoadable.setBacked(this, minSize).mapWithValues(valueGetter); + } + + + /* Mapping */ + + /** Maps this loader to another type, with error factory on both from and to */ + default Loadable xmap(BiFunction from, BiFunction to) { + return MappedLoadable.of(this, from, to); + } + + /** Maps this loader to another type, with error factory on from */ + default Loadable comapFlatMap(BiFunction from, Function to) { + return xmap(from, MappedLoadable.flatten(to)); + } + + /** Maps this loader to another type */ + default Loadable flatComap(Function from, BiFunction to) { + return xmap(MappedLoadable.flatten(from), to); + } + + /** Maps this loader to another type */ + default Loadable flatXmap(Function from, Function to) { + return xmap(MappedLoadable.flatten(from), MappedLoadable.flatten(to)); + } + + /** + * Validates the result of this map using the given function. This is equivelent to {@link #xmap(BiFunction, BiFunction)} with the same argument twice. + * @param validator Validator function, returns instance if valid. + * @return Validated loadable + */ + default Loadable validate(BiFunction validator) { + return xmap(validator, validator); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/LoadableCodec.java b/src/main/java/slimeknights/mantle/data/loadable/LoadableCodec.java new file mode 100644 index 000000000..49cf755bf --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/LoadableCodec.java @@ -0,0 +1,17 @@ +package slimeknights.mantle.data.loadable; + +import com.google.gson.JsonElement; +import slimeknights.mantle.data.JsonCodec; + +/** Implementation of a codec using a loadable. Note this will be inefficient comparatively when using {@link net.minecraft.nbt.NbtOps} */ +public record LoadableCodec(Loadable loadable) implements JsonCodec { + @Override + public T deserialize(JsonElement element) { + return loadable.convert(element, "codec"); + } + + @Override + public JsonElement serialize(T object) { + return loadable.serialize(object); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/Loadables.java b/src/main/java/slimeknights/mantle/data/loadable/Loadables.java new file mode 100644 index 000000000..761cf2375 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/Loadables.java @@ -0,0 +1,90 @@ +package slimeknights.mantle.data.loadable; + +import net.minecraft.ResourceLocationException; +import net.minecraft.core.Registry; +import net.minecraft.core.particles.ParticleType; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.tags.TagKey; +import net.minecraft.world.effect.MobEffect; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.ai.attributes.Attribute; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.alchemy.Potion; +import net.minecraft.world.item.enchantment.Enchantment; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.common.ToolAction; +import slimeknights.mantle.data.loadable.common.RegistryLoadable; +import slimeknights.mantle.data.loadable.primitive.ResourceLocationLoadable; +import slimeknights.mantle.data.loadable.primitive.StringLoadable; + +import java.util.function.BiFunction; + +/** Various loadable instances provided by this mod */ +@SuppressWarnings({"deprecation", "unused"}) +public class Loadables { + private Loadables() {} + + /** Loadable for a resource location */ + public static final StringLoadable RESOURCE_LOCATION = StringLoadable.DEFAULT.xmap((s, e) -> { + try { + return new ResourceLocation(s); + } catch (ResourceLocationException ex) { + throw e.create(ex); + } + }, (r, e) -> r.toString()); + public static final StringLoadable TOOL_ACTION = StringLoadable.DEFAULT.flatXmap(ToolAction::get, ToolAction::name); + + /* Registries */ + public static final ResourceLocationLoadable SOUND_EVENT = new RegistryLoadable<>(Registry.SOUND_EVENT); + public static final ResourceLocationLoadable FLUID = new RegistryLoadable<>(Registry.FLUID); + public static final ResourceLocationLoadable MOB_EFFECT = new RegistryLoadable<>(Registry.MOB_EFFECT); + public static final ResourceLocationLoadable BLOCK = new RegistryLoadable<>(Registry.BLOCK); + public static final ResourceLocationLoadable ENCHANTMENT = new RegistryLoadable<>(Registry.ENCHANTMENT); + public static final ResourceLocationLoadable> ENTITY_TYPE = new RegistryLoadable<>(Registry.ENTITY_TYPE); + public static final ResourceLocationLoadable ITEM = new RegistryLoadable<>(Registry.ITEM); + public static final ResourceLocationLoadable POTION = new RegistryLoadable<>(Registry.POTION); + public static final ResourceLocationLoadable> PARTICLE_TYPE = new RegistryLoadable<>(Registry.PARTICLE_TYPE); + public static final ResourceLocationLoadable> BLOCK_ENTITY_TYPE = new RegistryLoadable<>(Registry.BLOCK_ENTITY_TYPE); + public static final ResourceLocationLoadable ATTRIBUTE = new RegistryLoadable<>(Registry.ATTRIBUTE); + + /* Non-default registries */ + public static final StringLoadable NON_EMPTY_FLUID = notValue(FLUID, Fluids.EMPTY, "Fluid cannot be empty"); + public static final StringLoadable NON_EMPTY_BLOCK = notValue(BLOCK, Blocks.AIR, "Block cannot be air"); + public static final StringLoadable NON_EMPTY_ITEM = notValue(ITEM, Items.AIR, "Item cannot be empty"); + + /* Tag keys */ + public static final StringLoadable> FLUID_TAG = tagKey(Registry.FLUID_REGISTRY); + public static final StringLoadable> MOB_EFFECT_TAG = tagKey(Registry.MOB_EFFECT_REGISTRY); + public static final StringLoadable> BLOCK_TAG = tagKey(Registry.BLOCK_REGISTRY); + public static final StringLoadable> ENCHANTMENT_TAG = tagKey(Registry.ENCHANTMENT_REGISTRY); + public static final StringLoadable>> ENTITY_TYPE_TAG = tagKey(Registry.ENTITY_TYPE_REGISTRY); + public static final StringLoadable> ITEM_TAG = tagKey(Registry.ITEM_REGISTRY); + public static final StringLoadable> POTION_TAG = tagKey(Registry.POTION_REGISTRY); + public static final StringLoadable>> BLOCK_ENTITY_TYPE_TAG = tagKey(Registry.BLOCK_ENTITY_TYPE_REGISTRY); + + + /* Helpers */ + + /** Creates a tag key loadable */ + public static StringLoadable> tagKey(ResourceKey> registry) { + return RESOURCE_LOCATION.flatXmap(key -> TagKey.create(registry, key), TagKey::location); + } + + /** Maps a loadable to a variant that disallows a particular value */ + public static StringLoadable notValue(StringLoadable loadable, T notValue, String errorMsg) { + BiFunction mapper = (value, error) -> { + if (value == notValue) { + throw error.create(errorMsg); + } + return value; + }; + return loadable.xmap(mapper, mapper); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/Streamable.java b/src/main/java/slimeknights/mantle/data/loadable/Streamable.java new file mode 100644 index 000000000..b49d7a0ce --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/Streamable.java @@ -0,0 +1,22 @@ +package slimeknights.mantle.data.loadable; + +import net.minecraft.network.FriendlyByteBuf; + +/** This is a temporary interface intended to be replaced by Mojang's {@code StreamCodec} in the future. It means loadables will automatically work as stream codecs with minimal extra effort. */ +public interface Streamable { + /** + * Reads the object from the packet buffer + * @param buffer Buffer instance + * @return Instance read from network + * @throws io.netty.handler.codec.DecoderException If unable to decode a value from network + */ + T decode(FriendlyByteBuf buffer); + + /** + * Writes this object to the packet buffer + * @param buffer Buffer instance + * @param value Object to write + * @throws io.netty.handler.codec.EncoderException If unable to encode a value to network + */ + void encode(FriendlyByteBuf buffer, T value); +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/common/BlockStateLoadable.java b/src/main/java/slimeknights/mantle/data/loadable/common/BlockStateLoadable.java new file mode 100644 index 000000000..82eaff44b --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/common/BlockStateLoadable.java @@ -0,0 +1,118 @@ +package slimeknights.mantle.data.loadable.common; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.util.GsonHelper; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.Property; +import slimeknights.mantle.data.loadable.Loadables; +import slimeknights.mantle.data.loadable.record.RecordLoadable; +import slimeknights.mantle.util.typed.TypedMap; + +import java.util.Map.Entry; +import java.util.Optional; + +/** Loadable reading block state properties from JSON */ +public enum BlockStateLoadable implements RecordLoadable { + /** Serializes all state properties */ + ALL { + @Override + protected > void serializeProperty(BlockState serialize, Property property, BlockState defaultState, JsonObject json) { + json.addProperty(property.getName(), property.getName(serialize.getValue(property))); + } + }, + /** Serializes any properties different from the default state */ + DIFFERENCE { + @Override + protected > void serializeProperty(BlockState serialize, Property property, BlockState defaultState, JsonObject json) { + T value = serialize.getValue(property); + if (!value.equals(defaultState.getValue(property))) { + json.addProperty(property.getName(), property.getName(value)); + } + } + }; + + @Override + public BlockState convert(JsonElement element, String key) { + // primitive means parse the block with default properties + if (element.isJsonPrimitive()) { + return Loadables.BLOCK.convert(element, key).defaultBlockState(); + } + return RecordLoadable.super.convert(element, key); + } + + /** + * Sets the property + * @param state State before changes + * @param property Property to set + * @param name Value name + * @param Type of property + * @return State with the property + * @throws JsonSyntaxException if the property has no element with the given name + */ + private static > BlockState setValue(BlockState state, Property property, String name) { + Optional value = property.getValue(name); + if (value.isPresent()) { + return state.setValue(property, value.get()); + } + throw new JsonSyntaxException("Property " + property + " does not contain value " + name); + } + + @Override + public BlockState deserialize(JsonObject json, TypedMap context) { + Block block = Loadables.BLOCK.getIfPresent(json, "block"); + BlockState state = block.defaultBlockState(); + if (json.has("properties")) { + StateDefinition definition = block.getStateDefinition(); + for (Entry entry : GsonHelper.getAsJsonObject(json, "properties").entrySet()) { + String key = entry.getKey(); + Property property = definition.getProperty(key); + if (property == null) { + throw new JsonSyntaxException("Property " + key + " does not exist in block " + block); + } + state = setValue(state, property, GsonHelper.convertToString(entry.getValue(), key)); + } + } + return state; + } + + @Override + public JsonElement serialize(BlockState state) { + Block block = state.getBlock(); + if (this == DIFFERENCE && state == block.defaultBlockState()) { + return Loadables.BLOCK.serialize(block); + } + return RecordLoadable.super.serialize(state); + } + + /** Serializes the property if it differs in the default state */ + protected abstract > void serializeProperty(BlockState serialize, Property property, BlockState defaultState, JsonObject json); + + @Override + public void serialize(BlockState state, JsonObject json) { + Block block = state.getBlock(); + json.add("block", Loadables.BLOCK.serialize(block)); + BlockState defaultState = block.defaultBlockState(); + JsonObject properties = new JsonObject(); + for (Property property : block.getStateDefinition().getProperties()) { + serializeProperty(state, property, defaultState, properties); + } + if (properties.size() > 0) { + json.add("properties", properties); + } + } + + @Override + public BlockState decode(FriendlyByteBuf buffer, TypedMap context) { + return Block.stateById(buffer.readVarInt()); + } + + @Override + public void encode(FriendlyByteBuf buffer, BlockState object) { + buffer.writeVarInt(Block.getId(object)); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/common/CodecLoadable.java b/src/main/java/slimeknights/mantle/data/loadable/common/CodecLoadable.java new file mode 100644 index 000000000..8843a5fbf --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/common/CodecLoadable.java @@ -0,0 +1,31 @@ +package slimeknights.mantle.data.loadable.common; + +import com.google.gson.JsonElement; +import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.ErrorFactory; +import slimeknights.mantle.data.loadable.Loadable; + +/** Implementation of a loadable using a codec. Note this will be inefficient when reading from and writing to the network */ +public record CodecLoadable(Codec codec) implements Loadable { + @Override + public T convert(JsonElement element, String key) { + return codec.parse(JsonOps.INSTANCE, element).getOrThrow(false, ErrorFactory.JSON_SYNTAX_ERROR); + } + + @Override + public JsonElement serialize(T object) { + return codec.encodeStart(JsonOps.INSTANCE, object).getOrThrow(false, ErrorFactory.RUNTIME); + } + + @Override + public T decode(FriendlyByteBuf buffer) { + return buffer.readWithCodec(codec); + } + + @Override + public void encode(FriendlyByteBuf buffer, T object) { + buffer.writeWithCodec(codec, object); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/common/ColorLoadable.java b/src/main/java/slimeknights/mantle/data/loadable/common/ColorLoadable.java new file mode 100644 index 000000000..34dbdd91e --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/common/ColorLoadable.java @@ -0,0 +1,66 @@ +package slimeknights.mantle.data.loadable.common; + +import com.google.gson.JsonSyntaxException; +import lombok.RequiredArgsConstructor; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.primitive.StringLoadable; + +/** Loadable to fetch colors from JSON */ +@RequiredArgsConstructor +public enum ColorLoadable implements StringLoadable { + ALPHA { + @Override + public Integer parseString(String color, String key) { + // two options, 6 character or 8 character, must not start with - sign + if (color.charAt(0) != '-') { + try { + // length of 8 must parse as long, supports transparency + int length = color.length(); + if (length == 8) { + return (int)Long.parseLong(color, 16); + } + if (length == 6) { + return 0xFF000000 | Integer.parseInt(color, 16); + } + } catch (NumberFormatException ex) { + // NO-OP + } + } + throw new JsonSyntaxException("Invalid color '" + color + "' at " + key); + } + + @Override + public String getString(Integer color) { + return String.format("%08X", color); + } + }, + NO_ALPHA { + @Override + public Integer parseString(String color, String key) { + // only consider 6 digits with no alpha, will force to full alpha + if (color.charAt(0) != '-' && color.length() == 6) { + try { + return 0xFF000000 | Integer.parseInt(color, 16); + } catch (NumberFormatException ex) { + // NO-OP + } + } + throw new JsonSyntaxException("Invalid color '" + color + "' at " + key); + } + + @Override + public String getString(Integer color) { + return String.format("%06X", color & 0xFFFFFF); + } + }; + + @Override + public Integer decode(FriendlyByteBuf buffer) { + return buffer.readInt(); + } + + @Override + public void encode(FriendlyByteBuf buffer, Integer color) { + buffer.writeInt(color); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/common/FluidStackLoadable.java b/src/main/java/slimeknights/mantle/data/loadable/common/FluidStackLoadable.java new file mode 100644 index 000000000..ca1ccc463 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/common/FluidStackLoadable.java @@ -0,0 +1,105 @@ +package slimeknights.mantle.data.loadable.common; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.FluidType; +import slimeknights.mantle.data.loadable.ErrorFactory; +import slimeknights.mantle.data.loadable.Loadable; +import slimeknights.mantle.data.loadable.Loadables; +import slimeknights.mantle.data.loadable.field.LoadableField; +import slimeknights.mantle.data.loadable.primitive.IntLoadable; +import slimeknights.mantle.data.loadable.record.RecordLoadable; + +import javax.annotation.Nullable; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; + +/** Loadable for a fluid stack */ +@SuppressWarnings("unused") // API +public class FluidStackLoadable { + private FluidStackLoadable() {} + + /* reused lambdas */ + /** Getter for an item from a stack */ + private static final Function FLUID_GETTER = FluidStack::getFluid; + /** Checks if a stack can be serialized to a primitive, ignoring count */ + private static final Predicate COMPACT_NBT = stack -> !stack.hasTag(); + /** Maps a fluid stack that may be empty to a strictly not empty one */ + private static final BiFunction NOT_EMPTY = (stack, error) -> { + if (stack.isEmpty()) { + throw error.create("FluidStack cannot be empty"); + } + return stack; + }; + + /* fields */ + /** Field for an optional fluid */ + private static final LoadableField FLUID = Loadables.FLUID.defaultField("fluid", Fluids.EMPTY, false, FLUID_GETTER); + /** Field for fluid stack count that allows empty */ + private static final LoadableField AMOUNT = IntLoadable.FROM_ZERO.requiredField("amount", FluidStack::getAmount); + /** Field for fluid stack count */ + private static final LoadableField NBT = NBTLoadable.ALLOW_STRING.nullableField("nbt", FluidStack::getTag); + + + /* Optional */ + /** Single item which may be empty with an amount of 1000 */ + public static final Loadable OPTIONAL_BUCKET = fixedSize(FluidType.BUCKET_VOLUME); + /** Loadable for a stack that may be empty with variable count */ + public static final RecordLoadable OPTIONAL_STACK = RecordLoadable.create(FLUID, AMOUNT, (fluid, count) -> makeStack(fluid, count, null)); + /** Loadable for a stack that may be empty with NBT and an amount of 1000 */ + public static final RecordLoadable OPTIONAL_BUCKET_NBT = fixedSizeNBT(FluidType.BUCKET_VOLUME); + /** Loadable for a stack that may be empty with variable count and NBT */ + public static final RecordLoadable OPTIONAL_STACK_NBT = RecordLoadable.create(FLUID, AMOUNT, NBT, FluidStackLoadable::makeStack); + + + /* Required */ + /** Single item which may not be empty with an amount of 1000 */ + public static final Loadable REQUIRED_BUCKET = notEmpty(OPTIONAL_BUCKET); + /** Loadable for a stack that may not be empty with variable count */ + public static final RecordLoadable REQUIRED_STACK = notEmpty(OPTIONAL_STACK); + /** Loadable for a stack that may not be empty with NBT and an amount of 1000 */ + public static final RecordLoadable REQUIRED_BUCKET_NBT = notEmpty(OPTIONAL_BUCKET_NBT); + /** Loadable for a stack that may not be empty with variable count and NBT */ + public static final RecordLoadable REQUIRED_STACK_NBT = notEmpty(OPTIONAL_STACK_NBT); + + + /* Helpers */ + + /** Makes an item stack from the given parameters */ + private static FluidStack makeStack(Fluid fluid, int amount, @Nullable CompoundTag nbt) { + if (fluid == Fluids.EMPTY || amount <= 0) { + return FluidStack.EMPTY; + } + return new FluidStack(fluid, amount, nbt); + } + + /** Creates a loadable for a stack with a single item */ + public static Loadable fixedSize(int amount) { + if (amount <= 0) { + throw new IllegalArgumentException("Count must be positive, received " + amount); + } + return Loadables.FLUID.flatXmap(fluid -> makeStack(fluid, amount, null), FLUID_GETTER); + } + + /** Creates a loadable for a stack with a single item */ + public static RecordLoadable fixedSizeNBT(int amount) { + if (amount <= 0) { + throw new IllegalArgumentException("Amount must be positive, received " + amount); + } + return RecordLoadable.create(FLUID, NBT, (fluid, tag) -> makeStack(fluid, amount, tag)) + .compact(OPTIONAL_BUCKET, COMPACT_NBT); + } + + /** Creates a non-empty variant of the loadable */ + public static Loadable notEmpty(Loadable loadable) { + return loadable.validate(NOT_EMPTY); + } + + /** Creates a non-empty variant of the loadable */ + public static RecordLoadable notEmpty(RecordLoadable loadable) { + return loadable.validate(NOT_EMPTY); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/common/IngredientLoadable.java b/src/main/java/slimeknights/mantle/data/loadable/common/IngredientLoadable.java new file mode 100644 index 000000000..9abf6af27 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/common/IngredientLoadable.java @@ -0,0 +1,44 @@ +package slimeknights.mantle.data.loadable.common; + +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonSyntaxException; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.item.crafting.Ingredient; +import slimeknights.mantle.data.loadable.Loadable; + +/** Loadable for ingredients, handling Forge ingredients */ +public enum IngredientLoadable implements Loadable { + ALLOW_EMPTY, + DISALLOW_EMPTY; + + @Override + public Ingredient convert(JsonElement element, String key) { + Ingredient ingredient = Ingredient.fromJson(element); + if (ingredient == Ingredient.EMPTY && this == DISALLOW_EMPTY) { + throw new JsonSyntaxException("Ingredient cannot be empty"); + } + return ingredient; + } + + @Override + public JsonElement serialize(Ingredient object) { + if (object == Ingredient.EMPTY) { + if (this == ALLOW_EMPTY) { + return JsonNull.INSTANCE; + } + throw new IllegalArgumentException("Ingredient cannot be empty"); + } + return object.toJson(); + } + + @Override + public Ingredient decode(FriendlyByteBuf buffer) { + return Ingredient.fromNetwork(buffer); + } + + @Override + public void encode(FriendlyByteBuf buffer, Ingredient object) { + object.toNetwork(buffer); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/common/ItemStackLoadable.java b/src/main/java/slimeknights/mantle/data/loadable/common/ItemStackLoadable.java new file mode 100644 index 000000000..918c4eb25 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/common/ItemStackLoadable.java @@ -0,0 +1,171 @@ +package slimeknights.mantle.data.loadable.common; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import io.netty.handler.codec.EncoderException; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import slimeknights.mantle.data.loadable.ErrorFactory; +import slimeknights.mantle.data.loadable.Loadable; +import slimeknights.mantle.data.loadable.Loadables; +import slimeknights.mantle.data.loadable.field.LoadableField; +import slimeknights.mantle.data.loadable.primitive.IntLoadable; +import slimeknights.mantle.data.loadable.record.RecordLoadable; +import slimeknights.mantle.util.typed.TypedMap; + +import javax.annotation.Nullable; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** Loadable for an item stack */ +@SuppressWarnings("unused") // API +public class ItemStackLoadable { + private ItemStackLoadable() {} + + /* reused lambdas */ + /** Getter for an item from a stack */ + private static final Function ITEM_GETTER = ItemStack::getItem; + /** Maps an item stack that may be empty to a strictly not empty one */ + private static final BiFunction NOT_EMPTY = (stack, error) -> { + if (stack.isEmpty()) { + throw error.create("ItemStack cannot be empty"); + } + return stack; + }; + + /* fields */ + /** Field for an optional item */ + private static final LoadableField ITEM = Loadables.ITEM.defaultField("item", Items.AIR, false, ITEM_GETTER); + /** Field for item stack count that allows empty */ + private static final LoadableField COUNT = IntLoadable.FROM_ZERO.defaultField("count", 1, true, ItemStack::getCount); + /** Field for item stack count that allows empty */ + private static final LoadableField NBT = NBTLoadable.ALLOW_STRING.nullableField("nbt", ItemStack::getTag); + + + /* Optional */ + /** Single item which may be empty with a count of 1 */ + public static final Loadable OPTIONAL_ITEM = Loadables.ITEM.flatXmap(item -> makeStack(item, 1, null), ITEM_GETTER); + /** Loadable for a stack that may be empty with variable count */ + public static final RecordLoadable OPTIONAL_STACK = RecordLoadable.create(ITEM, COUNT, (item, count) -> makeStack(item, count, null)) + .compact(OPTIONAL_ITEM, stack -> stack.getCount() == 1); + /** Loadable for a stack that may be empty with NBT and a count of 1 */ + public static final RecordLoadable OPTIONAL_ITEM_NBT = NBTStack.FIXED_COUNT; + /** Loadable for a stack that may be empty with variable count and NBT */ + public static final RecordLoadable OPTIONAL_STACK_NBT = NBTStack.READ_COUNT; + + /* Required */ + /** Single item which may not be empty with a count of 1 */ + public static final Loadable REQUIRED_ITEM = notEmpty(OPTIONAL_ITEM); + /** Loadable for a stack that may not be empty with variable count */ + public static final RecordLoadable REQUIRED_STACK = notEmpty(OPTIONAL_STACK); + /** Loadable for a stack that may not be empty with NBT and a count of 1 */ + public static final RecordLoadable REQUIRED_ITEM_NBT = notEmpty(OPTIONAL_ITEM_NBT); + /** Loadable for a stack that may not be empty with variable count and NBT */ + public static final RecordLoadable REQUIRED_STACK_NBT = notEmpty(OPTIONAL_STACK_NBT); + + + /* Helpers */ + + /** Makes an item stack from the given parameters */ + private static ItemStack makeStack(Item item, int count, @Nullable CompoundTag nbt) { + if (item == Items.AIR || count == 0) { + return ItemStack.EMPTY; + } + ItemStack stack = new ItemStack(item, count); + if (nbt != null) { + stack.setTag(nbt); + } + return stack; + } + + /** Creates a non-empty variant of the loadable */ + public static Loadable notEmpty(Loadable loadable) { + return loadable.validate(NOT_EMPTY); + } + + /** Creates a non-empty variant of the loadable */ + public static RecordLoadable notEmpty(RecordLoadable loadable) { + return loadable.validate(NOT_EMPTY); + } + + /** Loadable for an item stack with NBT, requires special logic due to forges share tags */ + private enum NBTStack implements RecordLoadable { + /** Reads count from JSON */ + READ_COUNT, + /** Count is always 1 */ + FIXED_COUNT; + + + /* General JSON */ + + @Override + public ItemStack deserialize(JsonObject json, TypedMap context) { + int count = 1; + if (this == READ_COUNT) { + count = COUNT.get(json, context); + } + return makeStack(ITEM.get(json, context), count, NBT.get(json, context)); + } + + @Override + public void serialize(ItemStack stack, JsonObject json) { + ITEM.serialize(stack, json); + if (this == READ_COUNT) { + COUNT.serialize(stack, json); + } + NBT.serialize(stack, json); + } + + + /* Compact JSON */ + + @Override + public ItemStack convert(JsonElement element, String key) { + if (element.isJsonPrimitive()) { + return OPTIONAL_ITEM.convert(element, key); + } + return RecordLoadable.super.convert(element, key); + } + + @Override + public JsonElement serialize(ItemStack stack) { + if ((this == FIXED_COUNT || stack.getCount() == 1) && !stack.hasTag()) { + return OPTIONAL_ITEM.serialize(stack); + } + return RecordLoadable.super.serialize(stack); + } + + + /* Buffer */ + + @Override + public ItemStack decode(FriendlyByteBuf buffer, TypedMap context) { + // not using makeItemStack as we need to set the share tag NBT here + Item item = ITEM.decode(buffer, context); + int count = 1; + if (this == READ_COUNT) { + count = COUNT.decode(buffer, context); + } + CompoundTag nbt = buffer.readNbt(); + // not using make stack because we want to set share tag + if (item == Items.AIR || count <= 0) { + return ItemStack.EMPTY; + } + ItemStack stack = new ItemStack(item, count); + stack.readShareTag(nbt); + return stack; + } + + @Override + public void encode(FriendlyByteBuf buffer, ItemStack stack) throws EncoderException { + ITEM.encode(buffer, stack); + if (this == READ_COUNT) { + COUNT.encode(buffer, stack); + } + buffer.writeNbt(stack.getShareTag()); + } + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/common/NBTLoadable.java b/src/main/java/slimeknights/mantle/data/loadable/common/NBTLoadable.java new file mode 100644 index 000000000..68f80ccdc --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/common/NBTLoadable.java @@ -0,0 +1,102 @@ +package slimeknights.mantle.data.loadable.common; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.serialization.JsonOps; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.TagParser; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.Loadable; +import slimeknights.mantle.data.loadable.field.LoadableField; +import slimeknights.mantle.data.loadable.record.RecordLoadable; +import slimeknights.mantle.util.JsonHelper; +import slimeknights.mantle.util.typed.TypedMap; + +import javax.annotation.Nullable; +import java.util.function.Function; + +/** Loadable for reading NBT, converting from a JSON object to a tag.*/ +public enum NBTLoadable implements RecordLoadable { + /** Disallows reading NBT from a string in the Forge style*/ + DISALLOW_STRING, + /** Allows reading NBT from a string in the forge style */ + ALLOW_STRING; + + @Override + public CompoundTag deserialize(JsonObject json, TypedMap context) { + return (CompoundTag)JsonOps.INSTANCE.convertTo(NbtOps.INSTANCE, json); + } + + @Override + public CompoundTag convert(JsonElement element, String key) { + if (this == ALLOW_STRING && !element.isJsonObject()) { + try { + return TagParser.parseTag(JsonHelper.DEFAULT_GSON.toJson(element)); + } catch (CommandSyntaxException e) { + throw new JsonSyntaxException("Invalid NBT Entry: ", e); + } + } + return RecordLoadable.super.convert(element, key); + } + + @Override + public JsonObject serialize(CompoundTag object) { + return NbtOps.INSTANCE.convertTo(JsonOps.INSTANCE, object).getAsJsonObject(); + } + + @Override + public void serialize(CompoundTag object, JsonObject json) { + json.entrySet().addAll(serialize(object).entrySet()); + } + + @Override + public CompoundTag decode(FriendlyByteBuf buffer, TypedMap context) { + CompoundTag tag = buffer.readNbt(); + if (tag == null) { + return new CompoundTag(); + } + return tag; + } + + @Override + public void encode(FriendlyByteBuf buffer, CompoundTag object) { + buffer.writeNbt(object); + } + + @Override + public

LoadableField nullableField(String key, Function getter) { + return new NullableNBTField<>(this, key, getter); + } + + + /** Special implementation of nullable field to compact the buffer since it natively handles nullable NBT */ + private record NullableNBTField

(Loadable loadable, String key, Function getter) implements LoadableField { + @Nullable + @Override + public CompoundTag get(JsonObject json) { + return loadable.getOrDefault(json, key, null); + } + + @Override + public void serialize(P parent, JsonObject json) { + CompoundTag nbt = getter.apply(parent); + if (nbt != null) { + json.add(key, loadable.serialize(nbt)); + } + } + + @Nullable + @Override + public CompoundTag decode(FriendlyByteBuf buffer) { + return buffer.readNbt(); + } + + @Override + public void encode(FriendlyByteBuf buffer, P parent) { + buffer.writeNbt(getter.apply(parent)); + } + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/common/RegistryLoadable.java b/src/main/java/slimeknights/mantle/data/loadable/common/RegistryLoadable.java new file mode 100644 index 000000000..de06f1eff --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/common/RegistryLoadable.java @@ -0,0 +1,59 @@ +package slimeknights.mantle.data.loadable.common; + +import com.google.gson.JsonSyntaxException; +import io.netty.handler.codec.DecoderException; +import net.minecraft.core.Registry; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import slimeknights.mantle.data.loadable.primitive.ResourceLocationLoadable; +import slimeknights.mantle.util.RegistryHelper; + +import java.util.Objects; + +/** Loadable for a registry entry */ +public record RegistryLoadable(Registry registry, ResourceLocation registryId) implements ResourceLocationLoadable { + public RegistryLoadable(ResourceKey> registryId) { + this(Objects.requireNonNull(RegistryHelper.getRegistry(registryId), "Unknown registry " + registryId.location()), registryId.location()); + } + + @SuppressWarnings("unchecked") + public RegistryLoadable(Registry registry) { + this(registry, ((Registry>)Registry.REGISTRY).getKey(registry)); + } + + @Override + public T fromKey(ResourceLocation name, String key) { + if (registry.containsKey(name)) { + T value = registry.get(name); + if (value != null) { + return value; + } + } + throw new JsonSyntaxException("Unable to parse " + key + " as registry " + registryId + " does not contain ID " + name); + } + + @Override + public ResourceLocation getKey(T object) { + ResourceLocation location = registry.getKey(object); + if (location == null) { + throw new RuntimeException("Registry " + registryId + " does not contain object " + object); + } + return location; + } + + @Override + public T decode(FriendlyByteBuf buffer) { + int id = buffer.readVarInt(); + T value = registry.byId(id); + if (value == null) { + throw new DecoderException("Registry " + registryId + " does not contain ID " + id); + } + return value; + } + + @Override + public void encode(FriendlyByteBuf buffer, T object) { + buffer.writeId(registry, object); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/common/package-info.java b/src/main/java/slimeknights/mantle/data/loadable/common/package-info.java new file mode 100644 index 000000000..e325f7f46 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/common/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package slimeknights.mantle.data.loadable.common; + +import net.minecraft.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/main/java/slimeknights/mantle/data/loadable/field/AlwaysPresentLoadableField.java b/src/main/java/slimeknights/mantle/data/loadable/field/AlwaysPresentLoadableField.java new file mode 100644 index 000000000..1d84abdcf --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/field/AlwaysPresentLoadableField.java @@ -0,0 +1,24 @@ +package slimeknights.mantle.data.loadable.field; + +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.Loadable; + +import java.util.function.Function; + +/** Common networking logic for loadables that always have a network value */ +public interface AlwaysPresentLoadableField extends LoadableField { + /** Getter for the loadable */ + Loadable loadable(); + /** Getter for the given field */ + Function getter(); + + @Override + default T decode(FriendlyByteBuf buffer) { + return loadable().decode(buffer); + } + + @Override + default void encode(FriendlyByteBuf buffer, P parent) { + loadable().encode(buffer, getter().apply(parent)); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/field/ConstantField.java b/src/main/java/slimeknights/mantle/data/loadable/field/ConstantField.java new file mode 100644 index 000000000..5928ecf67 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/field/ConstantField.java @@ -0,0 +1,27 @@ +package slimeknights.mantle.data.loadable.field; + +import com.google.gson.JsonObject; +import net.minecraft.network.FriendlyByteBuf; + +/** Record field that always returns the same value, used mainly to pass a different object in JSON vs buffer parsing */ +public record ConstantField(T fromJson, T fromBuffer) implements LoadableField { + public ConstantField(T value) { + this(value, value); + } + + @Override + public T get(JsonObject json) { + return fromJson; + } + + @Override + public T decode(FriendlyByteBuf buffer) { + return fromBuffer; + } + + @Override + public void serialize(Object parent, JsonObject json) {} + + @Override + public void encode(FriendlyByteBuf buffer, Object parent) {} +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/field/ContextField.java b/src/main/java/slimeknights/mantle/data/loadable/field/ContextField.java new file mode 100644 index 000000000..7c1e4c386 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/field/ContextField.java @@ -0,0 +1,39 @@ +package slimeknights.mantle.data.loadable.field; + +import com.google.gson.JsonObject; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.ErrorFactory; +import slimeknights.mantle.util.typed.TypedMap; + +import javax.annotation.Nullable; + +/** + * Shared impl a field from context + */ +public interface ContextField extends RecordField { + /** + * Gets the value from context for the given key + * @param context Context + * @param error Place to print errors for failure to fetch + * @return Context value, or null if the field implementation does not handle it. + * @throws RuntimeException If the value is missing in context + */ + @Nullable + T get(TypedMap context, ErrorFactory error); + + @Override + default T get(JsonObject json, TypedMap context) { + return get(context, ErrorFactory.JSON_SYNTAX_ERROR); + } + + @Override + default void serialize(Object parent, JsonObject json) {} + + @Override + default T decode(FriendlyByteBuf buffer, TypedMap context) { + return get(context, ErrorFactory.DECODER_EXCEPTION); + } + + @Override + default void encode(FriendlyByteBuf buffer, Object parent) {} +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/field/ContextKey.java b/src/main/java/slimeknights/mantle/data/loadable/field/ContextKey.java new file mode 100644 index 000000000..c0af990ba --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/field/ContextKey.java @@ -0,0 +1,88 @@ +package slimeknights.mantle.data.loadable.field; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.minecraft.resources.ResourceLocation; +import slimeknights.mantle.data.loadable.ErrorFactory; +import slimeknights.mantle.util.typed.TypedMap; +import slimeknights.mantle.util.typed.TypedMap.Key; + +import javax.annotation.Nullable; +import java.util.function.BiFunction; + +/** + * Key for fetching properties from a loadable context. This key doubles as a record field for a required context key. + * @param Field type + */ +@RequiredArgsConstructor +public class ContextKey implements Key { + /** Context field representing the object's ID */ + public static final ContextKey ID = new ContextKey<>("id"); + + /** Name of the field, used primarily for debug */ + @Getter + private final String name; + + + + @Override + public String toString() { + return "ContextKey('" + name + "')'"; + } + + + /* Fields */ + private RecordField requiredField; + private RecordField nullableField; + + /** Gets a field requiring this context parameter */ + public RecordField requiredField() { + if (requiredField == null) { + requiredField = new Required<>(this, (t,e) -> t); + } + return requiredField; + } + + /** Gets a field requiring this context parameter, but mapped using the passed function */ + public RecordField mappedField(BiFunction mapper) { + return new Required<>(this, mapper); + } + + /** Field using null in place of this parameter if missing */ + public RecordField nullableField() { + if (nullableField == null) { + nullableField = new Defaulting<>(this, null); + } + return nullableField; + } + + /** Creates a defaulting field for this key */ + public RecordField defaultField(T defaultValue) { + return new Defaulting<>(this, defaultValue); + } + + + /** Field instance for making the key required */ + private record Required(ContextKey key, BiFunction mapper) implements ContextField { + @Override + public M get(TypedMap context, ErrorFactory error) { + T value = context.get(key); + if (value != null) { + return mapper.apply(value, error); + } + throw error.create("Unable to fetch " + key.name + " from context, this usually implements a broken JSON deserializer"); + } + } + + /** Field instance that defaults to a given value */ + private record Defaulting(ContextKey key, @Nullable T defaultValue) implements ContextField { + @Override + public T get(TypedMap context, ErrorFactory error) { + // it is potentially faster to use get over getOrDefault so call it directly if we have no default value + if (defaultValue == null) { + return context.get(key); + } + return context.getOrDefault(key, defaultValue); + } + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/field/DefaultingField.java b/src/main/java/slimeknights/mantle/data/loadable/field/DefaultingField.java new file mode 100644 index 000000000..8d368772a --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/field/DefaultingField.java @@ -0,0 +1,26 @@ +package slimeknights.mantle.data.loadable.field; + +import com.google.gson.JsonObject; +import slimeknights.mantle.data.loadable.Loadable; + +import java.util.function.Function; + +/** + * Optional field with a default value if missing + * @param

Parent object + * @param Loadable type + */ +public record DefaultingField(Loadable loadable, String key, T defaultValue, boolean serializeDefault, Function getter) implements AlwaysPresentLoadableField { + @Override + public T get(JsonObject json) { + return loadable.getOrDefault(json, key, defaultValue); + } + + @Override + public void serialize(P parent, JsonObject json) { + T object = getter.apply(parent); + if (serializeDefault || !defaultValue.equals(object)) { + json.add(key, loadable.serialize(object)); + } + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/field/DirectField.java b/src/main/java/slimeknights/mantle/data/loadable/field/DirectField.java new file mode 100644 index 000000000..0e7fbe970 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/field/DirectField.java @@ -0,0 +1,35 @@ +package slimeknights.mantle.data.loadable.field; + +import com.google.gson.JsonObject; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.record.RecordLoadable; +import slimeknights.mantle.util.typed.TypedMap; + +import java.util.function.Function; + +/** + * A record loadable that loads directly into the parent instead of nesting. + * @param

Parent object + * @param Loadable type + */ +public record DirectField(RecordLoadable loadable, Function getter) implements AlwaysPresentLoadableField { + @Override + public T get(JsonObject json) { + return loadable.deserialize(json); + } + + @Override + public T get(JsonObject json, TypedMap context) { + return loadable.deserialize(json, context); + } + + @Override + public void serialize(P parent, JsonObject json) { + loadable.serialize(getter.apply(parent), json); + } + + @Override + public T decode(FriendlyByteBuf buffer, TypedMap context) { + return loadable.decode(buffer, context); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/field/LoadableField.java b/src/main/java/slimeknights/mantle/data/loadable/field/LoadableField.java new file mode 100644 index 000000000..9c43056ee --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/field/LoadableField.java @@ -0,0 +1,39 @@ +package slimeknights.mantle.data.loadable.field; + +import com.google.gson.JsonObject; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.record.RecordLoadable; +import slimeknights.mantle.util.typed.TypedMap; + +/** + * Interface for a field in a JSON object, typically used in {@link RecordLoadable} but also usable statically. + * @param

Parent object + * @param Loadable type + */ +public interface LoadableField extends RecordField { + /** + * Gets the loadable from the given JSON + * @param json JSON object + * @return Parsed loadable value + * @throws com.google.gson.JsonSyntaxException If unable to read from JSON + */ + T get(JsonObject json); + + @Override + default T get(JsonObject json, TypedMap context) { + return get(json); + } + + /** + * Parses this loadable from the network + * @param buffer Buffer instance + * @return Parsed field value + * @throws io.netty.handler.codec.DecoderException If unable to decode + */ + T decode(FriendlyByteBuf buffer); + + @Override + default T decode(FriendlyByteBuf buffer, TypedMap context) { + return decode(buffer); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/field/NullableField.java b/src/main/java/slimeknights/mantle/data/loadable/field/NullableField.java new file mode 100644 index 000000000..433a3c3cc --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/field/NullableField.java @@ -0,0 +1,46 @@ +package slimeknights.mantle.data.loadable.field; + +import com.google.gson.JsonObject; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.Loadable; + +import java.util.function.Function; + +/** + * Optional field that may be null + * @param

Parent object + * @param Loadable type + */ +public record NullableField(Loadable loadable, String key, Function getter) implements LoadableField { + @Override + public T get(JsonObject json) { + return loadable.getOrDefault(json, key, null); + } + + @Override + public void serialize(P parent, JsonObject json) { + T object = getter.apply(parent); + if (object != null) { + json.add(key, loadable.serialize(object)); + } + } + + @Override + public T decode(FriendlyByteBuf buffer) { + if (buffer.readBoolean()) { + return loadable.decode(buffer); + } + return null; + } + + @Override + public void encode(FriendlyByteBuf buffer, P parent) { + T object = getter.apply(parent); + if (object != null) { + buffer.writeBoolean(true); + loadable.encode(buffer, object); + } else { + buffer.writeBoolean(false); + } + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/field/RecordField.java b/src/main/java/slimeknights/mantle/data/loadable/field/RecordField.java new file mode 100644 index 000000000..381496198 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/field/RecordField.java @@ -0,0 +1,50 @@ +package slimeknights.mantle.data.loadable.field; + +import com.google.gson.JsonObject; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.record.RecordLoadable; +import slimeknights.mantle.util.typed.TypedMap; + +/** + * Interface for fields in a {@link RecordLoadable}. + * Unlike {@link LoadableField}, this interface is not designed for use outside of loadables. + * @param

Parent object + * @param Loadable type + */ +public interface RecordField { + /** + * Gets the loadable from the given JSON + * @param json JSON object + * @param context Additional parsing context, used notably by recipe serializers to store the ID and serializer. + * Will be {@link TypedMap#EMPTY} in nested usages unless {@link DirectField} is used. + * @return Parsed loadable value + * @throws com.google.gson.JsonSyntaxException If unable to read from JSON + */ + T get(JsonObject json, TypedMap context); + + /** + * Serializes the passed object into the JSON instance + * @param json JSON instance + * @param parent Object + * @throws RuntimeException If unable to save the element + */ + void serialize(P parent, JsonObject json); + + /** + * Parses this loadable from the network + * @param buffer Buffer instance + * @param context Additional parsing context, used notably by recipe serializers to store the ID and serializer. + * Will be {@link TypedMap#EMPTY} in nested usages unless {@link DirectField} is used. + * @return Parsed field value + * @throws io.netty.handler.codec.DecoderException If unable to decode a value from network + */ + T decode(FriendlyByteBuf buffer, TypedMap context); + + /** + * Writes this field to the buffer + * @param buffer Buffer instance + * @param parent Parent to read values from + * @throws io.netty.handler.codec.EncoderException If unable to encode a value to network + */ + void encode(FriendlyByteBuf buffer, P parent); +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/field/RequiredField.java b/src/main/java/slimeknights/mantle/data/loadable/field/RequiredField.java new file mode 100644 index 000000000..56e06d607 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/field/RequiredField.java @@ -0,0 +1,28 @@ +package slimeknights.mantle.data.loadable.field; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import slimeknights.mantle.data.loadable.Loadable; + +import java.util.function.Function; + +/** + * A basic required field with a name + * @param serializeNull If true, null JsonElements are serialized, if false null is treated as don't serialize (for defaults) + * @param

Parent object + * @param Loadable type + */ +public record RequiredField(Loadable loadable, String key, boolean serializeNull, Function getter) implements AlwaysPresentLoadableField { + @Override + public T get(JsonObject json) { + return loadable.getIfPresent(json, key); + } + + @Override + public void serialize(P parent, JsonObject json) { + JsonElement element = loadable.serialize(getter.apply(parent)); + if (serializeNull || !element.isJsonNull()) { + json.add(key, element); + } + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/field/TryDirectField.java b/src/main/java/slimeknights/mantle/data/loadable/field/TryDirectField.java new file mode 100644 index 000000000..d782b5118 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/field/TryDirectField.java @@ -0,0 +1,61 @@ +package slimeknights.mantle.data.loadable.field; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import slimeknights.mantle.data.loadable.Loadable; + +import java.util.Map.Entry; +import java.util.function.Function; + +/** + * Field that tries to save the object directly, but falls back to saving it in a key if unable + * @param Field type + * @param

Parent type + */ +public record TryDirectField(Loadable loadable, String key, Function getter, String... conflicts) implements AlwaysPresentLoadableField { + @Override + public T get(JsonObject json) { + // if we have the nested key, read from that + if (json.has(key)) { + return loadable.convert(json.get(key), key); + } + // try reading from the current object, assumes the loadable supports JSON objects + return loadable.convert(json, key); + } + + /** Checks if the JSON has any conflicting keys */ + private boolean hasConflict(JsonObject parent, JsonObject serialized) { + if (serialized.has(key)) { + return true; + } + // check all the keys in the parent so far, if any of them exist then this conflicts + for (String conflict : parent.keySet()) { + if (serialized.has(conflict)) { + return true; + } + } + // check additional conflicts passed into the field, for the sake of optional fields mostly + for (String conflict : conflicts) { + if (serialized.has(conflict)) { + return true; + } + } + return false; + } + + @Override + public void serialize(P parent, JsonObject json) { + JsonElement element = loadable.serialize(getter.apply(parent)); + if (element.isJsonObject()) { + JsonObject serialized = element.getAsJsonObject(); + // if the serialized element contains the key, we cannot store it directly as that will confuse deserializing + if (!hasConflict(json, serialized)) { + for (Entry entry : serialized.entrySet()) { + json.add(entry.getKey(), entry.getValue()); + } + return; + } + } + json.add(key, element); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/field/package-info.java b/src/main/java/slimeknights/mantle/data/loadable/field/package-info.java new file mode 100644 index 000000000..ecf2fbe5c --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/field/package-info.java @@ -0,0 +1,4 @@ +@ParametersAreNonnullByDefault +package slimeknights.mantle.data.loadable.field; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/main/java/slimeknights/mantle/data/loadable/mapping/AnyCollectionLoadable.java b/src/main/java/slimeknights/mantle/data/loadable/mapping/AnyCollectionLoadable.java new file mode 100644 index 000000000..d040fa249 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/mapping/AnyCollectionLoadable.java @@ -0,0 +1,51 @@ +package slimeknights.mantle.data.loadable.mapping; + +import com.google.common.collect.ImmutableCollection.Builder; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import slimeknights.mantle.data.loadable.Loadable; + +import java.util.Collection; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** Collection loadable that does not declare a specific internal collection type, meaning we can serialize from any */ +public class AnyCollectionLoadable extends CollectionLoadable,Builder> { + private final Supplier> builder; + public AnyCollectionLoadable(Loadable base, int minSize, Supplier> builder) { + super(base, minSize); + this.builder = builder; + } + + /** Creates a list backed collection loadable */ + public static AnyCollectionLoadable listBacked(Loadable base, int minSize) { + return new AnyCollectionLoadable<>(base, minSize, ImmutableList::builder); + } + + /** Creates a set backed collection loadable */ + public static AnyCollectionLoadable setBacked(Loadable base, int minSize) { + return new AnyCollectionLoadable<>(base, minSize, ImmutableSet::builder); + } + + @Override + protected Builder makeBuilder() { + return builder.get(); + } + + @Override + protected Collection build(Builder builder) { + return builder.build(); + } + + /** Creates a map from this collection using the given getter to find keys for the map */ + public Loadable> mapWithKeys(Function keyGetter) { + return flatXmap(collection -> collection.stream().collect(Collectors.toUnmodifiableMap(keyGetter, Function.identity())), Map::values); + } + + /** Creates a map from this collection using the given getter to find values for the map */ + public Loadable> mapWithValues(Function valueGetter) { + return flatXmap(collection -> collection.stream().collect(Collectors.toUnmodifiableMap(Function.identity(), valueGetter)), Map::keySet); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/mapping/CollectionLoadable.java b/src/main/java/slimeknights/mantle/data/loadable/mapping/CollectionLoadable.java new file mode 100644 index 000000000..3ef16f58b --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/mapping/CollectionLoadable.java @@ -0,0 +1,70 @@ +package slimeknights.mantle.data.loadable.mapping; + +import com.google.common.collect.ImmutableCollection; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonSyntaxException; +import lombok.RequiredArgsConstructor; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.util.GsonHelper; +import slimeknights.mantle.data.loadable.Loadable; + +import java.util.Collection; + +/** Shared base class for a loadable of a collection of elements */ +@RequiredArgsConstructor +public abstract class CollectionLoadable,B extends ImmutableCollection.Builder> implements Loadable { + /** Loadable for an object */ + private final Loadable base; + /** If true, empty is an allowed value */ + private final int minSize; + + /** Creates a builder for the collection */ + protected abstract B makeBuilder(); + + /** Builds the final collection */ + protected abstract C build(B builder); + + @Override + public C convert(JsonElement element, String key) { + JsonArray array = GsonHelper.convertToJsonArray(element, key); + if (array.size() < minSize) { + throw new JsonSyntaxException(key + " must have at least " + minSize + " elements"); + } + B builder = makeBuilder(); + for (int i = 0; i < array.size(); i++) { + builder.add(base.convert(array.get(i), key + '[' + i + ']')); + } + return build(builder); + } + + @Override + public JsonArray serialize(C collection) { + if (collection.size() < minSize) { + throw new RuntimeException("Collection must have at least " + minSize + " elements"); + } + JsonArray array = new JsonArray(); + for (T element : collection) { + array.add(base.serialize(element)); + } + return array; + } + + @Override + public C decode(FriendlyByteBuf buffer) { + B builder = makeBuilder(); + int max = buffer.readVarInt(); + for (int i = 0; i < max; i++) { + builder.add(base.decode(buffer)); + } + return build(builder); + } + + @Override + public void encode(FriendlyByteBuf buffer, C collection) { + buffer.writeVarInt(collection.size()); + for (T element : collection) { + base.encode(buffer, element); + } + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/mapping/CompactLoadable.java b/src/main/java/slimeknights/mantle/data/loadable/mapping/CompactLoadable.java new file mode 100644 index 000000000..5c9a29f6b --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/mapping/CompactLoadable.java @@ -0,0 +1,98 @@ +package slimeknights.mantle.data.loadable.mapping; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.Loadable; +import slimeknights.mantle.data.loadable.record.RecordLoadable; +import slimeknights.mantle.util.typed.TypedMap; + +import java.util.function.Predicate; + +/** + * Loadable which uses a compact form if the condition is met + * @param Object type + */ +@RequiredArgsConstructor(access = AccessLevel.PROTECTED) +public class CompactLoadable implements Loadable { + private final Loadable loadable; + private final Loadable compact; + private final Predicate compactCondition; + + /** + * Creates a new instance for a general loadable + * @param loadable Base loadable, used under most circumstances + * @param compact Compact form, will be used to deserialize primitives, and in serialization based on conditions + * @param compactCondition If true, uses the compact loadable + * @param Object type + */ + public static Loadable of(Loadable loadable, Loadable compact, Predicate compactCondition) { + return new CompactLoadable<>(loadable, compact, compactCondition); + } + + /** + * Creates a new instance for a record loadable + * @param loadable Base loadable, used under most circumstances + * @param compact Compact form, will be used to deserialize primitives, and in serialization based on conditions + * @param compactCondition If true, uses the compact loadable + * @param Object type + */ + public static RecordLoadable of(RecordLoadable loadable, Loadable compact, Predicate compactCondition) { + return new Record<>(loadable, compact, compactCondition); + } + + @Override + public T convert(JsonElement element, String key) { + if (element.isJsonPrimitive()) { + return compact.convert(element, key); + } + return loadable.convert(element, key); + } + + @Override + public JsonElement serialize(T object) { + if (compactCondition.test(object)) { + return compact.serialize(object); + } + return loadable.serialize(object); + } + + + /* Networking */ + + @Override + public T decode(FriendlyByteBuf buffer) { + return loadable.decode(buffer); + } + + @Override + public void encode(FriendlyByteBuf buffer, T object) { + loadable.encode(buffer, object); + } + + /** Extension for records */ + private static class Record extends CompactLoadable implements RecordLoadable { + private final RecordLoadable loadable; + public Record(RecordLoadable loadable, Loadable compact, Predicate compactCondition) { + super(loadable, compact, compactCondition); + this.loadable = loadable; + } + + @Override + public T deserialize(JsonObject json, TypedMap context) { + return loadable.deserialize(json, context); + } + + @Override + public void serialize(T object, JsonObject json) { + loadable.serialize(object, json); + } + + @Override + public T decode(FriendlyByteBuf buffer, TypedMap context) { + return loadable.decode(buffer, context); + } + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/mapping/EitherLoadable.java b/src/main/java/slimeknights/mantle/data/loadable/mapping/EitherLoadable.java new file mode 100644 index 000000000..953d043d2 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/mapping/EitherLoadable.java @@ -0,0 +1,273 @@ +package slimeknights.mantle.data.loadable.mapping; + +import com.google.common.collect.ImmutableList; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; +import io.netty.handler.codec.DecoderException; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.ContextStreamable; +import slimeknights.mantle.data.loadable.IAmLoadable; +import slimeknights.mantle.data.loadable.Loadable; +import slimeknights.mantle.data.loadable.Streamable; +import slimeknights.mantle.data.loadable.record.RecordLoadable; +import slimeknights.mantle.util.typed.TypedMap; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.stream.Collectors; + +/** Record loadable that chooses a loadable based on the presence of a key. */ +public class EitherLoadable { + private EitherLoadable() {} + + /** Option in the record loadable */ + private record KeyOption(String key, RecordLoadable loadable) {} + + /** Creates a new builder for a multi-type loadable */ + public static TypedBuilder typed() { + return new TypedBuilder<>(); + } + + /** Creates a new builder for a record */ + public static RecordBuilder record() { + return new RecordBuilder<>(); + } + + /** Builder class */ + public static class TypedBuilder { + /** List loadable, if set allows parsing from arrays */ + private Loadable array = null; + /** Primitive loadable, if set, allows parsing from primitives */ + private Loadable primitive = null; + /** Object options to choose from by present key */ + protected final ImmutableList.Builder> keys = ImmutableList.builder(); + + private TypedBuilder() {} + + /** Adds a key option to the builder */ + public TypedBuilder key(String key, RecordLoadable loadable) { + keys.add(new KeyOption<>(key, loadable)); + return this; + } + + /** Adds a list option to the builder, disallows building a record loaable at the end*/ + public TypedBuilder array(Loadable loadable) { + if (this.array != null) { + throw new IllegalStateException("Duplicate array loadable, previous value " + this.array); + } + this.array = loadable; + return this; + } + + /** Adds a primitive option to the builder, disallows building a record loadable at the end */ + public TypedBuilder primitive(Loadable loadable) { + if (this.primitive != null) { + throw new IllegalStateException("Duplicate primitive loadable, previous value " + this.primitive); + } + this.primitive = loadable; + return this; + } + + /** Gets the keys for the builder */ + private List> getKeys() { + List> keys = this.keys.build(); + int size = keys.size() + (array != null ? 1 : 0) + (primitive != null ? 1 : 0); + if (size < 2) { + throw new IllegalStateException("EitherLoadable must have at least 2 options."); + } + return keys; + } + + /** Builds the loadable with custom network logic */ + public Loadable build(Streamable network) { + return new Typing<>(List.of(network), getKeys(), array, primitive); + } + + /** Builds the loadable with the default network logic */ + @SuppressWarnings("unchecked") + public Loadable build() { + List> keys = getKeys(); + ImmutableList.Builder> builder = ImmutableList.builder(); + keys.forEach(key -> builder.add((Loadable)key.loadable)); + if (array != null) { + builder.add((Loadable)array); + } + if (primitive != null) { + builder.add((Loadable)primitive); + } + return new Typing<>(builder.build(), getKeys(), array, primitive); + } + } + + /** Builder class */ + public static class RecordBuilder { + /** Object options to choose from by present key */ + protected final ImmutableList.Builder> keys = ImmutableList.builder(); + + private RecordBuilder() {} + + /** Adds a key option to the builder */ + public RecordBuilder key(String key, RecordLoadable loadable) { + keys.add(new KeyOption<>(key, loadable)); + return this; + } + + /** Gets the keys for the builder */ + private List> getKeys() { + List> keys = this.keys.build(); + if (keys.size() < 2) { + throw new IllegalStateException("EitherLoadable must have at least 2 options."); + } + return keys; + } + + /** Builds the loadable with custom network logic */ + public RecordLoadable build(ContextStreamable network) { + return new Record<>(List.of(network), getKeys()); + } + + /** Builds the loadable with default network logic */ + @SuppressWarnings("unchecked") // its safe with how we use it + public RecordLoadable build() { + List> keys = getKeys(); + List> network = keys.stream().>map(option -> (RecordLoadable)option.loadable).toList(); + return new Record<>(network, keys); + } + } + + + /** Common logic between the two implementations */ + private interface EitherImpl> extends Loadable { + /* Fields */ + List network(); + List> keys(); + @Nullable + default Loadable array() { + return null; + } + @Nullable + default Loadable primitive() { + return null; + } + + + /* JSON */ + + /** Deserializes from the given JSON object */ + default T deserializeObject(JsonElement element, TypedMap context, String key) { + List> keys = this.keys(); + if (element.isJsonObject()) { + JsonObject json = element.getAsJsonObject(); + for (KeyOption option : keys) { + if (json.has(option.key)) { + return option.loadable.deserialize(json, context); + } + } + } + StringBuilder builder = new StringBuilder(); + builder.append("JSON at ").append(key).append(" must be one of: "); + if (array() != null) { + builder.append("array, "); + } + if (primitive() != null) { + builder.append("primitive, "); + } + builder.append("object with key from [") + .append(keys.stream().map(KeyOption::key).collect(Collectors.joining(", "))) + .append(']'); + throw new JsonSyntaxException(builder.toString()); + } + + /** Gets the loadable instance from the buffer */ + default L loadableFromNetwork(FriendlyByteBuf buffer) { + List networks = network(); + // size 1 means we have a fixed network logic, use that + int size = networks.size(); + if (size == 1) { + return networks.get(0); + } + // the integer should be guaranteed to be a valid loadable, but just in case give a better exception + int networkIndex = buffer.readVarInt(); + if (networkIndex < size) { + return networks.get(networkIndex); + } + throw new DecoderException("Unknown network index " + networkIndex + " for EitherLoadable with network size " + size + ", this should not be possible."); + } + + @Override + default void encode(FriendlyByteBuf buffer, T object) { + List networks = network(); + // size 1 means we have a fixed network logic, use that + if (networks.size() == 1) { + networks.get(0).encode(buffer, object); + } else { + // we need to be able to recover which loadable was used on deserialization, so use the index in our list + Loadable objectLoadable = object.loadable(); + for (int i = 0; i < networks.size(); i++) { + L network = networks.get(i); + // indexof would do deep comparison, but reference comparison is way more efficient here + if (network == objectLoadable) { + buffer.writeVarInt(i); + network.encode(buffer, object); + return; + } + } + throw new IllegalArgumentException("Unable to serialize " + object + " to network as its loadable " + objectLoadable + " is not allows in the EitherLoadable"); + } + } + } + + /** Loadable supporting list and array */ + private record Typing(List> network, List> keys, @Nullable Loadable array, @Nullable Loadable primitive) implements EitherImpl> { + @Override + public T convert(JsonElement element, String key) { + if (array != null && element.isJsonArray()) { + return array.convert(element, key); + } + if (primitive != null && element.isJsonPrimitive()) { + return primitive.convert(element, key); + } + if (!keys.isEmpty()) { + return deserializeObject(element, TypedMap.empty(), key); + } + // no keys mean both array and primitive are valid, so that is the error + throw new JsonSyntaxException("JSON at " + key + " must be one of: array, primitive"); + } + + @SuppressWarnings("unchecked") + @Override + public JsonElement serialize(T object) { + return ((Loadable)object.loadable()).serialize(object); + } + + @Override + public T decode(FriendlyByteBuf buffer) { + return loadableFromNetwork(buffer).decode(buffer); + } + } + + /** Loadable only supporting record */ + private record Record(List> network, List> keys) implements RecordLoadable, EitherImpl> { + @Override + public T deserialize(JsonObject json, TypedMap context) { + return deserializeObject(json, context, "[root]"); + } + + @SuppressWarnings("unchecked") + @Override + public void serialize(T object, JsonObject json) { + ((RecordLoadable)object.loadable()).serialize(object, json); + } + + @Override + public T decode(FriendlyByteBuf buffer, TypedMap context) { + return loadableFromNetwork(buffer).decode(buffer, context); + } + + @Override + public void encode(FriendlyByteBuf buffer, T object) { + EitherImpl.super.encode(buffer, object); + } + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/mapping/ListLoadable.java b/src/main/java/slimeknights/mantle/data/loadable/mapping/ListLoadable.java new file mode 100644 index 000000000..84b183b93 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/mapping/ListLoadable.java @@ -0,0 +1,24 @@ +package slimeknights.mantle.data.loadable.mapping; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; +import slimeknights.mantle.data.loadable.Loadable; + +import java.util.List; + +/** Loadable of list of elements */ +public class ListLoadable extends CollectionLoadable,Builder> { + public ListLoadable(Loadable base, int minSize) { + super(base, minSize); + } + + @Override + protected Builder makeBuilder() { + return ImmutableList.builder(); + } + + @Override + protected List build(Builder builder) { + return builder.build(); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/mapping/MapLoadable.java b/src/main/java/slimeknights/mantle/data/loadable/mapping/MapLoadable.java new file mode 100644 index 000000000..95ea59e48 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/mapping/MapLoadable.java @@ -0,0 +1,74 @@ +package slimeknights.mantle.data.loadable.mapping; + +import com.google.common.collect.ImmutableMap; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.util.GsonHelper; +import slimeknights.mantle.data.loadable.Loadable; +import slimeknights.mantle.data.loadable.primitive.StringLoadable; + +import java.util.Map; +import java.util.Map.Entry; + +/** + * Loadable for a map type. + * @param keyLoadable Loadable for the map keys, parsed from strings + * @param valueLoadable Loadable for map values, parsed from elements + * @param Key type + * @param Value type + */ +public record MapLoadable(StringLoadable keyLoadable, Loadable valueLoadable, int minSize) implements Loadable> { + @Override + public Map convert(JsonElement element, String key) { + JsonObject json = GsonHelper.convertToJsonObject(element, key); + if (json.size() < minSize) { + throw new JsonSyntaxException(key + " must have at least " + minSize + " elements"); + } + ImmutableMap.Builder builder = ImmutableMap.builder(); + String mapKey = key + "'s key"; + for (Entry entry : json.entrySet()) { + String entryKey = entry.getKey(); + builder.put( + keyLoadable.parseString(entryKey, mapKey), + valueLoadable.convert(entry.getValue(), entryKey)); + } + return builder.build(); + } + + @Override + public JsonElement serialize(Map map) { + if (map.size() < minSize) { + throw new RuntimeException("Collection must have at least " + minSize + " elements"); + } + JsonObject json = new JsonObject(); + for (Entry entry : map.entrySet()) { + json.add( + keyLoadable.getString(entry.getKey()), + valueLoadable.serialize(entry.getValue())); + } + return json; + } + + @Override + public Map decode(FriendlyByteBuf buffer) { + int size = buffer.readVarInt(); + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (int i = 0; i < size; i++) { + builder.put( + keyLoadable.decode(buffer), + valueLoadable.decode(buffer)); + } + return builder.build(); + } + + @Override + public void encode(FriendlyByteBuf buffer, Map map) { + buffer.writeVarInt(map.size()); + for (Entry entry : map.entrySet()) { + keyLoadable.encode(buffer, entry.getKey()); + valueLoadable.encode(buffer, entry.getValue()); + } + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/mapping/MappedLoadable.java b/src/main/java/slimeknights/mantle/data/loadable/mapping/MappedLoadable.java new file mode 100644 index 000000000..3d77ecf26 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/mapping/MappedLoadable.java @@ -0,0 +1,106 @@ +package slimeknights.mantle.data.loadable.mapping; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.ErrorFactory; +import slimeknights.mantle.data.loadable.Loadable; +import slimeknights.mantle.data.loadable.primitive.StringLoadable; +import slimeknights.mantle.data.loadable.record.RecordLoadable; +import slimeknights.mantle.util.typed.TypedMap; + +import java.util.function.BiFunction; +import java.util.function.Function; + +/** Represents a trivially mapped loadable that serializes/writes to network like another loadable */ +@RequiredArgsConstructor(access = AccessLevel.PROTECTED) +public class MappedLoadable implements Loadable { + private final Loadable base; + protected final BiFunction from; + protected final BiFunction to; + + /** Creates a new loadable for a non-record loadable */ + public static Loadable of(Loadable base, BiFunction from, BiFunction to) { + return new MappedLoadable<>(base, from, to); + } + + /** Creates a new loadable for a record loadable */ + public static RecordLoadable of(RecordLoadable base, BiFunction from, BiFunction to) { + return new Record<>(base, from, to); + } + + /** Creates a new loadable for a record loadable */ + public static StringLoadable of(StringLoadable base, BiFunction from, BiFunction to) { + return new StringMapped<>(base, from, to); + } + + /** Flattens the given mapping function */ + public static BiFunction flatten(Function function) { + return (value, error) -> function.apply(value); + } + + @Override + public T convert(JsonElement element, String key) { + return from.apply(base.convert(element, key), ErrorFactory.JSON_SYNTAX_ERROR); + } + + @Override + public JsonElement serialize(T object) { + return base.serialize(to.apply(object, ErrorFactory.RUNTIME)); + } + + @Override + public T decode(FriendlyByteBuf buffer) { + return from.apply(base.decode(buffer), ErrorFactory.DECODER_EXCEPTION); + } + + @Override + public void encode(FriendlyByteBuf buffer, T object) { + base.encode(buffer, to.apply(object, ErrorFactory.ENCODER_EXCEPTION)); + } + + /** Implementation for records */ + private static class Record extends MappedLoadable implements RecordLoadable { + private final RecordLoadable base; + protected Record(RecordLoadable base, BiFunction from, BiFunction to) { + super(base, from, to); + this.base = base; + } + + @Override + public T deserialize(JsonObject json, TypedMap context) { + return from.apply(base.deserialize(json, context), ErrorFactory.JSON_SYNTAX_ERROR); + } + + @Override + public void serialize(T object, JsonObject json) { + base.serialize(to.apply(object, ErrorFactory.RUNTIME), json); + } + + @Override + public T decode(FriendlyByteBuf buffer, TypedMap context) { + return from.apply(base.decode(buffer, context), ErrorFactory.DECODER_EXCEPTION); + } + } + + /** Implementation for strings */ + private static class StringMapped extends MappedLoadable implements StringLoadable { + private final StringLoadable base; + protected StringMapped(StringLoadable base, BiFunction from, BiFunction to) { + super(base, from, to); + this.base = base; + } + + @Override + public T parseString(String value, String key) { + return from.apply(base.parseString(value, key), ErrorFactory.JSON_SYNTAX_ERROR); + } + + @Override + public String getString(T object) { + return base.getString(to.apply(object, ErrorFactory.RUNTIME)); + } + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/mapping/SetLoadable.java b/src/main/java/slimeknights/mantle/data/loadable/mapping/SetLoadable.java new file mode 100644 index 000000000..690d96dfa --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/mapping/SetLoadable.java @@ -0,0 +1,24 @@ +package slimeknights.mantle.data.loadable.mapping; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSet.Builder; +import slimeknights.mantle.data.loadable.Loadable; + +import java.util.Set; + +/** Loadable of set of elements */ +public class SetLoadable extends CollectionLoadable,Builder> { + public SetLoadable(Loadable base, int minSize) { + super(base, minSize); + } + + @Override + protected Builder makeBuilder() { + return ImmutableSet.builder(); + } + + @Override + protected Set build(Builder builder) { + return builder.build(); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/mapping/package-info.java b/src/main/java/slimeknights/mantle/data/loadable/mapping/package-info.java new file mode 100644 index 000000000..b8d3edad2 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/mapping/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package slimeknights.mantle.data.loadable.mapping; + +import net.minecraft.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/main/java/slimeknights/mantle/data/loadable/package-info.java b/src/main/java/slimeknights/mantle/data/loadable/package-info.java new file mode 100644 index 000000000..6abd11190 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package slimeknights.mantle.data.loadable; + +import net.minecraft.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/main/java/slimeknights/mantle/data/loadable/primitive/BooleanLoadable.java b/src/main/java/slimeknights/mantle/data/loadable/primitive/BooleanLoadable.java new file mode 100644 index 000000000..a1608798c --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/primitive/BooleanLoadable.java @@ -0,0 +1,60 @@ +package slimeknights.mantle.data.loadable.primitive; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSyntaxException; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.util.GsonHelper; +import slimeknights.mantle.data.loadable.field.LoadableField; + +import java.util.Locale; +import java.util.function.Function; + +/** Loadable for a boolean */ +public enum BooleanLoadable implements StringLoadable { + INSTANCE; + + @Override + public Boolean convert(JsonElement element, String key) { + return GsonHelper.convertToBoolean(element, key); + } + + @Override + public JsonElement serialize(Boolean object) { + return new JsonPrimitive(object); + } + + @Override + public Boolean decode(FriendlyByteBuf buffer) { + return buffer.readBoolean(); + } + + @Override + public void encode(FriendlyByteBuf buffer, Boolean object) { + buffer.writeBoolean(object); + } + + @Override + public

LoadableField defaultField(String key, Boolean defaultValue, Function getter) { + // booleans are cleaner if they serialize by default + return defaultField(key, defaultValue, true, getter); + } + + + /* String loadable */ + + @Override + public Boolean parseString(String value, String key) { + // Boolean#valueOf and Boolean#parseBoolean both just treat all non-true as false, which is less desirable for well-formed JSON + return switch (value.toLowerCase(Locale.ROOT)) { + case "true" -> true; + case "false" -> false; + default -> throw new JsonSyntaxException("Invalid boolean '" + value + '\''); + }; + } + + @Override + public String getString(Boolean object) { + return object.toString(); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/primitive/EnumLoadable.java b/src/main/java/slimeknights/mantle/data/loadable/primitive/EnumLoadable.java new file mode 100644 index 000000000..34cc58ae8 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/primitive/EnumLoadable.java @@ -0,0 +1,38 @@ +package slimeknights.mantle.data.loadable.primitive; + +import com.google.gson.JsonSyntaxException; +import net.minecraft.network.FriendlyByteBuf; + +import java.util.Locale; + +/** Loadable for an enum value */ +public record EnumLoadable>(Class enumClass, E[] allowedValues) implements StringLoadable { + public EnumLoadable(Class enumClass) { + this(enumClass, enumClass.getEnumConstants()); + } + + @Override + public E parseString(String name, String key) { + for (E value : allowedValues) { + if (value.name().toLowerCase(Locale.ROOT).equals(name)) { + return value; + } + } + throw new JsonSyntaxException("Invalid " + enumClass.getSimpleName() + " " + name); + } + + @Override + public String getString(E object) { + return object.name().toLowerCase(Locale.ROOT); + } + + @Override + public E decode(FriendlyByteBuf buffer) { + return buffer.readEnum(enumClass); + } + + @Override + public void encode(FriendlyByteBuf buffer, E object) { + buffer.writeEnum(object); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/primitive/FloatLoadable.java b/src/main/java/slimeknights/mantle/data/loadable/primitive/FloatLoadable.java new file mode 100644 index 000000000..a989bf9eb --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/primitive/FloatLoadable.java @@ -0,0 +1,65 @@ +package slimeknights.mantle.data.loadable.primitive; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSyntaxException; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.util.GsonHelper; +import slimeknights.mantle.data.loadable.Loadable; + +/** + * Loadable for a float + * @param min Minimum allowed value + * @param max Maximum allowed value + */ +public record FloatLoadable(float min, float max) implements Loadable { + /** Loadable ranging from negative infinity to positive infinity */ + public static final FloatLoadable ANY = range(Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY); + /** Loadable ranging from 0 to positive infinity */ + public static final FloatLoadable FROM_ZERO = min(0); + /** Loadable ranging from 0 to 1 */ + public static final FloatLoadable PERCENT = range(0, 1); + + /** Creates a loadable with the given range */ + public static FloatLoadable range(float min, float max) { + return new FloatLoadable(min, max); + } + + /** Creates a loadable ranging from the parameter to short max */ + public static FloatLoadable min(float min) { + return new FloatLoadable(min, Float.POSITIVE_INFINITY); + } + + protected float validate(float value, String key) { + if (min <= value && value <= max) { + return value; + } + if (min == Float.NEGATIVE_INFINITY) { + throw new JsonSyntaxException(key + " must not be greater than " + max); + } + if (max == Float.POSITIVE_INFINITY) { + throw new JsonSyntaxException(key + " must not be less than " + min); + } + throw new JsonSyntaxException(key + " must be between " + min + " and " + max); + } + + @Override + public Float convert(JsonElement element, String key) { + return validate(GsonHelper.convertToFloat(element, key), key); + } + + @Override + public Float decode(FriendlyByteBuf buffer) { + return buffer.readFloat(); + } + + @Override + public JsonElement serialize(Float object) { + return new JsonPrimitive(validate(object, "Value")); + } + + @Override + public void encode(FriendlyByteBuf buffer, Float object) { + buffer.writeFloat(object); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/primitive/IntLoadable.java b/src/main/java/slimeknights/mantle/data/loadable/primitive/IntLoadable.java new file mode 100644 index 000000000..0ca5c4a8f --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/primitive/IntLoadable.java @@ -0,0 +1,183 @@ +package slimeknights.mantle.data.loadable.primitive; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSyntaxException; +import lombok.RequiredArgsConstructor; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.util.GsonHelper; +import slimeknights.mantle.data.loadable.Loadable; + +/** + * Loadable for an integer. + * @see slimeknights.mantle.data.loadable.common.ColorLoadable + */ +@RequiredArgsConstructor +public class IntLoadable implements Loadable { + /** Loadable ranging from integer min to integer max */ + public static final IntLoadable ANY_FULL = range(Integer.MIN_VALUE, Integer.MAX_VALUE); + /** Loadable ranging from short min to short max */ + public static final IntLoadable ANY_SHORT = range(Short.MIN_VALUE, Short.MAX_VALUE); + /** Loadable ranging from -1 to integer max */ + public static final IntLoadable FROM_MINUS_ONE = range(-1, Short.MAX_VALUE); + /** Loadable ranging from zero to integer max */ + public static final IntLoadable FROM_ZERO = min(0); + /** Loadable ranging from one to integer max */ + public static final IntLoadable FROM_ONE = min(1); + + /** Minimum allowed value */ + private final int min; + /** Maximum allowed value */ + private final int max; + /** Method of writing to the network */ + private final IntNetwork network; + + /** Creates a loadable with defaulting networking */ + public static IntLoadable range(int min, int max) { + return new IntLoadable(min, max, IntNetwork.recommended(min, max)); + } + + /** Creates a loadable ranging from the parameter to short max */ + public static IntLoadable min(int min) { + return range(min, Integer.MAX_VALUE); + } + + /** ensures the int is within valid ranges */ + protected int validate(int value, String key) { + if (min <= value && value <= max) { + return value; + } + if (min == Integer.MIN_VALUE) { + throw new JsonSyntaxException(key + " must not be greater than " + max); + } + if (max == Integer.MAX_VALUE) { + throw new JsonSyntaxException(key + " must not be less than " + min); + } + throw new JsonSyntaxException(key + " must be between " + min + " and " + max); + } + + @Override + public Integer convert(JsonElement element, String key) { + return validate(GsonHelper.convertToInt(element, key), key); + } + + @Override + public JsonElement serialize(Integer value) { + return new JsonPrimitive(validate(value, "Value")); + } + + + /* Networking */ + + @Override + public Integer decode(FriendlyByteBuf buffer) { + return network.fromNetwork(buffer); + } + + @Override + public void encode(FriendlyByteBuf buffer, Integer object) { + network.toNetwork(object, buffer); + } + + /** Methods of writing an int to the network */ + public enum IntNetwork { + INT { + @Override + int fromNetwork(FriendlyByteBuf buffer) { + return buffer.readInt(); + } + + @Override + void toNetwork(int value, FriendlyByteBuf buffer) { + buffer.writeInt(value); + } + }, + VAR_INT { + @Override + int fromNetwork(FriendlyByteBuf buffer) { + return buffer.readVarInt(); + } + + @Override + void toNetwork(int value, FriendlyByteBuf buffer) { + buffer.writeVarInt(value); + } + }, + SHORT { + @Override + int fromNetwork(FriendlyByteBuf buffer) { + return buffer.readShort(); + } + + @Override + void toNetwork(int value, FriendlyByteBuf buffer) { + buffer.writeShort(value); + } + }; + + /** Reads the int from the network */ + abstract int fromNetwork(FriendlyByteBuf buffer); + + /** Writes the int to the network */ + abstract void toNetwork(int value, FriendlyByteBuf buffer); + + /** Recommended int network type based on the ranged */ + public static IntNetwork recommended(int min, int max) { + if (min >= 0) { + return IntNetwork.VAR_INT; + } + if (min >= Short.MIN_VALUE && max <= Short.MAX_VALUE) { + return IntNetwork.SHORT; + } + return IntNetwork.INT; + } + } + + + /* Strings */ + + /** + * Creates an int loadable that writes to JSON as a string, can be used as a map key. + * @param radix Base for conversion, base 10 is standard JSON numbers. + */ + public StringLoadable asString(int radix) { + return new StringIntLoadable(min, max, radix, network); + } + + + /** Writes to a string instead of to an integer */ + private static class StringIntLoadable extends IntLoadable implements StringLoadable { + private final int radix; + public StringIntLoadable(int min, int max, int radix, IntNetwork network) { + super(min, max, network); + if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX) { + throw new IllegalArgumentException("Invalid radix " + radix + ", must be between " + Character.MIN_RADIX + " and " + Character.MAX_RADIX); + } + this.radix = radix; + } + + @Override + public Integer parseString(String value, String key) { + try { + return validate(Integer.parseInt(value, radix), key); + } catch (NumberFormatException e) { + throw new JsonSyntaxException("Failed to parse integer at " + key, e); + } + } + + @Override + public Integer convert(JsonElement element, String key) { + return parseString(GsonHelper.convertToString(element, key), key); + } + + @Override + public String getString(Integer value) { + return Integer.toString(value, radix); + } + + @Override + public JsonElement serialize(Integer value) { + return new JsonPrimitive(getString(value)); + } + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/primitive/MaxLengthStringLoadable.java b/src/main/java/slimeknights/mantle/data/loadable/primitive/MaxLengthStringLoadable.java new file mode 100644 index 000000000..e44afde56 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/primitive/MaxLengthStringLoadable.java @@ -0,0 +1,36 @@ +package slimeknights.mantle.data.loadable.primitive; + +import com.google.gson.JsonSyntaxException; +import net.minecraft.network.FriendlyByteBuf; + +/** + * Implementation of a loadable for a string. Access through {@link StringLoadable#maxLength(int)}. + * @param maxLength Maximum length of string allowed + */ +record MaxLengthStringLoadable(int maxLength) implements StringLoadable { + @Override + public String parseString(String value, String key) { + if (value.length() > maxLength) { + throw new JsonSyntaxException(key + " may not be longer than " + maxLength); + } + return value; + } + + @Override + public String getString(String object) { + if (object.length() > maxLength) { + throw new RuntimeException("String may not be longer than " + maxLength); + } + return object; + } + + @Override + public String decode(FriendlyByteBuf buffer) { + return buffer.readUtf(maxLength); + } + + @Override + public void encode(FriendlyByteBuf buffer, String object) { + buffer.writeUtf(object, maxLength); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/primitive/ResourceLocationLoadable.java b/src/main/java/slimeknights/mantle/data/loadable/primitive/ResourceLocationLoadable.java new file mode 100644 index 000000000..cce7261b2 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/primitive/ResourceLocationLoadable.java @@ -0,0 +1,44 @@ +package slimeknights.mantle.data.loadable.primitive; + +import com.google.gson.JsonElement; +import net.minecraft.resources.ResourceLocation; +import slimeknights.mantle.data.loadable.Loadables; + +/** + * Helper for the common case of making a string loadable that uses resource locations. + * @param + * @see Loadables#RESOURCE_LOCATION + */ +public interface ResourceLocationLoadable extends StringLoadable { + /** + * Converts this value from a resource location. + * @param name Location to parse + * @param key Json key containing the value used for exceptions only. + * @return Converted value.' + * @throws com.google.gson.JsonSyntaxException If no value exists for that key + */ + T fromKey(ResourceLocation name, String key); + + @Override + default T parseString(String value, String key) { + return fromKey(Loadables.RESOURCE_LOCATION.parseString(value, key), key); + } + + @Override + default T convert(JsonElement element, String key) { + return fromKey(Loadables.RESOURCE_LOCATION.convert(element, key), key); + } + + /** + * Converts this object to its serialized representation. + * @param object Object to serialize + * @return String representation of the object. + * @throws RuntimeException if unable to serialize this to a string + */ + ResourceLocation getKey(T object); + + @Override + default String getString(T object) { + return getKey(object).toString(); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/primitive/StringLoadable.java b/src/main/java/slimeknights/mantle/data/loadable/primitive/StringLoadable.java new file mode 100644 index 000000000..8a8d4f549 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/primitive/StringLoadable.java @@ -0,0 +1,103 @@ +package slimeknights.mantle.data.loadable.primitive; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import net.minecraft.util.GsonHelper; +import slimeknights.mantle.data.loadable.ErrorFactory; +import slimeknights.mantle.data.loadable.Loadable; +import slimeknights.mantle.data.loadable.mapping.MapLoadable; +import slimeknights.mantle.data.loadable.mapping.MappedLoadable; + +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * Loadable that maps to a string, can be used as a key for a {@link com.google.gson.JsonObject} parsed as a {@link java.util.Map}. + * @param + */ +public interface StringLoadable extends Loadable { + /** Loadable for the default max string length */ + StringLoadable DEFAULT = maxLength(Short.MAX_VALUE); + + /** Creates a new string loadable with the given max length */ + static StringLoadable maxLength(int maxLength) { + return new MaxLengthStringLoadable(maxLength); + } + + /** + * Converts this value from a string. + * @param value Value to parse + * @param key Json key containing the value used for exceptions only. + * @return Converted value.' + * @throws com.google.gson.JsonSyntaxException If unable to parse the value + */ + T parseString(String value, String key); + + @Override + default T convert(JsonElement element, String key) { + return parseString(GsonHelper.convertToString(element, key), key); + } + + /** + * Converts this object to its serialized representation. + * @param object Object to serialize + * @return String representation of the object. + * @throws RuntimeException if unable to serialize this to a string + */ + String getString(T object); + + @Override + default JsonElement serialize(T object) { + return new JsonPrimitive(getString(object)); + } + + + /* Mapping - switches to the string version of the methods */ + + /** + * Creates a map loadable with this as the key + * @param valueLoadable Loadable for the map values + * @param minSize Min size of the map + * @param Map value type + * @return Map loadable + */ + default Loadable> mapWithValues(Loadable valueLoadable, int minSize) { + return new MapLoadable<>(this, valueLoadable, minSize); + } + + /** + * Creates a map loadable with this as the key with a min size of 1 + * @param valueLoadable Loadable for the map values + * @param Map value type + * @return Map loadable + */ + default Loadable> mapWithValues(Loadable valueLoadable) { + return mapWithValues(valueLoadable, 1); + } + + @Override + default StringLoadable xmap(BiFunction from, BiFunction to) { + return MappedLoadable.of(this, from, to); + } + + @Override + default StringLoadable comapFlatMap(BiFunction from, Function to) { + return xmap(from, MappedLoadable.flatten(to)); + } + + @Override + default StringLoadable flatComap(Function from, BiFunction to) { + return xmap(MappedLoadable.flatten(from), to); + } + + @Override + default StringLoadable flatXmap(Function from, Function to) { + return xmap(MappedLoadable.flatten(from), MappedLoadable.flatten(to)); + } + + @Override + default Loadable validate(BiFunction validator) { + return xmap(validator, validator); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/primitive/package-info.java b/src/main/java/slimeknights/mantle/data/loadable/primitive/package-info.java new file mode 100644 index 000000000..eb6d5150a --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/primitive/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package slimeknights.mantle.data.loadable.primitive; + +import net.minecraft.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable.java b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable.java new file mode 100644 index 000000000..9491b0084 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable.java @@ -0,0 +1,538 @@ +package slimeknights.mantle.data.loadable.record; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.mojang.datafixers.util.Function10; +import com.mojang.datafixers.util.Function11; +import com.mojang.datafixers.util.Function12; +import com.mojang.datafixers.util.Function13; +import com.mojang.datafixers.util.Function14; +import com.mojang.datafixers.util.Function15; +import com.mojang.datafixers.util.Function16; +import com.mojang.datafixers.util.Function3; +import com.mojang.datafixers.util.Function4; +import com.mojang.datafixers.util.Function5; +import com.mojang.datafixers.util.Function6; +import com.mojang.datafixers.util.Function7; +import com.mojang.datafixers.util.Function8; +import com.mojang.datafixers.util.Function9; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.util.GsonHelper; +import slimeknights.mantle.data.loadable.ContextStreamable; +import slimeknights.mantle.data.loadable.ErrorFactory; +import slimeknights.mantle.data.loadable.Loadable; +import slimeknights.mantle.data.loadable.field.DirectField; +import slimeknights.mantle.data.loadable.field.LoadableField; +import slimeknights.mantle.data.loadable.field.RecordField; +import slimeknights.mantle.data.loadable.mapping.CompactLoadable; +import slimeknights.mantle.data.loadable.mapping.MappedLoadable; +import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; +import slimeknights.mantle.util.typed.TypedMap; + +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * Base interface for record type loadables, and the home of their factory methods. + * Record loaders directly serialize into JSON objects, meaning they are compatible with {@link slimeknights.mantle.data.registry.GenericLoaderRegistry}. + * @param Type being loaded + */ +@SuppressWarnings("unused") // API +public interface RecordLoadable extends Loadable, IGenericLoader, ContextStreamable { + /* Deserializing */ + + /** + * Deserializes the object from json. + * @param json JSON object + * @param context Additional parsing context, used notably by recipe serializers to store the ID and serializer. + * Will be {@link TypedMap#EMPTY} in nested usages unless {@link DirectField} is used. + * @return Parsed loadable value + * @throws com.google.gson.JsonSyntaxException If unable to read from JSON + */ + T deserialize(JsonObject json, TypedMap context); + + /** Contextless implementation of {@link #deserialize(JsonObject, TypedMap)} for {@link IGenericLoader}. */ + @Override + default T deserialize(JsonObject json) { + return deserialize(json, TypedMap.empty()); + } + + @Override + default T convert(JsonElement element, String key) { + return deserialize(GsonHelper.convertToJsonObject(element, key)); + } + + + /* Serializing */ + + @Override + void serialize(T object, JsonObject json); + + @Override + default JsonElement serialize(T object) { + JsonObject json = new JsonObject(); + serialize(object, json); + return json; + } + + + /* IGenericLoader methods */ + + /** @deprecated use {@link #decode(FriendlyByteBuf)} */ + @Deprecated(forRemoval = true) + @Override + default T fromNetwork(FriendlyByteBuf buffer) { + return decode(buffer); + } + + /** @deprecated use {@link #encode(FriendlyByteBuf, Object)} */ + @Deprecated(forRemoval = true) + @Override + default void toNetwork(T object, FriendlyByteBuf buffer) { + encode(buffer, object); + } + + /* Fields */ + + /** Creates a field that loads this object directly into the parent JSON object */ + default

LoadableField directField(Function getter) { + return new DirectField<>(this, getter); + } + + /** Allows parsing from Json primitives and serializes compactly if the condition is met */ + default RecordLoadable compact(Loadable compact, Predicate condition) { + return CompactLoadable.of(this, compact, condition); + } + + + /* Mapping - switches to the record version of the methods */ + + @Override + default RecordLoadable xmap(BiFunction from, BiFunction to) { + return MappedLoadable.of(this, from, to); + } + + @Override + default RecordLoadable comapFlatMap(BiFunction from, Function to) { + return xmap(from, MappedLoadable.flatten(to)); + } + + @Override + default RecordLoadable flatComap(Function from, BiFunction to) { + return xmap(MappedLoadable.flatten(from), to); + } + + @Override + default RecordLoadable flatXmap(Function from, Function to) { + return xmap(MappedLoadable.flatten(from), MappedLoadable.flatten(to)); + } + + @Override + default RecordLoadable validate(BiFunction validator) { + return xmap(validator, validator); + } + + + /* Helpers to create the final loadable */ + + /** Creates a loadable with 1 parameters */ + static RecordLoadable create( + RecordField fieldA, + Function constructor) { + return new RecordLoadable1<>( + fieldA, + constructor + ); + } + + /** Creates a loadable with 2 parameters */ + static RecordLoadable create( + RecordField fieldA, + RecordField fieldB, + BiFunction constructor) { + return new RecordLoadable2<>( + fieldA, + fieldB, + constructor + ); + } + + /** Creates a loadable with 3 parameters */ + static RecordLoadable create( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + Function3 constructor) { + return new RecordLoadable3<>( + fieldA, + fieldB, + fieldC, + constructor + ); + } + + /** Creates a loadable with 4 parameters */ + static RecordLoadable create( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + RecordField fieldD, + Function4 constructor) { + return new RecordLoadable4<>( + fieldA, + fieldB, + fieldC, + fieldD, + constructor + ); + } + + /** Creates a loadable with 5 parameters */ + static RecordLoadable create( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + RecordField fieldD, + RecordField fieldE, + Function5 constructor) { + return new RecordLoadable5<>( + fieldA, + fieldB, + fieldC, + fieldD, + fieldE, + constructor + ); + } + + /** Creates a loadable with 6 parameters */ + static RecordLoadable create( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + RecordField fieldD, + RecordField fieldE, + RecordField fieldF, + Function6 constructor) { + return new RecordLoadable6<>( + fieldA, + fieldB, + fieldC, + fieldD, + fieldE, + fieldF, + constructor + ); + } + + /** Creates a loadable with 7 parameters */ + static RecordLoadable create( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + RecordField fieldD, + RecordField fieldE, + RecordField fieldF, + RecordField fieldG, + Function7 constructor) { + return new RecordLoadable7<>( + fieldA, + fieldB, + fieldC, + fieldD, + fieldE, + fieldF, + fieldG, + constructor + ); + } + + /** Creates a loadable with 8 parameters */ + static RecordLoadable create( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + RecordField fieldD, + RecordField fieldE, + RecordField fieldF, + RecordField fieldG, + RecordField fieldH, + Function8 constructor) { + return new RecordLoadable8<>( + fieldA, + fieldB, + fieldC, + fieldD, + fieldE, + fieldF, + fieldG, + fieldH, + constructor + ); + } + + /** Creates a loadable with 9 parameters */ + static RecordLoadable create( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + RecordField fieldD, + RecordField fieldE, + RecordField fieldF, + RecordField fieldG, + RecordField fieldH, + RecordField fieldI, + Function9 constructor) { + return new RecordLoadable9<>( + fieldA, + fieldB, + fieldC, + fieldD, + fieldE, + fieldF, + fieldG, + fieldH, + fieldI, + constructor + ); + } + + /** Creates a loadable with 10 parameters */ + static RecordLoadable create( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + RecordField fieldD, + RecordField fieldE, + RecordField fieldF, + RecordField fieldG, + RecordField fieldH, + RecordField fieldI, + RecordField fieldJ, + Function10 constructor) { + return new RecordLoadable10<>( + fieldA, + fieldB, + fieldC, + fieldD, + fieldE, + fieldF, + fieldG, + fieldH, + fieldI, + fieldJ, + constructor + ); + } + + /** Creates a loadable with 11 parameters */ + static RecordLoadable create( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + RecordField fieldD, + RecordField fieldE, + RecordField fieldF, + RecordField fieldG, + RecordField fieldH, + RecordField fieldI, + RecordField fieldJ, + RecordField fieldK, + Function11 constructor) { + return new RecordLoadable11<>( + fieldA, + fieldB, + fieldC, + fieldD, + fieldE, + fieldF, + fieldG, + fieldH, + fieldI, + fieldJ, + fieldK, + constructor + ); + } + + /** Creates a loadable with 12 parameters */ + static RecordLoadable create( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + RecordField fieldD, + RecordField fieldE, + RecordField fieldF, + RecordField fieldG, + RecordField fieldH, + RecordField fieldI, + RecordField fieldJ, + RecordField fieldK, + RecordField fieldL, + Function12 constructor) { + return new RecordLoadable12<>( + fieldA, + fieldB, + fieldC, + fieldD, + fieldE, + fieldF, + fieldG, + fieldH, + fieldI, + fieldJ, + fieldK, + fieldL, + constructor + ); + } + + /** Creates a loadable with 13 parameters */ + static RecordLoadable create( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + RecordField fieldD, + RecordField fieldE, + RecordField fieldF, + RecordField fieldG, + RecordField fieldH, + RecordField fieldI, + RecordField fieldJ, + RecordField fieldK, + RecordField fieldL, + RecordField fieldM, + Function13 constructor) { + return new RecordLoadable13<>( + fieldA, + fieldB, + fieldC, + fieldD, + fieldE, + fieldF, + fieldG, + fieldH, + fieldI, + fieldJ, + fieldK, + fieldL, + fieldM, + constructor + ); + } + + /** Creates a loadable with 14 parameters */ + static RecordLoadable create( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + RecordField fieldD, + RecordField fieldE, + RecordField fieldF, + RecordField fieldG, + RecordField fieldH, + RecordField fieldI, + RecordField fieldJ, + RecordField fieldK, + RecordField fieldL, + RecordField fieldM, + RecordField fieldN, + Function14 constructor) { + return new RecordLoadable14<>( + fieldA, + fieldB, + fieldC, + fieldD, + fieldE, + fieldF, + fieldG, + fieldH, + fieldI, + fieldJ, + fieldK, + fieldL, + fieldM, + fieldN, + constructor + ); + } + + /** Creates a loadable with 15 parameters */ + static RecordLoadable create( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + RecordField fieldD, + RecordField fieldE, + RecordField fieldF, + RecordField fieldG, + RecordField fieldH, + RecordField fieldI, + RecordField fieldJ, + RecordField fieldK, + RecordField fieldL, + RecordField fieldM, + RecordField fieldN, + RecordField fieldO, + Function15 constructor) { + return new RecordLoadable15<>( + fieldA, + fieldB, + fieldC, + fieldD, + fieldE, + fieldF, + fieldG, + fieldH, + fieldI, + fieldJ, + fieldK, + fieldL, + fieldM, + fieldN, + fieldO, + constructor + ); + } + + /** Creates a loadable with 16 parameters */ + static RecordLoadable create( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + RecordField fieldD, + RecordField fieldE, + RecordField fieldF, + RecordField fieldG, + RecordField fieldH, + RecordField fieldI, + RecordField fieldJ, + RecordField fieldK, + RecordField fieldL, + RecordField fieldM, + RecordField fieldN, + RecordField fieldO, + RecordField fieldP, + Function16 constructor) { + return new RecordLoadable16<>( + fieldA, + fieldB, + fieldC, + fieldD, + fieldE, + fieldF, + fieldG, + fieldH, + fieldI, + fieldJ, + fieldK, + fieldL, + fieldM, + fieldN, + fieldO, + fieldP, + constructor + ); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable1.java b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable1.java new file mode 100644 index 000000000..1f5bce249 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable1.java @@ -0,0 +1,34 @@ +package slimeknights.mantle.data.loadable.record; + +import com.google.gson.JsonObject; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.field.RecordField; +import slimeknights.mantle.util.typed.TypedMap; + +import java.util.function.Function; + +/** Record loadable with a single field */ +record RecordLoadable1( + RecordField fieldA, + Function constructor +) implements RecordLoadable { + @Override + public R deserialize(JsonObject json, TypedMap context) { + return constructor.apply(fieldA.get(json, context)); + } + + @Override + public void serialize(R object, JsonObject json) { + fieldA.serialize(object, json); + } + + @Override + public R decode(FriendlyByteBuf buffer, TypedMap context) { + return constructor.apply(fieldA.decode(buffer, context)); + } + + @Override + public void encode(FriendlyByteBuf buffer, R object) { + fieldA.encode(buffer, object); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable10.java b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable10.java new file mode 100644 index 000000000..626af9cd5 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable10.java @@ -0,0 +1,83 @@ +package slimeknights.mantle.data.loadable.record; + +import com.google.gson.JsonObject; +import com.mojang.datafixers.util.Function10; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.field.RecordField; +import slimeknights.mantle.util.typed.TypedMap; + +/** Record loadable with 10 fields */ +@SuppressWarnings("DuplicatedCode") +record RecordLoadable10( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + RecordField fieldD, + RecordField fieldE, + RecordField fieldF, + RecordField fieldG, + RecordField fieldH, + RecordField fieldI, + RecordField fieldJ, + Function10 constructor +) implements RecordLoadable { + @Override + public R deserialize(JsonObject json, TypedMap context) { + return constructor.apply( + fieldA.get(json, context), + fieldB.get(json, context), + fieldC.get(json, context), + fieldD.get(json, context), + fieldE.get(json, context), + fieldF.get(json, context), + fieldG.get(json, context), + fieldH.get(json, context), + fieldI.get(json, context), + fieldJ.get(json, context) + ); + } + + @Override + public void serialize(R object, JsonObject json) { + fieldA.serialize(object, json); + fieldB.serialize(object, json); + fieldC.serialize(object, json); + fieldD.serialize(object, json); + fieldE.serialize(object, json); + fieldF.serialize(object, json); + fieldG.serialize(object, json); + fieldH.serialize(object, json); + fieldI.serialize(object, json); + fieldJ.serialize(object, json); + } + + @Override + public R decode(FriendlyByteBuf buffer, TypedMap context) { + return constructor.apply( + fieldA.decode(buffer, context), + fieldB.decode(buffer, context), + fieldC.decode(buffer, context), + fieldD.decode(buffer, context), + fieldE.decode(buffer, context), + fieldF.decode(buffer, context), + fieldG.decode(buffer, context), + fieldH.decode(buffer, context), + fieldI.decode(buffer, context), + fieldJ.decode(buffer, context) + ); + } + + @Override + public void encode(FriendlyByteBuf buffer, R object) { + fieldA.encode(buffer, object); + fieldB.encode(buffer, object); + fieldC.encode(buffer, object); + fieldD.encode(buffer, object); + fieldE.encode(buffer, object); + fieldF.encode(buffer, object); + fieldG.encode(buffer, object); + fieldH.encode(buffer, object); + fieldI.encode(buffer, object); + fieldJ.encode(buffer, object); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable11.java b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable11.java new file mode 100644 index 000000000..a744bc3c3 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable11.java @@ -0,0 +1,88 @@ +package slimeknights.mantle.data.loadable.record; + +import com.google.gson.JsonObject; +import com.mojang.datafixers.util.Function11; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.field.RecordField; +import slimeknights.mantle.util.typed.TypedMap; + +/** Record loadable with 11 fields */ +@SuppressWarnings("DuplicatedCode") +record RecordLoadable11( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + RecordField fieldD, + RecordField fieldE, + RecordField fieldF, + RecordField fieldG, + RecordField fieldH, + RecordField fieldI, + RecordField fieldJ, + RecordField fieldK, + Function11 constructor +) implements RecordLoadable { + @Override + public R deserialize(JsonObject json, TypedMap context) { + return constructor.apply( + fieldA.get(json, context), + fieldB.get(json, context), + fieldC.get(json, context), + fieldD.get(json, context), + fieldE.get(json, context), + fieldF.get(json, context), + fieldG.get(json, context), + fieldH.get(json, context), + fieldI.get(json, context), + fieldJ.get(json, context), + fieldK.get(json, context) + ); + } + + @Override + public void serialize(R object, JsonObject json) { + fieldA.serialize(object, json); + fieldB.serialize(object, json); + fieldC.serialize(object, json); + fieldD.serialize(object, json); + fieldE.serialize(object, json); + fieldF.serialize(object, json); + fieldG.serialize(object, json); + fieldH.serialize(object, json); + fieldI.serialize(object, json); + fieldJ.serialize(object, json); + fieldK.serialize(object, json); + } + + @Override + public R decode(FriendlyByteBuf buffer, TypedMap context) { + return constructor.apply( + fieldA.decode(buffer, context), + fieldB.decode(buffer, context), + fieldC.decode(buffer, context), + fieldD.decode(buffer, context), + fieldE.decode(buffer, context), + fieldF.decode(buffer, context), + fieldG.decode(buffer, context), + fieldH.decode(buffer, context), + fieldI.decode(buffer, context), + fieldJ.decode(buffer, context), + fieldK.decode(buffer, context) + ); + } + + @Override + public void encode(FriendlyByteBuf buffer, R object) { + fieldA.encode(buffer, object); + fieldB.encode(buffer, object); + fieldC.encode(buffer, object); + fieldD.encode(buffer, object); + fieldE.encode(buffer, object); + fieldF.encode(buffer, object); + fieldG.encode(buffer, object); + fieldH.encode(buffer, object); + fieldI.encode(buffer, object); + fieldJ.encode(buffer, object); + fieldK.encode(buffer, object); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable12.java b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable12.java new file mode 100644 index 000000000..c3e55d928 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable12.java @@ -0,0 +1,93 @@ +package slimeknights.mantle.data.loadable.record; + +import com.google.gson.JsonObject; +import com.mojang.datafixers.util.Function12; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.field.RecordField; +import slimeknights.mantle.util.typed.TypedMap; + +/** Record loadable with 12 fields */ +@SuppressWarnings("DuplicatedCode") +record RecordLoadable12( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + RecordField fieldD, + RecordField fieldE, + RecordField fieldF, + RecordField fieldG, + RecordField fieldH, + RecordField fieldI, + RecordField fieldJ, + RecordField fieldK, + RecordField fieldL, + Function12 constructor +) implements RecordLoadable { + @Override + public R deserialize(JsonObject json, TypedMap context) { + return constructor.apply( + fieldA.get(json, context), + fieldB.get(json, context), + fieldC.get(json, context), + fieldD.get(json, context), + fieldE.get(json, context), + fieldF.get(json, context), + fieldG.get(json, context), + fieldH.get(json, context), + fieldI.get(json, context), + fieldJ.get(json, context), + fieldK.get(json, context), + fieldL.get(json, context) + ); + } + + @Override + public void serialize(R object, JsonObject json) { + fieldA.serialize(object, json); + fieldB.serialize(object, json); + fieldC.serialize(object, json); + fieldD.serialize(object, json); + fieldE.serialize(object, json); + fieldF.serialize(object, json); + fieldG.serialize(object, json); + fieldH.serialize(object, json); + fieldI.serialize(object, json); + fieldJ.serialize(object, json); + fieldK.serialize(object, json); + fieldL.serialize(object, json); + } + + @Override + public R decode(FriendlyByteBuf buffer, TypedMap context) { + return constructor.apply( + fieldA.decode(buffer, context), + fieldB.decode(buffer, context), + fieldC.decode(buffer, context), + fieldD.decode(buffer, context), + fieldE.decode(buffer, context), + fieldF.decode(buffer, context), + fieldG.decode(buffer, context), + fieldH.decode(buffer, context), + fieldI.decode(buffer, context), + fieldJ.decode(buffer, context), + fieldK.decode(buffer, context), + fieldL.decode(buffer, context) + ); + } + + @Override + public void encode(FriendlyByteBuf buffer, R object) { + fieldA.encode(buffer, object); + fieldB.encode(buffer, object); + fieldC.encode(buffer, object); + fieldD.encode(buffer, object); + fieldE.encode(buffer, object); + fieldF.encode(buffer, object); + fieldG.encode(buffer, object); + fieldH.encode(buffer, object); + fieldI.encode(buffer, object); + fieldJ.encode(buffer, object); + fieldK.encode(buffer, object); + fieldL.encode(buffer, object); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable13.java b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable13.java new file mode 100644 index 000000000..10e88a8a9 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable13.java @@ -0,0 +1,98 @@ +package slimeknights.mantle.data.loadable.record; + +import com.google.gson.JsonObject; +import com.mojang.datafixers.util.Function13; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.field.RecordField; +import slimeknights.mantle.util.typed.TypedMap; + +/** Record loadable with 13 fields */ +@SuppressWarnings("DuplicatedCode") +record RecordLoadable13( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + RecordField fieldD, + RecordField fieldE, + RecordField fieldF, + RecordField fieldG, + RecordField fieldH, + RecordField fieldI, + RecordField fieldJ, + RecordField fieldK, + RecordField fieldL, + RecordField fieldM, + Function13 constructor +) implements RecordLoadable { + @Override + public R deserialize(JsonObject json, TypedMap context) { + return constructor.apply( + fieldA.get(json, context), + fieldB.get(json, context), + fieldC.get(json, context), + fieldD.get(json, context), + fieldE.get(json, context), + fieldF.get(json, context), + fieldG.get(json, context), + fieldH.get(json, context), + fieldI.get(json, context), + fieldJ.get(json, context), + fieldK.get(json, context), + fieldL.get(json, context), + fieldM.get(json, context) + ); + } + + @Override + public void serialize(R object, JsonObject json) { + fieldA.serialize(object, json); + fieldB.serialize(object, json); + fieldC.serialize(object, json); + fieldD.serialize(object, json); + fieldE.serialize(object, json); + fieldF.serialize(object, json); + fieldG.serialize(object, json); + fieldH.serialize(object, json); + fieldI.serialize(object, json); + fieldJ.serialize(object, json); + fieldK.serialize(object, json); + fieldL.serialize(object, json); + fieldM.serialize(object, json); + } + + @Override + public R decode(FriendlyByteBuf buffer, TypedMap context) { + return constructor.apply( + fieldA.decode(buffer, context), + fieldB.decode(buffer, context), + fieldC.decode(buffer, context), + fieldD.decode(buffer, context), + fieldE.decode(buffer, context), + fieldF.decode(buffer, context), + fieldG.decode(buffer, context), + fieldH.decode(buffer, context), + fieldI.decode(buffer, context), + fieldJ.decode(buffer, context), + fieldK.decode(buffer, context), + fieldL.decode(buffer, context), + fieldM.decode(buffer, context) + ); + } + + @Override + public void encode(FriendlyByteBuf buffer, R object) { + fieldA.encode(buffer, object); + fieldB.encode(buffer, object); + fieldC.encode(buffer, object); + fieldD.encode(buffer, object); + fieldE.encode(buffer, object); + fieldF.encode(buffer, object); + fieldG.encode(buffer, object); + fieldH.encode(buffer, object); + fieldI.encode(buffer, object); + fieldJ.encode(buffer, object); + fieldK.encode(buffer, object); + fieldL.encode(buffer, object); + fieldM.encode(buffer, object); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable14.java b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable14.java new file mode 100644 index 000000000..04e6323ec --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable14.java @@ -0,0 +1,103 @@ +package slimeknights.mantle.data.loadable.record; + +import com.google.gson.JsonObject; +import com.mojang.datafixers.util.Function14; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.field.RecordField; +import slimeknights.mantle.util.typed.TypedMap; + +/** Record loadable with 14 fields */ +@SuppressWarnings("DuplicatedCode") +record RecordLoadable14( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + RecordField fieldD, + RecordField fieldE, + RecordField fieldF, + RecordField fieldG, + RecordField fieldH, + RecordField fieldI, + RecordField fieldJ, + RecordField fieldK, + RecordField fieldL, + RecordField fieldM, + RecordField fieldN, + Function14 constructor +) implements RecordLoadable { + @Override + public R deserialize(JsonObject json, TypedMap context) { + return constructor.apply( + fieldA.get(json, context), + fieldB.get(json, context), + fieldC.get(json, context), + fieldD.get(json, context), + fieldE.get(json, context), + fieldF.get(json, context), + fieldG.get(json, context), + fieldH.get(json, context), + fieldI.get(json, context), + fieldJ.get(json, context), + fieldK.get(json, context), + fieldL.get(json, context), + fieldM.get(json, context), + fieldN.get(json, context) + ); + } + + @Override + public void serialize(R object, JsonObject json) { + fieldA.serialize(object, json); + fieldB.serialize(object, json); + fieldC.serialize(object, json); + fieldD.serialize(object, json); + fieldE.serialize(object, json); + fieldF.serialize(object, json); + fieldG.serialize(object, json); + fieldH.serialize(object, json); + fieldI.serialize(object, json); + fieldJ.serialize(object, json); + fieldK.serialize(object, json); + fieldL.serialize(object, json); + fieldM.serialize(object, json); + fieldN.serialize(object, json); + } + + @Override + public R decode(FriendlyByteBuf buffer, TypedMap context) { + return constructor.apply( + fieldA.decode(buffer, context), + fieldB.decode(buffer, context), + fieldC.decode(buffer, context), + fieldD.decode(buffer, context), + fieldE.decode(buffer, context), + fieldF.decode(buffer, context), + fieldG.decode(buffer, context), + fieldH.decode(buffer, context), + fieldI.decode(buffer, context), + fieldJ.decode(buffer, context), + fieldK.decode(buffer, context), + fieldL.decode(buffer, context), + fieldM.decode(buffer, context), + fieldN.decode(buffer, context) + ); + } + + @Override + public void encode(FriendlyByteBuf buffer, R object) { + fieldA.encode(buffer, object); + fieldB.encode(buffer, object); + fieldC.encode(buffer, object); + fieldD.encode(buffer, object); + fieldE.encode(buffer, object); + fieldF.encode(buffer, object); + fieldG.encode(buffer, object); + fieldH.encode(buffer, object); + fieldI.encode(buffer, object); + fieldJ.encode(buffer, object); + fieldK.encode(buffer, object); + fieldL.encode(buffer, object); + fieldM.encode(buffer, object); + fieldN.encode(buffer, object); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable15.java b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable15.java new file mode 100644 index 000000000..4c8cf8c36 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable15.java @@ -0,0 +1,108 @@ +package slimeknights.mantle.data.loadable.record; + +import com.google.gson.JsonObject; +import com.mojang.datafixers.util.Function15; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.field.RecordField; +import slimeknights.mantle.util.typed.TypedMap; + +/** Record loadable with 15 fields */ +@SuppressWarnings("DuplicatedCode") +record RecordLoadable15( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + RecordField fieldD, + RecordField fieldE, + RecordField fieldF, + RecordField fieldG, + RecordField fieldH, + RecordField fieldI, + RecordField fieldJ, + RecordField fieldK, + RecordField fieldL, + RecordField fieldM, + RecordField fieldN, + RecordField fieldO, + Function15 constructor +) implements RecordLoadable { + @Override + public R deserialize(JsonObject json, TypedMap context) { + return constructor.apply( + fieldA.get(json, context), + fieldB.get(json, context), + fieldC.get(json, context), + fieldD.get(json, context), + fieldE.get(json, context), + fieldF.get(json, context), + fieldG.get(json, context), + fieldH.get(json, context), + fieldI.get(json, context), + fieldJ.get(json, context), + fieldK.get(json, context), + fieldL.get(json, context), + fieldM.get(json, context), + fieldN.get(json, context), + fieldO.get(json, context) + ); + } + + @Override + public void serialize(R object, JsonObject json) { + fieldA.serialize(object, json); + fieldB.serialize(object, json); + fieldC.serialize(object, json); + fieldD.serialize(object, json); + fieldE.serialize(object, json); + fieldF.serialize(object, json); + fieldG.serialize(object, json); + fieldH.serialize(object, json); + fieldI.serialize(object, json); + fieldJ.serialize(object, json); + fieldK.serialize(object, json); + fieldL.serialize(object, json); + fieldM.serialize(object, json); + fieldN.serialize(object, json); + fieldO.serialize(object, json); + } + + @Override + public R decode(FriendlyByteBuf buffer, TypedMap context) { + return constructor.apply( + fieldA.decode(buffer, context), + fieldB.decode(buffer, context), + fieldC.decode(buffer, context), + fieldD.decode(buffer, context), + fieldE.decode(buffer, context), + fieldF.decode(buffer, context), + fieldG.decode(buffer, context), + fieldH.decode(buffer, context), + fieldI.decode(buffer, context), + fieldJ.decode(buffer, context), + fieldK.decode(buffer, context), + fieldL.decode(buffer, context), + fieldM.decode(buffer, context), + fieldN.decode(buffer, context), + fieldO.decode(buffer, context) + ); + } + + @Override + public void encode(FriendlyByteBuf buffer, R object) { + fieldA.encode(buffer, object); + fieldB.encode(buffer, object); + fieldC.encode(buffer, object); + fieldD.encode(buffer, object); + fieldE.encode(buffer, object); + fieldF.encode(buffer, object); + fieldG.encode(buffer, object); + fieldH.encode(buffer, object); + fieldI.encode(buffer, object); + fieldJ.encode(buffer, object); + fieldK.encode(buffer, object); + fieldL.encode(buffer, object); + fieldM.encode(buffer, object); + fieldN.encode(buffer, object); + fieldO.encode(buffer, object); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable16.java b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable16.java new file mode 100644 index 000000000..29beeaa56 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable16.java @@ -0,0 +1,113 @@ +package slimeknights.mantle.data.loadable.record; + +import com.google.gson.JsonObject; +import com.mojang.datafixers.util.Function16; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.field.RecordField; +import slimeknights.mantle.util.typed.TypedMap; + +/** Record loadable with 16 fields */ +@SuppressWarnings("DuplicatedCode") +record RecordLoadable16( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + RecordField fieldD, + RecordField fieldE, + RecordField fieldF, + RecordField fieldG, + RecordField fieldH, + RecordField fieldI, + RecordField fieldJ, + RecordField fieldK, + RecordField fieldL, + RecordField fieldM, + RecordField fieldN, + RecordField fieldO, + RecordField fieldP, + Function16 constructor +) implements RecordLoadable { + @Override + public R deserialize(JsonObject json, TypedMap context) { + return constructor.apply( + fieldA.get(json, context), + fieldB.get(json, context), + fieldC.get(json, context), + fieldD.get(json, context), + fieldE.get(json, context), + fieldF.get(json, context), + fieldG.get(json, context), + fieldH.get(json, context), + fieldI.get(json, context), + fieldJ.get(json, context), + fieldK.get(json, context), + fieldL.get(json, context), + fieldM.get(json, context), + fieldN.get(json, context), + fieldO.get(json, context), + fieldP.get(json, context) + ); + } + + @Override + public void serialize(R object, JsonObject json) { + fieldA.serialize(object, json); + fieldB.serialize(object, json); + fieldC.serialize(object, json); + fieldD.serialize(object, json); + fieldE.serialize(object, json); + fieldF.serialize(object, json); + fieldG.serialize(object, json); + fieldH.serialize(object, json); + fieldI.serialize(object, json); + fieldJ.serialize(object, json); + fieldK.serialize(object, json); + fieldL.serialize(object, json); + fieldM.serialize(object, json); + fieldN.serialize(object, json); + fieldO.serialize(object, json); + fieldP.serialize(object, json); + } + + @Override + public R decode(FriendlyByteBuf buffer, TypedMap context) { + return constructor.apply( + fieldA.decode(buffer, context), + fieldB.decode(buffer, context), + fieldC.decode(buffer, context), + fieldD.decode(buffer, context), + fieldE.decode(buffer, context), + fieldF.decode(buffer, context), + fieldG.decode(buffer, context), + fieldH.decode(buffer, context), + fieldI.decode(buffer, context), + fieldJ.decode(buffer, context), + fieldK.decode(buffer, context), + fieldL.decode(buffer, context), + fieldM.decode(buffer, context), + fieldN.decode(buffer, context), + fieldO.decode(buffer, context), + fieldP.decode(buffer, context) + ); + } + + @Override + public void encode(FriendlyByteBuf buffer, R object) { + fieldA.encode(buffer, object); + fieldB.encode(buffer, object); + fieldC.encode(buffer, object); + fieldD.encode(buffer, object); + fieldE.encode(buffer, object); + fieldF.encode(buffer, object); + fieldG.encode(buffer, object); + fieldH.encode(buffer, object); + fieldI.encode(buffer, object); + fieldJ.encode(buffer, object); + fieldK.encode(buffer, object); + fieldL.encode(buffer, object); + fieldM.encode(buffer, object); + fieldN.encode(buffer, object); + fieldO.encode(buffer, object); + fieldP.encode(buffer, object); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable2.java b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable2.java new file mode 100644 index 000000000..05d512a63 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable2.java @@ -0,0 +1,43 @@ +package slimeknights.mantle.data.loadable.record; + +import com.google.gson.JsonObject; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.field.RecordField; +import slimeknights.mantle.util.typed.TypedMap; + +import java.util.function.BiFunction; + +/** Record loadable with 2 fields */ +record RecordLoadable2( + RecordField fieldA, + RecordField fieldB, + BiFunction constructor +) implements RecordLoadable { + @Override + public R deserialize(JsonObject json, TypedMap context) { + return constructor.apply( + fieldA.get(json, context), + fieldB.get(json, context) + ); + } + + @Override + public void serialize(R object, JsonObject json) { + fieldA.serialize(object, json); + fieldB.serialize(object, json); + } + + @Override + public R decode(FriendlyByteBuf buffer, TypedMap context) { + return constructor.apply( + fieldA.decode(buffer, context), + fieldB.decode(buffer, context) + ); + } + + @Override + public void encode(FriendlyByteBuf buffer, R object) { + fieldA.encode(buffer, object); + fieldB.encode(buffer, object); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable3.java b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable3.java new file mode 100644 index 000000000..f5136c93b --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable3.java @@ -0,0 +1,47 @@ +package slimeknights.mantle.data.loadable.record; + +import com.google.gson.JsonObject; +import com.mojang.datafixers.util.Function3; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.field.RecordField; +import slimeknights.mantle.util.typed.TypedMap; + +/** Record loadable with 3 fields */ +record RecordLoadable3( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + Function3 constructor +) implements RecordLoadable { + @Override + public R deserialize(JsonObject json, TypedMap context) { + return constructor.apply( + fieldA.get(json, context), + fieldB.get(json, context), + fieldC.get(json, context) + ); + } + + @Override + public void serialize(R object, JsonObject json) { + fieldA.serialize(object, json); + fieldB.serialize(object, json); + fieldC.serialize(object, json); + } + + @Override + public R decode(FriendlyByteBuf buffer, TypedMap context) { + return constructor.apply( + fieldA.decode(buffer, context), + fieldB.decode(buffer, context), + fieldC.decode(buffer, context) + ); + } + + @Override + public void encode(FriendlyByteBuf buffer, R object) { + fieldA.encode(buffer, object); + fieldB.encode(buffer, object); + fieldC.encode(buffer, object); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable4.java b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable4.java new file mode 100644 index 000000000..0ac39b738 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable4.java @@ -0,0 +1,52 @@ +package slimeknights.mantle.data.loadable.record; + +import com.google.gson.JsonObject; +import com.mojang.datafixers.util.Function4; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.field.RecordField; +import slimeknights.mantle.util.typed.TypedMap; + +/** Record loadable with 4 fields */ +record RecordLoadable4( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + RecordField fieldD, + Function4 constructor +) implements RecordLoadable { + @Override + public R deserialize(JsonObject json, TypedMap context) { + return constructor.apply( + fieldA.get(json, context), + fieldB.get(json, context), + fieldC.get(json, context), + fieldD.get(json, context) + ); + } + + @Override + public void serialize(R object, JsonObject json) { + fieldA.serialize(object, json); + fieldB.serialize(object, json); + fieldC.serialize(object, json); + fieldD.serialize(object, json); + } + + @Override + public R decode(FriendlyByteBuf buffer, TypedMap context) { + return constructor.apply( + fieldA.decode(buffer, context), + fieldB.decode(buffer, context), + fieldC.decode(buffer, context), + fieldD.decode(buffer, context) + ); + } + + @Override + public void encode(FriendlyByteBuf buffer, R object) { + fieldA.encode(buffer, object); + fieldB.encode(buffer, object); + fieldC.encode(buffer, object); + fieldD.encode(buffer, object); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable5.java b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable5.java new file mode 100644 index 000000000..6b92547da --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable5.java @@ -0,0 +1,57 @@ +package slimeknights.mantle.data.loadable.record; + +import com.google.gson.JsonObject; +import com.mojang.datafixers.util.Function5; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.field.RecordField; +import slimeknights.mantle.util.typed.TypedMap; + +/** Record loadable with 5 fields */ +record RecordLoadable5( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + RecordField fieldD, + RecordField fieldE, + Function5 constructor +) implements RecordLoadable { + @Override + public R deserialize(JsonObject json, TypedMap context) { + return constructor.apply( + fieldA.get(json, context), + fieldB.get(json, context), + fieldC.get(json, context), + fieldD.get(json, context), + fieldE.get(json, context) + ); + } + + @Override + public void serialize(R object, JsonObject json) { + fieldA.serialize(object, json); + fieldB.serialize(object, json); + fieldC.serialize(object, json); + fieldD.serialize(object, json); + fieldE.serialize(object, json); + } + + @Override + public R decode(FriendlyByteBuf buffer, TypedMap context) { + return constructor.apply( + fieldA.decode(buffer, context), + fieldB.decode(buffer, context), + fieldC.decode(buffer, context), + fieldD.decode(buffer, context), + fieldE.decode(buffer, context) + ); + } + + @Override + public void encode(FriendlyByteBuf buffer, R object) { + fieldA.encode(buffer, object); + fieldB.encode(buffer, object); + fieldC.encode(buffer, object); + fieldD.encode(buffer, object); + fieldE.encode(buffer, object); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable6.java b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable6.java new file mode 100644 index 000000000..6c47e1657 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable6.java @@ -0,0 +1,63 @@ +package slimeknights.mantle.data.loadable.record; + +import com.google.gson.JsonObject; +import com.mojang.datafixers.util.Function6; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.field.RecordField; +import slimeknights.mantle.util.typed.TypedMap; + +/** Record loadable with 6 fields */ +@SuppressWarnings("DuplicatedCode") +record RecordLoadable6( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + RecordField fieldD, + RecordField fieldE, + RecordField fieldF, + Function6 constructor +) implements RecordLoadable { + @Override + public R deserialize(JsonObject json, TypedMap context) { + return constructor.apply( + fieldA.get(json, context), + fieldB.get(json, context), + fieldC.get(json, context), + fieldD.get(json, context), + fieldE.get(json, context), + fieldF.get(json, context) + ); + } + + @Override + public void serialize(R object, JsonObject json) { + fieldA.serialize(object, json); + fieldB.serialize(object, json); + fieldC.serialize(object, json); + fieldD.serialize(object, json); + fieldE.serialize(object, json); + fieldF.serialize(object, json); + } + + @Override + public R decode(FriendlyByteBuf buffer, TypedMap context) { + return constructor.apply( + fieldA.decode(buffer, context), + fieldB.decode(buffer, context), + fieldC.decode(buffer, context), + fieldD.decode(buffer, context), + fieldE.decode(buffer, context), + fieldF.decode(buffer, context) + ); + } + + @Override + public void encode(FriendlyByteBuf buffer, R object) { + fieldA.encode(buffer, object); + fieldB.encode(buffer, object); + fieldC.encode(buffer, object); + fieldD.encode(buffer, object); + fieldE.encode(buffer, object); + fieldF.encode(buffer, object); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable7.java b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable7.java new file mode 100644 index 000000000..abf54322f --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable7.java @@ -0,0 +1,68 @@ +package slimeknights.mantle.data.loadable.record; + +import com.google.gson.JsonObject; +import com.mojang.datafixers.util.Function7; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.field.RecordField; +import slimeknights.mantle.util.typed.TypedMap; + +/** Record loadable with 7 fields */ +@SuppressWarnings("DuplicatedCode") +record RecordLoadable7( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + RecordField fieldD, + RecordField fieldE, + RecordField fieldF, + RecordField fieldG, + Function7 constructor +) implements RecordLoadable { + @Override + public R deserialize(JsonObject json, TypedMap context) { + return constructor.apply( + fieldA.get(json, context), + fieldB.get(json, context), + fieldC.get(json, context), + fieldD.get(json, context), + fieldE.get(json, context), + fieldF.get(json, context), + fieldG.get(json, context) + ); + } + + @Override + public void serialize(R object, JsonObject json) { + fieldA.serialize(object, json); + fieldB.serialize(object, json); + fieldC.serialize(object, json); + fieldD.serialize(object, json); + fieldE.serialize(object, json); + fieldF.serialize(object, json); + fieldG.serialize(object, json); + } + + @Override + public R decode(FriendlyByteBuf buffer, TypedMap context) { + return constructor.apply( + fieldA.decode(buffer, context), + fieldB.decode(buffer, context), + fieldC.decode(buffer, context), + fieldD.decode(buffer, context), + fieldE.decode(buffer, context), + fieldF.decode(buffer, context), + fieldG.decode(buffer, context) + ); + } + + @Override + public void encode(FriendlyByteBuf buffer, R object) { + fieldA.encode(buffer, object); + fieldB.encode(buffer, object); + fieldC.encode(buffer, object); + fieldD.encode(buffer, object); + fieldE.encode(buffer, object); + fieldF.encode(buffer, object); + fieldG.encode(buffer, object); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable8.java b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable8.java new file mode 100644 index 000000000..914fc95f6 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable8.java @@ -0,0 +1,73 @@ +package slimeknights.mantle.data.loadable.record; + +import com.google.gson.JsonObject; +import com.mojang.datafixers.util.Function8; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.field.RecordField; +import slimeknights.mantle.util.typed.TypedMap; + +/** Record loadable with 8 fields */ +@SuppressWarnings("DuplicatedCode") +record RecordLoadable8( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + RecordField fieldD, + RecordField fieldE, + RecordField fieldF, + RecordField fieldG, + RecordField fieldH, + Function8 constructor +) implements RecordLoadable { + @Override + public R deserialize(JsonObject json, TypedMap context) { + return constructor.apply( + fieldA.get(json, context), + fieldB.get(json, context), + fieldC.get(json, context), + fieldD.get(json, context), + fieldE.get(json, context), + fieldF.get(json, context), + fieldG.get(json, context), + fieldH.get(json, context) + ); + } + + @Override + public void serialize(R object, JsonObject json) { + fieldA.serialize(object, json); + fieldB.serialize(object, json); + fieldC.serialize(object, json); + fieldD.serialize(object, json); + fieldE.serialize(object, json); + fieldF.serialize(object, json); + fieldG.serialize(object, json); + fieldH.serialize(object, json); + } + + @Override + public R decode(FriendlyByteBuf buffer, TypedMap context) { + return constructor.apply( + fieldA.decode(buffer, context), + fieldB.decode(buffer, context), + fieldC.decode(buffer, context), + fieldD.decode(buffer, context), + fieldE.decode(buffer, context), + fieldF.decode(buffer, context), + fieldG.decode(buffer, context), + fieldH.decode(buffer, context) + ); + } + + @Override + public void encode(FriendlyByteBuf buffer, R object) { + fieldA.encode(buffer, object); + fieldB.encode(buffer, object); + fieldC.encode(buffer, object); + fieldD.encode(buffer, object); + fieldE.encode(buffer, object); + fieldF.encode(buffer, object); + fieldG.encode(buffer, object); + fieldH.encode(buffer, object); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable9.java b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable9.java new file mode 100644 index 000000000..82942ebe5 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/record/RecordLoadable9.java @@ -0,0 +1,78 @@ +package slimeknights.mantle.data.loadable.record; + +import com.google.gson.JsonObject; +import com.mojang.datafixers.util.Function9; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.field.RecordField; +import slimeknights.mantle.util.typed.TypedMap; + +/** Record loadable with 9 fields */ +@SuppressWarnings("DuplicatedCode") +record RecordLoadable9( + RecordField fieldA, + RecordField fieldB, + RecordField fieldC, + RecordField fieldD, + RecordField fieldE, + RecordField fieldF, + RecordField fieldG, + RecordField fieldH, + RecordField fieldI, + Function9 constructor +) implements RecordLoadable { + @Override + public R deserialize(JsonObject json, TypedMap context) { + return constructor.apply( + fieldA.get(json, context), + fieldB.get(json, context), + fieldC.get(json, context), + fieldD.get(json, context), + fieldE.get(json, context), + fieldF.get(json, context), + fieldG.get(json, context), + fieldH.get(json, context), + fieldI.get(json, context) + ); + } + + @Override + public void serialize(R object, JsonObject json) { + fieldA.serialize(object, json); + fieldB.serialize(object, json); + fieldC.serialize(object, json); + fieldD.serialize(object, json); + fieldE.serialize(object, json); + fieldF.serialize(object, json); + fieldG.serialize(object, json); + fieldH.serialize(object, json); + fieldI.serialize(object, json); + } + + @Override + public R decode(FriendlyByteBuf buffer, TypedMap context) { + return constructor.apply( + fieldA.decode(buffer, context), + fieldB.decode(buffer, context), + fieldC.decode(buffer, context), + fieldD.decode(buffer, context), + fieldE.decode(buffer, context), + fieldF.decode(buffer, context), + fieldG.decode(buffer, context), + fieldH.decode(buffer, context), + fieldI.decode(buffer, context) + ); + } + + @Override + public void encode(FriendlyByteBuf buffer, R object) { + fieldA.encode(buffer, object); + fieldB.encode(buffer, object); + fieldC.encode(buffer, object); + fieldD.encode(buffer, object); + fieldE.encode(buffer, object); + fieldF.encode(buffer, object); + fieldG.encode(buffer, object); + fieldH.encode(buffer, object); + fieldI.encode(buffer, object); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loadable/record/package-info.java b/src/main/java/slimeknights/mantle/data/loadable/record/package-info.java new file mode 100644 index 000000000..28960582a --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/loadable/record/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package slimeknights.mantle.data.loadable.record; + +import net.minecraft.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/main/java/slimeknights/mantle/data/loader/EnumLoader.java b/src/main/java/slimeknights/mantle/data/loader/EnumLoader.java deleted file mode 100644 index a036967eb..000000000 --- a/src/main/java/slimeknights/mantle/data/loader/EnumLoader.java +++ /dev/null @@ -1,42 +0,0 @@ -package slimeknights.mantle.data.loader; - -import com.google.gson.JsonObject; -import net.minecraft.network.FriendlyByteBuf; -import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; -import slimeknights.mantle.data.registry.GenericLoaderRegistry.IHaveLoader; -import slimeknights.mantle.util.JsonHelper; - -import java.util.Locale; -import java.util.function.Function; - -/** - * Loader for an object with a single enum key - * @param Object type - * @param Loader type - */ -public record EnumLoader, T extends Enum>( - String key, - Class enumClass, - Function constructor, - Function getter -) implements IGenericLoader { - @Override - public O deserialize(JsonObject json) { - return constructor.apply(JsonHelper.getAsEnum(json, key, enumClass)); - } - - @Override - public O fromNetwork(FriendlyByteBuf buffer) { - return constructor.apply(buffer.readEnum(enumClass)); - } - - @Override - public void serialize(O object, JsonObject json) { - json.addProperty(key, getter.apply(object).name().toLowerCase(Locale.ROOT)); - } - - @Override - public void toNetwork(O object, FriendlyByteBuf buffer) { - buffer.writeEnum(getter.apply(object)); - } -} diff --git a/src/main/java/slimeknights/mantle/data/loader/IntLoader.java b/src/main/java/slimeknights/mantle/data/loader/IntLoader.java deleted file mode 100644 index 312377d80..000000000 --- a/src/main/java/slimeknights/mantle/data/loader/IntLoader.java +++ /dev/null @@ -1,39 +0,0 @@ -package slimeknights.mantle.data.loader; - -import com.google.gson.JsonObject; -import lombok.RequiredArgsConstructor; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.util.GsonHelper; -import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; -import slimeknights.mantle.data.registry.GenericLoaderRegistry.IHaveLoader; - -import java.util.function.IntFunction; -import java.util.function.ToIntFunction; - -/** Generic serializer for classes that just have a single int value. */ -@RequiredArgsConstructor -public class IntLoader> implements IGenericLoader { - private final String key; - private final IntFunction constructor; - private final ToIntFunction getter; - - @Override - public void serialize(T object, JsonObject json) { - json.addProperty(key, getter.applyAsInt(object)); - } - - @Override - public T deserialize(JsonObject json) { - return constructor.apply(GsonHelper.getAsInt(json, key)); - } - - @Override - public void toNetwork(T object, FriendlyByteBuf buffer) { - buffer.writeVarInt(getter.applyAsInt(object)); - } - - @Override - public T fromNetwork(FriendlyByteBuf buffer) { - return constructor.apply(buffer.readVarInt()); - } -} diff --git a/src/main/java/slimeknights/mantle/data/loader/RegistryEntryLoader.java b/src/main/java/slimeknights/mantle/data/loader/RegistryEntryLoader.java deleted file mode 100644 index 1636d9633..000000000 --- a/src/main/java/slimeknights/mantle/data/loader/RegistryEntryLoader.java +++ /dev/null @@ -1,45 +0,0 @@ -package slimeknights.mantle.data.loader; - -import com.google.gson.JsonObject; -import net.minecraft.core.Registry; -import net.minecraft.network.FriendlyByteBuf; -import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; -import slimeknights.mantle.data.registry.GenericLoaderRegistry.IHaveLoader; -import slimeknights.mantle.util.JsonHelper; - -import java.util.Objects; -import java.util.function.Function; - -/** - * Serializer for an object with a registry entry parameter - * @param Object type - * @param Registry entry type - * @see RegistrySetLoader - */ -public record RegistryEntryLoader,V>( - String key, - Registry registry, - Function constructor, - Function getter -) implements IGenericLoader { - - @Override - public O deserialize(JsonObject json) { - return constructor.apply(JsonHelper.getAsEntry(registry, json, key)); - } - - @Override - public void serialize(O object, JsonObject json) { - json.addProperty(key, Objects.requireNonNull(registry.getKey(getter.apply(object))).toString()); - } - - @Override - public O fromNetwork(FriendlyByteBuf buffer) { - return constructor.apply(registry.byId(buffer.readVarInt())); - } - - @Override - public void toNetwork(O object, FriendlyByteBuf buffer) { - buffer.writeVarInt(registry.getId(getter.apply(object))); - } -} diff --git a/src/main/java/slimeknights/mantle/data/loader/RegistrySetLoader.java b/src/main/java/slimeknights/mantle/data/loader/RegistrySetLoader.java deleted file mode 100644 index 0f392159d..000000000 --- a/src/main/java/slimeknights/mantle/data/loader/RegistrySetLoader.java +++ /dev/null @@ -1,69 +0,0 @@ -package slimeknights.mantle.data.loader; - -import com.google.common.collect.ImmutableSet; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import com.google.gson.JsonSyntaxException; -import net.minecraft.core.Registry; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.resources.ResourceLocation; -import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; -import slimeknights.mantle.data.registry.GenericLoaderRegistry.IHaveLoader; -import slimeknights.mantle.util.JsonHelper; - -import java.util.Objects; -import java.util.Set; -import java.util.function.Function; - -/** - * Generic loader for reading a loadable object with a set of registry objects - * @param Registry type - * @param Loader object type - * @see RegistryEntryLoader - */ -public record RegistrySetLoader>( - String key, - Registry registry, - Function, T> constructor, - Function> getter -) implements IGenericLoader { - @Override - public T deserialize(JsonObject json) { - Set set = ImmutableSet.copyOf(JsonHelper.parseList(json, key, (element, jsonKey) -> { - ResourceLocation objectKey = JsonHelper.convertToResourceLocation(element, jsonKey); - if (registry.containsKey(objectKey)) { - return registry.get(objectKey); - } - throw new JsonSyntaxException("Unknown " + key + " '" + objectKey + "'"); - })); - return constructor.apply(set); - } - - @Override - public void serialize(T object, JsonObject json) { - JsonArray array = new JsonArray(); - for (R entry : getter.apply(object)) { - array.add(Objects.requireNonNull(registry.getKey(entry)).toString()); - } - json.add(key, array); - } - - @Override - public T fromNetwork(FriendlyByteBuf buffer) { - ImmutableSet.Builder builder = ImmutableSet.builder(); - int max = buffer.readVarInt(); - for (int i = 0; i < max; i++) { - builder.add(registry.byId(buffer.readVarInt())); - } - return constructor.apply(builder.build()); - } - - @Override - public void toNetwork(T object, FriendlyByteBuf buffer) { - Set set = getter.apply(object); - buffer.writeVarInt(set.size()); - for (R entry : set) { - buffer.writeVarInt(registry.getId(entry)); - } - } -} diff --git a/src/main/java/slimeknights/mantle/data/loader/ResourceLocationLoader.java b/src/main/java/slimeknights/mantle/data/loader/ResourceLocationLoader.java deleted file mode 100644 index 8b3e2b87e..000000000 --- a/src/main/java/slimeknights/mantle/data/loader/ResourceLocationLoader.java +++ /dev/null @@ -1,40 +0,0 @@ -package slimeknights.mantle.data.loader; - -import com.google.gson.JsonObject; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.resources.ResourceLocation; -import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; -import slimeknights.mantle.data.registry.GenericLoaderRegistry.IHaveLoader; -import slimeknights.mantle.util.JsonHelper; - -import java.util.function.Function; - -/** - * Loader for an object with a resource location - * @param Object type - */ -public record ResourceLocationLoader>( - String key, - Function constructor, - Function getter -) implements IGenericLoader { - @Override - public O deserialize(JsonObject json) { - return constructor.apply(JsonHelper.getResourceLocation(json, key)); - } - - @Override - public O fromNetwork(FriendlyByteBuf buffer) { - return constructor.apply(buffer.readResourceLocation()); - } - - @Override - public void serialize(O object, JsonObject json) { - json.addProperty(key, getter.apply(object).toString()); - } - - @Override - public void toNetwork(O object, FriendlyByteBuf buffer) { - buffer.writeResourceLocation(getter.apply(object)); - } -} diff --git a/src/main/java/slimeknights/mantle/data/loader/StringLoader.java b/src/main/java/slimeknights/mantle/data/loader/StringLoader.java deleted file mode 100644 index 5595e1bbe..000000000 --- a/src/main/java/slimeknights/mantle/data/loader/StringLoader.java +++ /dev/null @@ -1,39 +0,0 @@ -package slimeknights.mantle.data.loader; - -import com.google.gson.JsonObject; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.util.GsonHelper; -import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; -import slimeknights.mantle.data.registry.GenericLoaderRegistry.IHaveLoader; - -import java.util.function.Function; - -/** - * Loader for a string value - * @param - */ -public record StringLoader>( - String key, - Function constructor, - Function getter -) implements IGenericLoader { - @Override - public O deserialize(JsonObject json) { - return constructor.apply(GsonHelper.getAsString(json, key)); - } - - @Override - public void serialize(O object, JsonObject json) { - json.addProperty(key, getter.apply(object)); - } - - @Override - public O fromNetwork(FriendlyByteBuf buffer) { - return constructor.apply(buffer.readUtf(Short.MAX_VALUE)); - } - - @Override - public void toNetwork(O object, FriendlyByteBuf buffer) { - buffer.writeUtf(getter.apply(object)); - } -} diff --git a/src/main/java/slimeknights/mantle/data/loader/TagKeyLoader.java b/src/main/java/slimeknights/mantle/data/loader/TagKeyLoader.java deleted file mode 100644 index 49e796cfb..000000000 --- a/src/main/java/slimeknights/mantle/data/loader/TagKeyLoader.java +++ /dev/null @@ -1,45 +0,0 @@ -package slimeknights.mantle.data.loader; - -import com.google.gson.JsonObject; -import lombok.RequiredArgsConstructor; -import net.minecraft.core.Registry; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.resources.ResourceKey; -import net.minecraft.tags.TagKey; -import slimeknights.mantle.data.predicate.IJsonPredicate; -import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; -import slimeknights.mantle.util.JsonHelper; - -import java.util.function.Function; - -/** - * Generic loader for a tag based JSON predicate. - * @param Tag registry key - * @param Constructor for the predicate - */ -@RequiredArgsConstructor -public class TagKeyLoader> implements IGenericLoader { - private final ResourceKey> registry; - private final Function,C> constructor; - private final Function> getter; - - @Override - public C deserialize(JsonObject json) { - return constructor.apply(TagKey.create(registry, JsonHelper.getResourceLocation(json, "tag"))); - } - - @Override - public C fromNetwork(FriendlyByteBuf buffer) { - return constructor.apply(TagKey.create(registry, buffer.readResourceLocation())); - } - - @Override - public void serialize(C object, JsonObject json) { - json.addProperty("tag", getter.apply(object).location().toString()); - } - - @Override - public void toNetwork(C object, FriendlyByteBuf buffer) { - buffer.writeResourceLocation(getter.apply(object).location()); - } -} diff --git a/src/main/java/slimeknights/mantle/data/predicate/AndJsonPredicate.java b/src/main/java/slimeknights/mantle/data/predicate/AndJsonPredicate.java deleted file mode 100644 index 46e9d98b1..000000000 --- a/src/main/java/slimeknights/mantle/data/predicate/AndJsonPredicate.java +++ /dev/null @@ -1,39 +0,0 @@ -package slimeknights.mantle.data.predicate; - -import lombok.RequiredArgsConstructor; -import slimeknights.mantle.data.registry.GenericLoaderRegistry; -import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; - -import java.util.List; - -/** Predicate that requires all children to match */ -@RequiredArgsConstructor -public class AndJsonPredicate implements IJsonPredicate { - private final NestedJsonPredicateLoader> loader; - private final List> children; - - @Override - public boolean matches(I input) { - for (IJsonPredicate child : children) { - if (!child.matches(input)) { - return false; - } - } - return true; - } - - @Override - public IJsonPredicate inverted() { - return loader.invert(this); - } - - @Override - public IGenericLoader> getLoader() { - return loader; - } - - /** Creates a new loader for the given loader registry */ - public static NestedJsonPredicateLoader> createLoader(GenericLoaderRegistry> loader, InvertedJsonPredicate.Loader inverted) { - return new NestedJsonPredicateLoader<>(loader, inverted, AndJsonPredicate::new, t -> t.children); - } -} diff --git a/src/main/java/slimeknights/mantle/data/predicate/FallbackPredicateRegistry.java b/src/main/java/slimeknights/mantle/data/predicate/FallbackPredicateRegistry.java new file mode 100644 index 000000000..1c64ef168 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/predicate/FallbackPredicateRegistry.java @@ -0,0 +1,104 @@ +package slimeknights.mantle.data.predicate; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; +import lombok.RequiredArgsConstructor; +import net.minecraft.resources.ResourceLocation; +import slimeknights.mantle.Mantle; +import slimeknights.mantle.data.loadable.record.RecordLoadable; +import slimeknights.mantle.util.JsonHelper; + +import java.util.function.Function; + +/** Predicate registry that upon failure to find a predicate type will fallback to the fallback type */ +public class FallbackPredicateRegistry extends PredicateRegistry { + private final Function getter; + private final PredicateRegistry fallback; + private final RecordLoadable fallbackLoader; + + /** + * Creates a new instance + * @param defaultInstance Default instance, typically expected to be an any predicate. + */ + public FallbackPredicateRegistry(String name, IJsonPredicate defaultInstance, PredicateRegistry fallback, Function getter, String fallbackName) { + super(name, defaultInstance); + this.fallback = fallback; + this.getter = getter; + this.fallbackLoader = RecordLoadable.create(fallback.directField(fallbackName + "_type", p -> p.predicate), FallbackPredicate::new); + this.register(Mantle.getResource(fallbackName), fallbackLoader); + } + + /** Creates a fallback predicate instance */ + public IJsonPredicate fallback(IJsonPredicate predicate) { + return new FallbackPredicate(predicate); + } + + @Override + public IJsonPredicate convert(JsonElement element, String key) { + if (element.isJsonNull()) { + return getDefault(); + } + // identify type key, and the object we will load from + JsonObject object; + ResourceLocation type; + if (element.isJsonObject()) { + object = element.getAsJsonObject(); + type = JsonHelper.getResourceLocation(object, "type"); + } else if (compact && element.isJsonPrimitive()) { + EMPTY_OBJECT.entrySet().clear(); + object = EMPTY_OBJECT; + type = JsonHelper.convertToResourceLocation(element, "type"); + } else { + throw new JsonSyntaxException("Invalid " + getName() + " JSON at " + key + ", must be a JSON object" + (compact ? " or a string" : "")); + } + // see if we have a primary loader, if so parse that + IGenericLoader> loader = loaders.getValue(type); + if (loader != null) { + return loader.deserialize(object); + } + // primary loader failed, try a fallback loader + return new FallbackPredicate(this.fallback.convert(element, key)); + } + + @SuppressWarnings("unchecked") + @Override + public JsonElement serialize(IJsonPredicate src) { + // write the fallback directly to JSON instead of as a nested type + if (src instanceof NestedPredicate) { + return this.fallback.serialize(((NestedPredicate)src).predicate()); + } + return super.serialize(src); + } + + /** Helper interface to make the cast work */ + private interface NestedPredicate { + IJsonPredicate predicate(); + } + + /** Predicate matching another predicate type */ + @RequiredArgsConstructor + public class FallbackPredicate implements IJsonPredicate, NestedPredicate { + private final IJsonPredicate predicate; + + @Override + public IJsonPredicate predicate() { + return predicate; + } + + @Override + public boolean matches(T input) { + return predicate.matches(getter.apply(input)); + } + + @Override + public IJsonPredicate inverted() { + return invert(this); + } + + @Override + public IGenericLoader> getLoader() { + return fallbackLoader; + } + } +} diff --git a/src/main/java/slimeknights/mantle/data/predicate/IJsonPredicate.java b/src/main/java/slimeknights/mantle/data/predicate/IJsonPredicate.java index df4a0c913..6e48c6fd6 100644 --- a/src/main/java/slimeknights/mantle/data/predicate/IJsonPredicate.java +++ b/src/main/java/slimeknights/mantle/data/predicate/IJsonPredicate.java @@ -1,12 +1,16 @@ package slimeknights.mantle.data.predicate; +import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; import slimeknights.mantle.data.registry.GenericLoaderRegistry.IHaveLoader; /** Generic interface for predicate based JSON loaders */ -public interface IJsonPredicate extends IHaveLoader> { +public interface IJsonPredicate extends IHaveLoader { /** Returns true if this json predicate matches the given input */ boolean matches(I input); /** Inverts the given predicate */ IJsonPredicate inverted(); + + @Override + IGenericLoader> getLoader(); } diff --git a/src/main/java/slimeknights/mantle/data/predicate/InvertedJsonPredicate.java b/src/main/java/slimeknights/mantle/data/predicate/InvertedJsonPredicate.java deleted file mode 100644 index eff0d9669..000000000 --- a/src/main/java/slimeknights/mantle/data/predicate/InvertedJsonPredicate.java +++ /dev/null @@ -1,75 +0,0 @@ -package slimeknights.mantle.data.predicate; - -import com.google.gson.JsonObject; -import lombok.RequiredArgsConstructor; -import net.minecraft.network.FriendlyByteBuf; -import slimeknights.mantle.data.registry.GenericLoaderRegistry; -import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; -import slimeknights.mantle.data.loader.NestedLoader; - -/** - * Predicate that inverts the condition. - * Generally, this class should not be constructed directly, use {@link IJsonPredicate#inverted()} instead as it will simplify inverted forms. - */ -@RequiredArgsConstructor -public class InvertedJsonPredicate implements IJsonPredicate { - private final Loader loader; - private final IJsonPredicate base; - - @Override - public boolean matches(I input) { - return !base.matches(input); - } - - @Override - public IGenericLoader> getLoader() { - return loader; - } - - @Override - public IJsonPredicate inverted() { - return base; - } - - /** Loader for an inverted JSON predicate */ - @RequiredArgsConstructor - public static class Loader implements IGenericLoader> { - /** Loader for predicates of this type */ - private final GenericLoaderRegistry> loader; - /** If true, will support the nested method for predicates as a fallback, will still prefer the non-nested method for serializing */ - private final boolean allowNested; - - public Loader(GenericLoaderRegistry> loader) { - this(loader, true); - } - - /** Creates a new instance of an inverted predicate */ - public InvertedJsonPredicate create(IJsonPredicate predicate) { - return new InvertedJsonPredicate<>(this, predicate); - } - - @Override - public InvertedJsonPredicate deserialize(JsonObject json) { - if (allowNested && json.has("predicate")) { - return create(loader.getAndDeserialize(json, "predicate")); - } - NestedLoader.mapType(json, "inverted_type"); - return create(loader.deserialize(json)); - } - - @Override - public InvertedJsonPredicate fromNetwork(FriendlyByteBuf buffer) { - return create(loader.fromNetwork(buffer)); - } - - @Override - public void serialize(InvertedJsonPredicate object, JsonObject json) { - NestedLoader.serializeInto(json, "inverted_type", loader, object.base); - } - - @Override - public void toNetwork(InvertedJsonPredicate object, FriendlyByteBuf buffer) { - loader.toNetwork(object.base, buffer); - } - }; -} diff --git a/src/main/java/slimeknights/mantle/data/predicate/NestedJsonPredicateLoader.java b/src/main/java/slimeknights/mantle/data/predicate/NestedJsonPredicateLoader.java deleted file mode 100644 index de33258ad..000000000 --- a/src/main/java/slimeknights/mantle/data/predicate/NestedJsonPredicateLoader.java +++ /dev/null @@ -1,70 +0,0 @@ -package slimeknights.mantle.data.predicate; - -import com.google.common.collect.ImmutableList; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import lombok.RequiredArgsConstructor; -import net.minecraft.network.FriendlyByteBuf; -import slimeknights.mantle.data.registry.GenericLoaderRegistry; -import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; -import slimeknights.mantle.util.JsonHelper; - -import java.util.List; -import java.util.function.BiFunction; -import java.util.function.Function; - -/** Loader for AND and OR predicates */ -@RequiredArgsConstructor -public class NestedJsonPredicateLoader> implements IGenericLoader { - private final GenericLoaderRegistry> loader; - private final InvertedJsonPredicate.Loader inverter; - private final BiFunction,List>,T> constructor; - private final Function>> getter; - - /** Creates a new instance of the relevant predicate */ - @SafeVarargs - public final T create(IJsonPredicate... children) { - if (children.length < 2) { - throw new IllegalStateException("Too few children for nested predicate loader"); - } - return constructor.apply(this, ImmutableList.copyOf(children)); - } - - /** Inverts the given nested predicate condition */ - InvertedJsonPredicate invert(T instance) { - return inverter.create(instance); - } - - @Override - public T deserialize(JsonObject json) { - return constructor.apply(this, JsonHelper.parseList(json, "predicates", (e, s) -> loader.deserialize(e))); - } - - @Override - public T fromNetwork(FriendlyByteBuf buffer) { - int max = buffer.readVarInt(); - ImmutableList.Builder> builder = ImmutableList.builder(); - for (int i = 0; i < max; i++) { - builder.add(loader.fromNetwork(buffer)); - } - return constructor.apply(this, builder.build()); - } - - @Override - public void serialize(T object, JsonObject json) { - JsonArray array = new JsonArray(); - for (IJsonPredicate predicate : getter.apply(object)) { - array.add(loader.serialize(predicate)); - } - json.add("predicates", array); - } - - @Override - public void toNetwork(T object, FriendlyByteBuf buffer) { - List> list = getter.apply(object); - buffer.writeVarInt(list.size()); - for (IJsonPredicate predicate : list) { - loader.toNetwork(predicate, buffer); - } - } -} diff --git a/src/main/java/slimeknights/mantle/data/predicate/OrJsonPredicate.java b/src/main/java/slimeknights/mantle/data/predicate/OrJsonPredicate.java deleted file mode 100644 index 1aef347ac..000000000 --- a/src/main/java/slimeknights/mantle/data/predicate/OrJsonPredicate.java +++ /dev/null @@ -1,39 +0,0 @@ -package slimeknights.mantle.data.predicate; - -import lombok.RequiredArgsConstructor; -import slimeknights.mantle.data.registry.GenericLoaderRegistry; -import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; - -import java.util.List; - -/** Predicate that requires any child to match */ -@RequiredArgsConstructor -public class OrJsonPredicate implements IJsonPredicate { - private final NestedJsonPredicateLoader> loader; - private final List> children; - - @Override - public boolean matches(I input) { - for (IJsonPredicate child : children) { - if (child.matches(input)) { - return true; - } - } - return false; - } - - @Override - public IJsonPredicate inverted() { - return loader.invert(this); - } - - @Override - public IGenericLoader> getLoader() { - return loader; - } - - /** Creates a new loader for the given loader registry */ - public static NestedJsonPredicateLoader> createLoader(GenericLoaderRegistry> loader, InvertedJsonPredicate.Loader inverted) { - return new NestedJsonPredicateLoader<>(loader, inverted, OrJsonPredicate::new, t -> t.children); - } -} diff --git a/src/main/java/slimeknights/mantle/data/predicate/PredicateRegistry.java b/src/main/java/slimeknights/mantle/data/predicate/PredicateRegistry.java new file mode 100644 index 000000000..d349aef0a --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/predicate/PredicateRegistry.java @@ -0,0 +1,139 @@ +package slimeknights.mantle.data.predicate; + +import lombok.RequiredArgsConstructor; +import slimeknights.mantle.Mantle; +import slimeknights.mantle.data.loadable.Loadable; +import slimeknights.mantle.data.loadable.record.RecordLoadable; +import slimeknights.mantle.data.registry.DefaultingLoaderRegistry; + +import java.util.List; + +/** Extension of generic loader registry providing default implementations for common predicates */ +public class PredicateRegistry extends DefaultingLoaderRegistry> { + /** Loader for inverted predicates */ + private final RecordLoadable invertedLoader; + /** Loader for and predicates */ + private final RecordLoadable andLoader; + /** Loader for or predicates */ + private final RecordLoadable orLoader; + + /** + * Creates a new instance + * @param name Name to display in error messages + * @param defaultInstance Default instance, typically expected to be an any predicate. Will be used for nulls and missing fields + */ + public PredicateRegistry(String name, IJsonPredicate defaultInstance) { + super(name, defaultInstance, true); + // create common types + Loadable>> list = this.list(2); + invertedLoader = RecordLoadable.create(directField("inverted_type", p -> p.predicate), InvertedJsonPredicate::new); + andLoader = RecordLoadable.create(list.requiredField("predicates", p -> p.children), AndJsonPredicate::new); + orLoader = RecordLoadable.create(list.requiredField("predicates", p -> p.children), OrJsonPredicate::new); + // register common types + this.register(Mantle.getResource("any"), defaultInstance.getLoader()); + this.register(Mantle.getResource("inverted"), invertedLoader); + this.register(Mantle.getResource("and"), andLoader); + this.register(Mantle.getResource("or"), orLoader); + } + + /** + * Inverts the given predicate + * @param predicate Predicate to invert + * @return Inverted predicate + */ + public IJsonPredicate invert(IJsonPredicate predicate) { + return new InvertedJsonPredicate(predicate); + } + + /** + * Ands the given predicates together + * @param predicates Predicate list + * @return Predicate that is true if all the passed predicates are true + */ + public IJsonPredicate and(List> predicates) { + return new AndJsonPredicate(predicates); + } + + /** + * Ors the given predicates together + * @param predicates Predicate list + * @return Predicate that is true if any of the passed predicates are true + */ + public IJsonPredicate or(List> predicates) { + return new OrJsonPredicate(predicates); + } + + + /** Predicate that inverts the condition. */ + @RequiredArgsConstructor + public class InvertedJsonPredicate implements IJsonPredicate { + private final IJsonPredicate predicate; + + @Override + public boolean matches(T input) { + return !predicate.matches(input); + } + + @Override + public IGenericLoader> getLoader() { + return invertedLoader; + } + + @Override + public IJsonPredicate inverted() { + return predicate; + } + } + + /** Predicate that requires all children to match */ + @RequiredArgsConstructor + public class AndJsonPredicate implements IJsonPredicate { + private final List> children; + + @Override + public boolean matches(T input) { + for (IJsonPredicate child : children) { + if (!child.matches(input)) { + return false; + } + } + return true; + } + + @Override + public IJsonPredicate inverted() { + return invert(this); + } + + @Override + public IGenericLoader> getLoader() { + return andLoader; + } + } + + /** Predicate that requires any child to match */ + @RequiredArgsConstructor + public class OrJsonPredicate implements IJsonPredicate { + private final List> children; + + @Override + public boolean matches(T input) { + for (IJsonPredicate child : children) { + if (child.matches(input)) { + return true; + } + } + return false; + } + + @Override + public IJsonPredicate inverted() { + return invert(this); + } + + @Override + public IGenericLoader> getLoader() { + return orLoader; + } + } +} diff --git a/src/main/java/slimeknights/mantle/data/predicate/RegistryPredicateRegistry.java b/src/main/java/slimeknights/mantle/data/predicate/RegistryPredicateRegistry.java new file mode 100644 index 000000000..520c3c8a3 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/predicate/RegistryPredicateRegistry.java @@ -0,0 +1,94 @@ +package slimeknights.mantle.data.predicate; + +import lombok.RequiredArgsConstructor; +import net.minecraft.tags.TagKey; +import slimeknights.mantle.Mantle; +import slimeknights.mantle.data.loadable.Loadable; +import slimeknights.mantle.data.loadable.record.RecordLoadable; + +import java.util.Set; +import java.util.function.BiPredicate; +import java.util.function.Function; + +/** Predicate registry that implements tag and set predicates */ +public class RegistryPredicateRegistry extends PredicateRegistry { + private final Function getter; + private final BiPredicate,T> tagMatcher; + private final RecordLoadable setLoader; + private final RecordLoadable tagLoader; + + /** + * Creates a new instance + * @param defaultInstance Default instance, typically expected to be an any predicate. + * @param registry Loading logic for the backing registry + * @param getter Method mapping from the predicate type to the registry type + * @param setKey Key to use for the set predicate + * @param tagKey Loader for tag keys + * @param tagMatcher Logic to match a tag for the passed type + */ + public RegistryPredicateRegistry(String name, IJsonPredicate defaultInstance, Loadable registry, Function getter, String setKey, Loadable> tagKey, BiPredicate,T> tagMatcher) { + super(name, defaultInstance); + // fields for loaders + this.getter = getter; + this.tagMatcher = tagMatcher; + // create loaders + this.setLoader = RecordLoadable.create(registry.set().requiredField(setKey, p -> p.set), SetPredicate::new); + this.tagLoader = RecordLoadable.create(tagKey.requiredField("tag", p -> p.tag), TagPredicate::new); + // register loaders + this.register(Mantle.getResource("set"), setLoader); + this.register(Mantle.getResource("tag"), tagLoader); + } + + /** Creates a new set predicate given the passed values */ + public IJsonPredicate setOf(Set values) { + return new SetPredicate(values); + } + + /** Creates a new tag predicate */ + public IJsonPredicate tag(TagKey tag) { + return new TagPredicate(tag); + } + + + /** Predicate matching an entry from a set of values */ + @RequiredArgsConstructor + private class SetPredicate implements IJsonPredicate { + private final Set set; + + @Override + public boolean matches(T input) { + return set.contains(getter.apply(input)); + } + + @Override + public IJsonPredicate inverted() { + return invert(this); + } + + @Override + public IGenericLoader> getLoader() { + return setLoader; + } + } + + /** Predicate matching values in a tag */ + @RequiredArgsConstructor + private class TagPredicate implements IJsonPredicate { + private final TagKey tag; + + @Override + public boolean matches(T input) { + return tagMatcher.test(tag, input); + } + + @Override + public IJsonPredicate inverted() { + return invert(this); + } + + @Override + public IGenericLoader> getLoader() { + return tagLoader; + } + } +} diff --git a/src/main/java/slimeknights/mantle/data/predicate/block/BlockPredicate.java b/src/main/java/slimeknights/mantle/data/predicate/block/BlockPredicate.java index 1644bea11..db7ee0813 100644 --- a/src/main/java/slimeknights/mantle/data/predicate/block/BlockPredicate.java +++ b/src/main/java/slimeknights/mantle/data/predicate/block/BlockPredicate.java @@ -1,16 +1,17 @@ package slimeknights.mantle.data.predicate.block; +import com.google.common.collect.ImmutableSet; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockBehaviour.BlockStateBase; import net.minecraft.world.level.block.state.BlockState; -import slimeknights.mantle.data.registry.GenericLoaderRegistry; +import slimeknights.mantle.data.loadable.Loadables; +import slimeknights.mantle.data.predicate.IJsonPredicate; +import slimeknights.mantle.data.predicate.RegistryPredicateRegistry; import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; import slimeknights.mantle.data.registry.GenericLoaderRegistry.SingletonLoader; -import slimeknights.mantle.data.predicate.AndJsonPredicate; -import slimeknights.mantle.data.predicate.IJsonPredicate; -import slimeknights.mantle.data.predicate.InvertedJsonPredicate; -import slimeknights.mantle.data.predicate.NestedJsonPredicateLoader; -import slimeknights.mantle.data.predicate.OrJsonPredicate; +import java.util.List; import java.util.function.Predicate; /** @@ -20,18 +21,12 @@ public interface BlockPredicate extends IJsonPredicate { /** Predicate that matches any block */ BlockPredicate ANY = simple(state -> true); /** Loader for block state predicates */ - GenericLoaderRegistry> LOADER = new GenericLoaderRegistry<>(ANY, true); - /** Loader for inverted conditions */ - InvertedJsonPredicate.Loader INVERTED = new InvertedJsonPredicate.Loader<>(LOADER); - /** Loader for and conditions */ - NestedJsonPredicateLoader> AND = AndJsonPredicate.createLoader(LOADER, INVERTED); - /** Loader for or conditions */ - NestedJsonPredicateLoader> OR = OrJsonPredicate.createLoader(LOADER, INVERTED); + RegistryPredicateRegistry LOADER = new RegistryPredicateRegistry<>("Block Predicate", ANY, Loadables.BLOCK, BlockState::getBlock, "blocks", Loadables.BLOCK_TAG, (tag, state) -> state.is(tag)); /** Gets an inverted condition */ @Override default IJsonPredicate inverted() { - return INVERTED.create(this); + return LOADER.invert(this); } @@ -49,9 +44,34 @@ public boolean matches(BlockState state) { } @Override - public IGenericLoader> getLoader() { + public IGenericLoader getLoader() { return loader; } }); } + + + /* Helper methods */ + + /** Creates a block set predicate */ + static IJsonPredicate set(Block... blocks) { + return LOADER.setOf(ImmutableSet.copyOf(blocks)); + } + + /** Creates a tag predicate */ + static IJsonPredicate tag(TagKey tag) { + return LOADER.tag(tag); + } + + /** Creates an and predicate */ + @SafeVarargs + static IJsonPredicate and(IJsonPredicate... predicates) { + return LOADER.and(List.of(predicates)); + } + + /** Creates an or predicate */ + @SafeVarargs + static IJsonPredicate or(IJsonPredicate... predicates) { + return LOADER.or(List.of(predicates)); + } } diff --git a/src/main/java/slimeknights/mantle/data/predicate/block/BlockPropertiesPredicate.java b/src/main/java/slimeknights/mantle/data/predicate/block/BlockPropertiesPredicate.java index 529fc5a5d..7beceb677 100644 --- a/src/main/java/slimeknights/mantle/data/predicate/block/BlockPropertiesPredicate.java +++ b/src/main/java/slimeknights/mantle/data/predicate/block/BlockPropertiesPredicate.java @@ -16,9 +16,11 @@ import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.properties.Property; -import slimeknights.mantle.data.predicate.IJsonPredicate; +import slimeknights.mantle.data.loadable.Loadables; +import slimeknights.mantle.data.loadable.record.RecordLoadable; import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; import slimeknights.mantle.util.JsonHelper; +import slimeknights.mantle.util.typed.TypedMap; import javax.annotation.Nullable; import java.util.LinkedHashMap; @@ -53,7 +55,7 @@ public boolean matches(BlockState input) { } @Override - public IGenericLoader> getLoader() { + public IGenericLoader getLoader() { return LOADER; } @@ -66,10 +68,12 @@ private static Property parseProperty(Block block, String name, Function LOADER = new IGenericLoader<>() { + /** Loader instance */ + public static final RecordLoadable LOADER = new RecordLoadable<>() { @Override - public BlockPropertiesPredicate deserialize(JsonObject json) { - Block block = JsonHelper.getAsEntry(BuiltInRegistries.BLOCK, json, "block"); + public BlockPropertiesPredicate deserialize(JsonObject json, TypedMap context) { + Block block = Loadables.BLOCK.getIfPresent(json, "block"); + // TODO: this is a bit of a unique case for matcher, as its parsing from a map into a list, think about whether we can do something generic ImmutableList.Builder builder = ImmutableList.builder(); for (Entry entry : GsonHelper.getAsJsonObject(json, "properties").entrySet()) { Property property = parseProperty(block, entry.getKey(), JSON_EXCEPTION); @@ -80,7 +84,7 @@ public BlockPropertiesPredicate deserialize(JsonObject json) { @Override public void serialize(BlockPropertiesPredicate object, JsonObject json) { - json.addProperty("block", BuiltInRegistries.BLOCK.getKey(object.block).toString()); + json.add("block", Loadables.BLOCK.serialize(object.block)); JsonObject properties = new JsonObject(); for (Matcher matcher : object.properties) { properties.add(matcher.property().getName(), matcher.serialize()); @@ -89,8 +93,8 @@ public void serialize(BlockPropertiesPredicate object, JsonObject json) { } @Override - public BlockPropertiesPredicate fromNetwork(FriendlyByteBuf buffer) { - Block block = BuiltInRegistries.BLOCK.byId(buffer.readVarInt()); + public BlockPropertiesPredicate decode(FriendlyByteBuf buffer, TypedMap context) { + Block block = Loadables.BLOCK.decode(buffer); int size = buffer.readVarInt(); ImmutableList.Builder builder = ImmutableList.builder(); for (int i = 0; i < size; i++) { @@ -100,8 +104,8 @@ public BlockPropertiesPredicate fromNetwork(FriendlyByteBuf buffer) { } @Override - public void toNetwork(BlockPropertiesPredicate object, FriendlyByteBuf buffer) { - buffer.writeVarInt(BuiltInRegistries.BLOCK.getId(object.block)); + public void encode(FriendlyByteBuf buffer, BlockPropertiesPredicate object) { + Loadables.BLOCK.encode(buffer, object.block); buffer.writeVarInt(object.properties.size()); for (Matcher matcher : object.properties) { matcher.toNetwork(buffer); diff --git a/src/main/java/slimeknights/mantle/data/predicate/block/SetBlockPredicate.java b/src/main/java/slimeknights/mantle/data/predicate/block/SetBlockPredicate.java deleted file mode 100644 index cfc036d20..000000000 --- a/src/main/java/slimeknights/mantle/data/predicate/block/SetBlockPredicate.java +++ /dev/null @@ -1,30 +0,0 @@ -package slimeknights.mantle.data.predicate.block; - -import lombok.RequiredArgsConstructor; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.state.BlockState; -import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; -import slimeknights.mantle.data.loader.RegistrySetLoader; - -import java.util.Set; - -/** - * Modifier matching a block - */ -@RequiredArgsConstructor -public class SetBlockPredicate implements BlockPredicate { - public static final IGenericLoader LOADER = new RegistrySetLoader<>("blocks", BuiltInRegistries.BLOCK, SetBlockPredicate::new, predicate -> predicate.blocks); - - private final Set blocks; - - @Override - public boolean matches(BlockState state) { - return blocks.contains(state.getBlock()); - } - - @Override - public IGenericLoader getLoader() { - return LOADER; - } -} diff --git a/src/main/java/slimeknights/mantle/data/predicate/block/TagBlockPredicate.java b/src/main/java/slimeknights/mantle/data/predicate/block/TagBlockPredicate.java deleted file mode 100644 index 09d0aa512..000000000 --- a/src/main/java/slimeknights/mantle/data/predicate/block/TagBlockPredicate.java +++ /dev/null @@ -1,28 +0,0 @@ -package slimeknights.mantle.data.predicate.block; - -import lombok.RequiredArgsConstructor; -import net.minecraft.core.registries.Registries; -import net.minecraft.tags.TagKey; -import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.state.BlockState; -import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; -import slimeknights.mantle.data.loader.TagKeyLoader; - -/** - * Modifier matching a block tag - */ -@RequiredArgsConstructor -public class TagBlockPredicate implements BlockPredicate { - public static final TagKeyLoader LOADER = new TagKeyLoader<>(Registries.BLOCK, TagBlockPredicate::new, c -> c.tag); - private final TagKey tag; - - @Override - public boolean matches(BlockState state) { - return state.is(tag); - } - - @Override - public IGenericLoader getLoader() { - return LOADER; - } -} diff --git a/src/main/java/slimeknights/mantle/data/predicate/damage/DamageSourcePredicate.java b/src/main/java/slimeknights/mantle/data/predicate/damage/DamageSourcePredicate.java index 10e8e62bf..ff54332ad 100644 --- a/src/main/java/slimeknights/mantle/data/predicate/damage/DamageSourcePredicate.java +++ b/src/main/java/slimeknights/mantle/data/predicate/damage/DamageSourcePredicate.java @@ -3,14 +3,11 @@ import net.minecraft.tags.DamageTypeTags; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.damagesource.DamageTypes; -import slimeknights.mantle.data.registry.GenericLoaderRegistry; -import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; -import slimeknights.mantle.data.predicate.AndJsonPredicate; import slimeknights.mantle.data.predicate.IJsonPredicate; -import slimeknights.mantle.data.predicate.InvertedJsonPredicate; -import slimeknights.mantle.data.predicate.NestedJsonPredicateLoader; -import slimeknights.mantle.data.predicate.OrJsonPredicate; +import slimeknights.mantle.data.predicate.PredicateRegistry; +import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; +import java.util.List; import java.util.function.Predicate; import static slimeknights.mantle.data.registry.GenericLoaderRegistry.SingletonLoader.singleton; @@ -22,13 +19,7 @@ public interface DamageSourcePredicate extends IJsonPredicate { /** Predicate that matches all sources */ DamageSourcePredicate ANY = simple(source -> true); /** Loader for item predicates */ - GenericLoaderRegistry> LOADER = new GenericLoaderRegistry<>(ANY, true); - /** Loader for inverted conditions */ - InvertedJsonPredicate.Loader INVERTED = new InvertedJsonPredicate.Loader<>(LOADER, false); - /** Loader for and conditions */ - NestedJsonPredicateLoader> AND = AndJsonPredicate.createLoader(LOADER, INVERTED); - /** Loader for or conditions */ - NestedJsonPredicateLoader> OR = OrJsonPredicate.createLoader(LOADER, INVERTED); + PredicateRegistry LOADER = new PredicateRegistry<>("Damage Source Predicate", ANY); /* Vanilla getters */ DamageSourcePredicate PROJECTILE = simple(damageSource -> damageSource.is(DamageTypeTags.IS_PROJECTILE)); @@ -37,12 +28,13 @@ public interface DamageSourcePredicate extends IJsonPredicate { DamageSourcePredicate DAMAGE_HELMET = simple(damageSource -> damageSource.is(DamageTypeTags.DAMAGES_HELMET)); DamageSourcePredicate BYPASS_INVULNERABLE = simple(damageSource -> damageSource.is(DamageTypeTags.BYPASSES_INVULNERABILITY)); DamageSourcePredicate BYPASS_MAGIC = simple(damageSource -> damageSource.is(DamageTypeTags.BYPASSES_EFFECTS)); + DamageSourcePredicate BYPASS_ENCHANTMENTS = simple(DamageSource::isBypassEnchantments); DamageSourcePredicate FIRE = simple(damageSource -> damageSource.is(DamageTypeTags.IS_FIRE)); DamageSourcePredicate MAGIC = simple(damageSource -> damageSource.is(DamageTypes.MAGIC)); DamageSourcePredicate FALL = simple(damageSource -> damageSource.is(DamageTypeTags.IS_FALL)); /** Damage that protection works against */ - DamageSourcePredicate CAN_PROTECT = simple(source -> !source.is(DamageTypeTags.BYPASSES_EFFECTS) && !source.is(DamageTypeTags.BYPASSES_INVULNERABILITY)); + DamageSourcePredicate CAN_PROTECT = simple(source -> !source.is(DamageTypeTags.BYPASSES_EFFECTS) && !source.isBypassEnchantments() && !source.is(DamageTypeTags.BYPASSES_INVULNERABILITY)); /** Custom concept: damage dealt by non-projectile entities */ DamageSourcePredicate MELEE = simple(source -> { if (source.is(DamageTypeTags.IS_PROJECTILE)) { @@ -61,7 +53,7 @@ public interface DamageSourcePredicate extends IJsonPredicate { @Override default IJsonPredicate inverted() { - return INVERTED.create(this); + return LOADER.invert(this); } /** Creates a simple predicate with no parameters */ @@ -73,9 +65,24 @@ public boolean matches(DamageSource source) { } @Override - public IGenericLoader> getLoader() { + public IGenericLoader getLoader() { return loader; } }); } + + + /* Helper methods */ + + /** Creates an and predicate */ + @SafeVarargs + static IJsonPredicate and(IJsonPredicate... predicates) { + return LOADER.and(List.of(predicates)); + } + + /** Creates an or predicate */ + @SafeVarargs + static IJsonPredicate or(IJsonPredicate... predicates) { + return LOADER.or(List.of(predicates)); + } } diff --git a/src/main/java/slimeknights/mantle/data/predicate/damage/SourceAttackerPredicate.java b/src/main/java/slimeknights/mantle/data/predicate/damage/SourceAttackerPredicate.java index b071e0676..3704f268b 100644 --- a/src/main/java/slimeknights/mantle/data/predicate/damage/SourceAttackerPredicate.java +++ b/src/main/java/slimeknights/mantle/data/predicate/damage/SourceAttackerPredicate.java @@ -2,16 +2,16 @@ import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.LivingEntity; -import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; -import slimeknights.mantle.data.loader.NestedLoader; +import slimeknights.mantle.data.loadable.record.RecordLoadable; import slimeknights.mantle.data.predicate.IJsonPredicate; import slimeknights.mantle.data.predicate.entity.LivingEntityPredicate; +import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; /** * Predicate that checks for properties of the attacker in a damage source */ public record SourceAttackerPredicate(IJsonPredicate attacker) implements DamageSourcePredicate { - public static final IGenericLoader LOADER = new NestedLoader<>("entity_type", LivingEntityPredicate.LOADER, SourceAttackerPredicate::new, SourceAttackerPredicate::attacker); + public static final RecordLoadable LOADER = RecordLoadable.create(LivingEntityPredicate.LOADER.directField("entity_type", SourceAttackerPredicate::attacker), SourceAttackerPredicate::new); @Override public boolean matches(DamageSource source) { @@ -19,7 +19,7 @@ public boolean matches(DamageSource source) { } @Override - public IGenericLoader> getLoader() { + public IGenericLoader getLoader() { return LOADER; } } diff --git a/src/main/java/slimeknights/mantle/data/predicate/damage/SourceMessagePredicate.java b/src/main/java/slimeknights/mantle/data/predicate/damage/SourceMessagePredicate.java index 5e2b3db3a..1f060df27 100644 --- a/src/main/java/slimeknights/mantle/data/predicate/damage/SourceMessagePredicate.java +++ b/src/main/java/slimeknights/mantle/data/predicate/damage/SourceMessagePredicate.java @@ -1,13 +1,13 @@ package slimeknights.mantle.data.predicate.damage; import net.minecraft.world.damagesource.DamageSource; +import slimeknights.mantle.data.loadable.primitive.StringLoadable; +import slimeknights.mantle.data.loadable.record.RecordLoadable; import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; -import slimeknights.mantle.data.loader.StringLoader; -import slimeknights.mantle.data.predicate.IJsonPredicate; /** Predicate that matches a named source */ public record SourceMessagePredicate(String message) implements DamageSourcePredicate { - public static final IGenericLoader LOADER = new StringLoader<>("message", SourceMessagePredicate::new, SourceMessagePredicate::message); + public static final RecordLoadable LOADER = RecordLoadable.create(StringLoadable.DEFAULT.requiredField("message", SourceMessagePredicate::message), SourceMessagePredicate::new); public SourceMessagePredicate(DamageSource source) { this(source.getMsgId()); @@ -19,7 +19,7 @@ public boolean matches(DamageSource source) { } @Override - public IGenericLoader> getLoader() { + public IGenericLoader getLoader() { return LOADER; } } diff --git a/src/main/java/slimeknights/mantle/data/predicate/entity/EntitySetPredicate.java b/src/main/java/slimeknights/mantle/data/predicate/entity/EntitySetPredicate.java deleted file mode 100644 index 88920617c..000000000 --- a/src/main/java/slimeknights/mantle/data/predicate/entity/EntitySetPredicate.java +++ /dev/null @@ -1,26 +0,0 @@ -package slimeknights.mantle.data.predicate.entity; - -import net.minecraft.core.Registry; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.world.entity.EntityType; -import net.minecraft.world.entity.LivingEntity; -import slimeknights.mantle.data.loader.RegistrySetLoader; -import slimeknights.mantle.data.predicate.IJsonPredicate; -import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; - -import java.util.Set; - -/** Predicate matching entities from a set */ -public record EntitySetPredicate(Set> entities) implements LivingEntityPredicate { - public static final IGenericLoader LOADER = new RegistrySetLoader<>("entities", Registry.ENTITY_TYPE, EntitySetPredicate::new, EntitySetPredicate::entities); - - @Override - public boolean matches(LivingEntity entity) { - return entities.contains(entity.getType()); - } - - @Override - public IGenericLoader> getLoader() { - return LOADER; - } -} diff --git a/src/main/java/slimeknights/mantle/data/predicate/entity/HasEnchantmentEntityPredicate.java b/src/main/java/slimeknights/mantle/data/predicate/entity/HasEnchantmentEntityPredicate.java index 5a9c50cf8..2a6666a9a 100644 --- a/src/main/java/slimeknights/mantle/data/predicate/entity/HasEnchantmentEntityPredicate.java +++ b/src/main/java/slimeknights/mantle/data/predicate/entity/HasEnchantmentEntityPredicate.java @@ -4,15 +4,15 @@ import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.item.enchantment.Enchantment; import net.minecraft.world.item.enchantment.EnchantmentHelper; -import slimeknights.mantle.data.loader.RegistryEntryLoader; -import slimeknights.mantle.data.predicate.IJsonPredicate; +import slimeknights.mantle.data.loadable.Loadables; +import slimeknights.mantle.data.loadable.record.RecordLoadable; import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; /** * Predicate that checks if the given entity has the given enchantment on any of their equipment */ public record HasEnchantmentEntityPredicate(Enchantment enchantment) implements LivingEntityPredicate { - public static final IGenericLoader LOADER = new RegistryEntryLoader<>("enchantment", BuiltInRegistries.ENCHANTMENT, HasEnchantmentEntityPredicate::new, HasEnchantmentEntityPredicate::enchantment); + public static final RecordLoadable LOADER = RecordLoadable.create(Loadables.ENCHANTMENT.requiredField("enchantment", HasEnchantmentEntityPredicate::enchantment), HasEnchantmentEntityPredicate::new); @Override public boolean matches(LivingEntity entity) { @@ -20,7 +20,7 @@ public boolean matches(LivingEntity entity) { } @Override - public IGenericLoader> getLoader() { + public IGenericLoader getLoader() { return LOADER; } } diff --git a/src/main/java/slimeknights/mantle/data/predicate/entity/LivingEntityPredicate.java b/src/main/java/slimeknights/mantle/data/predicate/entity/LivingEntityPredicate.java index a153cf098..32f51ece2 100644 --- a/src/main/java/slimeknights/mantle/data/predicate/entity/LivingEntityPredicate.java +++ b/src/main/java/slimeknights/mantle/data/predicate/entity/LivingEntityPredicate.java @@ -1,38 +1,33 @@ package slimeknights.mantle.data.predicate.entity; +import com.google.common.collect.ImmutableSet; +import net.minecraft.tags.TagKey; import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.LivingEntity; -import slimeknights.mantle.data.predicate.AndJsonPredicate; +import slimeknights.mantle.data.loadable.Loadables; import slimeknights.mantle.data.predicate.IJsonPredicate; -import slimeknights.mantle.data.predicate.InvertedJsonPredicate; -import slimeknights.mantle.data.predicate.NestedJsonPredicateLoader; -import slimeknights.mantle.data.predicate.OrJsonPredicate; -import slimeknights.mantle.data.registry.GenericLoaderRegistry; +import slimeknights.mantle.data.predicate.RegistryPredicateRegistry; import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; import slimeknights.mantle.data.registry.GenericLoaderRegistry.SingletonLoader; +import java.util.List; import java.util.function.Predicate; /** Predicate matching an entity */ public interface LivingEntityPredicate extends IJsonPredicate { /** Predicate that matches all entities */ LivingEntityPredicate ANY = simple(entity -> true); - /** Loader for block state predicates */ - GenericLoaderRegistry> LOADER = new GenericLoaderRegistry<>(ANY, true); - /** Loader for inverted conditions */ - InvertedJsonPredicate.Loader INVERTED = new InvertedJsonPredicate.Loader<>(LOADER); - /** Loader for and conditions */ - NestedJsonPredicateLoader> AND = AndJsonPredicate.createLoader(LOADER, INVERTED); - /** Loader for or conditions */ - NestedJsonPredicateLoader> OR = OrJsonPredicate.createLoader(LOADER, INVERTED); + RegistryPredicateRegistry,LivingEntity> LOADER = new RegistryPredicateRegistry<>("Entity Predicate", ANY, Loadables.ENTITY_TYPE, Entity::getType, "entities", Loadables.ENTITY_TYPE_TAG, (tag, entity) -> entity.getType().is(tag)); /** Gets an inverted condition */ @Override default IJsonPredicate inverted() { - return INVERTED.create(this); + return LOADER.invert(this); } + /* Singletons */ /** Predicate that matches water sensitive entities */ @@ -65,9 +60,34 @@ public boolean matches(LivingEntity entity) { } @Override - public IGenericLoader> getLoader() { + public IGenericLoader getLoader() { return loader; } }); } + + + /* Helper methods */ + + /** Creates an entity set predicate */ + static IJsonPredicate set(EntityType... types) { + return LOADER.setOf(ImmutableSet.copyOf(types)); + } + + /** Creates a tag predicate */ + static IJsonPredicate tag(TagKey> tag) { + return LOADER.tag(tag); + } + + /** Creates an and predicate */ + @SafeVarargs + static IJsonPredicate and(IJsonPredicate... predicates) { + return LOADER.and(List.of(predicates)); + } + + /** Creates an or predicate */ + @SafeVarargs + static IJsonPredicate or(IJsonPredicate... predicates) { + return LOADER.or(List.of(predicates)); + } } diff --git a/src/main/java/slimeknights/mantle/data/predicate/entity/MobTypePredicate.java b/src/main/java/slimeknights/mantle/data/predicate/entity/MobTypePredicate.java index 61ee91327..0310d519b 100644 --- a/src/main/java/slimeknights/mantle/data/predicate/entity/MobTypePredicate.java +++ b/src/main/java/slimeknights/mantle/data/predicate/entity/MobTypePredicate.java @@ -1,23 +1,20 @@ package slimeknights.mantle.data.predicate.entity; -import com.google.gson.JsonObject; -import lombok.RequiredArgsConstructor; -import net.minecraft.network.FriendlyByteBuf; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.MobType; +import slimeknights.mantle.data.loadable.record.RecordLoadable; import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; import slimeknights.mantle.data.registry.NamedComponentRegistry; /** Predicate matching a specific mob type */ -@RequiredArgsConstructor -public class MobTypePredicate implements LivingEntityPredicate { +public record MobTypePredicate(MobType type) implements LivingEntityPredicate { /** * Registry of mob types, to allow addons to register types * TODO: support registering via IMC */ public static final NamedComponentRegistry MOB_TYPES = new NamedComponentRegistry<>("Unknown mob type"); - - private final MobType type; + /** Loader for a mob type predicate */ + public static RecordLoadable LOADER = RecordLoadable.create(MOB_TYPES.requiredField("mobs", MobTypePredicate::type), MobTypePredicate::new); @Override public boolean matches(LivingEntity input) { @@ -28,27 +25,4 @@ public boolean matches(LivingEntity input) { public IGenericLoader getLoader() { return LOADER; } - - /** Loader for a mob type predicate */ - public static final IGenericLoader LOADER = new IGenericLoader<>() { - @Override - public MobTypePredicate deserialize(JsonObject json) { - return new MobTypePredicate(MOB_TYPES.deserialize(json, "mobs")); - } - - @Override - public MobTypePredicate fromNetwork(FriendlyByteBuf buffer) { - return new MobTypePredicate(MOB_TYPES.fromNetwork(buffer)); - } - - @Override - public void serialize(MobTypePredicate object, JsonObject json) { - json.addProperty("mobs", MOB_TYPES.getKey(object.type).toString()); - } - - @Override - public void toNetwork(MobTypePredicate object, FriendlyByteBuf buffer) { - MOB_TYPES.toNetwork(object.type, buffer); - } - }; } diff --git a/src/main/java/slimeknights/mantle/data/predicate/entity/TagEntityPredicate.java b/src/main/java/slimeknights/mantle/data/predicate/entity/TagEntityPredicate.java deleted file mode 100644 index c8e392b57..000000000 --- a/src/main/java/slimeknights/mantle/data/predicate/entity/TagEntityPredicate.java +++ /dev/null @@ -1,29 +0,0 @@ -package slimeknights.mantle.data.predicate.entity; - -import lombok.RequiredArgsConstructor; -import net.minecraft.core.registries.Registries; -import net.minecraft.tags.TagKey; -import net.minecraft.world.entity.EntityType; -import net.minecraft.world.entity.LivingEntity; -import slimeknights.mantle.data.loader.TagKeyLoader; -import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; - -/** - * Predicate matching an entity tag - */ -@RequiredArgsConstructor -public class TagEntityPredicate implements LivingEntityPredicate { - public static final TagKeyLoader,TagEntityPredicate> LOADER = new TagKeyLoader<>(Registries.ENTITY_TYPE, TagEntityPredicate::new, c -> c.tag); - - private final TagKey> tag; - - @Override - public boolean matches(LivingEntity entity) { - return entity.getType().is(tag); - } - - @Override - public IGenericLoader getLoader() { - return LOADER; - } -} diff --git a/src/main/java/slimeknights/mantle/data/predicate/item/ItemPredicate.java b/src/main/java/slimeknights/mantle/data/predicate/item/ItemPredicate.java index 8704053ab..eb96cf894 100644 --- a/src/main/java/slimeknights/mantle/data/predicate/item/ItemPredicate.java +++ b/src/main/java/slimeknights/mantle/data/predicate/item/ItemPredicate.java @@ -1,15 +1,17 @@ package slimeknights.mantle.data.predicate.item; +import com.google.common.collect.ImmutableSet; +import net.minecraft.tags.TagKey; import net.minecraft.world.item.Item; -import slimeknights.mantle.data.registry.GenericLoaderRegistry; +import slimeknights.mantle.data.loadable.Loadables; +import slimeknights.mantle.data.predicate.IJsonPredicate; +import slimeknights.mantle.data.predicate.RegistryPredicateRegistry; import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; import slimeknights.mantle.data.registry.GenericLoaderRegistry.SingletonLoader; -import slimeknights.mantle.data.predicate.AndJsonPredicate; -import slimeknights.mantle.data.predicate.IJsonPredicate; -import slimeknights.mantle.data.predicate.InvertedJsonPredicate; -import slimeknights.mantle.data.predicate.NestedJsonPredicateLoader; -import slimeknights.mantle.data.predicate.OrJsonPredicate; +import slimeknights.mantle.util.RegistryHelper; +import java.util.List; +import java.util.function.Function; import java.util.function.Predicate; /** Simple serializable item predicate */ @@ -17,18 +19,11 @@ public interface ItemPredicate extends IJsonPredicate { /** Predicate that matches all items */ ItemPredicate ANY = simple(item -> true); /** Loader for item predicates */ - GenericLoaderRegistry> LOADER = new GenericLoaderRegistry<>(ANY, true); - /** Loader for inverted conditions */ - InvertedJsonPredicate.Loader INVERTED = new InvertedJsonPredicate.Loader<>(LOADER); - /** Loader for and conditions */ - NestedJsonPredicateLoader> AND = AndJsonPredicate.createLoader(LOADER, INVERTED); - /** Loader for or conditions */ - NestedJsonPredicateLoader> OR = OrJsonPredicate.createLoader(LOADER, INVERTED); - + RegistryPredicateRegistry LOADER = new RegistryPredicateRegistry<>("Item Predicate", ANY, Loadables.ITEM, Function.identity(), "items", Loadables.ITEM_TAG, RegistryHelper::contains); @Override default IJsonPredicate inverted() { - return INVERTED.create(this); + return LOADER.invert(this); } /** Creates a new predicate singleton */ @@ -40,9 +35,34 @@ public boolean matches(Item item) { } @Override - public IGenericLoader> getLoader() { + public IGenericLoader getLoader() { return loader; } }); } + + + /* Helper methods */ + + /** Creates am item set predicate */ + static IJsonPredicate set(Item... items) { + return LOADER.setOf(ImmutableSet.copyOf(items)); + } + + /** Creates a tag predicate */ + static IJsonPredicate tag(TagKey tag) { + return LOADER.tag(tag); + } + + /** Creates an and predicate */ + @SafeVarargs + static IJsonPredicate and(IJsonPredicate... predicates) { + return LOADER.and(List.of(predicates)); + } + + /** Creates an or predicate */ + @SafeVarargs + static IJsonPredicate or(IJsonPredicate... predicates) { + return LOADER.or(List.of(predicates)); + } } diff --git a/src/main/java/slimeknights/mantle/data/predicate/item/ItemSetPredicate.java b/src/main/java/slimeknights/mantle/data/predicate/item/ItemSetPredicate.java deleted file mode 100644 index e179a66cb..000000000 --- a/src/main/java/slimeknights/mantle/data/predicate/item/ItemSetPredicate.java +++ /dev/null @@ -1,28 +0,0 @@ -package slimeknights.mantle.data.predicate.item; - -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.world.item.Item; -import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; -import slimeknights.mantle.data.loader.RegistrySetLoader; -import slimeknights.mantle.data.predicate.IJsonPredicate; - -import java.util.Set; - -/** Predicate matching an item from a set */ -public record ItemSetPredicate(Set items) implements ItemPredicate { - public static final IGenericLoader LOADER = new RegistrySetLoader<>("items", BuiltInRegistries.ITEM, ItemSetPredicate::new, predicate -> predicate.items); - - public ItemSetPredicate(Item item) { - this(Set.of(item)); - } - - @Override - public boolean matches(Item item) { - return items.contains(item); - } - - @Override - public IGenericLoader> getLoader() { - return LOADER; - } -} diff --git a/src/main/java/slimeknights/mantle/data/predicate/item/ItemTagPredicate.java b/src/main/java/slimeknights/mantle/data/predicate/item/ItemTagPredicate.java deleted file mode 100644 index fbc900e85..000000000 --- a/src/main/java/slimeknights/mantle/data/predicate/item/ItemTagPredicate.java +++ /dev/null @@ -1,24 +0,0 @@ -package slimeknights.mantle.data.predicate.item; - -import net.minecraft.core.registries.Registries; -import net.minecraft.tags.TagKey; -import net.minecraft.world.item.Item; -import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; -import slimeknights.mantle.data.loader.TagKeyLoader; -import slimeknights.mantle.data.predicate.IJsonPredicate; -import slimeknights.mantle.util.RegistryHelper; - -/** Predicate matching an item tag */ -public record ItemTagPredicate(TagKey tag) implements ItemPredicate { - public static final TagKeyLoader LOADER = new TagKeyLoader<>(Registries.ITEM, ItemTagPredicate::new, c -> c.tag); - - @Override - public boolean matches(Item item) { - return RegistryHelper.contains(tag, item); - } - - @Override - public IGenericLoader> getLoader() { - return LOADER; - } -} diff --git a/src/main/java/slimeknights/mantle/data/registry/AbstractNamedComponentRegistry.java b/src/main/java/slimeknights/mantle/data/registry/AbstractNamedComponentRegistry.java new file mode 100644 index 000000000..90933e173 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/registry/AbstractNamedComponentRegistry.java @@ -0,0 +1,126 @@ +package slimeknights.mantle.data.registry; + +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; +import io.netty.handler.codec.DecoderException; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import slimeknights.mantle.data.loadable.field.LoadableField; +import slimeknights.mantle.data.loadable.primitive.ResourceLocationLoadable; + +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.function.Function; + +/** Shared logic for registries that map a resource location to an object. */ +public abstract class AbstractNamedComponentRegistry implements ResourceLocationLoadable { + /** Name to make exceptions clearer */ + protected final String errorText; + + public AbstractNamedComponentRegistry(String errorText) { + this.errorText = errorText + " "; + } + + /** Gets a value or null if missing */ + @Nullable + public abstract T getValue(ResourceLocation name); + + /** Gets all keys registered */ + public abstract Collection getKeys(); + + /** Gets all keys registered */ + public abstract Collection getValues(); + + + /* Json */ + + @Override + public T fromKey(ResourceLocation name, String key) { + T value = getValue(name); + if (value != null) { + return value; + } + throw new JsonSyntaxException(errorText + name + " at '" + key + '\''); + } + + + /* Network */ + + /** Writes the value to the buffer */ + @Override + public void encode(FriendlyByteBuf buffer, T value) { + buffer.writeResourceLocation(getKey(value)); + } + + /** Writes the value to the buffer */ + public void encodeOptional(FriendlyByteBuf buffer, @Nullable T value) { + // if null, just write an empty string, that is not a valid resource location anyways and saves us a byte + if (value != null) { + buffer.writeUtf(getKey(value).toString()); + } else { + buffer.writeUtf(""); + } + } + + /** Reads the given value from the network by resource location */ + private T decodeInternal(ResourceLocation name) { + T value = getValue(name); + if (value == null) { + throw new DecoderException(errorText + name); + } + return value; + } + + /** Parse the value from JSON */ + @Override + public T decode(FriendlyByteBuf buffer) { + return decodeInternal(buffer.readResourceLocation()); + } + + /** Parse the value from JSON */ + @Nullable + public T decodeOptional(FriendlyByteBuf buffer) { + // empty string is not a valid resource location, so its a nice value to use for null, saves us a byte + String key = buffer.readUtf(Short.MAX_VALUE); + if (key.isEmpty()) { + return null; + } + return decodeInternal(new ResourceLocation(key)); + } + + + /* Fields */ + + @Override + public

LoadableField nullableField(String key, Function getter) { + return new NullableField<>(this, key, getter); + } + + /** Custom implementation of nullable field using our networking optional logic */ + private record NullableField(AbstractNamedComponentRegistry registry, String key, Function getter) implements LoadableField { + @Nullable + @Override + public T get(JsonObject json) { + return registry.getOrDefault(json, key, null); + } + + @Override + public void serialize(P parent, JsonObject json) { + T object = getter.apply(parent); + if (object != null) { + json.add(key, registry.serialize(object)); + } + } + + @Nullable + @Override + public T decode(FriendlyByteBuf buffer) { + return registry.decodeOptional(buffer); + } + + @Override + public void encode(FriendlyByteBuf buffer, P parent) { + registry.encodeOptional(buffer, getter.apply(parent)); + } + } +} diff --git a/src/main/java/slimeknights/mantle/data/registry/DefaultingLoaderRegistry.java b/src/main/java/slimeknights/mantle/data/registry/DefaultingLoaderRegistry.java new file mode 100644 index 000000000..e5e1c5057 --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/registry/DefaultingLoaderRegistry.java @@ -0,0 +1,103 @@ +package slimeknights.mantle.data.registry; + +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import net.minecraft.network.FriendlyByteBuf; +import slimeknights.mantle.data.loadable.field.DefaultingField; +import slimeknights.mantle.data.loadable.field.LoadableField; +import slimeknights.mantle.data.registry.GenericLoaderRegistry.IHaveLoader; + +import java.lang.reflect.Type; +import java.util.function.Function; + +/** + * Extension of {@link GenericLoaderRegistry} with a default instance used for null or missing fields. + * @param + */ +public class DefaultingLoaderRegistry extends GenericLoaderRegistry { + /** Default instance, used for null values instead of null */ + private final T defaultInstance; + public DefaultingLoaderRegistry(String name, T defaultInstance, boolean compact) { + super(name, compact); + this.defaultInstance = defaultInstance; + } + + /** Gets the default value in this registry */ + public T getDefault() { + return defaultInstance; + } + + + /* Default in JSON */ + + @Override + public T convert(JsonElement element, String key) { + if (element.isJsonNull()) { + return defaultInstance; + } + return super.convert(element, key); + } + + @Override + public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context) { + if (src == defaultInstance) { + return JsonNull.INSTANCE; + } + return serialize(src); + } + + /** + * Gets then deserializes the given field, or returns the default value if missing. + * @param parent Parent to fetch field from + * @param key Field to get + * @return Value or default. + */ + public T getOrDefault(JsonObject parent, String key) { + return super.getOrDefault(parent, key, defaultInstance); + } + + + /* Default in network */ + + @SuppressWarnings("unchecked") // the cast is safe here as its just doing a map lookup, shouldn't cause harm if it fails. Besides, the loader has to extend T to work + @Override + public void encode(FriendlyByteBuf buffer, T src) { + if (src == defaultInstance) { + loaders.encodeOptional(buffer, null); + return; + } + loaders.encodeOptional(buffer, (IGenericLoader)src.getLoader()); + toNetwork(src.getLoader(), src, buffer); + } + + @Override + public T decode(FriendlyByteBuf buffer) { + IGenericLoader loader = loaders.decodeOptional(buffer); + if (loader == null) { + return defaultInstance; + } + return loader.fromNetwork(buffer); + } + + + /* Defaulting fields */ + + /** + * Creates a defaulting for this registry, using the internal default instance as the default + * @param key Json key + * @param serializeDefault If true, serializes the default instance. If false skips it + * @param getter Getter function + * @param

Field target + * @return Defaulting field instance + */ + public

LoadableField defaultField(String key, boolean serializeDefault, Function getter) { + return new DefaultingField<>(this, key, defaultInstance, serializeDefault, getter); + } + + /** Creates a defaulting field that does not serialize */ + public

LoadableField defaultField(String key, Function getter) { + return defaultField(key, false, getter); + } +} diff --git a/src/main/java/slimeknights/mantle/data/registry/DirectRegistryField.java b/src/main/java/slimeknights/mantle/data/registry/DirectRegistryField.java new file mode 100644 index 000000000..0bc5ba6cc --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/registry/DirectRegistryField.java @@ -0,0 +1,47 @@ +package slimeknights.mantle.data.registry; + +import com.google.gson.JsonElement; +import com.google.gson.JsonIOException; +import com.google.gson.JsonObject; +import slimeknights.mantle.data.loadable.Loadable; +import slimeknights.mantle.data.loadable.field.AlwaysPresentLoadableField; +import slimeknights.mantle.data.registry.GenericLoaderRegistry.IHaveLoader; + +import java.util.Map.Entry; +import java.util.function.Function; + +/** Direct field for a registry, leaves type unchanged */ +public record DirectRegistryField(Loadable loadable, Function getter) implements AlwaysPresentLoadableField { + /** + * Serializes the passed object into the passed JSON + * @param json JSON target for serializing + * @param loader Loader for serializing the value + * @param value Value to serialized + * @param Type of value + */ + public static void serializeInto(JsonObject json, Loadable loader, N value) { + JsonElement element = loader.serialize(value); + // if its an object, copy all the data over + if (element.isJsonObject()) { + JsonObject nestedObject = element.getAsJsonObject(); + for (Entry entry : nestedObject.entrySet()) { + json.add(entry.getKey(), entry.getValue()); + } + } else if (element.isJsonPrimitive()){ + // if its a primitive, its the type ID, add just that by itself + json.add("type", element); + } else { + throw new JsonIOException("Unable to serialize nested object, expected string or object"); + } + } + + @Override + public T get(JsonObject json) { + return loadable.convert(json, "[unknown]"); + } + + @Override + public void serialize(P parent, JsonObject json) { + serializeInto(json, loadable, getter.apply(parent)); + } +} diff --git a/src/main/java/slimeknights/mantle/data/registry/GenericLoaderRegistry.java b/src/main/java/slimeknights/mantle/data/registry/GenericLoaderRegistry.java index 4c7ab5d48..2402ee63c 100644 --- a/src/main/java/slimeknights/mantle/data/registry/GenericLoaderRegistry.java +++ b/src/main/java/slimeknights/mantle/data/registry/GenericLoaderRegistry.java @@ -1,57 +1,43 @@ package slimeknights.mantle.data.registry; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; -import com.google.gson.JsonNull; import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; import com.google.gson.JsonPrimitive; -import com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; import com.google.gson.JsonSyntaxException; import lombok.Getter; import lombok.RequiredArgsConstructor; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.resources.ResourceLocation; -import net.minecraft.util.GsonHelper; import slimeknights.mantle.data.gson.GenericRegisteredSerializer; +import slimeknights.mantle.data.loadable.Loadable; +import slimeknights.mantle.data.loadable.field.LoadableField; import slimeknights.mantle.data.registry.GenericLoaderRegistry.IHaveLoader; -import slimeknights.mantle.util.JsonHelper; -import javax.annotation.Nullable; -import java.lang.reflect.Type; import java.util.function.Function; /** * Generic registry for an object that can both be sent over a friendly byte buffer and serialized into JSON. * @param Type of the serializable object * @see GenericRegisteredSerializer GenericRegisteredSerializer for an alternative that does not need to handle network syncing + * @see DefaultingLoaderRegistry */ -@RequiredArgsConstructor -public class GenericLoaderRegistry> implements JsonSerializer, JsonDeserializer { +@SuppressWarnings("unused") // API +public class GenericLoaderRegistry implements Loadable { /** Empty object instance for compact deserialization */ - private static final JsonObject EMPTY_OBJECT = new JsonObject(); - /** Map of all serializers for implementations */ - private final NamedComponentRegistry> loaders = new NamedComponentRegistry<>("Unknown loader"); - + protected static final JsonObject EMPTY_OBJECT = new JsonObject(); - /** Default instance, used for null values instead of null */ - @Nullable - private final T defaultInstance; + /** Display name for this registry */ + @Getter + private final String name; + /** Map of all serializers for implementations */ + protected final NamedComponentRegistry> loaders; /** If true, single key serializations will not use a JSON object to serialize, ideal for loaders with many singletons */ - private final boolean compact; - - public GenericLoaderRegistry(T defaultInstance) { - this(defaultInstance, false); - } - - public GenericLoaderRegistry(boolean compact) { - this(null, compact); - } + protected final boolean compact; - public GenericLoaderRegistry() { - this(null, false); + public GenericLoaderRegistry(String name, boolean compact) { + this.name = name; + this.compact = compact; + this.loaders = new NamedComponentRegistry<>("Unknown " + name + " loader"); } /** Registers a deserializer by name */ @@ -59,60 +45,40 @@ public void register(ResourceLocation name, IGenericLoader loader) loaders.register(name, loader); } - /** - * Deserializes the object from JSON - * @param element JSON element - * @return Deserialized object - */ - public T deserialize(JsonElement element) { - if (defaultInstance != null && element.isJsonNull()) { - return defaultInstance; - } + @Override + public T convert(JsonElement element, String key) { + // first try object if (element.isJsonObject()) { JsonObject object = element.getAsJsonObject(); - return loaders.deserialize(object, "type").deserialize(object); + return loaders.getIfPresent(object, "type").deserialize(object); } - if (compact) { - if (element.isJsonPrimitive()) { - EMPTY_OBJECT.entrySet().clear(); - return loaders.convert(element, "type").deserialize(EMPTY_OBJECT); - } - throw new JsonSyntaxException("Invalid JSON for " + getClass().getSimpleName() + ", must be a JSON object or a string"); - } else { - throw new JsonSyntaxException("Invalid JSON for " + getClass().getSimpleName() + ", must be a JSON object"); + // try primitive if allowed + if (compact && element.isJsonPrimitive()) { + EMPTY_OBJECT.entrySet().clear(); + return loaders.convert(element, "type").deserialize(EMPTY_OBJECT); } + // neither? failed to parse + throw new JsonSyntaxException("Invalid " + name + " JSON at " + key + ", must be a JSON object" + (compact ? " or a string" : "")); } /** * Deserializes the object from JSON - * @param parent JSON object parent - * @param key Key in the parent + * @param element JSON element * @return Deserialized object */ - public T getAndDeserialize(JsonObject parent, String key) { - if (defaultInstance != null && !parent.has(key)) { - return defaultInstance; - } - if (compact) { - return deserialize(JsonHelper.getElement(parent, key)); - } - return deserialize(GsonHelper.getAsJsonObject(parent, key)); - } - - @Override - public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - return deserialize(json); + public T deserialize(JsonElement element) { + return convert(element, "[unknown]"); } /** Serializes the object to json, fighting generics */ @SuppressWarnings("unchecked") - private > JsonElement serialize(IGenericLoader loader, T src) { + private JsonElement serialize(IGenericLoader loader, T src) { JsonObject json = new JsonObject(); JsonElement type = new JsonPrimitive(loaders.getKey((IGenericLoader)loader).toString()); json.add("type", type); loader.serialize((L)src, json); if (json.get("type") != type) { - throw new IllegalStateException("Serializer " + type.getAsString() + " modified the type key, this is not allowed as it breaks deserialization"); + throw new IllegalStateException(name + " serializer " + type.getAsString() + " modified the type key, this is not allowed as it breaks deserialization"); } // nothing to serialize? use type directly if (compact && json.entrySet().size() == 1) { @@ -121,63 +87,59 @@ private > JsonElement serialize(IGenericLoader loade return json; } - /** Serializes the object to JSON */ + @Override public JsonElement serialize(T src) { return serialize(src.getLoader(), src); } - @Override - public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context) { - if (src == defaultInstance) { - return JsonNull.INSTANCE; - } - return serialize(src); - } - /** Writes the object to the network, fighting generics */ @SuppressWarnings("unchecked") - private > void toNetwork(IGenericLoader loader, T src, FriendlyByteBuf buffer) { + protected void toNetwork(IGenericLoader loader, T src, FriendlyByteBuf buffer) { loader.toNetwork((L)src, buffer); } - /** Writes the object to the network */ - public void toNetwork(T src, FriendlyByteBuf buffer) { - // if we have a default instance, reading the loader is optional - // if we match the default instance write no loader to save network space - if (defaultInstance != null) { - if (src == defaultInstance) { - loaders.toNetworkOptional(null, buffer); - return; - } - loaders.toNetworkOptional(src.getLoader(), buffer); - } else { - loaders.toNetwork(src.getLoader(), buffer); - } + @SuppressWarnings("unchecked") // the cast is safe here as its just doing a map lookup, shouldn't cause harm if it fails. Besides, the loader has to extend T to work + @Override + public void encode(FriendlyByteBuf buffer, T src) { + loaders.encode(buffer, (IGenericLoader)src.getLoader()); toNetwork(src.getLoader(), src, buffer); } - /** - * Reads the object from the buffer - * @param buffer Buffer instance - * @return Read object - */ + @Override + public T decode(FriendlyByteBuf buffer) { + return loaders.decode(buffer).fromNetwork(buffer); + } + + /** @deprecated use {@link #decode(FriendlyByteBuf)} */ + @Deprecated(forRemoval = true) + public void toNetwork(T src, FriendlyByteBuf buffer) { + encode(buffer, src); + } + + /** @deprecated use {@link #decode(FriendlyByteBuf)} */ + @Deprecated(forRemoval = true) public T fromNetwork(FriendlyByteBuf buffer) { - IGenericLoader loader; - // if we have a default instance, reading the loader is optional - // if missing, use default instance - if (defaultInstance != null) { - loader = loaders.fromNetworkOptional(buffer); - if (loader == null) { - return defaultInstance; - } - } else { - loader = loaders.fromNetwork(buffer); - } - return loader.fromNetwork(buffer); + return decode(buffer); + } + + /** Creates a field that loads this object directly into the parent JSON object, will conflict if the parent already has a type */ + public

LoadableField directField(Function getter) { + return new DirectRegistryField<>(this, getter); + } + + /** Creates a field that loads this object directly into the parent JSON object by mapping the type key */ + public

LoadableField directField(String typeKey, Function getter) { + return new MergingRegistryField<>(this, typeKey, getter); + } + + @Override + public String toString() { + return getClass().getName() + "('" + name + "')"; } - /** Interface for a loader */ - public interface IGenericLoader> { + /** @deprecated use {@link slimeknights.mantle.data.loadable.record.RecordLoadable}. Will fully replace it in 1.20. */ + @Deprecated + public interface IGenericLoader { /** Deserializes the object from json */ T deserialize(JsonObject json); @@ -191,15 +153,21 @@ public interface IGenericLoader> { void toNetwork(T object, FriendlyByteBuf buffer); } - /** Interface for an object with a loader */ - public interface IHaveLoader { - /** Gets the loader for the object */ - IGenericLoader getLoader(); + /** + * Interface for an object with a loader. + * TODO 1.20: replace with {@link slimeknights.mantle.data.loadable.IAmLoadable.Record} + */ + public interface IHaveLoader { + /** + * Gets the loader for the object. + * If you wish to suppress the deprecation warning, change the return type to {@link slimeknights.mantle.data.loadable.record.RecordLoadable}. + */ + IGenericLoader getLoader(); } /** Loader instance for an object with only a single implementation */ @RequiredArgsConstructor - public static class SingletonLoader> implements IGenericLoader { + public static class SingletonLoader implements IGenericLoader { @Getter private final T instance; @@ -225,7 +193,7 @@ public void serialize(T object, JsonObject json) {} public void toNetwork(T object, FriendlyByteBuf buffer) {} /** Helper to create a singleton object as an anonymous class */ - public static > T singleton(Function,T> instance) { + public static T singleton(Function,T> instance) { return new SingletonLoader<>(instance).getInstance(); } } diff --git a/src/main/java/slimeknights/mantle/data/registry/IdAwareComponentRegistry.java b/src/main/java/slimeknights/mantle/data/registry/IdAwareComponentRegistry.java new file mode 100644 index 000000000..ac48aa67e --- /dev/null +++ b/src/main/java/slimeknights/mantle/data/registry/IdAwareComponentRegistry.java @@ -0,0 +1,53 @@ +package slimeknights.mantle.data.registry; + +import net.minecraft.resources.ResourceLocation; +import slimeknights.mantle.registration.object.IdAwareObject; + +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * Generic registry of a {@link IdAwareObject}. + * @param Type of the component being registered. + */ +public class IdAwareComponentRegistry extends AbstractNamedComponentRegistry { + /** Registered box expansion types */ + private final Map values = new HashMap<>(); + + public IdAwareComponentRegistry(String errorText) { + super(errorText); + } + + /** Registers the value with the given name */ + public synchronized V register(V value) { + ResourceLocation name = value.getId(); + if (values.putIfAbsent(name, value) != null) { + throw new IllegalArgumentException("Duplicate registration " + name); + } + return value; + } + + /** Gets a value or null if missing */ + @Override + @Nullable + public T getValue(ResourceLocation name) { + return values.get(name); + } + + @Override + public ResourceLocation getKey(T object) { + return object.getId(); + } + + @Override + public Collection getKeys() { + return values.keySet(); + } + + @Override + public Collection getValues() { + return values.values(); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loader/NestedLoader.java b/src/main/java/slimeknights/mantle/data/registry/MergingRegistryField.java similarity index 59% rename from src/main/java/slimeknights/mantle/data/loader/NestedLoader.java rename to src/main/java/slimeknights/mantle/data/registry/MergingRegistryField.java index ec26896fa..2d9332887 100644 --- a/src/main/java/slimeknights/mantle/data/loader/NestedLoader.java +++ b/src/main/java/slimeknights/mantle/data/registry/MergingRegistryField.java @@ -1,41 +1,24 @@ -package slimeknights.mantle.data.loader; +package slimeknights.mantle.data.registry; import com.google.gson.JsonElement; import com.google.gson.JsonIOException; import com.google.gson.JsonObject; -import net.minecraft.network.FriendlyByteBuf; import net.minecraft.util.GsonHelper; -import slimeknights.mantle.data.registry.GenericLoaderRegistry; -import slimeknights.mantle.data.registry.GenericLoaderRegistry.IGenericLoader; +import slimeknights.mantle.data.loadable.Loadable; +import slimeknights.mantle.data.loadable.field.AlwaysPresentLoadableField; import slimeknights.mantle.data.registry.GenericLoaderRegistry.IHaveLoader; import java.util.Map.Entry; import java.util.function.Function; -/** - * Loader that loads from another loader - * @param Object being loaded - * @param Nested object type - */ -public record NestedLoader, N extends IHaveLoader>( - String typeKey, - GenericLoaderRegistry nestedLoader, - Function constructor, - Function getter -) implements IGenericLoader { +/** Direct field for a registry which maps the type to a different key to prevent conflict */ +public record MergingRegistryField(Loadable loadable, String typeKey, Function getter) implements AlwaysPresentLoadableField { /** Moves the passed type key to "type" */ public static void mapType(JsonObject json, String typeKey) { - // replace our type with the nested type, then run the nested loader json.addProperty("type", GsonHelper.getAsString(json, typeKey)); json.remove(typeKey); } - @Override - public T deserialize(JsonObject json) { - mapType(json, typeKey); - return constructor.apply(nestedLoader.deserialize(json)); - } - /** * Serializes the passed object into the passed JSON * @param json JSON target for serializing @@ -44,7 +27,7 @@ public T deserialize(JsonObject json) { * @param value Value to serialized * @param Type of value */ - public static > void serializeInto(JsonObject json, String typeKey, GenericLoaderRegistry loader, N value) { + public static void serializeInto(JsonObject json, String typeKey, Loadable loader, N value) { JsonElement element = loader.serialize(value); // if its an object, copy all the data over if (element.isJsonObject()) { @@ -68,17 +51,14 @@ public static > void serializeInto(JsonObject json, Str } @Override - public void serialize(T object, JsonObject json) { - serializeInto(json, typeKey, nestedLoader, getter.apply(object)); - } - - @Override - public T fromNetwork(FriendlyByteBuf buffer) { - return constructor.apply(nestedLoader.fromNetwork(buffer)); + public T get(JsonObject json) { + // replace our type with the nested type, then run the nested loader + mapType(json, typeKey); + return loadable.convert(json, "[unknown]"); } @Override - public void toNetwork(T object, FriendlyByteBuf buffer) { - nestedLoader.toNetwork(getter.apply(object), buffer); + public void serialize(P parent, JsonObject json) { + serializeInto(json, typeKey, loadable, getter.apply(parent)); } } diff --git a/src/main/java/slimeknights/mantle/data/registry/NamedComponentRegistry.java b/src/main/java/slimeknights/mantle/data/registry/NamedComponentRegistry.java index 0cb295eb6..953468444 100644 --- a/src/main/java/slimeknights/mantle/data/registry/NamedComponentRegistry.java +++ b/src/main/java/slimeknights/mantle/data/registry/NamedComponentRegistry.java @@ -2,39 +2,32 @@ import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonSyntaxException; -import io.netty.handler.codec.DecoderException; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.resources.ResourceLocation; -import slimeknights.mantle.util.JsonHelper; import javax.annotation.Nullable; +import java.util.Collection; /** * Generic registry of a component named by a resource location. Supports any arbitrary object without making any changes to it. * @param Type of the component being registered. */ -public class NamedComponentRegistry { +public class NamedComponentRegistry extends AbstractNamedComponentRegistry { /** Registered box expansion types */ private final BiMap values = HashBiMap.create(); - /** Name to make exceptions clearer */ - private final String errorText; - public NamedComponentRegistry(String errorText) { - this.errorText = errorText + " "; + super(errorText); } /** Registers the value with the given name */ - public V register(ResourceLocation name, V value) { + public synchronized V register(ResourceLocation name, V value) { if (values.putIfAbsent(name, value) != null) { throw new IllegalArgumentException("Duplicate registration " + name); } return value; } - /** Gets a value or null if missing */ + @Override @Nullable public T getValue(ResourceLocation name) { return values.get(name); @@ -46,7 +39,7 @@ public ResourceLocation getOptionalKey(T value) { return values.inverse().get(value); } - /** Gets the key associated with a value */ + @Override public ResourceLocation getKey(T value) { ResourceLocation key = getOptionalKey(value); if (key == null) { @@ -55,68 +48,41 @@ public ResourceLocation getKey(T value) { return key; } - - /* Json */ - - /** Shared logic for deserialize */ - private T deserialize(ResourceLocation name) { - T value = getValue(name); - if (value == null) { - throw new JsonSyntaxException(errorText + name); - } - return value; - } - - /** Parse the value from JSON */ - public T convert(JsonElement element, String key) { - return deserialize(JsonHelper.convertToResourceLocation(element, key)); + @Override + public Collection getKeys() { + return values.keySet(); } - /** Parse the value from JSON */ - public T deserialize(JsonObject parent, String key) { - return deserialize(JsonHelper.getResourceLocation(parent, key)); + @Override + public Collection getValues() { + return values.values(); } - /* Network */ + /* Deprecated aliases */ - /** Writes the value to the buffer */ - public void toNetwork(T value, FriendlyByteBuf buffer) { - buffer.writeResourceLocation(getKey(value)); + /** @deprecated use {@link #decode(FriendlyByteBuf)} */ + @Deprecated(forRemoval = true) + public void toNetwork(T src, FriendlyByteBuf buffer) { + encode(buffer, src); } - /** Writes the value to the buffer */ - public void toNetworkOptional(@Nullable T value, FriendlyByteBuf buffer) { - // if null, just write an empty string, that is not a valid resource location anyways and saves us a byte - if (value != null) { - buffer.writeUtf(getKey(value).toString()); - } else { - buffer.writeUtf(""); - } - } - - /** Reads the given value from the network by resource location */ - private T fromNetwork(ResourceLocation name) { - T value = getValue(name); - if (value == null) { - throw new DecoderException(errorText + name); - } - return value; + /** @deprecated use {@link #decode(FriendlyByteBuf)} */ + @Deprecated(forRemoval = true) + public T fromNetwork(FriendlyByteBuf buffer) { + return decode(buffer); } - /** Parse the value from JSON */ - public T fromNetwork(FriendlyByteBuf buffer) { - return fromNetwork(buffer.readResourceLocation()); + /** @deprecated use {@link #decode(FriendlyByteBuf)} */ + @Deprecated(forRemoval = true) + public void toNetworkOptional(@Nullable T src, FriendlyByteBuf buffer) { + encodeOptional(buffer, src); } - /** Parse the value from JSON */ + /** @deprecated use {@link #decode(FriendlyByteBuf)} */ @Nullable + @Deprecated(forRemoval = true) public T fromNetworkOptional(FriendlyByteBuf buffer) { - // empty string is not a valid resource location, so its a nice value to use for null, saves us a byte - String key = buffer.readUtf(Short.MAX_VALUE); - if (key.isEmpty()) { - return null; - } - return fromNetwork(new ResourceLocation(key)); + return decodeOptional(buffer); } } diff --git a/src/main/java/slimeknights/mantle/fluid/FluidTransferHelper.java b/src/main/java/slimeknights/mantle/fluid/FluidTransferHelper.java index 4cd72d0ee..957a5683c 100644 --- a/src/main/java/slimeknights/mantle/fluid/FluidTransferHelper.java +++ b/src/main/java/slimeknights/mantle/fluid/FluidTransferHelper.java @@ -153,13 +153,13 @@ public static boolean interactWithBucket(Level world, BlockPos pos, Player playe } /** Plays the sound from filling a TE */ - private static void playEmptySound(Level world, BlockPos pos, Player player, FluidStack transferred) { + public static void playEmptySound(Level world, BlockPos pos, Player player, FluidStack transferred) { world.playSound(null, pos, getEmptySound(transferred), SoundSource.BLOCKS, 1.0F, 1.0F); player.displayClientMessage(Component.translatable(KEY_FILLED, transferred.getAmount(), transferred.getDisplayName()), true); } /** Plays the sound from draining a TE */ - private static void playFillSound(Level world, BlockPos pos, Player player, FluidStack transferred) { + public static void playFillSound(Level world, BlockPos pos, Player player, FluidStack transferred) { world.playSound(null, pos, getFillSound(transferred), SoundSource.BLOCKS, 1.0F, 1.0F); player.displayClientMessage(Component.translatable(KEY_DRAINED, transferred.getAmount(), transferred.getDisplayName()), true); } diff --git a/src/main/java/slimeknights/mantle/fluid/texture/FluidTexture.java b/src/main/java/slimeknights/mantle/fluid/texture/FluidTexture.java index f0bd8a36c..fba1285b6 100644 --- a/src/main/java/slimeknights/mantle/fluid/texture/FluidTexture.java +++ b/src/main/java/slimeknights/mantle/fluid/texture/FluidTexture.java @@ -7,7 +7,9 @@ import lombok.Setter; import lombok.experimental.Accessors; import net.minecraft.resources.ResourceLocation; -import net.minecraft.util.GsonHelper; +import net.minecraftforge.fluids.FluidType; +import net.minecraftforge.registries.ForgeRegistries; +import slimeknights.mantle.data.loadable.common.ColorLoadable; import slimeknights.mantle.util.IdExtender.LocationExtender; import slimeknights.mantle.util.JsonHelper; @@ -43,7 +45,7 @@ public static FluidTexture deserialize(JsonObject json) { if (json.has("camera")) { camera = LocationExtender.INSTANCE.wrap(JsonHelper.getResourceLocation(json, "camera"), "textures/", ".png"); } - int color = JsonHelper.parseColor(GsonHelper.getAsString(json, "color")); + int color = ColorLoadable.ALPHA.getOrDefault(json, "color", -1); return new FluidTexture(still, flowing, overlay, camera, color); } diff --git a/src/main/java/slimeknights/mantle/fluid/texture/FluidTextureManager.java b/src/main/java/slimeknights/mantle/fluid/texture/FluidTextureManager.java index bd94d70f5..df9e02eda 100644 --- a/src/main/java/slimeknights/mantle/fluid/texture/FluidTextureManager.java +++ b/src/main/java/slimeknights/mantle/fluid/texture/FluidTextureManager.java @@ -9,6 +9,12 @@ import net.minecraft.server.packs.resources.Resource; import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.util.GsonHelper; +import net.minecraftforge.client.event.TextureStitchEvent; +import net.minecraftforge.eventbus.api.EventPriority; +import net.minecraftforge.fluids.FluidType; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.registries.ForgeRegistries; +import net.minecraftforge.registries.IForgeRegistry; import slimeknights.mantle.Mantle; import slimeknights.mantle.util.JsonHelper; @@ -25,9 +31,6 @@ public class FluidTextureManager implements Consumer { /** Folder containing the logic */ public static final String FOLDER = "mantle/fluid_texture"; - private static final int FOLDER_LENGTH = FOLDER.length() + 1; - private static final int EXTENSION_LENGTH = ".json".length(); - /* Instance data */ private static final FluidTextureManager INSTANCE = new FluidTextureManager(); /** Map of fluid type to texture */ @@ -37,15 +40,15 @@ public class FluidTextureManager implements Consumer { /** * Initializes this manager, registering it with the resource manager - * @param manager Manager */ - public static void init(RegisterClientReloadListenersEvent manager) { - MinecraftForge.EVENT_BUS.addListener(EventPriority.NORMAL, false, TextureStitchEvent.Pre.class, INSTANCE); + public static void init() { + FMLJavaModLoadingContext.get().getModEventBus().addListener(EventPriority.NORMAL, false, TextureStitchEvent.Pre.class, INSTANCE); } @Override public void accept(TextureStitchEvent.Pre event) { if (event.getAtlas().location().equals(TextureAtlas.LOCATION_BLOCKS)) { + long time = System.nanoTime(); // first, load in all fluid texture files, done in this event as otherwise we cannot guarantee it happens before the atlas stitches Map map = new HashMap<>(); @@ -54,7 +57,7 @@ public void accept(TextureStitchEvent.Pre event) { for (Map.Entry entry : manager.listResources(FOLDER, location -> location.getPath().endsWith(".json")).entrySet()) { ResourceLocation fullPath = entry.getKey(); String path = fullPath.getPath(); - ResourceLocation id = new ResourceLocation(fullPath.getNamespace(), path.substring(FOLDER_LENGTH, path.length() - EXTENSION_LENGTH)); + ResourceLocation id = JsonHelper.localize(fullPath, FOLDER, ".json"); try (Reader reader = entry.getValue().openAsReader()) { // first step is to find the matching fluid type, if there is none ignore the file FluidType type = fluidTypeRegistry.getValue(id); @@ -86,6 +89,7 @@ public void accept(TextureStitchEvent.Pre event) { } // no registering camera as its not stitched, its just drawn directly } + Mantle.logger.info("Loaded {} fluid textures in {} ms", map.size(), (System.nanoTime() - time) / 1000000f); } } diff --git a/src/main/java/slimeknights/mantle/fluid/tooltip/FluidTooltipHandler.java b/src/main/java/slimeknights/mantle/fluid/tooltip/FluidTooltipHandler.java index 19347df0d..7ec06f31d 100644 --- a/src/main/java/slimeknights/mantle/fluid/tooltip/FluidTooltipHandler.java +++ b/src/main/java/slimeknights/mantle/fluid/tooltip/FluidTooltipHandler.java @@ -51,7 +51,7 @@ public class FluidTooltipHandler extends SimpleJsonResourceReloadListener implem // TODO: do we even need GSON here? I feel a classical serializer is sufficient as this class is pretty simple public static final Gson GSON = (new GsonBuilder()) .registerTypeAdapter(ResourceLocation.class, new ResourceLocation.Serializer()) - .registerTypeAdapter(FluidIngredient.class, FluidIngredient.SERIALIZER) + .registerTypeAdapter(FluidIngredient.class, FluidIngredient.LOADABLE) .registerTypeAdapter(TagKey.class, new TagKeySerializer<>(Registries.FLUID)) .setPrettyPrinting() .disableHtmlEscaping() @@ -146,7 +146,7 @@ protected void apply(Map splashList, ResourceManag unitLists = builder.build(); fallback = this.unitLists.getOrDefault(DEFAULT_ID, DEFAULT_LIST); listCache.clear(); - log.info("Loaded {} fluid unit lists in {} ms", listCache.size(), (System.nanoTime() - time) / 1000000f); + log.info("Loaded {} fluid unit lists in {} ms", unitLists.size(), (System.nanoTime() - time) / 1000000f); } /** Gets the unit list for the given fluid */ diff --git a/src/main/java/slimeknights/mantle/fluid/transfer/EmptyFluidContainerTransfer.java b/src/main/java/slimeknights/mantle/fluid/transfer/EmptyFluidContainerTransfer.java index 2b3d0ddb2..246ecfb10 100644 --- a/src/main/java/slimeknights/mantle/fluid/transfer/EmptyFluidContainerTransfer.java +++ b/src/main/java/slimeknights/mantle/fluid/transfer/EmptyFluidContainerTransfer.java @@ -77,7 +77,7 @@ public JsonObject serialize(JsonSerializationContext context) { JsonObject json = new JsonObject(); json.addProperty("type", ID.toString()); json.add("input", input.toJson()); - json.add("filled", filled.serialize()); + json.add("filled", filled.serialize(false)); json.add("fluid", RecipeHelper.serializeFluidStack(fluid)); return json; } @@ -93,7 +93,7 @@ public record Deserializer(TriFunction(TriFunction getNoItemIcon() { } @Override - public Slot setBackground(ResourceLocation atlas, ResourceLocation sprite) { - return this.parent.setBackground(atlas, sprite); + public ItemStack remove(int amount) { + return this.parent.remove(amount); } @Override - public ItemStack remove(int amount) { - return this.parent.remove(amount); + public boolean mayPickup(Player playerIn) { + return this.parent.mayPickup(playerIn); } @Override public boolean isActive() { return this.parent.isActive(); } + + @Override + public Slot setBackground(ResourceLocation atlas, ResourceLocation sprite) { + return this.parent.setBackground(atlas, sprite); + } + + @Override + public Optional tryRemove(int pCount, int pDecrement, Player pPlayer) { + return this.parent.tryRemove(pCount, pDecrement, pPlayer); + } + + @Override + public ItemStack safeTake(int pCount, int pDecrement, Player pPlayer) { + return this.parent.safeTake(pCount, pDecrement, pPlayer); + } + + @Override + public ItemStack safeInsert(ItemStack pStack, int pIncrement) { + return this.parent.safeInsert(pStack, pIncrement); + } + + @Override + public boolean allowModification(Player pPlayer) { + return this.parent.allowModification(pPlayer); + } } diff --git a/src/main/java/slimeknights/mantle/loot/AddEntryLootModifier.java b/src/main/java/slimeknights/mantle/loot/AddEntryLootModifier.java index 70c9d2c77..5bd3752d3 100644 --- a/src/main/java/slimeknights/mantle/loot/AddEntryLootModifier.java +++ b/src/main/java/slimeknights/mantle/loot/AddEntryLootModifier.java @@ -1,6 +1,5 @@ package slimeknights.mantle.loot; -import com.google.gson.Gson; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; import io.github.fabricators_of_create.porting_lib.loot.IGlobalLootModifier; @@ -9,7 +8,6 @@ import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import net.minecraft.world.item.ItemStack; -import net.minecraft.world.level.storage.loot.Deserializers; import net.minecraft.world.level.storage.loot.LootContext; import net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer; import net.minecraft.world.level.storage.loot.functions.LootItemFunction; @@ -31,8 +29,6 @@ public class AddEntryLootModifier extends LootModifier { MantleCodecs.LOOT_ENTRY.fieldOf("entry").forGetter(m -> m.entry), MantleCodecs.LOOT_FUNCTIONS.fieldOf("functions").forGetter(m -> m.functions))).apply(inst, AddEntryLootModifier::new)); - static final Gson GSON = Deserializers.createFunctionSerializer().registerTypeHierarchyAdapter(ILootModifierCondition.class, ILootModifierCondition.MODIFIER_CONDITIONS).create(); - /** Additional conditions that can consider the previously generated loot */ private final List modifierConditions; /** Entry for generating loot */ diff --git a/src/main/java/slimeknights/mantle/loot/ReplaceItemLootModifier.java b/src/main/java/slimeknights/mantle/loot/ReplaceItemLootModifier.java index d15e51e0a..a18c295c5 100644 --- a/src/main/java/slimeknights/mantle/loot/ReplaceItemLootModifier.java +++ b/src/main/java/slimeknights/mantle/loot/ReplaceItemLootModifier.java @@ -28,7 +28,7 @@ public class ReplaceItemLootModifier extends LootModifier { public static final Codec CODEC = RecordCodecBuilder.create(inst -> codecStart(inst).and( inst.group( MantleCodecs.INGREDIENT.fieldOf("original").forGetter(m -> m.original), - ItemOutput.CODEC.fieldOf("replacement").forGetter(m -> m.replacement), + ItemOutput.REQUIRED_STACK_CODEC.fieldOf("replacement").forGetter(m -> m.replacement), MantleCodecs.LOOT_FUNCTIONS.fieldOf("functions").forGetter(m -> m.functions) )).apply(inst, ReplaceItemLootModifier::new)); diff --git a/src/main/java/slimeknights/mantle/plugin/jei/JEIPlugin.java b/src/main/java/slimeknights/mantle/plugin/jei/JEIPlugin.java index 73f1c8c10..6fa3e8d29 100644 --- a/src/main/java/slimeknights/mantle/plugin/jei/JEIPlugin.java +++ b/src/main/java/slimeknights/mantle/plugin/jei/JEIPlugin.java @@ -4,14 +4,18 @@ import mezz.jei.api.JeiPlugin; import mezz.jei.api.gui.handlers.IGuiContainerHandler; import mezz.jei.api.registration.IGuiHandlerRegistration; +import mezz.jei.api.registration.IModIngredientRegistration; import mezz.jei.api.registration.IVanillaCategoryExtensionRegistration; import net.minecraft.client.renderer.Rect2i; import net.minecraft.resources.ResourceLocation; import slimeknights.mantle.Mantle; import slimeknights.mantle.client.screen.MultiModuleScreen; import slimeknights.mantle.inventory.MultiModuleContainerMenu; +import slimeknights.mantle.plugin.jei.entity.EntityIngredientHelper; +import slimeknights.mantle.plugin.jei.entity.EntityIngredientRenderer; import slimeknights.mantle.recipe.crafting.ShapedRetexturedRecipe; +import java.util.Collections; import java.util.List; @JeiPlugin @@ -21,6 +25,11 @@ public ResourceLocation getPluginUid() { return Mantle.getResource("jei"); } + @Override + public void registerIngredients(IModIngredientRegistration registration) { + registration.register(MantleJEIConstants.ENTITY_TYPE, Collections.emptyList(), new EntityIngredientHelper(), new EntityIngredientRenderer(16)); + } + @Override public void registerVanillaCategoryExtensions(IVanillaCategoryExtensionRegistration registry) { // registry.getCraftingCategory().addCategoryExtension(ShapedRetexturedRecipe.class, RetexturableRecipeExtension::new); diff --git a/src/main/java/slimeknights/mantle/plugin/jei/MantleJEIConstants.java b/src/main/java/slimeknights/mantle/plugin/jei/MantleJEIConstants.java new file mode 100644 index 000000000..6f6dff51a --- /dev/null +++ b/src/main/java/slimeknights/mantle/plugin/jei/MantleJEIConstants.java @@ -0,0 +1,9 @@ +package slimeknights.mantle.plugin.jei; + +import mezz.jei.api.ingredients.IIngredientType; +import slimeknights.mantle.recipe.ingredient.EntityIngredient.EntityInput; + +public class MantleJEIConstants { + /** Ingredient for an entity */ + public static final IIngredientType ENTITY_TYPE = () -> EntityInput.class; +} diff --git a/src/main/java/slimeknights/mantle/plugin/jei/entity/EntityIngredientHelper.java b/src/main/java/slimeknights/mantle/plugin/jei/entity/EntityIngredientHelper.java new file mode 100644 index 000000000..e10f57ba1 --- /dev/null +++ b/src/main/java/slimeknights/mantle/plugin/jei/entity/EntityIngredientHelper.java @@ -0,0 +1,47 @@ +package slimeknights.mantle.plugin.jei.entity; + +import mezz.jei.api.ingredients.IIngredientHelper; +import mezz.jei.api.ingredients.IIngredientType; +import mezz.jei.api.ingredients.subtypes.UidContext; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceLocation; +import slimeknights.mantle.plugin.jei.MantleJEIConstants; +import slimeknights.mantle.recipe.ingredient.EntityIngredient; + +import javax.annotation.Nullable; + +/** Handler for working with entity types as ingredients */ +public class EntityIngredientHelper implements IIngredientHelper { + @Override + public IIngredientType getIngredientType() { + return MantleJEIConstants.ENTITY_TYPE; + } + + @Override + public String getDisplayName(EntityIngredient.EntityInput type) { + return type.type().getDescription().getString(); + } + + @Override + public String getUniqueId(EntityIngredient.EntityInput type, UidContext context) { + return getResourceLocation(type).toString(); + } + + @Override + public ResourceLocation getResourceLocation(EntityIngredient.EntityInput type) { + return Registry.ENTITY_TYPE.getKey(type.type()); + } + + @Override + public EntityIngredient.EntityInput copyIngredient(EntityIngredient.EntityInput type) { + return type; + } + + @Override + public String getErrorInfo(@Nullable EntityIngredient.EntityInput type) { + if (type == null) { + return "null"; + } + return getResourceLocation(type).toString(); + } +} diff --git a/src/main/java/slimeknights/mantle/plugin/jei/entity/EntityIngredientRenderer.java b/src/main/java/slimeknights/mantle/plugin/jei/entity/EntityIngredientRenderer.java new file mode 100644 index 000000000..4705ebdde --- /dev/null +++ b/src/main/java/slimeknights/mantle/plugin/jei/entity/EntityIngredientRenderer.java @@ -0,0 +1,119 @@ +package slimeknights.mantle.plugin.jei.entity; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.PoseStack; +import lombok.RequiredArgsConstructor; +import mezz.jei.api.ingredients.IIngredientRenderer; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.InventoryScreen; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.core.Registry; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.Level; +import slimeknights.mantle.Mantle; +import slimeknights.mantle.recipe.ingredient.EntityIngredient; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Renderer for entity type ingredients + */ +@RequiredArgsConstructor +public class EntityIngredientRenderer implements IIngredientRenderer { + private static final ResourceLocation MISSING = Mantle.getResource("textures/item/missingno.png"); + /** Entity types that will not render, as they either errored or are the wrong type */ + private static final Set> IGNORED_ENTITIES = new HashSet<>(); + + /** Square size of the renderer in pixels */ + private final int size; + + /** Cache of entities for each entity type */ + private final Map,Entity> ENTITY_MAP = new HashMap<>(); + + @Override + public int getWidth() { + return size; + } + + @Override + public int getHeight() { + return size; + } + + @Override + public void render(PoseStack matrixStack, @Nullable EntityIngredient.EntityInput input) { + if (input != null) { + Level world = Minecraft.getInstance().level; + EntityType type = input.type(); + if (world != null && !IGNORED_ENTITIES.contains(type)) { + Entity entity; + // players cannot be created using the type, but we can use the client player + // side effect is it renders armor/items + if (type == EntityType.PLAYER) { + entity = Minecraft.getInstance().player; + } else { + // entity is created with the client world, but the entity map is thrown away when JEI restarts so they should be okay I think + entity = ENTITY_MAP.computeIfAbsent(type, t -> t.create(world)); + } + // only can draw living entities, plus non-living ones don't get recipes anyways + if (entity instanceof LivingEntity livingEntity) { + // scale down large mobs, but don't scale up small ones + int scale = size / 2; + float height = entity.getBbHeight(); + float width = entity.getBbWidth(); + if (height > 2 || width > 2) { + scale = (int)(size / Math.max(height, width)); + } + // catch exceptions drawing the entity to be safe, any caught exceptions blacklist the entity + try { + PoseStack modelView = RenderSystem.getModelViewStack(); + modelView.pushPose(); + modelView.mulPoseMatrix(matrixStack.last().pose()); + InventoryScreen.renderEntityInInventory(size / 2, size, scale, 0, 10, livingEntity); + modelView.popPose(); + RenderSystem.applyModelViewMatrix(); + return; + } catch (Exception e) { + Mantle.logger.error("Error drawing entity " + Registry.ENTITY_TYPE.getKey(type), e); + IGNORED_ENTITIES.add(type); + ENTITY_MAP.remove(type); + } + } else { + // not living, so might as well skip next time + IGNORED_ENTITIES.add(type); + ENTITY_MAP.remove(type); + } + } + + // fallback, draw a pink and black "spawn egg" + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderTexture(0, MISSING); + RenderSystem.setShaderColor(1, 1, 1, 1); + int offset = (size - 16) / 2; + Screen.blit(matrixStack, offset, offset, 0, 0, 16, 16, 16, 16); + } + } + + @Override + public List getTooltip(EntityIngredient.EntityInput type, TooltipFlag flag) { + List tooltip = new ArrayList<>(); + tooltip.add(type.type().getDescription()); + if (flag.isAdvanced()) { + tooltip.add((Component.literal(Registry.ENTITY_TYPE.getKey(type.type()).toString())).withStyle(ChatFormatting.DARK_GRAY)); + } + return tooltip; + } +} diff --git a/src/main/java/slimeknights/mantle/plugin/jei/entity/package-info.java b/src/main/java/slimeknights/mantle/plugin/jei/entity/package-info.java new file mode 100644 index 000000000..4dd958947 --- /dev/null +++ b/src/main/java/slimeknights/mantle/plugin/jei/entity/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package slimeknights.mantle.plugin.jei.entity; + +import net.minecraft.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/main/java/slimeknights/mantle/recipe/data/AbstractRecipeBuilder.java b/src/main/java/slimeknights/mantle/recipe/data/AbstractRecipeBuilder.java index 85b7c566d..90a55bebe 100644 --- a/src/main/java/slimeknights/mantle/recipe/data/AbstractRecipeBuilder.java +++ b/src/main/java/slimeknights/mantle/recipe/data/AbstractRecipeBuilder.java @@ -10,6 +10,9 @@ import net.minecraft.advancements.critereon.RecipeUnlockedTrigger; import net.minecraft.data.recipes.FinishedRecipe; import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeSerializer; +import slimeknights.mantle.data.loadable.record.RecordLoadable; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -135,4 +138,25 @@ public JsonObject serializeAdvancement() { return advancementBuilder.serializeToJson(); } } + + /** Finished recipe using a loadable */ + protected class LoadableFinishedRecipe> extends AbstractFinishedRecipe { + private final R recipe; + private final RecordLoadable loadable; + public LoadableFinishedRecipe(R recipe, RecordLoadable loadable, @Nullable ResourceLocation advancementId) { + super(recipe.getId(), advancementId); + this.recipe = recipe; + this.loadable = loadable; + } + + @Override + public void serializeRecipeData(JsonObject json) { + loadable.serialize(recipe, json); + } + + @Override + public RecipeSerializer getType() { + return recipe.getSerializer(); + } + } } diff --git a/src/main/java/slimeknights/mantle/recipe/data/FluidNameIngredient.java b/src/main/java/slimeknights/mantle/recipe/data/FluidNameIngredient.java index 2d28f11ed..44f431979 100644 --- a/src/main/java/slimeknights/mantle/recipe/data/FluidNameIngredient.java +++ b/src/main/java/slimeknights/mantle/recipe/data/FluidNameIngredient.java @@ -1,11 +1,13 @@ package slimeknights.mantle.recipe.data; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import io.github.fabricators_of_create.porting_lib.fluids.FluidStack; import lombok.RequiredArgsConstructor; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.material.Fluid; +import net.minecraftforge.fluids.FluidStack; +import slimeknights.mantle.data.loadable.Loadable; +import slimeknights.mantle.data.loadable.Loadables; +import slimeknights.mantle.data.loadable.primitive.IntLoadable; +import slimeknights.mantle.data.loadable.record.RecordLoadable; import slimeknights.mantle.recipe.ingredient.FluidIngredient; import java.util.List; @@ -13,9 +15,19 @@ /** Datagen fluid ingredient to create an ingredient matching a fluid from another mod, should not be used outside datagen */ @RequiredArgsConstructor(staticName = "of") public class FluidNameIngredient extends FluidIngredient { + private static final RecordLoadable LOADABLE = RecordLoadable.create( + Loadables.RESOURCE_LOCATION.requiredField("fluid", i -> i.fluidName), + IntLoadable.FROM_ONE.requiredField("amount", i -> i.amount), + FluidNameIngredient::new); + private final ResourceLocation fluidName; private final long amount; + @Override + public Loadable loadable() { + return LOADABLE; + } + @Override public boolean test(Fluid fluid) { throw new UnsupportedOperationException(); @@ -30,12 +42,4 @@ public long getAmount(Fluid fluid) { protected List getAllFluids() { throw new UnsupportedOperationException(); } - - @Override - public JsonElement serialize() { - JsonObject object = new JsonObject(); - object.addProperty("name", this.fluidName.toString()); - object.addProperty("amount", this.amount); - return object; - } } diff --git a/src/main/java/slimeknights/mantle/recipe/data/ItemNameOutput.java b/src/main/java/slimeknights/mantle/recipe/data/ItemNameOutput.java index 7b31b88f7..0ceeff94f 100644 --- a/src/main/java/slimeknights/mantle/recipe/data/ItemNameOutput.java +++ b/src/main/java/slimeknights/mantle/recipe/data/ItemNameOutput.java @@ -4,9 +4,10 @@ import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import lombok.RequiredArgsConstructor; -import net.minecraft.world.item.ItemStack; import net.minecraft.nbt.CompoundTag; import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import slimeknights.mantle.data.loadable.common.NBTLoadable; import slimeknights.mantle.recipe.helper.ItemOutput; import javax.annotation.Nullable; @@ -46,21 +47,19 @@ public ItemStack get() { } @Override - public JsonElement serialize() { + public JsonElement serialize(boolean writeCount) { String itemName = name.toString(); - if (nbt == null && count <= 1) { + if (nbt == null && (count <= 1 || !writeCount)) { return new JsonPrimitive(itemName); } else { JsonObject jsonResult = new JsonObject(); jsonResult.addProperty("item", itemName); - if (count > 1) { + if (writeCount) { jsonResult.addProperty("count", count); } - if (nbt != null) { - jsonResult.addProperty("nbt", nbt.toString()); + jsonResult.add("nbt", NBTLoadable.ALLOW_STRING.serialize(nbt)); } - return jsonResult; } } diff --git a/src/main/java/slimeknights/mantle/recipe/helper/ItemOutput.java b/src/main/java/slimeknights/mantle/recipe/helper/ItemOutput.java index 4451f5ac0..bea20dbc2 100644 --- a/src/main/java/slimeknights/mantle/recipe/helper/ItemOutput.java +++ b/src/main/java/slimeknights/mantle/recipe/helper/ItemOutput.java @@ -2,46 +2,36 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; -import com.google.gson.JsonSyntaxException; import com.mojang.serialization.Codec; import io.github.fabricators_of_create.porting_lib.util.CraftingHelper; import lombok.RequiredArgsConstructor; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.core.registries.Registries; -import net.minecraft.nbt.CompoundTag; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.tags.TagKey; import net.minecraft.util.GsonHelper; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.ItemLike; -import slimeknights.mantle.data.JsonCodec; -import slimeknights.mantle.util.JsonHelper; +import slimeknights.mantle.data.loadable.LoadableCodec; +import slimeknights.mantle.data.loadable.Loadables; +import slimeknights.mantle.data.loadable.common.ItemStackLoadable; +import slimeknights.mantle.data.loadable.field.LoadableField; +import slimeknights.mantle.data.loadable.primitive.IntLoadable; +import slimeknights.mantle.data.loadable.record.RecordLoadable; +import java.util.Optional; +import java.util.function.Function; import java.util.function.Supplier; /** * Class representing an item stack output. Supports both direct stacks and tag output, behaving like an ingredient used for output */ public abstract class ItemOutput implements Supplier { - /** Codec instance */ // TODO: implement as a proper codec - public static Codec CODEC = new JsonCodec<>() { - @Override - public ItemOutput deserialize(JsonElement element) { - return ItemOutput.fromJson(element); - } + /* Codecs - just adding these as needed */ + /** Codec for an output that may not be empty with any size */ + public static Codec REQUIRED_STACK_CODEC = new LoadableCodec<>(Loadable.REQUIRED_STACK); - @Override - public JsonElement serialize(ItemOutput output) { - return output.serialize(); - } - - @Override - public String toString() { - return "ItemOutput"; - } - }; + /** Empty instance */ + public static final ItemOutput EMPTY = new OfStack(ItemStack.EMPTY); /** @@ -53,9 +43,10 @@ public String toString() { /** * Writes this output to JSON + * @param writeCount If true, serializes the count * @return Json element */ - public abstract JsonElement serialize(); + public abstract JsonElement serialize(boolean writeCount); /** * Creates a new output for the given stack @@ -63,6 +54,9 @@ public String toString() { * @return Output */ public static ItemOutput fromStack(ItemStack stack) { + if (stack.isEmpty()) { + return EMPTY; + } return new OfStack(stack); } @@ -87,7 +81,8 @@ public static ItemOutput fromItem(ItemLike item) { /** * Creates a new output for the given tag - * @param tag Tag + * @param tag Tag + * @param count Stack count * @return Output */ public static ItemOutput fromTag(TagKey tag, int count) { @@ -95,27 +90,12 @@ public static ItemOutput fromTag(TagKey tag, int count) { } /** - * Reads an item output from JSON - * @param element Json element - * @return Read output + * Creates a new output for the given tag + * @param tag Tag + * @return Output */ - public static ItemOutput fromJson(JsonElement element) { - if (element.isJsonPrimitive()) { - return fromItem(GsonHelper.convertToItem(element, "item")); - } - if (!element.isJsonObject()) { - throw new JsonSyntaxException("Invalid item output, must be a string or an object"); - } - // if it has a tag, parse as tag - JsonObject json = element.getAsJsonObject(); - if (json.has("tag")) { - TagKey tag = TagKey.create(Registries.ITEM, JsonHelper.getResourceLocation(json, "tag")); - int count = GsonHelper.getAsInt(json, "count", 1); - return fromTag(tag, count); - } - - // default: parse as item stack using Forge - return fromStack(CraftingHelper.getItemStack(json, true)); + public static ItemOutput fromTag(TagKey tag) { + return fromTag(tag, 1); } /** @@ -151,15 +131,15 @@ public ItemStack get() { } @Override - public JsonElement serialize() { - String itemName = BuiltInRegistries.ITEM.getKey(item).toString(); - if (count > 1) { + public JsonElement serialize(boolean writeCount) { + JsonElement item = Loadables.ITEM.serialize(this.item); + if (writeCount && count > 1) { JsonObject json = new JsonObject(); - json.addProperty("item", itemName); + json.add("item", item); json.addProperty("count", count); return json; } else { - return new JsonPrimitive(itemName); + return item; } } } @@ -175,24 +155,11 @@ public ItemStack get() { } @Override - public JsonElement serialize() { - String itemName = BuiltInRegistries.ITEM.getKey(stack.getItem()).toString(); - int count = stack.getCount(); - // if the item has NBT or a count, write as object - if (stack.hasTag() || count > 1) { - JsonObject jsonResult = new JsonObject(); - jsonResult.addProperty("item", itemName); - if (count > 1) { - jsonResult.addProperty("count", count); - } - CompoundTag nbt = stack.getTag(); - if (nbt != null) { - jsonResult.addProperty("nbt", nbt.toString()); - } - return jsonResult; - } else { - return new JsonPrimitive(itemName); + public JsonElement serialize(boolean writeCount) { + if (writeCount) { + return ItemStackLoadable.OPTIONAL_STACK_NBT.serialize(stack); } + return ItemStackLoadable.OPTIONAL_ITEM_NBT.serialize(stack); } } @@ -208,21 +175,109 @@ public ItemStack get() { // cache the result from the tag preference to save effort, especially helpful if the tag becomes invalid // this object should only exist in recipes so no need to invalidate the cache if (cachedResult == null) { - cachedResult = TagPreference.getPreference(tag) - .map(item -> new ItemStack(item, count)) - .orElse(ItemStack.EMPTY); + // if the preference is empty, do not cache it. + // This should only happen if someone scans recipes before tag are computed in which case we cache the wrong resolt. + // We protect against empty tags in our recipes via conditions. + Optional preference = TagPreference.getPreference(tag); + if (preference.isEmpty()) { + return ItemStack.EMPTY; + } + cachedResult = new ItemStack(preference.orElseThrow(), count); } return cachedResult; } @Override - public JsonElement serialize() { + public JsonElement serialize(boolean writeCount) { JsonObject json = new JsonObject(); json.addProperty("tag", tag.location().toString()); - if (count != 1) { + if (writeCount) { json.addProperty("count", count); } return json; } } + + /** Loadable logic for an ItemOutput */ + public enum Loadable implements slimeknights.mantle.data.loadable.Loadable { + /** Loadable for an output that may be empty with a fixed size of 1 */ + OPTIONAL_ITEM(false, false), + /** Loadable for an output that may be empty with any size */ + OPTIONAL_STACK(false, true), + /** Loadable for an output that may not empty with a fixed size of 1 */ + REQUIRED_ITEM(true, false), + /** Loadable for an output that may not be empty with any size */ + REQUIRED_STACK(true, true); + + private final boolean nonEmpty; + private final boolean readCount; + private final RecordLoadable stack; + Loadable(boolean nonEmpty, boolean readCount) { + this.nonEmpty = nonEmpty; + this.readCount = readCount; + // figure out the stack serializer to use based on the two parameters + // we always do NBT, just those that vary + if (nonEmpty) { + this.stack = readCount ? ItemStackLoadable.REQUIRED_STACK_NBT : ItemStackLoadable.REQUIRED_ITEM_NBT; + } else { + this.stack = readCount ? ItemStackLoadable.OPTIONAL_STACK_NBT : ItemStackLoadable.OPTIONAL_ITEM_NBT; + } + } + + @Override + public ItemOutput convert(JsonElement element, String key) { + // if it's a primitive, parse it directly with the stack logic + // that handles single items and ensures both count and non-empty + if (element.isJsonPrimitive()) { + return fromStack(stack.convert(element, key)); + } + JsonObject json = GsonHelper.convertToJsonObject(element, key); + if (json.has("tag")) { + TagKey tag = Loadables.ITEM_TAG.getIfPresent(json, "tag"); + int count = 1; + // 0 count field means we load count from JSON + if (readCount) { + count = IntLoadable.FROM_ONE.getOrDefault(json, "count", 1); + } + return fromTag(tag, count); + } + return fromStack(stack.deserialize(json)); + } + + @Override + public JsonElement serialize(ItemOutput output) { + if (nonEmpty && (output instanceof OfItem || output instanceof OfStack) && output.get().isEmpty()) { + throw new IllegalArgumentException("ItemOutput cannot be empty for this recipe"); + } + return output.serialize(readCount); + } + + @Override + public ItemOutput decode(FriendlyByteBuf buffer) { + return fromStack(stack.decode(buffer)); + } + + @Override + public void encode(FriendlyByteBuf buffer, ItemOutput object) { + stack.encode(buffer, object.get()); + } + + + /* Defaulting behavior */ + + /** Gets the output, defaulting to empty. Note this will not stop you from getting empty with a non-empty loadable, thats on you for weirdly calling. */ + public ItemOutput getOrEmpty(JsonObject parent, String key) { + return getOrDefault(parent, key, ItemOutput.EMPTY); + } + + /** Creates a field defaulting to empty */ + public

LoadableField emptyField(String key, boolean serializeDefault, Function getter) { + return defaultField(key, ItemOutput.EMPTY, serializeDefault, getter); + } + + /** Creates a field defaulting to empty that does not serialize if empty */ + public

LoadableField emptyField(String key, Function getter) { + return emptyField(key, false, getter); + } + } } diff --git a/src/main/java/slimeknights/mantle/recipe/helper/LoadableRecipeSerializer.java b/src/main/java/slimeknights/mantle/recipe/helper/LoadableRecipeSerializer.java new file mode 100644 index 000000000..bc7418b4b --- /dev/null +++ b/src/main/java/slimeknights/mantle/recipe/helper/LoadableRecipeSerializer.java @@ -0,0 +1,108 @@ +package slimeknights.mantle.recipe.helper; + +import com.google.gson.JsonObject; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; +import slimeknights.mantle.Mantle; +import slimeknights.mantle.data.loadable.field.ContextKey; +import slimeknights.mantle.data.loadable.field.LoadableField; +import slimeknights.mantle.data.loadable.primitive.StringLoadable; +import slimeknights.mantle.data.loadable.record.RecordLoadable; +import slimeknights.mantle.util.typed.TypedMapBuilder; + +import javax.annotation.Nullable; +import java.util.function.Supplier; + +/** + * Recipe serializer instance using loadables. Use {@link ContextKey#ID} to get the recipe ID. + * @param Recipe type + */ +@RequiredArgsConstructor(access = AccessLevel.PROTECTED) +public class LoadableRecipeSerializer> implements LoggingRecipeSerializer { + /** Context key to use if you want the recipe serializer passed into your recipe */ + public static final ContextKey> SERIALIZER = new ContextKey<>("serializer"); + /** Context key to use if you want a type aware serializer in the recipe, requires {@link #of(RecordLoadable, Supplier)} for your serializer. */ + public static final ContextKey> TYPED_SERIALIZER = new ContextKey<>("typed_serializer"); + /** Context key to use if you want the recipe type passed into your recipe, requires {@link #of(RecordLoadable, Supplier)} for your serializer. */ + public static final ContextKey> TYPE = new ContextKey<>("type"); + /** Field for a group key in a recipe (common requirement) */ + public static final LoadableField> RECIPE_GROUP = StringLoadable.DEFAULT.defaultField("group", "", Recipe::getGroup); + + + protected final RecordLoadable loadable; + + /** Creates a standard serializer from a loadable */ + public static > RecipeSerializer of(RecordLoadable loadable) { + return new LoadableRecipeSerializer<>(loadable); + } + + /** Creates a type aware serializer from a loadable */ + public static > TypeAwareRecipeSerializer of(RecordLoadable loadable, Supplier> type) { + return new TypeAware<>(loadable, type); + } + + /** Builds a context for the given ID */ + protected TypedMapBuilder buildContext(ResourceLocation id) { + return TypedMapBuilder.builder().put(ContextKey.ID, id).put(SERIALIZER, this); + } + + @Override + public T fromJson(ResourceLocation id, JsonObject json) { + return loadable.deserialize(json, buildContext(id).build()); + } + + @Override + public T fromNetworkSafe(ResourceLocation id, FriendlyByteBuf buffer) { + return loadable.decode(buffer, buildContext(id).build()); + } + + @Nullable + @Override + public T fromNetwork(ResourceLocation id, FriendlyByteBuf buffer) { + try { + return fromNetworkSafe(id, buffer); + } catch (RuntimeException e) { + Mantle.logger.error("{}: Error reading recipe {} from packet using loadable {}", this.getClass().getSimpleName(), id, loadable, e); + throw e; + } + } + + @Override + public void toNetworkSafe(FriendlyByteBuf buffer, T recipe) { + loadable.encode(buffer, recipe); + } + + public static class TypeAware> extends LoadableRecipeSerializer implements TypeAwareRecipeSerializer { + private final Supplier> type; + protected TypeAware(RecordLoadable loadable, Supplier> type) { + super(loadable); + this.type = type; + } + + @Override + protected TypedMapBuilder buildContext(ResourceLocation id) { + return super.buildContext(id).put(TYPE, getType()).put(TYPED_SERIALIZER, this); + } + + @Override + public RecipeType getType() { + return type.get(); + } + + @Nullable + @Override + public T fromNetwork(ResourceLocation id, FriendlyByteBuf buffer) { + try { + return fromNetworkSafe(id, buffer); + } catch (RuntimeException e) { + Mantle.logger.error("{}: Error reading recipe {} of type {} from packet using loadable {}", this.getClass().getSimpleName(), id, getType(), loadable, e); + throw e; + } + } + } +} diff --git a/src/main/java/slimeknights/mantle/recipe/helper/LoggingRecipeSerializer.java b/src/main/java/slimeknights/mantle/recipe/helper/LoggingRecipeSerializer.java index 66f83e10f..efbdf0d80 100644 --- a/src/main/java/slimeknights/mantle/recipe/helper/LoggingRecipeSerializer.java +++ b/src/main/java/slimeknights/mantle/recipe/helper/LoggingRecipeSerializer.java @@ -37,7 +37,7 @@ default T fromNetwork(ResourceLocation id, FriendlyByteBuf buffer) { try { return fromNetworkSafe(id, buffer); } catch (RuntimeException e) { - Mantle.logger.error("{}: Error writing recipe to packet", this.getClass().getSimpleName(), e); + Mantle.logger.error("{}: Error reading recipe {} from packet", this.getClass().getSimpleName(), id, e); throw e; } } @@ -47,7 +47,7 @@ default void toNetwork(FriendlyByteBuf buffer, T recipe) { try { toNetworkSafe(buffer, recipe); } catch (RuntimeException e) { - Mantle.logger.error("{}: Error reading recipe from packet", this.getClass().getSimpleName(), e); + Mantle.logger.error("{}: Error writing recipe {} of class {} and type {} to packet", this.getClass().getSimpleName(), recipe.getId(), recipe.getClass().getSimpleName(), recipe.getType(), e); throw e; } } diff --git a/src/main/java/slimeknights/mantle/recipe/helper/TagPreference.java b/src/main/java/slimeknights/mantle/recipe/helper/TagPreference.java index 4a1e0f8ff..c81654baf 100644 --- a/src/main/java/slimeknights/mantle/recipe/helper/TagPreference.java +++ b/src/main/java/slimeknights/mantle/recipe/helper/TagPreference.java @@ -15,6 +15,8 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; /** * Utility that helps get the preferred item from a tag based on mod ID. @@ -23,10 +25,9 @@ public class TagPreference { /** Just an alphabetically late RL to simplify null checks */ private static final ResourceLocation DEFAULT_ID = new ResourceLocation("zzzzz:zzzzz"); // simplfies null checks - /** Specific cache to this tag preference class type */ - private static final Map> PREFERENCE_CACHE = new HashMap<>(); - - /** Specific cache to this tag preference class type */ + /** Cache from any tag key to its value */ + private static final Map, Optional> PREFERENCE_CACHE = new ConcurrentHashMap<>(); + /** Cache of comparator instances, not concurrent because it's only used inside {@link #getUncachedPreference(TagKey)} which is only used inside the concurrent {@link #PREFERENCE_CACHE}. */ private static final Map, RegistryComparator> COMPARATOR_CACHE = new HashMap<>(); /** Registers the listener with the event bus */ @@ -51,6 +52,9 @@ private static Optional getUncachedPreference(TagKey tag) { return RegistryHelper.getTagValueStream(tag).min(getComparator(registry)); } + /** Don't create a new lambda instance every time we call {@link #getPreference(TagKey)} */ + private static final Function,Optional> PREFERENCE_LOOKUP = TagPreference::getUncachedPreference; + /** * Gets the preferred value from a tag based on mod ID * @param tag Tag to fetch @@ -59,7 +63,7 @@ private static Optional getUncachedPreference(TagKey tag) { @SuppressWarnings("unchecked") public static Optional getPreference(TagKey tag) { // fetch cached value if we have one - return (Optional) PREFERENCE_CACHE.computeIfAbsent(tag.location(), name -> getUncachedPreference(tag)); + return (Optional) PREFERENCE_CACHE.computeIfAbsent(tag, PREFERENCE_LOOKUP); } /** Logic to compare two registry values */ diff --git a/src/main/java/slimeknights/mantle/recipe/helper/TypeAwareRecipeSerializer.java b/src/main/java/slimeknights/mantle/recipe/helper/TypeAwareRecipeSerializer.java new file mode 100644 index 000000000..f09dde904 --- /dev/null +++ b/src/main/java/slimeknights/mantle/recipe/helper/TypeAwareRecipeSerializer.java @@ -0,0 +1,11 @@ +package slimeknights.mantle.recipe.helper; + +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; + +/** Type aware recipe serializer, for the sake of recipes that serializers to swap out the type. */ +public interface TypeAwareRecipeSerializer> extends RecipeSerializer { + /** Gets the type of this recipe */ + RecipeType getType(); +} diff --git a/src/main/java/slimeknights/mantle/recipe/ingredient/EntityIngredient.java b/src/main/java/slimeknights/mantle/recipe/ingredient/EntityIngredient.java index 2f4044cf7..f376c72ea 100644 --- a/src/main/java/slimeknights/mantle/recipe/ingredient/EntityIngredient.java +++ b/src/main/java/slimeknights/mantle/recipe/ingredient/EntityIngredient.java @@ -1,193 +1,168 @@ package slimeknights.mantle.recipe.ingredient; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; -import com.google.gson.JsonArray; import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonSyntaxException; import lombok.RequiredArgsConstructor; -import net.minecraft.core.Holder; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.resources.ResourceLocation; import net.minecraft.tags.TagKey; -import net.minecraft.util.GsonHelper; import net.minecraft.world.entity.EntityType; -import slimeknights.mantle.util.JsonHelper; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraftforge.common.ForgeSpawnEggItem; +import slimeknights.mantle.data.loadable.IAmLoadable; +import slimeknights.mantle.data.loadable.Loadable; +import slimeknights.mantle.data.loadable.Loadables; +import slimeknights.mantle.data.loadable.mapping.EitherLoadable; +import slimeknights.mantle.data.loadable.record.RecordLoadable; +import slimeknights.mantle.util.RegistryHelper; -import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; /** * Ingredient accepting an entity or an entity tag as an input */ -public abstract class EntityIngredient implements Predicate> { - /** Empty entity ingredient, matching nothing */ - public static final EntityIngredient EMPTY = new SetMatch(Collections.emptySet()); +public abstract class EntityIngredient implements Predicate>, IAmLoadable { + /** Empty entity ingredient, matching nothing. This ingredient does not parse from JSON, use defaulting methods if you wish to use it */ + public static final EntityIngredient EMPTY = new Compound(Collections.emptyList()); - /** - * Gets a list of entity types matched by this ingredient - * @return List of types - */ - public abstract Collection> getTypes(); - /** - * Serializes this ingredient to JSON - * @return Json element of this ingredient - */ - public abstract JsonElement serialize(); - - /** Writes this ingredient to the packet buffer */ - public void write(FriendlyByteBuf buffer) { - Collection> collection = getTypes(); - buffer.writeVarInt(collection.size()); - for (EntityType type : collection) { - buffer.writeVarInt(BuiltInRegistries.ENTITY_TYPE.getId(type)); - } - } + /* Loadables */ - /** - * Creates an ingredient to match a single type - */ - public static EntityIngredient of(EntityType type) { - return new Single(type); + /** Creates a builder with set and tag */ + private static EitherLoadable.TypedBuilder loadableBuilder() { + return EitherLoadable.typed().key("types", SET_MATCH).key("type", ENTRY_MATCH).key("tag", TAG_MATCH); } + /** Loadable for a single value, notably not used for networking (though that probably would still work fine due to how EitherLoadable works) */ + private static final RecordLoadable ENTRY_MATCH = RecordLoadable.create(Loadables.ENTITY_TYPE.requiredField("type", i -> { + Set> types = i.getTypes(); + if (types.size() == 1) { + return types.iterator().next(); + } + throw new IllegalStateException("Cannot use entry match to serialize more than 1 entity"); + }), EntityIngredient::of); + /** Loadable for a set match */ + private static final RecordLoadable SET_MATCH = RecordLoadable.create(Loadables.ENTITY_TYPE.set().requiredField("types", EntityIngredient::getTypes), EntityIngredient::of); + /** Loadable for a tag match */ + private static final RecordLoadable TAG_MATCH = RecordLoadable.create(Loadables.ENTITY_TYPE_TAG.requiredField("tag", t -> t.tag), TagMatch::new); + /** Loadable disallows nested lists, just handles nested tags and sets */ + private static final Loadable COMPOUND = EntityIngredient.loadableBuilder().build(SET_MATCH).list(2).flatXmap(Compound::new, c -> c.ingredients); + /** Loadable for any fluid ingredient */ + public static final Loadable LOADABLE = loadableBuilder().array(COMPOUND).build(SET_MATCH); + + /* Constructors */ /** * Creates an ingredient to match a set of types */ public static EntityIngredient of(Set> set) { + if (set.isEmpty()) { + return EMPTY; + } return new SetMatch(set); } - /** - * Creates an ingredient to match a set of types - */ + /** Creates an ingredient to match a set of types */ public static EntityIngredient of(EntityType ... types) { return of(ImmutableSet.copyOf(types)); } - /** - * Creates an ingredient to match a tags - */ + /** Creates an ingredient to match a tags */ public static EntityIngredient of(TagKey> tag) { return new TagMatch(tag); } - /** - * Creates an ingredient from a list of ingredients - */ + /** Creates an ingredient from a list of ingredients */ public static EntityIngredient of(EntityIngredient... ingredients) { - return new Compound(Arrays.asList(ingredients)); + return of(List.of(ingredients)); } - /** - * Reads an ingredient from the packet buffer - * @param buffer Buffer instance - * @return Ingredient instnace - */ - public static EntityIngredient read(FriendlyByteBuf buffer) { - int count = buffer.readVarInt(); - if (count == 1) { - return new Single(BuiltInRegistries.ENTITY_TYPE.byId(buffer.readVarInt())); + /** Creates an ingredient from a list of ingredients */ + private static EntityIngredient of(List ingredients) { + if (ingredients.isEmpty()) { + return EMPTY; } - List> list = new ArrayList<>(count); - for (int i = 0; i < count; i++) { - list.add(BuiltInRegistries.ENTITY_TYPE.byId(buffer.readVarInt())); + if (ingredients.size() == 1) { + return ingredients.get(0); } - return new SetMatch(ImmutableSet.copyOf(list)); + return new Compound(ingredients); } + + /* Common methods */ + /** - * Finds an entity type for the given key - * @param name Entity type name - * @return Entity type + * Gets a list of entity types matched by this ingredient + * @return List of types */ - private static EntityType findEntityType(ResourceLocation name) { - if (BuiltInRegistries.ENTITY_TYPE.containsKey(name)) { - EntityType type = BuiltInRegistries.ENTITY_TYPE.get(name); - if (type != null) { - return type; - } - } - throw new JsonSyntaxException("Invalid entity type " + name); - } + public abstract Set> getTypes(); /** - * Deserializes an ingredient from JSON - * @param root Json - * @return Ingredient + * Serializes this ingredient to JSON + * @return Json element of this ingredient */ - public static EntityIngredient deserialize(JsonElement root) { - if (root.isJsonArray()) { - JsonArray array = root.getAsJsonArray(); - ImmutableList.Builder builder = ImmutableList.builder(); - for (JsonElement element : array) { - builder.add(deserialize(element)); - } - return new Compound(builder.build()); - } - if (!root.isJsonObject()) { - throw new JsonSyntaxException("Entity ingredient must be either an object or an array"); - } - JsonObject json = root.getAsJsonObject(); + public JsonElement serialize() { + return LOADABLE.serialize(this); + } - // type is just a name - if (json.has("type")) { - ResourceLocation name = new ResourceLocation(GsonHelper.getAsString(json, "type")); - return new Single(findEntityType(name)); - } - // tag is also a name - if (json.has("tag")) { - return new TagMatch(TagKey.create(Registries.ENTITY_TYPE, JsonHelper.getResourceLocation(json, "tag"))); - } - // types is a list - if (json.has("types")) { - List> types = JsonHelper.parseList(json, "types", (element, key) -> findEntityType(new ResourceLocation(GsonHelper.convertToString(element, key)))); - return new SetMatch(ImmutableSet.copyOf(types)); - } - // missed all keys - throw new JsonSyntaxException("Invalid entity type ingredient, must have 'type', 'types', or 'tag'"); + /** Writes this ingredient to the packet buffer */ + public void write(FriendlyByteBuf buffer) { + SET_MATCH.encode(buffer, this); } - /** Ingredient matching a single type */ - @RequiredArgsConstructor - private static class Single extends EntityIngredient { - private final EntityType type; + /** + * Reads an ingredient from the packet buffer + * @param buffer Buffer instance + * @return Ingredient instance + */ + public static EntityIngredient read(FriendlyByteBuf buffer) { + return SET_MATCH.decode(buffer); + } - @Override - public boolean test(EntityType type) { - return type == this.type; - } - @Override - public List> getTypes() { - return Collections.singletonList(type); + /* JEI */ + + private List display; + private List eggs; + + /** Gets the list of eggs matching this ingredient, used for display in JEI as it cannot do entity type */ + public List getDisplay() { + if (display == null) { + display = EntityInput.wrap(getTypes()); } + return display; + } - @Override - public JsonElement serialize() { - JsonObject object = new JsonObject(); - object.addProperty("type", Registry.ENTITY_TYPE.getKey(type).toString()); - return object; + /** Gets the list of eggs matching this ingredient, used for focus links in JEI */ + public List getEggs() { + if (eggs == null) { + // use getDisplay to guarantee order is the same, just in case + eggs = getDisplay().stream().map(type -> new ItemStack(Objects.requireNonNullElse(ForgeSpawnEggItem.fromEntityType(type.type), Items.AIR))).toList(); } + return eggs; } + + + /* Impls */ + /** Ingredient that matches any entity from a set */ @RequiredArgsConstructor private static class SetMatch extends EntityIngredient { private final Set> types; + @Override + public Loadable loadable() { + return types.size() == 1 ? ENTRY_MATCH : SET_MATCH; + } + @Override public boolean test(EntityType type) { return types.contains(type); @@ -197,24 +172,18 @@ public boolean test(EntityType type) { public Set> getTypes() { return types; } - - @Override - public JsonElement serialize() { - JsonObject object = new JsonObject(); - JsonArray array = new JsonArray(); - for (EntityType type : getTypes()) { - array.add(Registry.ENTITY_TYPE.getKey(type).toString()); - } - object.add("types", array); - return object; - } } /** Ingredient that matches any entity from a tag */ @RequiredArgsConstructor private static class TagMatch extends EntityIngredient { private final TagKey> tag; - private List> types; + private Set> types; + + @Override + public Loadable loadable() { + return TAG_MATCH; + } @Override public boolean test(EntityType type) { @@ -222,31 +191,24 @@ public boolean test(EntityType type) { } @Override - public List> getTypes() { + public Set> getTypes() { if (types == null) { - types = StreamSupport.stream(BuiltInRegistries.ENTITY_TYPE.getTagOrEmpty(tag).spliterator(), false) - .filter(Holder::isBound) - .map(Holder::value) - .collect(Collectors.toList()); + types = RegistryHelper.getTagValueStream(BuiltInRegistries.ENTITY_TYPE, tag).collect(ImmutableSet.toImmutableSet()); } return types; } - - @Override - public JsonElement serialize() { - JsonObject object = new JsonObject(); - object.addProperty("tag", tag.location().toString()); - return object; - } } - /** - * Ingredient combining multiple - */ + /** Ingredient combining multiple */ @RequiredArgsConstructor private static class Compound extends EntityIngredient { private final List ingredients; - private List> allTypes; + private Set> allTypes; + + @Override + public Loadable loadable() { + return COMPOUND; + } @Override public boolean test(EntityType type) { @@ -259,23 +221,21 @@ public boolean test(EntityType type) { } @Override - public Collection> getTypes() { + public Set> getTypes() { if (allTypes == null) { allTypes = ingredients.stream() .flatMap(ingredient -> ingredient.getTypes().stream()) - .distinct() - .collect(Collectors.toList()); + .collect(ImmutableSet.toImmutableSet()); } return allTypes; } + } - @Override - public JsonElement serialize() { - JsonArray array = new JsonArray(); - for (EntityIngredient ingredient : ingredients) { - array.add(ingredient.serialize()); - } - return array; + /** Simple wrapper around entity type for usage in JEI */ + public record EntityInput(EntityType type) { + /** Wraps the given list into a list of entity inputs */ + public static List wrap(Collection> types) { + return types.stream().map(EntityInput::new).toList(); } } } diff --git a/src/main/java/slimeknights/mantle/recipe/ingredient/FluidContainerIngredient.java b/src/main/java/slimeknights/mantle/recipe/ingredient/FluidContainerIngredient.java index 9794a4c69..78b2eb90f 100644 --- a/src/main/java/slimeknights/mantle/recipe/ingredient/FluidContainerIngredient.java +++ b/src/main/java/slimeknights/mantle/recipe/ingredient/FluidContainerIngredient.java @@ -145,11 +145,11 @@ public void write(FriendlyByteBuf buffer, FluidContainerIngredient ingredient) { public FluidContainerIngredient read(JsonObject json) { json = json.getAsJsonObject("fluid"); FluidIngredient fluidIngredient; - // if we have fluid, its a nested ingredient. Otherwise this object itself is the ingredient - if (json.has("fluid")) { - fluidIngredient = FluidIngredient.deserialize(json, "fluid"); + // if we have fluid and its not a primitive, then its nested + if (json.has("fluid") && !json.get("fluid").isJsonPrimitive()) { + fluidIngredient = FluidIngredient.LOADABLE.getIfPresent(json, "fluid"); } else { - fluidIngredient = FluidIngredient.deserialize((JsonElement) json, "fluid"); + fluidIngredient = FluidIngredient.LOADABLE.convert(json, "fluid"); } Ingredient display = null; if (json.has("display")) { diff --git a/src/main/java/slimeknights/mantle/recipe/ingredient/FluidIngredient.java b/src/main/java/slimeknights/mantle/recipe/ingredient/FluidIngredient.java index 3ab2c7a9a..96ceece25 100644 --- a/src/main/java/slimeknights/mantle/recipe/ingredient/FluidIngredient.java +++ b/src/main/java/slimeknights/mantle/recipe/ingredient/FluidIngredient.java @@ -1,44 +1,121 @@ package slimeknights.mantle.recipe.ingredient; -import com.google.gson.JsonArray; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; -import com.google.gson.JsonSyntaxException; -import io.github.fabricators_of_create.porting_lib.fluids.FluidStack; import lombok.AccessLevel; import lombok.AllArgsConstructor; -import lombok.NoArgsConstructor; -import net.minecraft.core.Holder; +import lombok.RequiredArgsConstructor; import net.minecraft.core.Registry; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.resources.ResourceLocation; import net.minecraft.tags.TagKey; -import net.minecraft.util.GsonHelper; import net.minecraft.world.level.material.Fluid; import net.minecraft.world.level.material.Fluids; -import slimeknights.mantle.util.JsonHelper; +import net.minecraftforge.fluids.FluidStack; +import slimeknights.mantle.Mantle; +import slimeknights.mantle.data.loadable.IAmLoadable; +import slimeknights.mantle.data.loadable.Loadable; +import slimeknights.mantle.data.loadable.Loadables; +import slimeknights.mantle.data.loadable.common.FluidStackLoadable; +import slimeknights.mantle.data.loadable.mapping.EitherLoadable; +import slimeknights.mantle.data.loadable.primitive.IntLoadable; +import slimeknights.mantle.data.loadable.record.RecordLoadable; +import slimeknights.mantle.util.RegistryHelper; -import java.lang.reflect.Type; -import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; -import java.util.stream.StreamSupport; +/** + * Simple displayable ingredient type for fluids. + */ @SuppressWarnings("unused") -public abstract class FluidIngredient { - /** Empty fluid ingredient, matches nothing */ - public static final FluidIngredient EMPTY = new Empty(); - /** Fluid json serializer instance */ - public static Serializer SERIALIZER = new Serializer(); +public abstract class FluidIngredient implements IAmLoadable { + /** Empty fluid ingredient, matching empty stacks. This ingredient does not parse from JSON, use use defaulting methods if you wish to use it */ + public static final FluidMatch EMPTY = new FluidMatch(Fluids.EMPTY, 0); + + + /* Loadables */ + + /** Creates a builder with set and tag */ + private static EitherLoadable.TypedBuilder loadableBuilder() { + return EitherLoadable.typed().key("fluid", FLUID_MATCH).key("tag", TAG_MATCH).key("name", NAME_MATCH); + } + /** Loadable for network writing of fluids */ + private static final Loadable NETWORK = FluidStackLoadable.REQUIRED_STACK.list(0).flatXmap(fluids -> FluidIngredient.of(fluids.stream().map(FluidIngredient::of).toList()), FluidIngredient::getFluids); + /** Loadable for fluid matches */ + private static final RecordLoadable FLUID_MATCH = RecordLoadable.create(Loadables.FLUID.requiredField("fluid", i -> i.fluid), IntLoadable.FROM_ONE.requiredField("amount", i -> i.amount), FluidIngredient::of); + /** @deprecated Old key for fluid ingredients, remove sometime in 1.20 or 1.21 */ + @Deprecated(forRemoval = true) + private static final RecordLoadable NAME_MATCH = RecordLoadable.create(Loadables.FLUID.requiredField("name", i -> i.fluid), IntLoadable.FROM_ONE.requiredField("amount", i -> i.amount), (fluid, amount) -> { + // TODO: is there a good way to get recipe context here? Cannot think of a way short of a global static context. + Mantle.logger.warn("Using deprecated key 'name' for fluid ingredient, use 'fluid' instead. This will be removed in the future"); + return FluidIngredient.of(fluid, amount); + }); + /** Loadable for tag matches */ + private static final RecordLoadable TAG_MATCH = RecordLoadable.create(Loadables.FLUID_TAG.requiredField("tag", i -> i.tag), IntLoadable.FROM_ONE.requiredField("amount", i -> i.amount), FluidIngredient::of); + /** Loadable for tag matches */ + private static final Loadable COMPOUND = loadableBuilder().build(NETWORK).list(2).flatXmap(Compound::new, c -> c.ingredients); + /** Loadable for any fluid ingredient */ + public static final Loadable LOADABLE = loadableBuilder().array(COMPOUND).build(NETWORK); + + + /* Constructors */ + + /** + * Creates a new ingredient using the given fluid and amount + * @param fluid Fluid to check + * @param amount Minimum fluid amount + * @return Fluid ingredient for this fluid + */ + public static FluidMatch of(Fluid fluid, int amount) { + if (fluid == Fluids.EMPTY || amount <= 0) { + return EMPTY; + } + return new FluidMatch(fluid, amount); + } + + /** + * Creates a new ingredient using the given fluidstack + * @param stack Fluid stack + * @return Fluid ingredient for this fluid stack + */ + public static FluidIngredient of(FluidStack stack) { + return of(stack.getFluid(), stack.getAmount()); + } + + /** + * Creates a new fluid ingredient from the given tag + * @param fluid Fluid tag + * @param amount Minimum fluid amount + * @return Fluid ingredient from a tag + */ + public static TagMatch of(TagKey fluid, int amount) { + return new TagMatch(fluid, amount); + } + + /** + * Creates a new compound ingredient from the given list of ingredients + * @param ingredients Ingredient list + * @return Compound ingredient + */ + public static FluidIngredient of(FluidIngredient... ingredients) { + return of(List.of(ingredients)); + } + + /** + * Creates a new compound ingredient from the given list of ingredients + * @param ingredients Ingredient list + * @return Compound ingredient + */ + public static FluidIngredient of(List ingredients) { + if (ingredients.size() == 1) { + return ingredients.get(0); + } + return new Compound(ingredients); + } + /** Cached list of display fluids */ private List displayFluids; @@ -91,198 +168,53 @@ public List getFluids() { * Serializes the Fluid Ingredient into JSON * @return FluidIngredient JSON */ - public abstract JsonElement serialize(); - - /** - * Writes the ingredient into the packet buffer - * @param buffer Packet buffer instance - */ - public void write(FriendlyByteBuf buffer) { - Collection fluids = getAllFluids(); - buffer.writeInt(fluids.size()); - for (FluidStack stack : fluids) { - buffer.writeUtf(Registry.FLUID.getKey(stack.getFluid()).toString()); - buffer.writeLong(stack.getAmount()); - } + public JsonElement serialize() { + return LOADABLE.serialize(this); } - - /* - * Instance creation - */ - - /** - * Creates a new ingredient using the given fluid and amount - * @param fluid Fluid to check - * @param amount Minimum fluid amount - * @return Fluid ingredient for this fluid - */ - public static FluidIngredient of(Fluid fluid, long amount) { - return new FluidIngredient.FluidMatch(fluid, amount); + /** Gets the fluid ingredient from the parent and deserializes it */ + public static FluidIngredient deserialize(JsonObject parent, String key) { + return LOADABLE.getIfPresent(parent, key); } - /** - * Creates a new ingredient using the given fluidstack - * @param stack Fluid stack - * @return Fluid ingredient for this fluid stack - */ - public static FluidIngredient of(FluidStack stack) { - return of(stack.getFluid(), stack.getAmount()); - } - - /** - * Creates a new fluid ingredient from the given tag - * @param fluid Fluid tag - * @param amount Minimum fluid amount - * @return Fluid ingredient from a tag - */ - public static FluidIngredient of(TagKey fluid, long amount) { - return new FluidIngredient.TagMatch(fluid, amount); - } - - /** - * Creates a new compound ingredient from the given list of ingredients - * @param ingredients Ingredient list - * @return Compound ingredient - */ - public static FluidIngredient of(FluidIngredient... ingredients) { - return new FluidIngredient.Compound(ingredients); - } - - - /* - * JSON deserializing - */ - - /** - * Deserializes the fluid ingredient from JSON - * @param parent Parent containing the fluid JSON - * @param name Name of the key to fetch from the parent object - * @return Fluid ingredient instance - * @throws JsonSyntaxException if syntax is invalid - */ - public static FluidIngredient deserialize(JsonObject parent, String name) { - return deserialize(JsonHelper.getElement(parent, name), name); + /** @deprecated use {@link #LOADABLE} with {@link Loadable#convert(JsonElement, String)} */ + @Deprecated + public static FluidIngredient deserialize(JsonElement element, String key) { + return LOADABLE.convert(element, key); } /** - * Deserializes the fluid ingredient from JSON - * @param json Json element instance - * @param name Name of the object for error messages - * @return Fluid ingredient instance - * @throws JsonSyntaxException if syntax is invalid - */ - public static FluidIngredient deserialize(JsonElement json, String name) { - // single ingredient object - if (json.isJsonObject()) { - return deserializeObject(json.getAsJsonObject()); - } - - // array - if (json.isJsonArray()) { - return Compound.deserialize(json.getAsJsonArray(), name); - } - - throw new JsonSyntaxException("Fluid ingredient " + name + " must be either an object or array"); - } - - /** - * Deserializes the fluid ingredient from JSON - * @param json JSON object - * @return Fluid Ingredient - * @throws JsonSyntaxException if syntax is invalid + * Writes the ingredient into the packet buffer + * @param buffer Packet buffer instance */ - private static FluidIngredient deserializeObject(JsonObject json) { - if (json.entrySet().isEmpty()) { - return EMPTY; - } - - // fluid match - if (json.has("name")) { - // don't set both, obviously an error - if (json.has("tag")) { - throw new JsonSyntaxException("An ingredient entry is either a tag or an fluid, not both"); - } - - // parse a fluid - return FluidMatch.deserialize(json); - } - - // tag match - if (json.has("tag")) { - return TagMatch.deserialize(json); - } - - throw new JsonSyntaxException("An ingredient entry needs either a tag or an fluid"); + public void write(FriendlyByteBuf buffer) { + NETWORK.encode(buffer, this); } - - /* - * Packet buffers - */ - /** * Reads a fluid ingredient from the packet buffer * @param buffer Buffer instance * @return Fluid ingredient instance */ public static FluidIngredient read(FriendlyByteBuf buffer) { - int count = buffer.readInt(); - FluidIngredient[] ingredients = new FluidIngredient[count]; - for (int i = 0; i < count; i++) { - Fluid fluid = BuiltInRegistries.FLUID.get(new ResourceLocation(buffer.readUtf(32767))); - if (fluid == null) { - fluid = Fluids.EMPTY; - } - long amount = buffer.readLong(); - ingredients[i] = of(fluid, amount); - } - // if a single ingredient, do not wrap in compound - if (count == 1) { - return ingredients[0]; - } - // compound for anything else - return of(ingredients); + return NETWORK.decode(buffer); } - /** - * Empty fluid ingredient, matches only empty fluid stacks - */ - @NoArgsConstructor(access = AccessLevel.PRIVATE) - private static class Empty extends FluidIngredient { - @Override - public boolean test(Fluid fluid) { - return fluid == Fluids.EMPTY; - } - @Override - public boolean test(FluidStack fluid) { - return fluid.isEmpty(); - } - - @Override - public long getAmount(Fluid fluid) { - return 0; - } - - @Override - public List getAllFluids() { - return Collections.emptyList(); - } - - @Override - public JsonElement serialize() { - return new JsonObject(); - } - } /** * Fluid ingredient that matches a single fluid */ @AllArgsConstructor(access=AccessLevel.PRIVATE) private static class FluidMatch extends FluidIngredient { + private final Fluid fluid; private final long amount; + @Override + public Loadable loadable() { + return FLUID_MATCH; + } + @Override public boolean test(Fluid fluid) { return fluid == this.fluid; @@ -297,48 +229,21 @@ public long getAmount(Fluid fluid) { public List getAllFluids() { return Collections.singletonList(new FluidStack(fluid, amount)); } - - @Override - public JsonElement serialize() { - JsonObject object = new JsonObject(); - object.addProperty("name", Registry.FLUID.getKey(fluid).toString()); - object.addProperty("amount", amount); - return object; - } - - @Override - public void write(FriendlyByteBuf buffer) { - // count - buffer.writeInt(1); - // single fluid - buffer.writeUtf(Registry.FLUID.getKey(fluid).toString()); - buffer.writeLong(amount); - } - - /** - * Deserailizes the ingredient from JSON - * @param json JSON object - * @return Fluid ingredient instance - */ - private static FluidMatch deserialize(JsonObject json) { - String fluidName = GsonHelper.getAsString(json, "name"); - Fluid fluid = BuiltInRegistries.FLUID.get(new ResourceLocation(fluidName)); - if (fluid == null || fluid == Fluids.EMPTY) { - throw new JsonSyntaxException("Unknown fluid '" + fluidName + "'"); - } - long amount = GsonHelper.getAsLong(json, "amount"); - return new FluidMatch(fluid, amount); - } } /** * Fluid ingredient that matches a tag */ - @AllArgsConstructor(access=AccessLevel.PRIVATE) + @AllArgsConstructor private static class TagMatch extends FluidIngredient { private final TagKey tag; private final long amount; + @Override + public Loadable loadable() { + return TAG_MATCH; + } + @Override public boolean test(Fluid fluid) { return fluid.is(tag); @@ -351,58 +256,52 @@ public long getAmount(Fluid fluid) { @Override public List getAllFluids() { - return StreamSupport.stream(BuiltInRegistries.FLUID.getTagOrEmpty(tag).spliterator(), false) - .filter(Holder::isBound) - .map(fluid -> new FluidStack(fluid.value(), amount)) + return RegistryHelper.getTagValueStream(BuiltInRegistries.FLUID, tag) + .map(fluid -> new FluidStack(fluid, amount)) .toList(); } - - @Override - public JsonElement serialize() { - JsonObject object = new JsonObject(); - object.addProperty("tag", this.tag.location().toString()); - object.addProperty("amount", amount); - return object; - } - - /** - * Deseralizes the ingredient from JSON - * @param json JSON object - * @return Fluid ingredient instance - */ - private static TagMatch deserialize(JsonObject json) { - TagKey tag = TagKey.create(Registries.FLUID, JsonHelper.getResourceLocation(json, "tag")); - long amount = GsonHelper.getAsLong(json, "amount"); - return new TagMatch(tag, amount); - } } /** * Fluid ingredient that matches a list of ingredients */ + @RequiredArgsConstructor private static class Compound extends FluidIngredient { private final List ingredients; - private Compound(FluidIngredient[] ingredients) { - this.ingredients = Arrays.asList(ingredients); + + @Override + public Loadable loadable() { + return COMPOUND; } @Override public boolean test(Fluid fluid) { - return ingredients.stream().anyMatch(ingredient -> ingredient.test(fluid)); + for (FluidIngredient ingredient : ingredients) { + if (ingredient.test(fluid)) { + return true; + } + } + return false; } @Override public boolean test(FluidStack stack) { - return ingredients.stream().anyMatch(ingredient -> ingredient.test(stack)); + for (FluidIngredient ingredient : ingredients) { + if (ingredient.test(stack)) { + return true; + } + } + return false; } @Override - public long getAmount(Fluid fluid) { - return ingredients.stream() - .filter(ingredient -> ingredient.test(fluid)) - .mapToLong(ingredient -> ingredient.getAmount(fluid)) - .findFirst() - .orElse(0); + public int getAmount(Fluid fluid) { + for (FluidIngredient ingredient : ingredients) { + if (ingredient.test(fluid)) { + return ingredient.getAmount(fluid); + } + } + return 0; } @Override @@ -411,49 +310,5 @@ public List getAllFluids() { .flatMap(ingredient -> ingredient.getFluids().stream()) .collect(Collectors.toList()); } - - @Override - public JsonElement serialize() { - return ingredients.stream() - .map(FluidIngredient::serialize) - .collect(JsonArray::new, JsonArray::add, JsonArray::addAll); - } - - /** - * Deserializes a compound ingredient from JSON - * @param array JSON array - * @param name Array key - * @return Compound fluid ingredient instance - */ - private static Compound deserialize(JsonArray array, String name) { - // size must be valid - int size = array.size(); - if (size == 0) { - throw new JsonSyntaxException("Fluid array cannot be empty, at least one fluid must be defined"); - } - - // parse all ingredients - FluidIngredient[] ingredients = new FluidIngredient[size]; - for (int i = 0; i < size; i++) { - // no reason to an array in an array - ingredients[i] = deserializeObject(GsonHelper.convertToJsonObject(array.get(i), name + "[" + i + "]")); - } - return new Compound(ingredients); - } - } - - /** Json serializer for fluids */ - public static class Serializer implements JsonDeserializer, JsonSerializer { - private Serializer() {} - - @Override - public FluidIngredient deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - return FluidIngredient.deserialize(json, "ingredient"); - } - - @Override - public JsonElement serialize(FluidIngredient src, Type typeOfSrc, JsonSerializationContext context) { - return src.serialize(); - } } } diff --git a/src/main/java/slimeknights/mantle/recipe/ingredient/SizedIngredient.java b/src/main/java/slimeknights/mantle/recipe/ingredient/SizedIngredient.java index e356b7ca8..7c5da34f4 100644 --- a/src/main/java/slimeknights/mantle/recipe/ingredient/SizedIngredient.java +++ b/src/main/java/slimeknights/mantle/recipe/ingredient/SizedIngredient.java @@ -1,17 +1,17 @@ package slimeknights.mantle.recipe.ingredient; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; import lombok.Getter; import lombok.RequiredArgsConstructor; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.tags.TagKey; -import net.minecraft.util.GsonHelper; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.Ingredient; import net.minecraft.world.level.ItemLike; -import slimeknights.mantle.util.JsonHelper; +import slimeknights.mantle.data.loadable.common.IngredientLoadable; +import slimeknights.mantle.data.loadable.primitive.IntLoadable; +import slimeknights.mantle.data.loadable.record.RecordLoadable; import java.lang.ref.WeakReference; import java.util.Arrays; @@ -27,6 +27,11 @@ public class SizedIngredient implements Predicate { /** Empty sized ingredient wrapper. Matches only the empty stack of size 0 */ public static final SizedIngredient EMPTY = of(Ingredient.EMPTY, 0); + public static final RecordLoadable LOADABLE = RecordLoadable.create( + IngredientLoadable.DISALLOW_EMPTY.tryDirectField("ingredient", SizedIngredient::getIngredient, "amount_needed"), + IntLoadable.FROM_ONE.defaultField("amount_needed", 1, SizedIngredient::getAmountNeeded), + SizedIngredient::new); + /** Ingredient to use in recipe match */ @Getter private final Ingredient ingredient; @@ -95,7 +100,7 @@ public boolean test(ItemStack stack) { * Checks if the ingredient has no matching stacks * @return True if the ingredient has no matching stacks */ - public boolean hasNoMatchingStacks() { + public boolean isEmpty() { return ingredient.isEmpty(); } @@ -124,8 +129,7 @@ public List getMatchingStacks() { * @param buffer Buffer instance */ public void write(FriendlyByteBuf buffer) { - buffer.writeVarInt(amountNeeded); - ingredient.toNetwork(buffer); + LOADABLE.encode(buffer, this); } /** @@ -133,25 +137,8 @@ public void write(FriendlyByteBuf buffer) { * @return JsonObject of sized ingredient */ public JsonObject serialize() { - JsonElement ingredient = this.ingredient.toJson(); - JsonObject json = null; - // try using the object itself as our JSON - if (ingredient.isJsonObject()) { - json = ingredient.getAsJsonObject(); - // if it has a property conflict, do nested - if (json.has("ingredient") || json.has("amount_needed")) { - json = null; - } - } - // if we could not use the ingredient, nest it - if (json == null) { - json = new JsonObject(); - json.add("ingredient", ingredient); - } - // add amount needed and return - if (amountNeeded != 1) { - json.addProperty("amount_needed", amountNeeded); - } + JsonObject json = new JsonObject(); + LOADABLE.serialize(this, json); return json; } @@ -161,9 +148,7 @@ public JsonObject serialize() { * @return Sized ingredient */ public static SizedIngredient read(FriendlyByteBuf buffer) { - int amountNeeded = buffer.readVarInt(); - Ingredient ingredient = Ingredient.fromNetwork(buffer); - return of(ingredient, amountNeeded); + return LOADABLE.decode(buffer); } /** @@ -172,16 +157,6 @@ public static SizedIngredient read(FriendlyByteBuf buffer) { * @return Sized ingredient */ public static SizedIngredient deserialize(JsonObject json) { - int amountNeeded = GsonHelper.getAsInt(json, "amount_needed", 1); - // if we have a nested value, read as nested - Ingredient ingredient; - if (json.has("ingredient")) { - ingredient = Ingredient.fromJson(JsonHelper.getElement(json, "ingredient")); - } else { - ingredient = Ingredient.fromJson(json); - } - - // return ingredient - return of(ingredient, amountNeeded); + return LOADABLE.deserialize(json); } } diff --git a/src/main/java/slimeknights/mantle/util/JsonHelper.java b/src/main/java/slimeknights/mantle/util/JsonHelper.java index 2ae7921ba..1bc7a1813 100644 --- a/src/main/java/slimeknights/mantle/util/JsonHelper.java +++ b/src/main/java/slimeknights/mantle/util/JsonHelper.java @@ -7,19 +7,20 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; -import com.google.gson.JsonPrimitive; import com.google.gson.JsonSyntaxException; -import net.minecraft.core.Registry; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.packs.resources.Resource; import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.util.GsonHelper; -import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.block.state.StateDefinition; -import net.minecraft.world.level.block.state.properties.Property; +import net.minecraftforge.event.OnDatapackSyncEvent; +import net.minecraftforge.network.PacketDistributor; +import net.minecraftforge.network.PacketDistributor.PacketTarget; +import net.minecraftforge.registries.IForgeRegistry; import slimeknights.mantle.Mantle; +import slimeknights.mantle.data.loadable.common.BlockStateLoadable; +import slimeknights.mantle.data.loadable.common.ColorLoadable; import slimeknights.mantle.network.NetworkWrapper; import slimeknights.mantle.network.packet.ISimplePacket; @@ -28,9 +29,7 @@ import java.io.Reader; import java.util.List; import java.util.Locale; -import java.util.Map.Entry; import java.util.Objects; -import java.util.Optional; import java.util.function.BiFunction; import java.util.function.Function; @@ -158,7 +157,7 @@ public static ResourceLocation convertToResourceLocation(JsonElement json, Strin String text = GsonHelper.convertToString(json, key); ResourceLocation location = ResourceLocation.tryParse(text); if (location == null) { - throw new JsonSyntaxException("Expected " + key + " to be a Resource location, was '" + text + "'"); + throw new JsonSyntaxException("Expected " + key + " to be a resource location, was '" + text + "'"); } return location; } @@ -222,27 +221,14 @@ public static > T getAsEnum(JsonObject json, String key, Class * Parses a color as a string * @param color Color to parse * @return Parsed string + * @deprecated use {@link ColorLoadable#parseString(String, String)} */ + @Deprecated(forRemoval = true) public static int parseColor(@Nullable String color) { if (color == null || color.isEmpty()) { return -1; } - // two options, 6 character or 8 character, must not start with - sign - if (color.charAt(0) != '-') { - try { - // length of 8 must parse as long, supports transparency - int length = color.length(); - if (length == 8) { - return (int)Long.parseLong(color, 16); - } - if (length == 6) { - return 0xFF000000 | Integer.parseInt(color, 16); - } - } catch (NumberFormatException ex) { - // NO-OP - } - } - throw new JsonSyntaxException("Invalid color '" + color + "'"); + return ColorLoadable.ALPHA.parseString(color, "[unknown]"); } @@ -299,6 +285,28 @@ public static void syncPackets(ServerPlayer targetedPlayer, boolean joined, Netw sendPackets(network, targetedPlayer, packets); } + /** + * Localizes the given resource location to one within the folder + * @param path Path to localize + * @param folder Folder to trim (without trailing /), it is not validated so make sure you call correctly + * @param extension Extension to trim + * @return Localized location + */ + public static String localize(String path, String folder, String extension) { + return path.substring(folder.length() + 1, path.length() - extension.length()); + } + + /** + * Localizes the given resource location to one within the folder + * @param location Location to localize + * @param folder Folder to trim (without trailing /), it is not validated so make sure you call correctly + * @param extension Extension to trim + * @return Localized location + */ + public static ResourceLocation localize(ResourceLocation location, String folder, String extension) { + return new ResourceLocation(location.getNamespace(), localize(location.getPath(), folder, extension)); + } + /* Block States */ @@ -310,14 +318,7 @@ public static void syncPackets(ServerPlayer targetedPlayer, boolean joined, Netw * @throws JsonSyntaxException if a property does not parse or the element is the wrong type */ public static BlockState convertToBlockState(JsonElement element, String key) { - // primitive means its a block directly - if (element.isJsonPrimitive()) { - return JsonHelper.convertToEntry(BuiltInRegistries.BLOCK, element, key).defaultBlockState(); - } - if (element.isJsonObject()) { - return convertToBlockState(element.getAsJsonObject()); - } - throw new JsonSyntaxException("Expected " + key + " to be a string or an object, was " + GsonHelper.getType(element)); + return BlockStateLoadable.DIFFERENCE.convert(element, key); } /** @@ -328,27 +329,7 @@ public static BlockState convertToBlockState(JsonElement element, String key) { * @throws JsonSyntaxException if a property does not parse or the element is missing or the wrong type */ public static BlockState getAsBlockState(JsonObject parent, String key) { - if (parent.has(key)) { - return convertToBlockState(parent.get(key), key); - } - throw new JsonSyntaxException("Missing " + key + ", expected to find a string or an object"); - } - - /** - * Sets the property - * @param state State before changes - * @param property Property to set - * @param name Value name - * @param Type of property - * @return State with the property - * @throws JsonSyntaxException if the property has no element with the given name - */ - private static > BlockState setValue(BlockState state, Property property, String name) { - Optional value = property.getValue(name); - if (value.isPresent()) { - return state.setValue(property, value.get()); - } - throw new JsonSyntaxException("Property " + property + " does not contain value " + name); + return BlockStateLoadable.DIFFERENCE.getIfPresent(parent, key); } /** @@ -358,20 +339,7 @@ private static > BlockState setValue(BlockState state, P * @throws JsonSyntaxException if any property name or property value is invalid */ public static BlockState convertToBlockState(JsonObject json) { - Block block = JsonHelper.getAsEntry(BuiltInRegistries.BLOCK, json, "block"); - BlockState state = block.defaultBlockState(); - if (json.has("properties")) { - StateDefinition definition = block.getStateDefinition(); - for (Entry entry : GsonHelper.getAsJsonObject(json, "properties").entrySet()) { - String key = entry.getKey(); - Property property = definition.getProperty(key); - if (property == null) { - throw new JsonSyntaxException("Property " + key + " does not exist in block " + block); - } - state = setValue(state, property, GsonHelper.convertToString(entry.getValue(), key)); - } - } - return state; + return BlockStateLoadable.DIFFERENCE.deserialize(json); } /** @@ -380,19 +348,7 @@ public static BlockState convertToBlockState(JsonObject json) { * @return JsonPrimitive of the block name if it matches the default state, JsonObject otherwise */ public static JsonElement serializeBlockState(BlockState state) { - Block block = state.getBlock(); - if (state == block.defaultBlockState()) { - return new JsonPrimitive(Registry.BLOCK.getKey(block).toString()); - } - return serializeBlockState(state, new JsonObject()); - } - - /** Serializes the property if it differs in the default state */ - private static > void serializeProperty(BlockState serialize, Property property, BlockState defaultState, JsonObject json) { - T value = serialize.getValue(property); - if (!value.equals(defaultState.getValue(property))) { - json.addProperty(property.getName(), property.getName(value)); - } + return BlockStateLoadable.DIFFERENCE.serialize(state); } /** @@ -401,16 +357,7 @@ private static > void serializeProperty(BlockState seria * @return JsonObject containing properties that differ from the default state */ public static JsonObject serializeBlockState(BlockState state, JsonObject json) { - Block block = state.getBlock(); - json.addProperty("block", Registry.BLOCK.getKey(block).toString()); - BlockState defaultState = block.defaultBlockState(); - JsonObject properties = new JsonObject(); - for (Property property : block.getStateDefinition().getProperties()) { - serializeProperty(state, property, defaultState, properties); - } - if (properties.size() > 0) { - json.add("properties", properties); - } + BlockStateLoadable.DIFFERENCE.serialize(state, json); return json; } } diff --git a/src/main/java/slimeknights/mantle/util/typed/BackedTypedMap.java b/src/main/java/slimeknights/mantle/util/typed/BackedTypedMap.java new file mode 100644 index 000000000..13dbe0318 --- /dev/null +++ b/src/main/java/slimeknights/mantle/util/typed/BackedTypedMap.java @@ -0,0 +1,71 @@ +package slimeknights.mantle.util.typed; + +import javax.annotation.Nullable; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; + +/** + * Typed map backed by a map. It is the callers responsibility to ensure the passed map contains valid key to value pairs. + * Note this class implements {@link MutableTypedMap}, but it will throw if the backing map is not mutable. It is the caller's responsibility to use the correct interface for non-mutable maps. + */ +@SuppressWarnings("unchecked") // the nature of this map means we inherently have unchecked operations +public record BackedTypedMap(Map, Object> map) implements MutableTypedMap { + /** Creates a new mutable backed map */ + public BackedTypedMap() { + // using identity as keys are typically identity objects, if you have non-identity keys you can use the regular constructor. + this(new IdentityHashMap<>()); + } + + @Override + public int size() { + return map.size(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public boolean containsKey(Key key) { + return map.containsKey(key); + } + + @Nullable + @Override + public K get(Key key) { + return (K) map.get(key); + } + + @Override + public Set> keySet() { + return map.keySet(); + } + + @Nullable + @Override + public R getOrDefault(Key key, @Nullable R defaultValue) { + return (R) map.getOrDefault(key, defaultValue); + } + + @Override + public void put(Key key, K value) { + map.put(key, value); + } + + @Override + public void remove(Key key) { + map.remove(key); + } + + @Override + public void clear() { + map.clear(); + } + + @Override + public K computeIfAbsent(ComputingKey key) { + return (K) map.computeIfAbsent(key, k -> key.get()); + } +} diff --git a/src/main/java/slimeknights/mantle/util/typed/MutableTypedMap.java b/src/main/java/slimeknights/mantle/util/typed/MutableTypedMap.java new file mode 100644 index 000000000..928f518e6 --- /dev/null +++ b/src/main/java/slimeknights/mantle/util/typed/MutableTypedMap.java @@ -0,0 +1,30 @@ +package slimeknights.mantle.util.typed; + +import java.util.function.Supplier; + +/** + * Extension of {@link TypedMap} for a map that can be actively modified + */ +public interface MutableTypedMap extends TypedMap { + /** Adds the given value to the map */ + void put(Key key, K value); + + /** Gets the value from the map, computing it using the key if absent */ + default K computeIfAbsent(ComputingKey key) { + K value = get(key); + if (value == null) { + value = key.get(); + put(key, value); + } + return value; + } + + /** Removes the entry associated with the given key */ + void remove(Key key); + + /** Removes all keys from the map */ + void clear(); + + /** Key which has a value to compute if missing */ + interface ComputingKey extends Key, Supplier {} +} diff --git a/src/main/java/slimeknights/mantle/util/typed/TypedMap.java b/src/main/java/slimeknights/mantle/util/typed/TypedMap.java new file mode 100644 index 000000000..967da8ddb --- /dev/null +++ b/src/main/java/slimeknights/mantle/util/typed/TypedMap.java @@ -0,0 +1,73 @@ +package slimeknights.mantle.util.typed; + +import javax.annotation.Nullable; +import java.util.Set; + +/** + * Interface for a map where keys are typed so the resulting value is typed. This interface is for a read only map, see {@link MutableTypedMap} for a modifiable variant. + */ +public interface TypedMap { + /** Gets the number of entries in the map */ + int size(); + + /** Checks if the map has no values */ + boolean isEmpty(); + + /** Checks if the map contains the given key */ + boolean containsKey(Key key); + + /** Gets the of the type of key from the map, or the default value if missing */ + @Nullable + R getOrDefault(Key key, @Nullable R defaultValue); + + /** Gets the value from the map, or null if missing */ + @Nullable + default K get(Key key) { + return getOrDefault(key, null); + } + + /** Gets a set of all keys in the map */ + Set> keySet(); + + /** + * Interface for a typed key + * @param Type of the return value from the map + */ + @SuppressWarnings("unused") // Key is used by the typed map for validation + interface Key {} + + + /** Empty instance */ + TypedMap EMPTY = new TypedMap() { + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public boolean containsKey(Key key) { + return false; + } + + @Nullable + @Override + public R getOrDefault(Key key, @Nullable R defaultValue) { + return null; + } + + @Override + public Set> keySet() { + return Set.of(); + } + }; + + /** Gets an empty map for the given type */ + static TypedMap empty() { + return EMPTY; + } +} diff --git a/src/main/java/slimeknights/mantle/util/typed/TypedMapBuilder.java b/src/main/java/slimeknights/mantle/util/typed/TypedMapBuilder.java new file mode 100644 index 000000000..c7030717b --- /dev/null +++ b/src/main/java/slimeknights/mantle/util/typed/TypedMapBuilder.java @@ -0,0 +1,30 @@ +package slimeknights.mantle.util.typed; + +import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import lombok.NoArgsConstructor; +import slimeknights.mantle.util.typed.TypedMap.Key; + +import java.util.Map; + +/** Builder for a typed map, ensures key value pairs are consistent */ +@NoArgsConstructor(staticName = "builder") +public class TypedMapBuilder { + private final ImmutableMap.Builder,Object> builder = ImmutableMap.builder(); + + /** Adds a value to the map */ + @CanIgnoreReturnValue + public TypedMapBuilder put(Key key, K value) { + builder.put(key, value); + return this; + } + + /** Builds the final map */ + public TypedMap build() { + Map,Object> map = builder.build(); + if (map.isEmpty()) { + return TypedMap.empty(); + } + return new BackedTypedMap(map); + } +} diff --git a/src/main/java/slimeknights/mantle/data/loader/package-info.java b/src/main/java/slimeknights/mantle/util/typed/package-info.java similarity index 80% rename from src/main/java/slimeknights/mantle/data/loader/package-info.java rename to src/main/java/slimeknights/mantle/util/typed/package-info.java index 00338570d..7fe2c20d4 100644 --- a/src/main/java/slimeknights/mantle/data/loader/package-info.java +++ b/src/main/java/slimeknights/mantle/util/typed/package-info.java @@ -1,6 +1,6 @@ @ParametersAreNonnullByDefault @MethodsReturnNonnullByDefault -package slimeknights.mantle.data.loader; +package slimeknights.mantle.util.typed; import net.minecraft.MethodsReturnNonnullByDefault; diff --git a/src/main/resources/assets/mantle/textures/item/missingno.png b/src/main/resources/assets/mantle/textures/item/missingno.png new file mode 100644 index 000000000..1bbf4ebc5 Binary files /dev/null and b/src/main/resources/assets/mantle/textures/item/missingno.png differ