diff --git a/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/ComponentRegistry.java b/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/ComponentRegistry.java index e4f10f2a..b91da10b 100644 --- a/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/ComponentRegistry.java +++ b/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/ComponentRegistry.java @@ -22,12 +22,19 @@ */ package org.ladysnake.cca.api.v3.component; +import com.mojang.serialization.MapCodec; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; import net.minecraft.util.Identifier; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Contract; +import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponent; +import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponentKey; import org.ladysnake.cca.internal.base.ComponentRegistryImpl; import javax.annotation.Nullable; +import java.util.Optional; import java.util.stream.Stream; /** @@ -76,6 +83,22 @@ public static ComponentKey getOrCreate(Identifier compo return ComponentRegistryV3.INSTANCE.getOrCreate(componentId, componentClass); } + public static ImmutableComponentKey getOrCreateTransient(Identifier componentId, Class componentClass) { + return ComponentRegistryV3.INSTANCE.getOrCreateImmutable(componentId, componentClass, null, null); + } + + public static ImmutableComponentKey getOrCreateTransient(Identifier componentId, Class componentClass, PacketCodec packetCodec) { + return ComponentRegistryV3.INSTANCE.getOrCreateImmutable(componentId, componentClass, null, packetCodec); + } + + public static ImmutableComponentKey getOrCreate(Identifier componentId, Class componentClass, MapCodec mapCodec, PacketCodec packetCodec) { + return ComponentRegistryV3.INSTANCE.getOrCreateImmutable(componentId, componentClass, mapCodec, packetCodec); + } + + public static ImmutableComponentKey getOrCreate(Identifier componentId, Class componentClass, MapCodec mapCodec) { + return ComponentRegistryV3.INSTANCE.getOrCreateImmutable(componentId, componentClass, mapCodec, null); + } + /** * Directly retrieves a ComponentKey using its id. * diff --git a/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/ComponentRegistryV3.java b/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/ComponentRegistryV3.java index 305cb3d4..7c04e779 100644 --- a/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/ComponentRegistryV3.java +++ b/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/ComponentRegistryV3.java @@ -22,12 +22,18 @@ */ package org.ladysnake.cca.api.v3.component; +import com.mojang.serialization.MapCodec; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; import net.minecraft.util.Identifier; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Contract; +import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponent; +import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponentKey; import org.ladysnake.cca.internal.base.ComponentRegistryImpl; import javax.annotation.Nullable; +import java.util.Optional; import java.util.stream.Stream; /** @@ -80,6 +86,8 @@ public interface ComponentRegistryV3 { */ ComponentKey getOrCreate(Identifier componentId, Class componentClass); + ImmutableComponentKey getOrCreateImmutable(Identifier componentId, Class componentClass, @org.jetbrains.annotations.Nullable MapCodec mapCodec, @org.jetbrains.annotations.Nullable PacketCodec packetCodec); + /** * Directly retrieves a ComponentKey using its id. * diff --git a/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/StaticComponentInitializer.java b/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/StaticComponentInitializer.java index 05ef4b6e..b7c7775f 100644 --- a/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/StaticComponentInitializer.java +++ b/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/StaticComponentInitializer.java @@ -48,6 +48,10 @@ default Collection getSupportedComponentKeys() { return Collections.emptySet(); } + default Collection getSupportedImmutableComponentKeys() { + return Collections.emptySet(); + } + /** * Called when static component bootstrap is finished. * diff --git a/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponent.java b/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponent.java new file mode 100644 index 00000000..5af81e64 --- /dev/null +++ b/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponent.java @@ -0,0 +1,17 @@ +package org.ladysnake.cca.api.v3.component.immutable; + +public interface ImmutableComponent { + @FunctionalInterface + interface Modifier { + C modify(C component, O attachedTo); + } + @FunctionalInterface + interface Listener extends Modifier { + void listen(C component, O attachedTo); + @Override + default C modify(C component, O attachedTo) { + this.listen(component, attachedTo); + return component; + } + } +} diff --git a/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponentFactory.java b/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponentFactory.java new file mode 100644 index 00000000..99595b02 --- /dev/null +++ b/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponentFactory.java @@ -0,0 +1,48 @@ +/* + * Cardinal-Components-API + * Copyright (C) 2019-2024 Ladysnake + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package org.ladysnake.cca.api.v3.component.immutable; + +import org.jetbrains.annotations.Contract; +import org.ladysnake.cca.api.v3.component.Component; + +/** + * A single-arg component factory. + * + *

When invoked, the factory must return a {@link Component} of the right type. + * + * @since 3.0.0 + */ +@FunctionalInterface +public interface ImmutableComponentFactory { + /** + * Instantiates a {@link Component} for the given provider. + * + *

The component returned by this method will be available + * on the provider as soon as all component factories have been invoked. + * + * @param t the factory argument + * @return a new {@link Component} + */ + @Contract(value = "_ -> new", pure = true) + C createComponent(T t); +} diff --git a/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponentHookType.java b/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponentHookType.java new file mode 100644 index 00000000..0ac77797 --- /dev/null +++ b/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponentHookType.java @@ -0,0 +1,44 @@ +package org.ladysnake.cca.api.v3.component.immutable; + +import org.ladysnake.cca.api.v3.component.load.ClientLoadAwareComponent; +import org.ladysnake.cca.api.v3.component.load.ClientUnloadAwareComponent; +import org.ladysnake.cca.api.v3.component.load.ServerLoadAwareComponent; +import org.ladysnake.cca.api.v3.component.load.ServerUnloadAwareComponent; +import org.ladysnake.cca.api.v3.component.tick.ClientTickingComponent; +import org.ladysnake.cca.api.v3.component.tick.ServerTickingComponent; + +import java.lang.invoke.MethodType; +import java.lang.reflect.Modifier; +import java.util.Arrays; + +public record ImmutableComponentHookType(Class itf, + String methodName, + MethodType exposedType, + MethodType implType) { + public ImmutableComponentHookType { + if (!itf.isInterface() + || Arrays.stream(itf.getDeclaredMethods()) + .filter(m -> Modifier.isAbstract(m.getModifiers())) + .count() != 1) { + throw new IllegalArgumentException("ImmutableComponentHookType accepts only functional interfaces"); + } + } + + public static ImmutableComponentHookType fromFunctionalInterface(Class itf) { + var method = Arrays.stream(itf.getDeclaredMethods()) + .filter(m -> Modifier.isAbstract(m.getModifiers())) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("ImmutableComponentHookType accepts only functional interfaces")); + String name = method.getName(); + var type = MethodType.methodType(method.getReturnType(), method.getParameterTypes()); + var implType = type.insertParameterTypes(0, ImmutableComponentWrapper.class); + return new ImmutableComponentHookType<>(itf, name, type, implType); + } + + public static final ImmutableComponentHookType SERVER_TICK = fromFunctionalInterface(ServerTickingComponent.class); + public static final ImmutableComponentHookType CLIENT_TICK = fromFunctionalInterface(ClientTickingComponent.class); + public static final ImmutableComponentHookType SERVER_LOAD = fromFunctionalInterface(ServerLoadAwareComponent.class); + public static final ImmutableComponentHookType CLIENT_LOAD = fromFunctionalInterface(ClientLoadAwareComponent.class); + public static final ImmutableComponentHookType SERVER_UNLOAD = fromFunctionalInterface(ServerUnloadAwareComponent.class); + public static final ImmutableComponentHookType CLIENT_UNLOAD = fromFunctionalInterface(ClientUnloadAwareComponent.class); +} diff --git a/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponentKey.java b/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponentKey.java new file mode 100644 index 00000000..4a977a3f --- /dev/null +++ b/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponentKey.java @@ -0,0 +1,75 @@ +package org.ladysnake.cca.api.v3.component.immutable; + +import com.mojang.serialization.MapCodec; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.ladysnake.cca.api.v3.component.ComponentKey; + +import java.util.function.UnaryOperator; + +public abstract class ImmutableComponentKey extends ComponentKey> { + private final @Nullable MapCodec mapCodec; + private final @Nullable PacketCodec packetCodec; + private final Class immutableComponentClass; + + protected ImmutableComponentKey(Identifier id, Class immutableComponentClass, Class> wrapperClass, @Nullable MapCodec mapCodec, @Nullable PacketCodec packetCodec) { + super(id, wrapperClass); + this.immutableComponentClass = immutableComponentClass; + this.mapCodec = mapCodec; + this.packetCodec = packetCodec; + } + + public Class getImmutableComponentClass() { + return this.immutableComponentClass; + } + + public MapCodec getMapCodec() { + return this.mapCodec; + } + + public PacketCodec getPacketCodec() { + return this.packetCodec; + } + + public C getValue(Object owner) { + return this.get(owner).getData(); + } + + public void set(Object owner, C data) { + var wrapper = this.get(owner); + wrapper.setData(data); + } + + public void setAndSync(Object owner, C data) { + var wrapper = this.get(owner); + wrapper.setAndSync(data); + } + + public void update(Object owner, UnaryOperator operator) { + var wrapper = this.get(owner); + wrapper.update(operator); + } + + public void updateAndSync(Object owner, UnaryOperator operator) { + var wrapper = this.get(owner); + wrapper.updateAndSync(operator); + } + + public void update(O owner, ImmutableComponent.Modifier modifier) { + var wrapper = this.getWrapper(owner); + wrapper.update(modifier); + } + + public void updateAndSync(O owner, ImmutableComponent.Modifier modifier) { + var wrapper = this.getWrapper(owner); + wrapper.updateAndSync(modifier); + } + + @SuppressWarnings("unchecked") + private @NotNull ImmutableComponentWrapper getWrapper(O owner) { + return (ImmutableComponentWrapper) this.get(owner); + } +} diff --git a/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponentWrapper.java b/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponentWrapper.java new file mode 100644 index 00000000..01f28db8 --- /dev/null +++ b/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponentWrapper.java @@ -0,0 +1,81 @@ +package org.ladysnake.cca.api.v3.component.immutable; + +import net.minecraft.nbt.NbtCompound; +import net.minecraft.registry.RegistryWrapper; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.ladysnake.cca.api.v3.component.Component; +import org.ladysnake.cca.api.v3.component.CopyableComponent; + +import java.util.function.BiFunction; +import java.util.function.UnaryOperator; + +@ApiStatus.NonExtendable +public abstract class ImmutableComponentWrapper implements + Component, + CopyableComponent> { + private final ImmutableComponentKey key; + private final O owner; + private @NotNull C data; + + protected ImmutableComponentWrapper(ImmutableComponentKey key, O owner, C data) { + this.key = key; + this.owner = owner; + this.data = data; + } + + @Override + public void writeToNbt(NbtCompound tag, RegistryWrapper.WrapperLookup registryLookup) { + // overridden if key.mapCodec != null + } + + @Override + public void readFromNbt(NbtCompound tag, RegistryWrapper.WrapperLookup registryLookup) { + // overridden if key.mapCodec != null + } + + @Override + public void copyFrom(ImmutableComponentWrapper other, RegistryWrapper.WrapperLookup registryLookup) { + this.data = other.data; + } + + public ImmutableComponentKey getKey() { + return key; + } + + public O getOwner() { + return owner; + } + + public C getData() { + return this.data; + } + + public void setData(C data) { + this.data = data; + } + + public void setAndSync(C data) { + boolean shouldSync = !this.data.equals(data); + this.setData(this.data); + if (shouldSync) { + this.key.sync(this.owner); + } + } + + public void update(UnaryOperator operator) { + this.setData(operator.apply(this.data)); + } + + public void updateAndSync(UnaryOperator operator) { + this.setAndSync(operator.apply(this.data)); + } + + public void update(ImmutableComponent.Modifier modifier) { + this.setData(modifier.modify(this.data, this.owner)); + } + + public void updateAndSync(ImmutableComponent.Modifier modifier) { + this.setAndSync(modifier.modify(this.data, this.owner)); + } +} diff --git a/cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/ComponentRegistryImpl.java b/cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/ComponentRegistryImpl.java index 2a66af78..0734c442 100644 --- a/cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/ComponentRegistryImpl.java +++ b/cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/ComponentRegistryImpl.java @@ -24,10 +24,16 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.mojang.serialization.MapCodec; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; import net.minecraft.util.Identifier; import org.ladysnake.cca.api.v3.component.Component; import org.ladysnake.cca.api.v3.component.ComponentKey; import org.ladysnake.cca.api.v3.component.ComponentRegistryV3; +import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponent; +import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponentKey; +import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponentWrapper; import org.ladysnake.cca.internal.base.asm.CcaBootstrap; import javax.annotation.Nullable; @@ -35,6 +41,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Optional; import java.util.stream.Stream; public final class ComponentRegistryImpl implements ComponentRegistryV3 { @@ -68,6 +75,40 @@ public synchronized ComponentKey getOrCreate(Identifier } } + @Override + public synchronized ImmutableComponentKey getOrCreateImmutable(Identifier componentId, Class immutableComponentClass, @org.jetbrains.annotations.Nullable MapCodec cMapCodec, @org.jetbrains.annotations.Nullable PacketCodec registryByteBufCPacketCodec) { + Preconditions.checkArgument(ImmutableComponent.class.isAssignableFrom(immutableComponentClass), "Component interface must extend " + ImmutableComponent.class.getCanonicalName()); + // make sure 2+ components cannot get registered at the same time + @SuppressWarnings("unchecked") + ImmutableComponentKey existing = (ImmutableComponentKey) this.get(componentId); + + if (existing != null) { + if (existing.getImmutableComponentClass() != immutableComponentClass) { + throw new IllegalStateException("Registered component " + componentId + " twice with 2 different classes: " + existing.getComponentClass() + ", " + immutableComponentClass); + } + return existing; + } else { + Class> generated = CcaBootstrap.INSTANCE.getGeneratedComponentTypeClass(componentId); + + if (generated == null) { + throw new IllegalStateException(componentId + " was not registered through mod metadata or plugin"); + } + + if (!ImmutableComponentKey.class.isAssignableFrom(generated)) { + throw new IllegalStateException(componentId + " was registered as a classic component, not an immutable one"); + } + + ImmutableComponentKey registered = this.instantiateStaticImmutableType( + (Class>) generated, + componentId, + immutableComponentClass, + cMapCodec, + registryByteBufCPacketCodec); + this.keys.put(componentId, registered); + return registered; + } + } + private ComponentKey instantiateStaticType(Class> generated, Identifier componentId, Class componentClass) { try { @SuppressWarnings("unchecked") ComponentKey ret = (ComponentKey) generated.getConstructor(Identifier.class, Class.class).newInstance(componentId, componentClass); @@ -77,6 +118,16 @@ private ComponentKey instantiateStaticType(Class ImmutableComponentKey instantiateStaticImmutableType(Class> generated, Identifier componentId, Class componentClass, @org.jetbrains.annotations.Nullable MapCodec mapCodec, @org.jetbrains.annotations.Nullable PacketCodec packetCodec) { + try { + @SuppressWarnings("unchecked") ImmutableComponentKey ret = (ImmutableComponentKey) generated.getConstructor(Identifier.class, Class.class, Class.class, MapCodec.class, PacketCodec.class) + .newInstance(componentId, componentClass, ImmutableComponentWrapper.class, mapCodec, packetCodec); + return ret; + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new IllegalStateException("Failed to create statically declared component type", e); + } + } + @Nullable @Override public ComponentKey get(Identifier id) { diff --git a/cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/ImmutableInternals.java b/cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/ImmutableInternals.java new file mode 100644 index 00000000..ca0c1f2f --- /dev/null +++ b/cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/ImmutableInternals.java @@ -0,0 +1,97 @@ +package org.ladysnake.cca.internal.base; + +import com.mojang.datafixers.util.Pair; +import net.minecraft.entity.Entity; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtOps; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.registry.RegistryOps; +import net.minecraft.registry.RegistryWrapper; +import net.minecraft.util.Identifier; +import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponent; +import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponentHookType; +import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponentKey; +import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponentWrapper; + +import java.lang.invoke.*; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class ImmutableInternals { + private static final MethodHandle RUN_TRANSFORMER; + + static { + try { + RUN_TRANSFORMER = MethodHandles.lookup().findStatic(ImmutableInternals.class, "runTransformer", MethodType.methodType(void.class, ImmutableComponent.Modifier.class, ImmutableComponentWrapper.class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new IllegalStateException("Failed to find one or more method handles", e); + } + } + + //TODO id and type do not uniquely describe a component implementation - e.g. predicates in entity component registration + public static final Map, Map, ImmutableComponent.Modifier>> HOOKS = new HashMap<>(); + public static final Set> HOOK_TYPES = Set.of( + ImmutableComponentHookType.SERVER_TICK, + ImmutableComponentHookType.CLIENT_TICK, + ImmutableComponentHookType.SERVER_LOAD, + ImmutableComponentHookType.CLIENT_LOAD, + ImmutableComponentHookType.SERVER_UNLOAD, + ImmutableComponentHookType.CLIENT_UNLOAD + ); + + public static void addHook(ImmutableComponentKey key, Class target, ImmutableComponentHookType type, ImmutableComponent.Modifier modifier) { + HOOKS.computeIfAbsent(type, $ -> new HashMap<>()).put(Pair.of(key.getId(), target), modifier); + } + + public static Object bootstrap(MethodHandles.Lookup lookup, + String methodName, + MethodType methodType, + String id, + Type targetClass) throws Throwable { + ImmutableComponentHookType callbackType = HOOK_TYPES.stream() + .filter(t -> t.methodName().equals(methodName)) + .filter(t -> t.implType().equals(methodType)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Invalid method name/type: %s:%s".formatted(methodName, methodType.descriptorString()))); + MethodHandle handle = makeModifierHandler(lookup, id, targetClass, HOOKS.getOrDefault(callbackType, Map.of())); + return new ConstantCallSite(handle); + } + + private static MethodHandle makeModifierHandler(MethodHandles.Lookup lookup, String id, Type targetClass, Map, ImmutableComponent.Modifier> handlers) throws NoSuchMethodException, IllegalAccessException { + var modifier = handlers.get(Pair.of(Identifier.of(id), targetClass)); + if (modifier == null) { + return MethodHandles.empty(MethodType.methodType(void.class, ImmutableComponentWrapper.class)); + } + + return RUN_TRANSFORMER.bindTo(modifier); + } + + public static void runTransformer(ImmutableComponent.Modifier transformer, ImmutableComponentWrapper wrapper) { + wrapper.setData(transformer.modify(wrapper.getData(), wrapper.getOwner())); + } + + public static void wrapperRead(ImmutableComponentWrapper wrapper, NbtCompound compound, RegistryWrapper.WrapperLookup registries) { + var ops = RegistryOps.of(NbtOps.INSTANCE, registries); + var decoded = wrapper.getKey().getMapCodec().decode(ops, ops.getMap(compound).getOrThrow()) + .getOrThrow(e -> new RuntimeException("Error decoding component %s:\n%s".formatted(wrapper.getKey().getId(), e))); + wrapper.setData(decoded); + } + + public static void wrapperWrite(ImmutableComponentWrapper wrapper, NbtCompound compound, RegistryWrapper.WrapperLookup registries) { + var ops = RegistryOps.of(NbtOps.INSTANCE, registries); + wrapper.getKey().getMapCodec().encode(wrapper.getData(), ops, ops.mapBuilder()) + .build(compound) + .getOrThrow(e -> new RuntimeException("Error encoding component %s:\n%s".formatted(wrapper.getKey().getId(), e))); + } + + public static void wrapperApplySync(ImmutableComponentWrapper wrapper, RegistryByteBuf buf) { + var decoded = wrapper.getKey().getPacketCodec().decode(buf); + wrapper.setData(decoded); + } + + public static void wrapperWriteSync(ImmutableComponentWrapper wrapper, RegistryByteBuf buf) { + wrapper.getKey().getPacketCodec().encode(buf, wrapper.getData()); + } +} diff --git a/cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/asm/CcaAsmHelper.java b/cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/asm/CcaAsmHelper.java index c9182373..c18aaf5a 100644 --- a/cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/asm/CcaAsmHelper.java +++ b/cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/asm/CcaAsmHelper.java @@ -25,6 +25,10 @@ import it.unimi.dsi.fastutil.objects.ReferenceArraySet; import net.fabricmc.fabric.api.event.Event; import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.registry.RegistryWrapper; +import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.util.Identifier; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -32,7 +36,14 @@ import org.ladysnake.cca.api.v3.component.ComponentContainer; import org.ladysnake.cca.api.v3.component.ComponentKey; import org.ladysnake.cca.api.v3.component.ComponentProvider; +import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponent; +import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponentKey; +import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponentWrapper; +import org.ladysnake.cca.api.v3.component.sync.AutoSyncedComponent; +import org.ladysnake.cca.api.v3.component.tick.ClientTickingComponent; +import org.ladysnake.cca.api.v3.component.tick.ServerTickingComponent; import org.ladysnake.cca.internal.base.AbstractComponentContainer; +import org.ladysnake.cca.internal.base.ImmutableInternals; import org.ladysnake.cca.internal.base.QualifiedComponentFactory; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; @@ -72,14 +83,23 @@ public final class CcaAsmHelper { public static final String COMPONENT = Type.getInternalName(Component.class); public static final String COMPONENT_CONTAINER = Type.getInternalName(ComponentContainer.class); public static final String COMPONENT_TYPE = Type.getInternalName(ComponentKey.class); + public static final String IMMUTABLE_COMPONENT_TYPE = Type.getInternalName(ImmutableComponentKey.class); + public static final String IMMUTABLE_COMPONENT_WRAPPER = Type.getInternalName(ImmutableComponentWrapper.class); + public static final String IMMUTABLE_INTERNALS = Type.getInternalName(ImmutableInternals.class); + public static final String AUTO_SYNCED_COMPONENT = Type.getInternalName(AutoSyncedComponent.class); + public static final String SERVER_TICKING_COMPONENT = Type.getInternalName(ServerTickingComponent.class); + public static final String CLIENT_TICKING_COMPONENT = Type.getInternalName(ClientTickingComponent.class); public static final String DYNAMIC_COMPONENT_CONTAINER_IMPL = Type.getInternalName(AbstractComponentContainer.class); public static final String IDENTIFIER = FabricLoader.getInstance().getMappingResolver().mapClassName("intermediary", "net.minecraft.class_2960").replace('.', '/'); public static final String EVENT = Type.getInternalName(Event.class); + public static final MethodType MODIFIER_MODIFY_TYPE = MethodType.methodType(void.class, ImmutableComponent.class, Object.class); // generated references public static final String STATIC_COMPONENT_CONTAINER = createClassName("GeneratedComponentContainer"); public static final String STATIC_CONTAINER_GETTER_DESC = "()L" + COMPONENT + ";"; public static final String STATIC_COMPONENT_TYPE = createClassName("ComponentType"); + public static final String STATIC_IMMUTABLE_COMPONENT_TYPE = createClassName("ImmutableComponentType"); public static final String STATIC_CONTAINER_FACTORY = createClassName("GeneratedContainerFactory"); + public static final String STATIC_IMMUTABLE_COMPONENT_WRAPPER = createClassName("GeneratedImmutableComponentWrapper"); public static final String ABSTRACT_COMPONENT_CONTAINER_CTOR_DESC; private static final List asmGeneratedCallbacks = findAsmComponentCallbacks(); diff --git a/cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/asm/CcaBootstrap.java b/cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/asm/CcaBootstrap.java index 77c3f057..f8ed8b8a 100644 --- a/cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/asm/CcaBootstrap.java +++ b/cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/asm/CcaBootstrap.java @@ -23,11 +23,15 @@ package org.ladysnake.cca.internal.base.asm; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Iterables; +import com.mojang.serialization.MapCodec; import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.ModContainer; import net.fabricmc.loader.api.entrypoint.EntrypointContainer; import net.fabricmc.loader.api.metadata.CustomValue; import net.fabricmc.loader.api.metadata.ModMetadata; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; import net.minecraft.util.Identifier; import net.minecraft.util.InvalidIdentifierException; import org.ladysnake.cca.api.v3.component.ComponentKey; @@ -52,8 +56,8 @@ import java.util.TreeSet; public final class CcaBootstrap extends LazyDispatcher { - public static final String COMPONENT_TYPE_INIT_DESC = Type.getMethodDescriptor(Type.VOID_TYPE, Type.getObjectType(CcaAsmHelper.IDENTIFIER), Type.getType(Class.class)); + public static final String IMMUTABLE_COMPONENT_TYPE_INIT_DESC = Type.getMethodDescriptor(Type.VOID_TYPE, Type.getObjectType(CcaAsmHelper.IDENTIFIER), Type.getType(Class.class), Type.getType(Class.class), Type.getType(MapCodec.class), Type.getType(PacketCodec.class)); public static final String COMPONENT_TYPE_GET0_DESC = "(L" + CcaAsmHelper.COMPONENT_CONTAINER + ";)L" + CcaAsmHelper.COMPONENT + ";"; public static final String STATIC_INIT_ENTRYPOINT = "cardinal-components:static-init"; public static final CcaBootstrap INSTANCE = new CcaBootstrap(); @@ -61,6 +65,7 @@ public final class CcaBootstrap extends LazyDispatcher { private final List> staticComponentInitializers = FabricLoader.getInstance().getEntrypointContainers(STATIC_INIT_ENTRYPOINT, StaticComponentInitializer.class); @VisibleForTesting Collection additionalComponentIds = new ArrayList<>(); + @VisibleForTesting Collection additionalImmutableComponentIds = new ArrayList<>(); private Map>> generatedComponentTypes = new HashMap<>(); public CcaBootstrap() { @@ -83,6 +88,7 @@ public Class> getGeneratedComponentTypeClass(Identifie protected void init() { try { Set staticComponentTypes = new TreeSet<>(Comparator.comparing(Identifier::toString)); + Set staticImmutableComponentTypes = new TreeSet<>(Comparator.comparing(Identifier::toString)); for (ModContainer mod : FabricLoader.getInstance().getAllMods()) { ModMetadata metadata = mod.getMetadata(); @@ -95,11 +101,21 @@ protected void init() { throw new StaticComponentLoadingException("Failed to load component ids declared by " + metadata.getName() + "(" + metadata.getId() + ")", e); } } + if (metadata.containsCustomValue("cardinal-components-immutable")) { + try { + for (CustomValue value : metadata.getCustomValue("cardinal-components-immutable").getAsArray()) { + staticImmutableComponentTypes.add(Identifier.of(value.getAsString())); + } + } catch (ClassCastException | InvalidIdentifierException e) { + throw new StaticComponentLoadingException("Failed to load component ids declared by " + metadata.getName() + "(" + metadata.getId() + ")", e); + } + } } for (EntrypointContainer staticInitializer : this.staticComponentInitializers) { try { staticComponentTypes.addAll(staticInitializer.getEntrypoint().getSupportedComponentKeys()); + staticImmutableComponentTypes.addAll(staticInitializer.getEntrypoint().getSupportedImmutableComponentKeys()); } catch (Throwable e) { ModMetadata badMod = staticInitializer.getProvider().getMetadata(); throw new StaticComponentLoadingException(String.format("Exception while querying %s (%s) for supported static component types", badMod.getName(), badMod.getId()), e); @@ -107,9 +123,10 @@ protected void init() { } staticComponentTypes.addAll(this.additionalComponentIds); + staticImmutableComponentTypes.addAll(this.additionalImmutableComponentIds); - this.spinStaticContainerItf(staticComponentTypes); - this.generatedComponentTypes = this.spinStaticComponentKeys(staticComponentTypes); + this.spinStaticContainerItf(staticComponentTypes, staticImmutableComponentTypes); + this.generatedComponentTypes = this.spinStaticComponentKeys(staticComponentTypes, staticImmutableComponentTypes); } catch (IOException | UncheckedIOException e) { throw new StaticComponentLoadingException("Failed to load statically defined components", e); } @@ -127,10 +144,11 @@ protected void postInit() { * a global {@link ComponentProvider} specialized interface * that declares a direct getter for every {@link ComponentKey} that has been scanned by plugins. * - * @param staticComponentKeys the set of all statically declared {@link ComponentKey} ids + * @param staticComponentKeys the set of all statically declared {@link ComponentKey} ids + * @param staticImmutableComponentTypes * @return a map of {@link ComponentKey} ids to specialized implementations */ - private Map>> spinStaticComponentKeys(Set staticComponentKeys) throws IOException { + private Map>> spinStaticComponentKeys(Set staticComponentKeys, Set staticImmutableComponentKeys) throws IOException { Map>> generatedComponentTypes = new HashMap<>(staticComponentKeys.size()); for (Identifier componentId : staticComponentKeys) { @@ -163,13 +181,47 @@ private Map>> spinStaticComponentKey @SuppressWarnings("unchecked") Class> ct = (Class>) CcaAsmHelper.generateClass(componentTypeWriter, true, null); generatedComponentTypes.put(componentId, ct); } + + for (Identifier componentId : staticImmutableComponentKeys) { + /* generate the component type class */ + + ClassNode componentTypeWriter = new ClassNode(CcaAsmHelper.ASM_VERSION); + String componentTypeName = CcaAsmHelper.STATIC_IMMUTABLE_COMPONENT_TYPE+"Impl"; + componentTypeWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL, componentTypeName, null, CcaAsmHelper.IMMUTABLE_COMPONENT_TYPE, null); + + MethodVisitor init = componentTypeWriter.visitMethod(Opcodes.ACC_PUBLIC, "", IMMUTABLE_COMPONENT_TYPE_INIT_DESC, null, null); + init.visitCode(); + init.visitVarInsn(Opcodes.ALOAD, 0); // this + init.visitVarInsn(Opcodes.ALOAD, 1); // id + init.visitVarInsn(Opcodes.ALOAD, 2); // class + init.visitVarInsn(Opcodes.ALOAD, 3); // wrapper class + init.visitVarInsn(Opcodes.ALOAD, 4); // map codec + init.visitVarInsn(Opcodes.ALOAD, 5); // packet codec + init.visitMethodInsn(Opcodes.INVOKESPECIAL, CcaAsmHelper.IMMUTABLE_COMPONENT_TYPE, "", IMMUTABLE_COMPONENT_TYPE_INIT_DESC, false); + init.visitInsn(Opcodes.RETURN); + init.visitEnd(); + + MethodVisitor get = componentTypeWriter.visitMethod(Opcodes.ACC_PROTECTED, "getInternal", COMPONENT_TYPE_GET0_DESC, null, null); + get.visitCode(); + get.visitVarInsn(Opcodes.ALOAD, 1); + // stack: object + get.visitTypeInsn(Opcodes.CHECKCAST, CcaAsmHelper.STATIC_COMPONENT_CONTAINER); + // stack: generatedComponentContainer + get.visitMethodInsn(Opcodes.INVOKEVIRTUAL, CcaAsmHelper.STATIC_COMPONENT_CONTAINER, CcaAsmHelper.getStaticStorageGetterName(componentId), CcaAsmHelper.STATIC_CONTAINER_GETTER_DESC, false); + // stack: component + get.visitInsn(Opcodes.ARETURN); + get.visitEnd(); + + @SuppressWarnings("unchecked") Class> ct = (Class>) CcaAsmHelper.generateClass(componentTypeWriter, true, null); + generatedComponentTypes.put(componentId, ct); + } return generatedComponentTypes; } /** * Generate the component container interface implemented by all component containers */ - private void spinStaticContainerItf(Set staticComponentTypes) throws IOException { + private void spinStaticContainerItf(Set staticComponentTypes, Set staticImmutableComponentTypes) throws IOException { ClassNode staticContainerWriter = new ClassNode(CcaAsmHelper.ASM_VERSION); staticContainerWriter.visit(Opcodes.V1_8, Opcodes.ACC_ABSTRACT | Opcodes.ACC_PUBLIC, CcaAsmHelper.STATIC_COMPONENT_CONTAINER, null, CcaAsmHelper.DYNAMIC_COMPONENT_CONTAINER_IMPL, null); @@ -180,7 +232,7 @@ private void spinStaticContainerItf(Set staticComponentTypes) throws init.visitInsn(Opcodes.RETURN); init.visitEnd(); - for (Identifier componentId : staticComponentTypes) { + for (Identifier componentId : Iterables.concat(staticComponentTypes, staticImmutableComponentTypes)) { MethodVisitor methodWriter = staticContainerWriter.visitMethod(Opcodes.ACC_PUBLIC, CcaAsmHelper.getStaticStorageGetterName(componentId), CcaAsmHelper.STATIC_CONTAINER_GETTER_DESC, null, null); methodWriter.visitInsn(Opcodes.ACONST_NULL); methodWriter.visitInsn(Opcodes.ARETURN); diff --git a/cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/asm/ImmutableComponentsAsm.java b/cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/asm/ImmutableComponentsAsm.java new file mode 100644 index 00000000..a428ef96 --- /dev/null +++ b/cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/asm/ImmutableComponentsAsm.java @@ -0,0 +1,180 @@ +/* + * Cardinal-Components-API + * Copyright (C) 2019-2024 Ladysnake + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ +package org.ladysnake.cca.internal.base.asm; + +import net.minecraft.nbt.NbtCompound; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.registry.RegistryWrapper; +import net.minecraft.server.network.ServerPlayerEntity; +import org.ladysnake.cca.api.v3.component.Component; +import org.ladysnake.cca.api.v3.component.ComponentFactory; +import org.ladysnake.cca.api.v3.component.immutable.*; +import org.ladysnake.cca.api.v3.component.sync.AutoSyncedComponent; +import org.ladysnake.cca.internal.base.ImmutableInternals; +import org.objectweb.asm.Handle; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.ClassNode; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; + +import static org.objectweb.asm.Opcodes.H_INVOKESTATIC; + +public final class ImmutableComponentsAsm { + public static final String IMMUTABLE_COMPONENT_WRAPPER_CTOR_DESC; + public static final String COMPONENT_READ_FROM_NBT_DESC; + public static final String COMPONENT_WRITE_TO_NBT_DESC; + public static final String AUTO_SYNCED_COMPONENT_APPLY_SYNC_PACKET_DESC; + public static final String AUTO_SYNCED_COMPONENT_WRITE_SYNC_PACKET_DESC; + public static final String IMMUTABLE_WRAPPER_READ_DESC; + public static final String IMMUTABLE_WRAPPER_WRITE_DESC; + public static final String IMMUTABLE_WRAPPER_APPLY_SYNC_DESC; + public static final String IMMUTABLE_WRAPPER_WRITE_SYNC_DESC; + public static final String IMMUTABLE_BSM_DESC; + + static { + try { + IMMUTABLE_COMPONENT_WRAPPER_CTOR_DESC = Type.getConstructorDescriptor(ImmutableComponentWrapper.class.getDeclaredConstructor(ImmutableComponentKey.class, Object.class, ImmutableComponent.class)); + COMPONENT_READ_FROM_NBT_DESC = Type.getMethodDescriptor(Component.class.getMethod("readFromNbt", NbtCompound.class, RegistryWrapper.WrapperLookup.class)); + COMPONENT_WRITE_TO_NBT_DESC = Type.getMethodDescriptor(Component.class.getMethod("writeToNbt", NbtCompound.class, RegistryWrapper.WrapperLookup.class)); + AUTO_SYNCED_COMPONENT_APPLY_SYNC_PACKET_DESC = Type.getMethodDescriptor(AutoSyncedComponent.class.getMethod("applySyncPacket", RegistryByteBuf.class)); + AUTO_SYNCED_COMPONENT_WRITE_SYNC_PACKET_DESC = Type.getMethodDescriptor(AutoSyncedComponent.class.getMethod("writeSyncPacket", RegistryByteBuf.class, ServerPlayerEntity.class)); + IMMUTABLE_WRAPPER_READ_DESC = Type.getMethodDescriptor(ImmutableInternals.class.getMethod("wrapperRead", ImmutableComponentWrapper.class, NbtCompound.class, RegistryWrapper.WrapperLookup.class)); + IMMUTABLE_WRAPPER_WRITE_DESC = Type.getMethodDescriptor(ImmutableInternals.class.getMethod("wrapperWrite", ImmutableComponentWrapper.class, NbtCompound.class, RegistryWrapper.WrapperLookup.class)); + IMMUTABLE_WRAPPER_APPLY_SYNC_DESC = Type.getMethodDescriptor(ImmutableInternals.class.getMethod("wrapperApplySync", ImmutableComponentWrapper.class, RegistryByteBuf.class)); + IMMUTABLE_WRAPPER_WRITE_SYNC_DESC = Type.getMethodDescriptor(ImmutableInternals.class.getMethod("wrapperWriteSync", ImmutableComponentWrapper.class, RegistryByteBuf.class)); + IMMUTABLE_BSM_DESC = Type.getMethodDescriptor(ImmutableInternals.class.getMethod("bootstrap", MethodHandles.Lookup.class, String.class, MethodType.class, String.class, java.lang.reflect.Type.class)); + } catch (NoSuchMethodException e) { + throw new IllegalStateException("Failed to find one or more method descriptors", e); + } + } + + public static > Class makeWrapper( + ImmutableComponentKey key, + Class targetClass, + Iterable> hookTypes + ) throws IOException, NoSuchMethodException, IllegalAccessException { + ClassNode writer = new ClassNode(CcaAsmHelper.ASM_VERSION); + writer.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, CcaAsmHelper.STATIC_IMMUTABLE_COMPONENT_WRAPPER + "$" + CcaAsmHelper.getJavaIdentifierName(key.getId()), null, CcaAsmHelper.IMMUTABLE_COMPONENT_WRAPPER, null); + + MethodVisitor init = writer.visitMethod(Opcodes.ACC_PUBLIC, "", IMMUTABLE_COMPONENT_WRAPPER_CTOR_DESC, null, null); + init.visitCode(); + init.visitVarInsn(Opcodes.ALOAD, 0); // this + init.visitVarInsn(Opcodes.ALOAD, 1); // key + init.visitVarInsn(Opcodes.ALOAD, 2); // owner + init.visitVarInsn(Opcodes.ALOAD, 3); // data + init.visitMethodInsn(Opcodes.INVOKESPECIAL, CcaAsmHelper.IMMUTABLE_COMPONENT_WRAPPER, "", IMMUTABLE_COMPONENT_WRAPPER_CTOR_DESC, false); + init.visitInsn(Opcodes.RETURN); + init.visitEnd(); + + if (key.getMapCodec() != null) { + MethodVisitor readFromNbt = writer.visitMethod(Opcodes.ACC_PUBLIC, "readFromNbt", COMPONENT_READ_FROM_NBT_DESC, null, null); + readFromNbt.visitVarInsn(Opcodes.ALOAD, 0); // this + readFromNbt.visitVarInsn(Opcodes.ALOAD, 1); // nbt + readFromNbt.visitVarInsn(Opcodes.ALOAD, 2); // registries + readFromNbt.visitMethodInsn(Opcodes.INVOKESTATIC, CcaAsmHelper.IMMUTABLE_INTERNALS, "wrapperRead", IMMUTABLE_WRAPPER_READ_DESC, false); + readFromNbt.visitInsn(Opcodes.RETURN); + readFromNbt.visitEnd(); + + MethodVisitor writeToNbt = writer.visitMethod(Opcodes.ACC_PUBLIC, "writeToNbt", COMPONENT_WRITE_TO_NBT_DESC, null, null); + writeToNbt.visitVarInsn(Opcodes.ALOAD, 0); // this + writeToNbt.visitVarInsn(Opcodes.ALOAD, 1); // nbt + writeToNbt.visitVarInsn(Opcodes.ALOAD, 2); // registries + writeToNbt.visitMethodInsn(Opcodes.INVOKESTATIC, CcaAsmHelper.IMMUTABLE_INTERNALS, "wrapperWrite", IMMUTABLE_WRAPPER_WRITE_DESC, false); + writeToNbt.visitInsn(Opcodes.RETURN); + writeToNbt.visitEnd(); + } + + if (key.getPacketCodec() != null) { + writer.interfaces.add(CcaAsmHelper.AUTO_SYNCED_COMPONENT); + + MethodVisitor applySyncPacket = writer.visitMethod(Opcodes.ACC_PUBLIC, "applySyncPacket", AUTO_SYNCED_COMPONENT_APPLY_SYNC_PACKET_DESC, null, null); + applySyncPacket.visitVarInsn(Opcodes.ALOAD, 0); // this + applySyncPacket.visitVarInsn(Opcodes.ALOAD, 1); // buf + applySyncPacket.visitMethodInsn(Opcodes.INVOKESTATIC, CcaAsmHelper.IMMUTABLE_INTERNALS, "wrapperApplySync", IMMUTABLE_WRAPPER_APPLY_SYNC_DESC, false); + applySyncPacket.visitInsn(Opcodes.RETURN); + applySyncPacket.visitEnd(); + + MethodVisitor writeSyncPacket = writer.visitMethod(Opcodes.ACC_PUBLIC, "writeSyncPacket", AUTO_SYNCED_COMPONENT_WRITE_SYNC_PACKET_DESC, null, null); + writeSyncPacket.visitVarInsn(Opcodes.ALOAD, 0); // this + writeSyncPacket.visitVarInsn(Opcodes.ALOAD, 1); // buf + writeSyncPacket.visitMethodInsn(Opcodes.INVOKESTATIC, CcaAsmHelper.IMMUTABLE_INTERNALS, "wrapperWriteSync", IMMUTABLE_WRAPPER_WRITE_SYNC_DESC, false); + writeSyncPacket.visitInsn(Opcodes.RETURN); + writeSyncPacket.visitEnd(); + } + + for (var hookType : hookTypes) { + implementHookItf(key, targetClass, writer, hookType); + } + + writer.visitEnd(); + return (Class) CcaAsmHelper.generateClass(writer, false, null); + } + + private static void implementHookItf(ImmutableComponentKey key, Class targetClass, ClassNode writer, ImmutableComponentHookType hookType) { + String interfaceName = Type.getInternalName(hookType.itf()); + writer.interfaces.add(interfaceName); + String methodName = hookType.methodName(); + String methodDesc = hookType.exposedType().toMethodDescriptorString(); + var dynMethodType = hookType.exposedType().insertParameterTypes(0, ImmutableComponentWrapper.class); + String dynMethodDesc = dynMethodType.toMethodDescriptorString(); + MethodVisitor onTick = writer.visitMethod(Opcodes.ACC_PUBLIC, methodName, methodDesc, null, null); + for (int i = 0; i < dynMethodType.parameterCount(); i++) { + onTick.visitVarInsn(Opcodes.ALOAD, i); + } + onTick.visitInvokeDynamicInsn( + methodName, + dynMethodDesc, + new Handle( + H_INVOKESTATIC, + CcaAsmHelper.IMMUTABLE_INTERNALS, + "bootstrap", + IMMUTABLE_BSM_DESC, + false), + key.getId().toString(), + Type.getType(targetClass)); + onTick.visitInsn(Opcodes.RETURN); + onTick.visitEnd(); + } + + public static > ComponentFactory makeFactory( + ImmutableComponentKey key, + Class targetClass, + Class wrapperClass, + ImmutableComponentFactory dataFactory + ) throws NoSuchMethodException, IllegalAccessException { + var constructor = MethodHandles.lookup() + .findConstructor(wrapperClass, MethodType.methodType(void.class, ImmutableComponentKey.class, Object.class, ImmutableComponent.class)) + .bindTo(key); + return o -> { + try { + return (W) constructor.invoke(o, dataFactory.createComponent(o)); + } catch (Throwable e) { + throw new RuntimeException(e); + } + }; + } +} diff --git a/cardinal-components-base/src/testmod/java/org/ladysnake/cca/test/base/Energy.java b/cardinal-components-base/src/testmod/java/org/ladysnake/cca/test/base/Energy.java new file mode 100644 index 00000000..018a29f1 --- /dev/null +++ b/cardinal-components-base/src/testmod/java/org/ladysnake/cca/test/base/Energy.java @@ -0,0 +1,27 @@ +package org.ladysnake.cca.test.base; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.util.Identifier; +import org.ladysnake.cca.api.v3.component.ComponentRegistry; +import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponent; +import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponentKey; + +public record Energy(int amount) implements ImmutableComponent { + public static final PacketCodec STREAM_CODEC = PacketCodec.tuple( + PacketCodecs.VAR_INT, + Energy::amount, + Energy::new); + + public static final MapCodec MAP_CODEC = Codec.INT.fieldOf("energy_amount") + .xmap(Energy::new, Energy::amount); + + public static final ImmutableComponentKey KEY = ComponentRegistry.getOrCreate( + Identifier.of("cca-base-test", "energy"), + Energy.class, + MAP_CODEC, + STREAM_CODEC); +} diff --git a/cardinal-components-base/src/testmod/resources/fabric.mod.json b/cardinal-components-base/src/testmod/resources/fabric.mod.json index 79bd67cd..70fb7586 100644 --- a/cardinal-components-base/src/testmod/resources/fabric.mod.json +++ b/cardinal-components-base/src/testmod/resources/fabric.mod.json @@ -19,7 +19,10 @@ "testmod:test", "testmod:test_2", "testmod:test_3" - ] + ], + "cardinal-components-immutable": [ + "cca-base-test:energy" + ] }, "depends": { "fabric-api-base": "*" diff --git a/cardinal-components-entity/src/main/java/org/ladysnake/cca/api/v3/entity/EntityComponentFactoryRegistry.java b/cardinal-components-entity/src/main/java/org/ladysnake/cca/api/v3/entity/EntityComponentFactoryRegistry.java index bd09bd78..1d128fc2 100644 --- a/cardinal-components-entity/src/main/java/org/ladysnake/cca/api/v3/entity/EntityComponentFactoryRegistry.java +++ b/cardinal-components-entity/src/main/java/org/ladysnake/cca/api/v3/entity/EntityComponentFactoryRegistry.java @@ -28,6 +28,7 @@ import org.ladysnake.cca.api.v3.component.Component; import org.ladysnake.cca.api.v3.component.ComponentFactory; import org.ladysnake.cca.api.v3.component.ComponentKey; +import org.ladysnake.cca.api.v3.component.immutable.*; import java.util.function.Predicate; @@ -98,6 +99,8 @@ public interface EntityComponentFactoryRegistry { */ void registerForPlayers(ComponentKey key, ComponentFactory factory, RespawnCopyStrategy respawnStrategy); + ImmutableRegistration beginImmutableRegistration(Class target, ImmutableComponentKey key); + interface Registration { /** * Registers a {@link ComponentFactory} for all instances of classes that pass the {@code test}. @@ -151,4 +154,20 @@ interface Registration { */ void end(ComponentFactory factory); } + + + interface ImmutableRegistration { + ImmutableRegistration filter(Predicate> test); + ImmutableRegistration after(ComponentKey dependency); + ImmutableRegistration respawnStrategy(RespawnCopyStrategy> strategy); + ImmutableRegistration onHook(ImmutableComponentHookType type, ImmutableComponent.Modifier modifier); + ImmutableRegistration onServerTick(ImmutableComponent.Modifier modifier); + ImmutableRegistration onClientTick(ImmutableComponent.Modifier modifier); + ImmutableRegistration onServerLoad(ImmutableComponent.Modifier modifier); + ImmutableRegistration onClientLoad(ImmutableComponent.Modifier modifier); + ImmutableRegistration onServerUnload(ImmutableComponent.Modifier modifier); + ImmutableRegistration onClientUnload(ImmutableComponent.Modifier modifier); + + void end(ImmutableComponentFactory factory); + } } diff --git a/cardinal-components-entity/src/main/java/org/ladysnake/cca/internal/entity/StaticEntityComponentPlugin.java b/cardinal-components-entity/src/main/java/org/ladysnake/cca/internal/entity/StaticEntityComponentPlugin.java index a6cb804b..68d19671 100644 --- a/cardinal-components-entity/src/main/java/org/ladysnake/cca/internal/entity/StaticEntityComponentPlugin.java +++ b/cardinal-components-entity/src/main/java/org/ladysnake/cca/internal/entity/StaticEntityComponentPlugin.java @@ -28,15 +28,19 @@ import org.ladysnake.cca.api.v3.component.ComponentContainer; import org.ladysnake.cca.api.v3.component.ComponentFactory; import org.ladysnake.cca.api.v3.component.ComponentKey; +import org.ladysnake.cca.api.v3.component.immutable.*; import org.ladysnake.cca.api.v3.entity.EntityComponentFactoryRegistry; import org.ladysnake.cca.api.v3.entity.EntityComponentInitializer; import org.ladysnake.cca.api.v3.entity.RespawnableComponent; import org.ladysnake.cca.api.v3.entity.RespawnCopyStrategy; +import org.ladysnake.cca.internal.base.ImmutableInternals; import org.ladysnake.cca.internal.base.LazyDispatcher; import org.ladysnake.cca.internal.base.QualifiedComponentFactory; +import org.ladysnake.cca.internal.base.asm.ImmutableComponentsAsm; import org.ladysnake.cca.internal.base.asm.StaticComponentLoadingException; import org.ladysnake.cca.internal.base.asm.StaticComponentPluginBase; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -124,6 +128,11 @@ public Registration beginRegistrat return new RegistrationImpl<>(target, key); } + @Override + public ImmutableRegistration beginImmutableRegistration(Class target, ImmutableComponentKey key) { + return new ImmutableRegistrationImpl<>(target, key); + } + @Override public > void registerForPlayers(ComponentKey key, ComponentFactory factory) { this.registerForPlayers(key, factory, CardinalEntityInternals.DEFAULT_COPY_STRATEGY); @@ -228,4 +237,109 @@ public void end(ComponentFactory factory) { } } } + + private final class ImmutableRegistrationImpl implements ImmutableRegistration { + private final Class target; + private final ImmutableComponentKey key; + private final Set> dependencies; + private final Map, ImmutableComponent.Modifier> hooks; + private Predicate> test; + + ImmutableRegistrationImpl(Class target, ImmutableComponentKey key) { + this.target = target; + this.dependencies = new LinkedHashSet<>(); + this.hooks = new HashMap<>(); + this.test = null; + this.key = key; + } + + @Override + public ImmutableRegistration filter(Predicate> test) { + this.test = this.test == null ? test : this.test.and(test); + return this; + } + + @Override + public ImmutableRegistration after(ComponentKey dependency) { + this.dependencies.add(dependency); + return this; + } + + @Override + public ImmutableRegistration respawnStrategy(RespawnCopyStrategy> strategy) { + CardinalEntityInternals.registerRespawnCopyStrat(this.key, this.target, strategy); + return this; + } + + @Override + public ImmutableRegistration onHook(ImmutableComponentHookType type, ImmutableComponent.Modifier modifier) { + this.hooks.put(type, modifier); + return this; + } + + @Override + public ImmutableRegistration onServerTick(ImmutableComponent.Modifier modifier) { + return this.onHook(ImmutableComponentHookType.SERVER_TICK, modifier); + } + + @Override + public ImmutableRegistration onClientTick(ImmutableComponent.Modifier modifier) { + return this.onHook(ImmutableComponentHookType.CLIENT_TICK, modifier); + } + + @Override + public ImmutableRegistration onServerLoad(ImmutableComponent.Modifier modifier) { + return this.onHook(ImmutableComponentHookType.SERVER_LOAD, modifier); + } + + @Override + public ImmutableRegistration onClientLoad(ImmutableComponent.Modifier modifier) { + return this.onHook(ImmutableComponentHookType.CLIENT_LOAD, modifier); + } + + @Override + public ImmutableRegistration onServerUnload(ImmutableComponent.Modifier modifier) { + return this.onHook(ImmutableComponentHookType.SERVER_UNLOAD, modifier); + } + + @Override + public ImmutableRegistration onClientUnload(ImmutableComponent.Modifier modifier) { + return this.onHook(ImmutableComponentHookType.CLIENT_UNLOAD, modifier); + } + + @Override + public void end(ImmutableComponentFactory factory) { + try { + StaticEntityComponentPlugin.this.checkLoading(Registration.class, "end"); + Class> componentClass = ImmutableComponentsAsm.makeWrapper( + this.key, + this.target, + this.hooks.keySet() + ); + ComponentFactory> componentFactory = ImmutableComponentsAsm.makeFactory(this.key, this.target, componentClass, factory); + if (this.test == null) { + StaticEntityComponentPlugin.this.register0( + this.target, + this.key, + new QualifiedComponentFactory<>(componentFactory, componentClass, this.dependencies) + ); + } else { + StaticEntityComponentPlugin.this.dynamicFactories.add(new PredicatedComponentFactory<>( + c -> this.target.isAssignableFrom(c) && this.test.test(c.asSubclass(this.target)), + this.key, + new QualifiedComponentFactory<>( + entity -> componentFactory.createComponent(this.target.cast(entity)), + componentClass, + this.dependencies + ) + )); + } + for (var callback : hooks.entrySet()) { + ImmutableInternals.addHook(this.key, this.target, callback.getKey(), callback.getValue()); + } + } catch (IOException | NoSuchMethodException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } } diff --git a/cardinal-components-entity/src/testmod/java/org/ladysnake/cca/test/entity/CcaEntityTestMod.java b/cardinal-components-entity/src/testmod/java/org/ladysnake/cca/test/entity/CcaEntityTestMod.java index 82ffee52..78253e1d 100644 --- a/cardinal-components-entity/src/testmod/java/org/ladysnake/cca/test/entity/CcaEntityTestMod.java +++ b/cardinal-components-entity/src/testmod/java/org/ladysnake/cca/test/entity/CcaEntityTestMod.java @@ -38,6 +38,7 @@ import org.ladysnake.cca.api.v3.entity.EntityComponentInitializer; import org.ladysnake.cca.api.v3.entity.RespawnCopyStrategy; import org.ladysnake.cca.test.base.BaseVita; +import org.ladysnake.cca.test.base.Energy; import org.ladysnake.cca.test.base.LoadAwareTestComponent; import org.ladysnake.cca.test.base.Vita; @@ -57,6 +58,13 @@ public void registerEntityComponentFactories(EntityComponentFactoryRegistry regi registry.beginRegistration(PlayerEntity.class, Vita.KEY).impl(PlayerVita.class).end(PlayerVita::new); registry.beginRegistration(CamelEntity.class, Vita.KEY).impl(EntityVita.class).respawnStrategy(RespawnCopyStrategy.ALWAYS_COPY).end(owner -> new EntityVita(owner, CAMEL_BASE_VITA)); registry.beginRegistration(ShulkerEntity.class, LoadAwareTestComponent.KEY).impl(LoadAwareTestComponent.class).end(e -> new LoadAwareTestComponent()); + registry.beginImmutableRegistration(PlayerEntity.class, Energy.KEY) + .onServerTick(PlayerEnergy::onServerTick) + .onClientTick(PlayerEnergy::onClientTick) + .onServerLoad(PlayerEnergy::onServerLoad) + .onClientLoad(PlayerEnergy::onClientLoad) + .respawnStrategy(RespawnCopyStrategy.INVENTORY) + .end($ -> new Energy(0)); } @Override diff --git a/cardinal-components-entity/src/testmod/java/org/ladysnake/cca/test/entity/PlayerEnergy.java b/cardinal-components-entity/src/testmod/java/org/ladysnake/cca/test/entity/PlayerEnergy.java new file mode 100644 index 00000000..91fd880d --- /dev/null +++ b/cardinal-components-entity/src/testmod/java/org/ladysnake/cca/test/entity/PlayerEnergy.java @@ -0,0 +1,24 @@ +package org.ladysnake.cca.test.entity; + +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.text.Text; +import org.ladysnake.cca.test.base.Energy; + +public class PlayerEnergy { + public static Energy onServerTick(Energy energy, PlayerEntity player) { + return energy; + } + + public static Energy onClientTick(Energy energy, PlayerEntity player) { + player.sendMessage(Text.literal("hi!!"), false); + return energy; + } + + public static Energy onServerLoad(Energy energy, PlayerEntity player) { + return energy; + } + + public static Energy onClientLoad(Energy energy, PlayerEntity player) { + return energy; + } +}