diff --git a/gradle.properties b/gradle.properties index abe6c11b..7a3037dd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ minecraft_version=1.18.1 yarn_mappings=1.18.1+build.7 loader_version=0.12.12 # Mod Properties -mod_version=0.3.15 +mod_version=0.3.15+pre2 maven_group=io.wispforest archives_base_name=owo-lib # Dependencies diff --git a/src/main/java/io/wispforest/owo/itemgroup/json/GroupTabLoader.java b/src/main/java/io/wispforest/owo/itemgroup/json/GroupTabLoader.java index f792bbf0..240328b6 100644 --- a/src/main/java/io/wispforest/owo/itemgroup/json/GroupTabLoader.java +++ b/src/main/java/io/wispforest/owo/itemgroup/json/GroupTabLoader.java @@ -49,7 +49,6 @@ public String getDataSubdirectory() { return "item_group_tabs"; } - @SuppressWarnings("ConstantConditions") @Override public void acceptParsedFile(Identifier id, JsonObject json) { String targetGroup = JsonHelper.getString(json, "target_group"); @@ -87,12 +86,18 @@ public void acceptParsedFile(Identifier id, JsonObject json) { for (ItemGroup group : ItemGroup.GROUPS) { if (!group.getName().equals(targetGroup)) continue; - final var wrappedGroup = new WrapperGroup(group.getIndex(), group.getName(), createdTabs, createdButtons, group::createIcon); - wrappedGroup.initialize(); - for (var item : Registry.ITEM) { - if (item.getGroup() != group) continue; - ((OwoItemExtensions) item).setItemGroup(wrappedGroup); + if (group instanceof WrapperGroup wrapper) { + wrapper.addTabs(createdTabs); + wrapper.addButtons(createdButtons); + } else { + final var wrappedGroup = new WrapperGroup(group.getIndex(), group.getName(), createdTabs, createdButtons, group::createIcon); + wrappedGroup.initialize(); + + for (var item : Registry.ITEM) { + if (item.getGroup() != group) continue; + ((OwoItemExtensions) item).setItemGroup(wrappedGroup); + } } return; diff --git a/src/main/java/io/wispforest/owo/itemgroup/json/WrapperGroup.java b/src/main/java/io/wispforest/owo/itemgroup/json/WrapperGroup.java index bc506bd9..5f8cc3a8 100644 --- a/src/main/java/io/wispforest/owo/itemgroup/json/WrapperGroup.java +++ b/src/main/java/io/wispforest/owo/itemgroup/json/WrapperGroup.java @@ -6,6 +6,7 @@ import net.minecraft.item.ItemStack; import org.jetbrains.annotations.ApiStatus; +import java.util.Collection; import java.util.List; import java.util.function.Supplier; @@ -30,6 +31,14 @@ public WrapperGroup(int index, String name, List tabs, List tabs) { + this.tabs.addAll(tabs); + } + + public void addButtons(Collection buttons) { + this.buttons.addAll(buttons); + } + @Override protected void setup() {} diff --git a/src/main/java/io/wispforest/owo/network/OwoNetChannel.java b/src/main/java/io/wispforest/owo/network/OwoNetChannel.java index c99a4c9a..8eee2033 100644 --- a/src/main/java/io/wispforest/owo/network/OwoNetChannel.java +++ b/src/main/java/io/wispforest/owo/network/OwoNetChannel.java @@ -10,14 +10,45 @@ import net.fabricmc.fabric.api.networking.v1.PlayerLookup; import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.block.entity.BlockEntity; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.network.PacketByteBuf; import net.minecraft.server.MinecraftServer; import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; import net.minecraft.util.Identifier; +import net.minecraft.util.math.BlockPos; import java.util.*; - +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * An efficient networking abstraction that uses {@code record}s to store + * and define packet data. Serialization for most types is fully automatic + * and no custom handling needs to be done, should one of your record + * components be of an unsupported type use {@link io.wispforest.owo.network.serialization.TypeAdapter#register(Class, BiConsumer, Function)} + * to register a custom serializer. + * + *

To define a packet class suited for use with this wrapper, simply create a + * standard Java {@code record} class and put the desired data into the record header. + * + *

