Skip to content

Commit

Permalink
Implement full maps in loadables
Browse files Browse the repository at this point in the history
To make this work, added a new sub-interface of Loadable: StringLoadable. Represents any loadable that can be written directly into a string or read from a string.
MapLoadables require a string loadable as a key, but accept any loadable as a value. Intentionally did not implement RecordLoadable for them as the map keys will conflict with other objects
Strings, resource locations, booleans, enums, colors, and tag keys all implement StringLoadable and work as keys
Registries and named component registry both implement StringLoadable via the helper ResourceLocationLoadable
IntLoadable does not implement StringLoadable, but has a new asString() method to create a loadable for strings as int, with a radix argument.
Since StringLoadable used to be a record for different max lengths, that logic was moved to a package private class, can be built using a static helper on StringLoadable
  • Loading branch information
KnightMiner committed Mar 28, 2024
1 parent b8d0147 commit 24eb72c
Show file tree
Hide file tree
Showing 13 changed files with 400 additions and 125 deletions.
53 changes: 26 additions & 27 deletions src/main/java/slimeknights/mantle/data/loadable/Loadables.java
Expand Up @@ -31,54 +31,53 @@ public class Loadables {
private Loadables() {}

/** Loadable for a resource location */
public static final Loadable<ResourceLocation> RESOURCE_LOCATION = StringLoadable.DEFAULT.map((s, e) -> {
public static final StringLoadable<ResourceLocation> RESOURCE_LOCATION = StringLoadable.DEFAULT.map((s, e) -> {
try {
return new ResourceLocation(s);
} catch (ResourceLocationException ex) {
throw e.create(ex);
}
}, (r, e) -> r.toString());
public static final Loadable<ToolAction> TOOL_ACTION = StringLoadable.DEFAULT.flatMap(ToolAction::get, ToolAction::name);
public static final StringLoadable<ToolAction> TOOL_ACTION = StringLoadable.DEFAULT.flatMap(ToolAction::get, ToolAction::name);

/* Registries */
public static final Loadable<SoundEvent> SOUND_EVENT = new RegistryLoadable<>(Registry.SOUND_EVENT);
public static final Loadable<Fluid> FLUID = new RegistryLoadable<>(Registry.FLUID);
public static final Loadable<MobEffect> MOB_EFFECT = new RegistryLoadable<>(Registry.MOB_EFFECT);
public static final Loadable<Block> BLOCK = new RegistryLoadable<>(Registry.BLOCK);
public static final Loadable<Enchantment> ENCHANTMENT = new RegistryLoadable<>(Registry.ENCHANTMENT);
public static final Loadable<EntityType<?>> ENTITY_TYPE = new RegistryLoadable<>(Registry.ENTITY_TYPE);
public static final Loadable<Item> ITEM = new RegistryLoadable<>(Registry.ITEM);
public static final Loadable<Potion> POTION = new RegistryLoadable<>(Registry.POTION);
public static final Loadable<ParticleType<?>> PARTICLE_TYPE = new RegistryLoadable<>(Registry.PARTICLE_TYPE);
public static final Loadable<BlockEntityType<?>> BLOCK_ENTITY_TYPE = new RegistryLoadable<>(Registry.BLOCK_ENTITY_TYPE);
public static final Loadable<Attribute> ATTRIBUTE = new RegistryLoadable<>(Registry.ATTRIBUTE);
public static final StringLoadable<SoundEvent> SOUND_EVENT = new RegistryLoadable<>(Registry.SOUND_EVENT);
public static final StringLoadable<Fluid> FLUID = new RegistryLoadable<>(Registry.FLUID);
public static final StringLoadable<MobEffect> MOB_EFFECT = new RegistryLoadable<>(Registry.MOB_EFFECT);
public static final StringLoadable<Block> BLOCK = new RegistryLoadable<>(Registry.BLOCK);
public static final StringLoadable<Enchantment> ENCHANTMENT = new RegistryLoadable<>(Registry.ENCHANTMENT);
public static final StringLoadable<EntityType<?>> ENTITY_TYPE = new RegistryLoadable<>(Registry.ENTITY_TYPE);
public static final StringLoadable<Item> ITEM = new RegistryLoadable<>(Registry.ITEM);
public static final StringLoadable<Potion> POTION = new RegistryLoadable<>(Registry.POTION);
public static final StringLoadable<ParticleType<?>> PARTICLE_TYPE = new RegistryLoadable<>(Registry.PARTICLE_TYPE);
public static final StringLoadable<BlockEntityType<?>> BLOCK_ENTITY_TYPE = new RegistryLoadable<>(Registry.BLOCK_ENTITY_TYPE);
public static final StringLoadable<Attribute> ATTRIBUTE = new RegistryLoadable<>(Registry.ATTRIBUTE);

/* Non-default registries */
public static final Loadable<Fluid> NON_EMPTY_FLUID = notValue(FLUID, Fluids.EMPTY, "Fluid cannot be empty");
public static final Loadable<Block> NON_EMPTY_BLOCK = notValue(BLOCK, Blocks.AIR, "Block cannot be air");
public static final Loadable<Item> NON_EMPTY_ITEM = notValue(ITEM, Items.AIR, "Item cannot be empty");

public static final StringLoadable<Fluid> NON_EMPTY_FLUID = notValue(FLUID, Fluids.EMPTY, "Fluid cannot be empty");
public static final StringLoadable<Block> NON_EMPTY_BLOCK = notValue(BLOCK, Blocks.AIR, "Block cannot be air");
public static final StringLoadable<Item> NON_EMPTY_ITEM = notValue(ITEM, Items.AIR, "Item cannot be empty");

/* Tag keys */
public static final Loadable<TagKey<Fluid>> FLUID_TAG = tagKey(Registry.FLUID_REGISTRY);
public static final Loadable<TagKey<MobEffect>> MOB_EFFECT_TAG = tagKey(Registry.MOB_EFFECT_REGISTRY);
public static final Loadable<TagKey<Block>> BLOCK_TAG = tagKey(Registry.BLOCK_REGISTRY);
public static final Loadable<TagKey<Enchantment>> ENCHANTMENT_TAG = tagKey(Registry.ENCHANTMENT_REGISTRY);
public static final Loadable<TagKey<EntityType<?>>> ENTITY_TYPE_TAG = tagKey(Registry.ENTITY_TYPE_REGISTRY);
public static final Loadable<TagKey<Item>> ITEM_TAG = tagKey(Registry.ITEM_REGISTRY);
public static final Loadable<TagKey<Potion>> POTION_TAG = tagKey(Registry.POTION_REGISTRY);
public static final Loadable<TagKey<BlockEntityType<?>>> BLOCK_ENTITY_TYPE_TAG = tagKey(Registry.BLOCK_ENTITY_TYPE_REGISTRY);
public static final StringLoadable<TagKey<Fluid>> FLUID_TAG = tagKey(Registry.FLUID_REGISTRY);
public static final StringLoadable<TagKey<MobEffect>> MOB_EFFECT_TAG = tagKey(Registry.MOB_EFFECT_REGISTRY);
public static final StringLoadable<TagKey<Block>> BLOCK_TAG = tagKey(Registry.BLOCK_REGISTRY);
public static final StringLoadable<TagKey<Enchantment>> ENCHANTMENT_TAG = tagKey(Registry.ENCHANTMENT_REGISTRY);
public static final StringLoadable<TagKey<EntityType<?>>> ENTITY_TYPE_TAG = tagKey(Registry.ENTITY_TYPE_REGISTRY);
public static final StringLoadable<TagKey<Item>> ITEM_TAG = tagKey(Registry.ITEM_REGISTRY);
public static final StringLoadable<TagKey<Potion>> POTION_TAG = tagKey(Registry.POTION_REGISTRY);
public static final StringLoadable<TagKey<BlockEntityType<?>>> BLOCK_ENTITY_TYPE_TAG = tagKey(Registry.BLOCK_ENTITY_TYPE_REGISTRY);


/* Helpers */

/** Creates a tag key loadable */
public static <T> Loadable<TagKey<T>> tagKey(ResourceKey<? extends Registry<T>> registry) {
public static <T> StringLoadable<TagKey<T>> tagKey(ResourceKey<? extends Registry<T>> registry) {
return RESOURCE_LOCATION.flatMap(key -> TagKey.create(registry, key), TagKey::location);
}

/** Maps a loadable to a variant that disallows a particular value */
public static <T> Loadable<T> notValue(Loadable<T> loadable, T notValue, String errorMsg) {
public static <T> StringLoadable<T> notValue(StringLoadable<T> loadable, T notValue, String errorMsg) {
BiFunction<T,ErrorFactory,T> mapper = (value, error) -> {
if (value == notValue) {
throw error.create(errorMsg);
Expand Down
@@ -1,19 +1,16 @@
package slimeknights.mantle.data.loadable.common;

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;
import slimeknights.mantle.data.loadable.primitive.StringLoadable;

/** Loadable to fetch colors from JSON */
@RequiredArgsConstructor
public enum ColorLoadable implements Loadable<Integer> {
public enum ColorLoadable implements StringLoadable<Integer> {
ALPHA {
@Override
public int parseColor(String color) {
public Integer parseString(String color, String key) {
// two options, 6 character or 8 character, must not start with - sign
if (color.charAt(0) != '-') {
try {
Expand All @@ -29,17 +26,17 @@ public int parseColor(String color) {
// NO-OP
}
}
throw new JsonSyntaxException("Invalid color '" + color + "'");
throw new JsonSyntaxException("Invalid color '" + color + "' at " + key);
}

@Override
public String colorString(int color) {
public String getString(Integer color) {
return String.format("%08X", color);
}
},
NO_ALPHA {
@Override
public int parseColor(String color) {
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 {
Expand All @@ -48,35 +45,15 @@ public int parseColor(String color) {
// NO-OP
}
}
throw new JsonSyntaxException("Invalid color '" + color + "'");
throw new JsonSyntaxException("Invalid color '" + color + "' at " + key);
}

@Override
public String colorString(int color) {
public String getString(Integer color) {
return String.format("%06X", color & 0xFFFFFF);
}
};

/**
* Parses the color from the given string
* @param color Color string
* @return Color value
*/
public abstract int parseColor(String color);

/** Writes the given color as a string */
public abstract String colorString(int color);

@Override
public Integer convert(JsonElement element, String key) {
return parseColor(GsonHelper.convertToString(element, key));
}

@Override
public JsonElement serialize(Integer color) {
return new JsonPrimitive(colorString(color));
}

@Override
public Integer decode(FriendlyByteBuf buffer) {
return buffer.readInt();
Expand Down
@@ -1,21 +1,18 @@
package slimeknights.mantle.data.loadable.common;

import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
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.Loadable;
import slimeknights.mantle.util.JsonHelper;
import slimeknights.mantle.data.loadable.primitive.ResourceLocationLoadable;
import slimeknights.mantle.util.RegistryHelper;

import java.util.Objects;

/** Loadable for a registry entry */
public record RegistryLoadable<T>(Registry<T> registry, ResourceLocation registryId) implements Loadable<T> {
public record RegistryLoadable<T>(Registry<T> registry, ResourceLocation registryId) implements ResourceLocationLoadable<T> {
public RegistryLoadable(ResourceKey<? extends Registry<T>> registryId) {
this(Objects.requireNonNull(RegistryHelper.getRegistry(registryId), "Unknown registry " + registryId.location()), registryId.location());
}
Expand All @@ -26,24 +23,23 @@ public RegistryLoadable(Registry<T> registry) {
}

@Override
public T convert(JsonElement element, String key) throws JsonSyntaxException {
ResourceLocation name = JsonHelper.convertToResourceLocation(element, key);
public T fromKey(ResourceLocation name, String key) {
if (registry.containsKey(name)) {
T value = registry.get(name);
if (value != null) {
return value;
}
}
throw new JsonSyntaxException("Registry " + registryId + " does not contain ID " + name);
throw new JsonSyntaxException("Unable to parse " + key + " as registry " + registryId + " does not contain ID " + name);
}

@Override
public JsonElement serialize(T object) {
public ResourceLocation getKey(T object) {
ResourceLocation location = registry.getKey(object);
if (location == null) {
throw new RuntimeException("Registry " + registryId + " does not contain object " + object);
}
return new JsonPrimitive(location.toString());
return location;
}

@Override
Expand Down
@@ -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 <K> Key type
* @param <V> Value type
*/
public record MapLoadable<K,V>(StringLoadable<K> keyLoadable, Loadable<V> valueLoadable, int minSize) implements Loadable<Map<K,V>> {
@Override
public Map<K,V> 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<K,V> builder = ImmutableMap.builder();
String mapKey = key + "'s key";
for (Entry<String, JsonElement> 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<K,V> map) {
if (map.size() < minSize) {
throw new RuntimeException("Collection must have at least " + minSize + " elements");
}
JsonObject json = new JsonObject();
for (Entry<K,V> entry : map.entrySet()) {
json.add(
keyLoadable.getString(entry.getKey()),
valueLoadable.serialize(entry.getValue()));
}
return json;
}

@Override
public Map<K,V> decode(FriendlyByteBuf buffer) {
int size = buffer.readVarInt();
ImmutableMap.Builder<K,V> 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<K,V> map) {
buffer.writeVarInt(map.size());
for (Entry<K,V> entry : map.entrySet()) {
keyLoadable.encode(buffer, entry.getKey());
valueLoadable.encode(buffer, entry.getValue());
}
}
}
Expand Up @@ -7,6 +7,7 @@
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;

Expand All @@ -30,6 +31,11 @@ public static <T,F> RecordLoadable<T> of(RecordLoadable<F> base, BiFunction<F,Er
return new Record<>(base, from, to);
}

/** Creates a new loadable for a record loadable */
public static <T,F> StringLoadable<T> of(StringLoadable<F> base, BiFunction<F,ErrorFactory,T> from, BiFunction<T,ErrorFactory,F> to) {
return new StringMapped<>(base, from, to);
}

/** Flattens the given mapping function */
public static <T,R> BiFunction<T,ErrorFactory,R> flatten(Function<T,R> function) {
return (value, error) -> function.apply(value);
Expand Down Expand Up @@ -78,4 +84,23 @@ public T decode(FriendlyByteBuf buffer, TypedMap<Object> context) {
return from.apply(base.decode(buffer, context), ErrorFactory.DECODER_EXCEPTION);
}
}

/** Implementation for strings */
private static class StringMapped<F,T> extends MappedLoadable<F,T> implements StringLoadable<T> {
private final StringLoadable<F> base;
protected StringMapped(StringLoadable<F> base, BiFunction<F,ErrorFactory,T> from, BiFunction<T,ErrorFactory,F> 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));
}
}
}
Expand Up @@ -2,15 +2,16 @@

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;
import slimeknights.mantle.data.loadable.field.LoadableField;

import java.util.Locale;
import java.util.function.Function;

/** Loadable for a boolean */
public enum BooleanLoadable implements Loadable<Boolean> {
public enum BooleanLoadable implements StringLoadable<Boolean> {
INSTANCE;

@Override
Expand Down Expand Up @@ -38,4 +39,22 @@ public <P> LoadableField<Boolean,P> defaultField(String key, Boolean defaultValu
// 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();
}
}

0 comments on commit 24eb72c

Please sign in to comment.