To register a packet onto this channel, use either {@link #registerClientbound(Class, ChannelHandler)} + * or {@link #registerServerbound(Class, ChannelHandler)}, depending on which direction the packet goes. + * Bidirectional registration of the same class is explicitly supported. For synchronization purposes, + * all registration must happen on both client and server, even for clientbound packets. + * + *

To send a packet, use any of the {@code handle} methods to obtain a handle for sending. These are + * named after where the packet is sent from, meaning the {@link #clientHandle()} is used for sending + * to the server and vice-versa. + * + *

The registered packet handlers are executed synchronously on the target environment's + * game thread instead of Netty's event loops - there is no need to call {@code .execute(...)} + * + * @see io.wispforest.owo.network.serialization.TypeAdapter#register(Class, BiConsumer, Function) + * @see io.wispforest.owo.network.serialization.TypeAdapter#registerCollectionProvider(Class, Supplier) + */ public class OwoNetChannel { private static final Map REGISTERED_CHANNELS = new HashMap<>(); @@ -33,6 +64,16 @@ public class OwoNetChannel { private ClientHandle clientHandle = null; private ServerHandle serverHandle = null; + /** + * Creates a new channel with given ID. Duplicate channel IDs + * are not allowed - if there is a collision, the name of the + * class that previously registered the channel will be part of + * the exception. This may be called at any stage during + * mod initialization + * + * @param id The desired channel ID + * @return The created channel + */ public static OwoNetChannel create(Identifier id) { return new OwoNetChannel(id, ReflectionUtils.getCallingClassName(2)); } @@ -63,6 +104,18 @@ private OwoNetChannel(Identifier id, String ownerClassName) { REGISTERED_CHANNELS.put(id, ownerClassName); } + /** + * Registers a handler on the client for the specified message class. + * This also ensures the required serializer is available. If an exception + * about a missing type adapter is thrown, register one + * + * @param messageClass The type of packet data to send and serialize + * @param handler The handler that will receive the deserialized + * @see #serverHandle(PlayerEntity) + * @see #serverHandle(MinecraftServer) + * @see #serverHandle(ServerWorld, BlockPos) + * @see io.wispforest.owo.network.serialization.TypeAdapter#register(Class, BiConsumer, Function) + */ @SuppressWarnings("unchecked") public void registerClientbound(Class messageClass, ChannelHandler handler) { int index = this.clientHandlers.size(); @@ -70,6 +123,16 @@ public void registerClientbound(Class messageClass, Channe this.clientHandlers.add((ChannelHandler) handler); } + /** + * Registers a handler on the server for the specified message class. + * This also ensures the required serializer is available. If an exception + * about a missing type adapter is thrown, register one + * + * @param messageClass The type of packet data to send and serialize + * @param handler The handler that will receive the deserialized + * @see #clientHandle() + * @see io.wispforest.owo.network.serialization.TypeAdapter#register(Class, BiConsumer, Function) + */ @SuppressWarnings("unchecked") public void registerServerbound(Class messageClass, ChannelHandler handler) { int index = this.serverHandlers.size(); @@ -77,6 +140,12 @@ public void registerServerbound(Class messageClass, Channe this.serverHandlers.add((ChannelHandler) handler); } + /** + * Obtains the client handle of this channel, used to + * send packets to the server + * + * @return The client handle of this channel + */ public ClientHandle clientHandle() { if (FabricLoader.getInstance().getEnvironmentType() != EnvType.CLIENT) throw new NetworkException("Cannot obtain client handle in environment type '" + FabricLoader.getInstance().getEnvironmentType() + "'"); @@ -85,18 +154,50 @@ public ClientHandle clientHandle() { return clientHandle; } + /** + * Obtains a server handle used to send packets + * to all players on the given server + *

+ * This handle will be reused - do not retain references + * + * @param server The server to target + * @return A server handle configured for sending packets + * to all players on the given server + */ public ServerHandle serverHandle(MinecraftServer server) { var handle = getServerHandle(); handle.targets = PlayerLookup.all(server); return handle; } + /** + * Obtains a server handle used to send packets + * to all given players. Use {@link PlayerLookup} to obtain + * the required collections + *

+ * This handle will be reused - do not retain references + * + * @param targets The players to target + * @return A server handle configured for sending packets + * to all players in the given collection + * @see PlayerLookup + */ public ServerHandle serverHandle(Collection targets) { var handle = getServerHandle(); handle.targets = targets; return handle; } + /** + * Obtains a server handle used to send packets + * to the given player only + *

+ * This handle will be reused - do not retain references + * + * @param player The player to target + * @return A server handle configured for sending packets + * to the given player only + */ public ServerHandle serverHandle(PlayerEntity player) { if (!(player instanceof ServerPlayerEntity serverPlayer)) throw new NetworkException("'player' must be a 'ServerPlayerEntity'"); @@ -105,6 +206,36 @@ public ServerHandle serverHandle(PlayerEntity player) { return handle; } + /** + * Obtains a server handle used to send packets + * to all players tracking the given block entity + *

+ * This handle will be reused - do not retain references + * + * @param entity The block entity to look up trackers for + * @return A server handle configured for sending packets + * to all players tracking the given block entity + */ + public ServerHandle serverHandle(BlockEntity entity) { + if (entity.getWorld().isClient) throw new NetworkException("Server handle cannot be obtained on the client"); + return serverHandle(PlayerLookup.tracking(entity)); + } + + /** + * Obtains a server handle used to send packets to all + * players tracking the given position in the given world + *

+ * This handle will be reused - do not retain references + * + * @param world The world to look up players in + * @param pos The position to look up trackers for + * @return A server handle configured for sending packets + * to all players tracking the given position in the given world + */ + public ServerHandle serverHandle(ServerWorld world, BlockPos pos) { + return serverHandle(PlayerLookup.tracking(world, pos)); + } + private ServerHandle getServerHandle() { if (this.serverHandle == null) this.serverHandle = new ServerHandle(); return serverHandle; @@ -145,30 +276,102 @@ private PacketByteBuf encode(R message, EnvType target) { } public class ClientHandle { + + /** + * Sends the given message to the server + * + * @param message The message to send + * @see #send(Record[]) + */ public void send(R message) { ClientPlayNetworking.send(OwoNetChannel.this.packetId, OwoNetChannel.this.encode(message, EnvType.SERVER)); } + + /** + * Sends the given messages to the server + * + * @param messages The messages to send + */ + @SafeVarargs + public final void send(R... messages) { + for (R message : messages) send(message); + } } public class ServerHandle { private Collection targets = Collections.emptySet(); + /** + * Sends the given message to the configured target(s) + * Resets the target(s) after sending - this cannot be used + * for multiple messages on the same handle + * + * @param message The message to send + * @see #send(Record[]) + */ public void send(R message) { this.targets.forEach(player -> ServerPlayNetworking.send(player, OwoNetChannel.this.packetId, OwoNetChannel.this.encode(message, EnvType.CLIENT))); this.targets = null; } + + /** + * Sends the given messages to the configured target(s) + * Resets the target(s) after sending - this cannot be used + * multiple times on the same handle + * + * @param messages The messages to send + */ + @SafeVarargs + public final void send(R... messages) { + this.targets.forEach(player -> { + for (R message : messages) { + ServerPlayNetworking.send(player, OwoNetChannel.this.packetId, OwoNetChannel.this.encode(message, EnvType.CLIENT)); + } + }); + this.targets = null; + } } public interface ChannelHandler> { + + /** + * Executed on the game thread to handle the incoming + * message - this can safely modify game state + * + * @param message The message that was received + * @param access The {@link EnvironmentAccess} used to obtain references + * to the execution environment + */ void handle(R message, E access); } + /** + * A simple wrapper that provides access to the environment a packet + * is being received / message is being handled in + * + * @param

The type of player to receive the packet + * @param The runtime that the packet is being received in + * @param The network handler that received the packet + */ public interface EnvironmentAccess

{ + + /** + * @return The player that received the packet + */ P player(); + /** + * @return The environment the packet is being received in, + * either a {@link MinecraftServer} or a {@link net.minecraft.client.MinecraftClient} + */ R runtime(); + /** + * @return The network handler of the player or client that received the packet, + * either a {@link net.minecraft.client.network.ClientPlayNetworkHandler} or a + * {@link net.minecraft.server.network.ServerPlayNetworkHandler} + */ N netHandler(); } diff --git a/src/main/java/io/wispforest/owo/network/serialization/RecordSerializer.java b/src/main/java/io/wispforest/owo/network/serialization/RecordSerializer.java index 5e1e1ae5..dd20b0e1 100644 --- a/src/main/java/io/wispforest/owo/network/serialization/RecordSerializer.java +++ b/src/main/java/io/wispforest/owo/network/serialization/RecordSerializer.java @@ -1,21 +1,15 @@ package io.wispforest.owo.network.serialization; import com.google.common.collect.ImmutableMap; +import io.wispforest.owo.Owo; import io.wispforest.owo.network.annotations.CollectionType; import io.wispforest.owo.network.annotations.MapTypes; -import io.wispforest.owo.util.VectorSerializer; -import net.minecraft.item.ItemStack; import net.minecraft.network.PacketByteBuf; -import net.minecraft.util.Identifier; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.Vec3d; -import net.minecraft.util.math.Vec3f; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.RecordComponent; -import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -23,10 +17,22 @@ import java.util.function.BiConsumer; import java.util.function.Function; +/** + * A utility for serializing {@code record} classes into {@link PacketByteBuf}s. + * Use {@link #create(Class)} to create (or obtain if it already exists) + * the instance for a specific class. Should an exception + * about a missing type adapter be thrown, register one + * using {@link TypeAdapter#register(Class, BiConsumer, Function)} + * + *

To serialize an instance use {@link #write(PacketByteBuf, Record)}, + * to read it back again use {@link #read(PacketByteBuf)} + * + * @param The type of record this serializer can handle + */ @SuppressWarnings({"unchecked", "rawtypes"}) public class RecordSerializer { - private static final Map, TypeAdapter> TYPE_ADAPTERS = new HashMap<>(); + private static final Map, RecordSerializer> SERIALIZERS = new HashMap<>(); private final Map, TypeAdapter> adapters; private final Constructor instanceCreator; @@ -38,7 +44,17 @@ private RecordSerializer(Class recordClass, Constructor instanceCreator, I this.fieldCount = recordClass.getRecordComponents().length; } + /** + * Creates a new serializer for the given record type, or retrieves the + * existing one if it was already created + * + * @param recordClass The type of record to (de-)serialize + * @param The type of record to (de-)serialize + * @return The serializer for the given record type + */ public static RecordSerializer create(Class recordClass) { + if (SERIALIZERS.containsKey(recordClass)) return (RecordSerializer) SERIALIZERS.get(recordClass); + final ImmutableMap.Builder, TypeAdapter> adapters = new ImmutableMap.Builder<>(); final Class[] canonicalConstructorArgs = new Class[recordClass.getRecordComponents().length]; @@ -50,12 +66,21 @@ public static RecordSerializer create(Class recordClass } try { - return new RecordSerializer<>(recordClass, recordClass.getConstructor(canonicalConstructorArgs), adapters.build()); + final var serializer = new RecordSerializer<>(recordClass, recordClass.getConstructor(canonicalConstructorArgs), adapters.build()); + SERIALIZERS.put(recordClass, serializer); + return serializer; } catch (NoSuchMethodException e) { throw new IllegalStateException("Could not locate canonical record constructor"); } } + /** + * Attempts to read a record of this serializer's + * type from the given buffer + * + * @param buffer The buffer to read from + * @return The deserialized record + */ public R read(PacketByteBuf buffer) { Object[] messageContents = new Object[fieldCount]; @@ -65,88 +90,53 @@ public R read(PacketByteBuf buffer) { try { return instanceCreator.newInstance(messageContents); } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) { - e.printStackTrace(); + Owo.LOGGER.error("Error while deserializing record", e); } return null; } - public void write(PacketByteBuf buffer, R instance) { + /** + * Writes the given record instance + * to the given buffer + * + * @param buffer The buffer to write to + * @param instance The record instance to serialize + */ + public RecordSerializer write(PacketByteBuf buffer, R instance) { adapters.forEach((rFunction, typeAdapter) -> typeAdapter.serializer().accept(buffer, rFunction.apply(instance))); + return this; } private static Object getRecordEntry(R instance, Method accessor) { try { return accessor.invoke(instance); } catch (IllegalAccessException | InvocationTargetException e) { - throw new IllegalStateException("Unable to get message contents", e); + throw new IllegalStateException("Unable to get record entry", e); } } - public static void registerTypeAdapter(Class clazz, BiConsumer serializer, Function deserializer) { - if (TYPE_ADAPTERS.containsKey(clazz)) throw new IllegalStateException("Class '" + clazz.getName() + "' already has a type adapter"); - TYPE_ADAPTERS.put(clazz, new TypeAdapter<>(serializer, deserializer)); - } - private static TypeAdapter createAdapter(Class componentClass, RecordComponent component) { if (Map.class.isAssignableFrom(componentClass)) { var typeAnnotation = component.getAnnotation(MapTypes.class); - return (TypeAdapter) getMapAdapter(conform(componentClass, Map.class), typeAnnotation.keys(), typeAnnotation.values()); + return (TypeAdapter) TypeAdapter.createMapAdapter(conform(componentClass, Map.class), typeAnnotation.keys(), typeAnnotation.values()); } if (Collection.class.isAssignableFrom(componentClass)) { var typeAnnotation = component.getAnnotation(CollectionType.class); - return (TypeAdapter) getCollectionAdapter(conform(componentClass, Collection.class), typeAnnotation.value()); - } - - return getTypeAdapter(componentClass); - } - - public static TypeAdapter getTypeAdapter(Class clazz) { - if (!TYPE_ADAPTERS.containsKey(clazz)) { - throw new IllegalStateException(clazz.isPrimitive() ? - "Primitive type '" + clazz.getName() + "' can not be serialized. Use the boxed type instead" : - "No type adapter available for class '" + clazz.getName() + "'"); + return (TypeAdapter) TypeAdapter.createCollectionAdapter(conform(componentClass, Collection.class), typeAnnotation.value()); } - return (TypeAdapter) TYPE_ADAPTERS.get(clazz); - } + if (Record.class.isAssignableFrom(componentClass)) return (TypeAdapter) TypeAdapter.createRecordAdapter(conform(componentClass, Record.class)); + if (componentClass.isEnum()) return (TypeAdapter) TypeAdapter.createEnumAdapter(conform(componentClass, Enum.class)); + if (componentClass.isArray()) return (TypeAdapter) TypeAdapter.createArrayAdapter(componentClass.getComponentType()); - public static > TypeAdapter getMapAdapter(Class clazz, Class keyClass, Class valueClass) { - var keyAdapter = getTypeAdapter(keyClass); - var valueAdapter = getTypeAdapter(valueClass); - return new TypeAdapter<>((buf, t) -> buf.writeMap(t, keyAdapter.serializer(), valueAdapter.serializer()), - buf -> buf.readMap(buf1 -> (T) new HashMap<>(), keyAdapter.deserializer(), valueAdapter.deserializer())); - } - - public static > TypeAdapter getCollectionAdapter(Class clazz, Class elementClass) { - var elementAdapter = getTypeAdapter(elementClass); - return new TypeAdapter<>((buf, t) -> buf.writeCollection(t, elementAdapter.serializer()), - buf -> buf.readCollection(value -> (T) new ArrayList<>(), elementAdapter.deserializer())); + return TypeAdapter.get(componentClass); } private static Class conform(Class clazz, Class target) { return (Class) clazz; } - static { - registerTypeAdapter(Boolean.class, PacketByteBuf::writeBoolean, PacketByteBuf::readBoolean); - registerTypeAdapter(Double.class, PacketByteBuf::writeDouble, PacketByteBuf::readDouble); - registerTypeAdapter(Float.class, PacketByteBuf::writeFloat, PacketByteBuf::readFloat); - - registerTypeAdapter(Byte.class, (BiConsumer) PacketByteBuf::writeByte, PacketByteBuf::readByte); - registerTypeAdapter(Short.class, (BiConsumer) PacketByteBuf::writeShort, PacketByteBuf::readShort); - registerTypeAdapter(Integer.class, PacketByteBuf::writeVarInt, PacketByteBuf::readVarInt); - registerTypeAdapter(Long.class, PacketByteBuf::writeLong, PacketByteBuf::readLong); - - registerTypeAdapter(String.class, PacketByteBuf::writeString, PacketByteBuf::readString); - registerTypeAdapter(BlockPos.class, PacketByteBuf::writeBlockPos, PacketByteBuf::readBlockPos); - registerTypeAdapter(ItemStack.class, PacketByteBuf::writeItemStack, PacketByteBuf::readItemStack); - registerTypeAdapter(Identifier.class, PacketByteBuf::writeIdentifier, PacketByteBuf::readIdentifier); - - registerTypeAdapter(Vec3d.class, (buf, vec3d) -> VectorSerializer.write(vec3d, buf), VectorSerializer::read); - registerTypeAdapter(Vec3f.class, (buf, vec3d) -> VectorSerializer.writef(vec3d, buf), VectorSerializer::readf); - } - } diff --git a/src/main/java/io/wispforest/owo/network/serialization/TypeAdapter.java b/src/main/java/io/wispforest/owo/network/serialization/TypeAdapter.java index 7a0ceb8b..74054df8 100644 --- a/src/main/java/io/wispforest/owo/network/serialization/TypeAdapter.java +++ b/src/main/java/io/wispforest/owo/network/serialization/TypeAdapter.java @@ -1,8 +1,243 @@ + package io.wispforest.owo.network.serialization; +import io.wispforest.owo.util.VectorSerializer; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; import net.minecraft.network.PacketByteBuf; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; +import net.minecraft.util.math.Vec3f; +import java.lang.reflect.Array; +import java.util.*; import java.util.function.BiConsumer; import java.util.function.Function; +import java.util.function.Supplier; + +/** + * A simple wrapper for (de-)serialization methods on {@link PacketByteBuf}s. For + * collection types like Maps and Lists, providers must be registered via + * {@link #registerCollectionProvider(Class, Supplier)} if types other + * than {@link Collection}, {@link List} and {@link Map} are desired + * + * @param The type of object this adapter can handle + */ +public record TypeAdapter(BiConsumer serializer, Function deserializer) { + + private static final Map, Supplier> COLLECTION_PROVIDERS = new HashMap<>(); + private static final Map, TypeAdapter> TYPE_ADAPTERS = new HashMap<>(); + + /** + * Enables (de-)serialization for the given class + * + * @param clazz The object class to serialize + * @param serializer The serialization method + * @param deserializer The deserialization method + * @param The type of object to register an adapter for + */ + public static void register(Class clazz, BiConsumer serializer, Function deserializer) { + if (TYPE_ADAPTERS.containsKey(clazz)) throw new IllegalStateException("Class '" + clazz.getName() + "' already has a type adapter"); + TYPE_ADAPTERS.put(clazz, new TypeAdapter<>(serializer, deserializer)); + } + + @SafeVarargs + private static void register(BiConsumer serializer, Function deserializer, Class... classes) { + final var adapter = new TypeAdapter(serializer, deserializer); + for (var clazz : classes) { + if (TYPE_ADAPTERS.containsKey(clazz)) throw new IllegalStateException("Class '" + clazz + "' already has a type adapter"); + TYPE_ADAPTERS.put(clazz, adapter); + } + } + + /** + * Gets the type adapter for the given class, or throws + * an exception if none is registered + * + * @param clazz The class to obtain an adapter for + * @return The respective type adapter instance + */ + public static TypeAdapter get(Class clazz) { + if (!TYPE_ADAPTERS.containsKey(clazz)) { + throw new IllegalStateException("No type adapter available for class '" + clazz.getName() + "'"); + } + + //noinspection unchecked + return (TypeAdapter) TYPE_ADAPTERS.get(clazz); + } + + /** + * Tries to get the type adapter for the given class + * + * @param clazz The class to obtain an adapter for + * @return An empty optional if no adapter is registered + */ + public static Optional> maybeGet(Class clazz) { + //noinspection unchecked + return Optional.ofNullable((TypeAdapter) TYPE_ADAPTERS.get(clazz)); + } + + /** + * Registers a supplier that creates empty collections for the + * map and collection adapters to use + * + * @param clazz The container class to register a provider for + * @param provider A provider that creates some default type for the given + * class + */ + public static void registerCollectionProvider(Class clazz, Supplier provider) { + if (COLLECTION_PROVIDERS.containsKey(clazz)) throw new IllegalStateException("Collection class '" + clazz.getName() + "' already has a provider"); + COLLECTION_PROVIDERS.put(clazz, provider); + } + + /** + * Creates a new collection instance + * for the given container class + * + * @param clazz The container class + * @return The created collection + */ + public static T createCollection(Class clazz) { + if (!COLLECTION_PROVIDERS.containsKey(clazz)) { + throw new IllegalStateException("No collection provider registered for collection class " + clazz.getName()); + } + + //noinspection unchecked + return ((Supplier) COLLECTION_PROVIDERS.get(clazz)).get(); + } + + /** + * Tries to create an adapter capable of + * serializing the given map type + * + * @param clazz The map type + * @param keyClass The type of the map's keys + * @param valueClass The type of the map's values + * @return The created adapter + */ + public static > TypeAdapter createMapAdapter(Class clazz, Class keyClass, Class valueClass) { + createCollection(clazz); + + var keyAdapter = get(keyClass); + var valueAdapter = get(valueClass); + return new TypeAdapter<>((buf, t) -> buf.writeMap(t, keyAdapter.serializer(), valueAdapter.serializer()), + buf -> buf.readMap(buf1 -> createCollection(clazz), keyAdapter.deserializer(), valueAdapter.deserializer())); + } + + /** + * Tries to create an adapter capable of + * serializing the given collection type + * + * @param clazz The collection type + * @param elementClass The type of the collections elements + * @return The created adapter + */ + public static > TypeAdapter createCollectionAdapter(Class clazz, Class elementClass) { + createCollection(clazz); + + var elementAdapter = get(elementClass); + return new TypeAdapter<>((buf, t) -> buf.writeCollection(t, elementAdapter.serializer()), + buf -> buf.readCollection(value -> createCollection(clazz), elementAdapter.deserializer())); + } + + /** + * Tries to create an adapter capable of + * serializing arrays of the given element type + * + * @param elementClass The array element type + * @return The created adapter + */ + @SuppressWarnings("unchecked") + public static TypeAdapter createArrayAdapter(Class elementClass) { + var elementAdapter = get(elementClass); + return new TypeAdapter<>((buf, t) -> { + final int length = Array.getLength(t); + buf.writeVarInt(length); + for (int i = 0; i < length; i++) { + elementAdapter.serializer().accept(buf, (E) Array.get(t, i)); + } + }, buf -> { + final int length = buf.readVarInt(); + Object array = Array.newInstance(elementClass, length); + for (int i = 0; i < length; i++) { + Array.set(array, i, elementAdapter.deserializer().apply(buf)); + } + return (E[]) array; + }); + } + + /** + * Tries to create an adapter capable of + * serializing the given record class + * + * @param clazz The class to create an adapter for + * @return The created adapter + */ + public static TypeAdapter createRecordAdapter(Class clazz) { + var serializer = RecordSerializer.create(clazz); + return new TypeAdapter<>(serializer::write, serializer::read); + } + + /** + * Tries to create an adapter capable of serializing + * the given enum type + * + * @param enumClass The type of enum to create an adapter for + * @return The created adapter + */ + public static > TypeAdapter createEnumAdapter(Class enumClass) { + return new TypeAdapter<>(PacketByteBuf::writeEnumConstant, buf -> buf.readEnumConstant(enumClass)); + } + + static { + + // ---------- + // Primitives + // ---------- + + register(PacketByteBuf::writeBoolean, PacketByteBuf::readBoolean, Boolean.class, boolean.class); + register(PacketByteBuf::writeVarInt, PacketByteBuf::readVarInt, Integer.class, int.class); + register(PacketByteBuf::writeVarLong, PacketByteBuf::readVarLong, Long.class, long.class); + register(PacketByteBuf::writeFloat, PacketByteBuf::readFloat, Float.class, float.class); + register(PacketByteBuf::writeDouble, PacketByteBuf::readDouble, Double.class, double.class); + + register((BiConsumer) PacketByteBuf::writeByte, PacketByteBuf::readByte, Byte.class, byte.class); + register((BiConsumer) PacketByteBuf::writeShort, PacketByteBuf::readShort, Short.class, short.class); + register((BiConsumer) PacketByteBuf::writeChar, PacketByteBuf::readChar, Character.class, char.class); + + // ---- + // Misc + // ---- + + register(String.class, PacketByteBuf::writeString, PacketByteBuf::readString); + register(UUID.class, PacketByteBuf::writeUuid, PacketByteBuf::readUuid); + register(Date.class, PacketByteBuf::writeDate, PacketByteBuf::readDate); + + // -------- + // MC Types + // -------- + + register(BlockPos.class, PacketByteBuf::writeBlockPos, PacketByteBuf::readBlockPos); + register(ItemStack.class, PacketByteBuf::writeItemStack, PacketByteBuf::readItemStack); + register(Identifier.class, PacketByteBuf::writeIdentifier, PacketByteBuf::readIdentifier); + register(NbtCompound.class, PacketByteBuf::writeNbt, PacketByteBuf::readNbt); + register(BlockHitResult.class, PacketByteBuf::writeBlockHitResult, PacketByteBuf::readBlockHitResult); + register(BitSet.class, PacketByteBuf::writeBitSet, PacketByteBuf::readBitSet); + register(Text.class, PacketByteBuf::writeText, PacketByteBuf::readText); + + register(Vec3d.class, (buf, vec3d) -> VectorSerializer.write(vec3d, buf), VectorSerializer::read); + register(Vec3f.class, (buf, vec3d) -> VectorSerializer.writef(vec3d, buf), VectorSerializer::readf); + + // ----------- + // Collections + // ----------- + + registerCollectionProvider(Collection.class, HashSet::new); + registerCollectionProvider(List.class, ArrayList::new); + registerCollectionProvider(Map.class, HashMap::new); + } -public record TypeAdapter(BiConsumer serializer, Function deserializer) {} +} diff --git a/src/main/java/io/wispforest/owo/util/ReflectionUtils.java b/src/main/java/io/wispforest/owo/util/ReflectionUtils.java index 9222e676..58e97919 100644 --- a/src/main/java/io/wispforest/owo/util/ReflectionUtils.java +++ b/src/main/java/io/wispforest/owo/util/ReflectionUtils.java @@ -5,11 +5,13 @@ import org.apache.logging.log4j.util.TriConsumer; import org.jetbrains.annotations.ApiStatus; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.function.Function; @ApiStatus.Experimental public class ReflectionUtils { @@ -24,13 +26,48 @@ public class ReflectionUtils { */ public static C tryInstantiateWithNoArgs(Class clazz) { try { - return clazz.getDeclaredConstructor().newInstance(); + return clazz.getConstructor().newInstance(); } catch (InstantiationException | NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { e.printStackTrace(); throw new RuntimeException((e instanceof NoSuchMethodException ? "No zero-args constructor defined on class " : "Could not instantiate class ") + clazz, e); } } + /** + * Calls the {@link Constructor#newInstance(Object...)} method and + * wraps the exception in a {@link RuntimeException}, thus making it unchecked. + * Use this when you would otherwise rethrow + * + * @param constructor The constructor to call + * @param args The arguments to pass the constructor + * @param The type of object to create + * @return The created object + */ + public static C instantiate(Constructor constructor, Object... args) { + try { + return constructor.newInstance(args); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException("Wrapped object creation failure, look below for reason", e); + } + } + + /** + * Tries to obtain the public zero-args constructor of the given class. + * Use this when no constructor constitutes an error condition or + * you previously checked for its existence with {@link #requireZeroArgsConstructor(Class, Function)} + * + * @param clazz The class to get the constructor from + * @param The type of object the constructor will create + * @return The public zero-args constructor of the given class + */ + public static Constructor getNoArgsConstructor(Class clazz) { + try { + return clazz.getConstructor(); + } catch (NoSuchMethodException e) { + throw new IllegalStateException("Class " + clazz.getName() + " does not declare a zero-args constructor", e); + } + } + @ApiStatus.ScheduledForRemoval @Deprecated(forRemoval = true, since = "0.3.13") public static void iterateAccessibleStaticFields(Class clazz, Class targetFieldType, BiConsumer fieldConsumer) { @@ -63,13 +100,23 @@ public static void iterateAccessibleStaticFields(Class clazz, Class if (value == null || !targetFieldType.isAssignableFrom(value.getClass())) continue; if (field.isAnnotationPresent(IterationIgnored.class)) continue; - var fieldId = field.getName().toLowerCase(); - if (field.isAnnotationPresent(AssignedName.class)) fieldId = field.getAnnotation(AssignedName.class).value(); - - fieldConsumer.accept(value, fieldId, field); + fieldConsumer.accept(value, getFieldName(field), field); } } + /** + * Returns the name of field in all lowercase, or + * the name defined by an {@link AssignedName} annotation + * + * @param field The field to check + * @return the properly formatted field name + */ + public static String getFieldName(Field field) { + var fieldId = field.getName().toLowerCase(); + if (field.isAnnotationPresent(AssignedName.class)) fieldId = field.getAnnotation(AssignedName.class).value(); + return fieldId; + } + /** * Executes the given consumer on all subclasses that match {@code targetType} * @@ -84,6 +131,25 @@ public static void forApplicableSubclasses(Class parent, Class targetType, } } + /** + * Verifies that the given class provides a public zero-args constructor. + * Throws an exception with a caller-controlled message if the constructor + * doesn't exist + * + * @param clazz The class to check the existence of a zero-args constructor for + * @param reasonFormatter The error message to throw, gets the class name passed + */ + public static void requireZeroArgsConstructor(Class clazz, Function reasonFormatter) { + boolean found = false; + for (var constructor : clazz.getConstructors()) { + if (constructor.getParameterCount() != 0) continue; + found = true; + break; + } + + if (!found) throw new IllegalStateException(reasonFormatter.apply(clazz.getName())); + } + /** * Tries to acquire the name of the calling class, * {@code depth} frames up the call stack diff --git a/src/main/resources/owo.mixins.json b/src/main/resources/owo.mixins.json index be562fcb..e3c20862 100644 --- a/src/main/resources/owo.mixins.json +++ b/src/main/resources/owo.mixins.json @@ -10,13 +10,13 @@ "ItemSettingsMixin", "LevelInfoMixin", "ScreenHandlerInvoker", - "TagGroupLoaderMixin", - "TranslationStorageMixin" + "TagGroupLoaderMixin" ], "client": [ "CreativeInventoryScreenMixin", "TextFieldWidgetMixin", - "OperatingSystemMixin" + "OperatingSystemMixin", + "TranslationStorageMixin" ], "injectors": { "defaultRequire": 1 diff --git a/src/testmod/java/io/wispforest/uwu/network/UwuNetworkExample.java b/src/testmod/java/io/wispforest/uwu/network/UwuNetworkExample.java index dc45820e..17bf638d 100644 --- a/src/testmod/java/io/wispforest/uwu/network/UwuNetworkExample.java +++ b/src/testmod/java/io/wispforest/uwu/network/UwuNetworkExample.java @@ -1,13 +1,11 @@ package io.wispforest.uwu.network; import io.wispforest.owo.network.OwoNetChannel; -import io.wispforest.owo.network.serialization.RecordSerializer; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; import net.minecraft.client.option.KeyBinding; -import net.minecraft.network.PacketByteBuf; import net.minecraft.text.Text; import net.minecraft.util.Identifier; import org.lwjgl.glfw.GLFW; @@ -39,8 +37,4 @@ public static void init() { }); } } - - static { - RecordSerializer.registerTypeAdapter(Text.class, PacketByteBuf::writeText, PacketByteBuf::readText); - } } diff --git a/src/testmod/java/io/wispforest/uwu/network/UwuNetworkTest.java b/src/testmod/java/io/wispforest/uwu/network/UwuNetworkTest.java new file mode 100644 index 00000000..6c6a2c6b --- /dev/null +++ b/src/testmod/java/io/wispforest/uwu/network/UwuNetworkTest.java @@ -0,0 +1,44 @@ + +package io.wispforest.uwu.network; + +import io.wispforest.owo.network.annotations.CollectionType; +import io.wispforest.owo.network.serialization.RecordSerializer; +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.function.BiPredicate; +import java.util.function.Function; + +public class UwuNetworkTest { + + public static void main(String[] args) { + var test = new TestRecord(new LinkedList<>(List.of("hahayes epic text")), TestEnum.ANOTHER_VALUE); + var serializer = RecordSerializer.create(TestRecord.class); + var sameSerializer = RecordSerializer.create(TestRecord.class); + + testEquals(serializer, sameSerializer); + + var buffer = PacketByteBufs.create(); + var read = serializer.write(buffer, test).read(buffer); + + testEquals(test, read); + } + + public static final record TestRecord(@CollectionType(String.class) Collection text, TestEnum enumValue) {} + + public enum TestEnum {ONE_VALUE, ANOTHER_VALUE} + + private static void testEquals(T object, T other) { + testEquals(object, other, Objects::toString, Object::equals); + } + + private static void testEquals(T object, T other, Function formatter, BiPredicate predicate) { + System.out.println("Comparing '" + formatter.apply(object) + "' to '" + formatter.apply(other) + "'"); + System.out.println("object == other -> " + (object == other)); + System.out.println("predicate.test(object, other) -> " + predicate.test(object, other)); + } + +} diff --git a/src/testmod/resources/data/uwu/item_group_tabs/misc_extension.json b/src/testmod/resources/data/uwu/item_group_tabs/misc_extension.json new file mode 100644 index 00000000..db81589b --- /dev/null +++ b/src/testmod/resources/data/uwu/item_group_tabs/misc_extension.json @@ -0,0 +1,10 @@ +{ + "target_group": "misc", + "tabs": [ + { + "tag": "minecraft:diamond_ores", + "name": "tab_0", + "icon": "minecraft:deepslate" + } + ] +} diff --git a/src/testmod/resources/data/uwu/item_group_tabs/misc_extension_1.json b/src/testmod/resources/data/uwu/item_group_tabs/misc_extension_1.json new file mode 100644 index 00000000..982322a0 --- /dev/null +++ b/src/testmod/resources/data/uwu/item_group_tabs/misc_extension_1.json @@ -0,0 +1,10 @@ +{ + "target_group": "misc", + "tabs": [ + { + "tag": "minecraft:gold_ores", + "name": "tab_1", + "icon": "minecraft:emerald" + } + ] +}