From 7ff175a05311630ba60a0c41c4eff1e3a9aea17a Mon Sep 17 00:00:00 2001 From: williambl Date: Sun, 17 Nov 2024 22:55:36 +0000 Subject: [PATCH 1/5] feat: immutable components --- .../api/v3/component/ComponentRegistry.java | 23 ++++++ .../api/v3/component/ComponentRegistryV3.java | 8 ++ .../component/StaticComponentInitializer.java | 4 + .../immutable/ImmutableComponent.java | 11 +++ .../immutable/ImmutableComponentFactory.java | 48 +++++++++++ .../immutable/ImmutableComponentKey.java | 33 ++++++++ .../immutable/ImmutableComponentWrapper.java | 38 +++++++++ .../internal/base/ComponentRegistryImpl.java | 51 ++++++++++++ .../cca/internal/base/asm/CcaAsmHelper.java | 9 ++ .../cca/internal/base/asm/CcaBootstrap.java | 66 +++++++++++++-- .../base/asm/CcaImmutableBootstrap.java | 82 +++++++++++++++++++ .../org/ladysnake/cca/test/base/Energy.java | 27 ++++++ .../src/testmod/resources/fabric.mod.json | 5 +- .../EntityComponentFactoryRegistry.java | 14 ++++ .../entity/StaticEntityComponentPlugin.java | 72 ++++++++++++++++ .../cca/test/entity/CcaEntityTestMod.java | 8 ++ 16 files changed, 491 insertions(+), 8 deletions(-) create mode 100644 cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponent.java create mode 100644 cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponentFactory.java create mode 100644 cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponentKey.java create mode 100644 cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponentWrapper.java create mode 100644 cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/asm/CcaImmutableBootstrap.java create mode 100644 cardinal-components-base/src/testmod/java/org/ladysnake/cca/test/base/Energy.java 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..126eace0 --- /dev/null +++ b/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponent.java @@ -0,0 +1,11 @@ +package org.ladysnake.cca.api.v3.component.immutable; + +import net.minecraft.nbt.NbtCompound; +import net.minecraft.registry.RegistryWrapper; +import org.ladysnake.cca.api.v3.component.Component; + +public interface ImmutableComponent { + interface Ticker { + C onTick(C component, O attachedTo); + } +} 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/ImmutableComponentKey.java b/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponentKey.java new file mode 100644 index 00000000..0a7b2b05 --- /dev/null +++ b/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponentKey.java @@ -0,0 +1,33 @@ +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.Nullable; +import org.ladysnake.cca.api.v3.component.ComponentKey; + +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; + } +} 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..393eb45e --- /dev/null +++ b/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponentWrapper.java @@ -0,0 +1,38 @@ +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; + +@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; + } +} 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/asm/CcaAsmHelper.java b/cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/asm/CcaAsmHelper.java index c9182373..5d346e39 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 @@ -32,6 +32,9 @@ 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.internal.base.AbstractComponentContainer; import org.ladysnake.cca.internal.base.QualifiedComponentFactory; import org.objectweb.asm.ClassReader; @@ -72,6 +75,8 @@ 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 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); @@ -79,8 +84,11 @@ public final class CcaAsmHelper { 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; + public static final String IMMUTABLE_COMPONENT_WRAPPER_CTOR_DESC; private static final List asmGeneratedCallbacks = findAsmComponentCallbacks(); @@ -89,6 +97,7 @@ record AsmGeneratedCallbackInfo(String containerCallbackName, Class> 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/CcaImmutableBootstrap.java b/cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/asm/CcaImmutableBootstrap.java new file mode 100644 index 00000000..759842ce --- /dev/null +++ b/cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/asm/CcaImmutableBootstrap.java @@ -0,0 +1,82 @@ +/* + * 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.entity.Entity; +import net.minecraft.util.Identifier; +import org.ladysnake.cca.api.v3.component.ComponentFactory; +import org.ladysnake.cca.api.v3.component.ComponentKey; +import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponent; +import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponentFactory; +import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponentKey; +import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponentWrapper; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.ClassNode; + +import java.io.IOException; +import java.lang.invoke.LambdaMetafactory; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; + +public final class CcaImmutableBootstrap { + public static > Class makeWrapper( + ImmutableComponentKey key, + Class targetClass + ) throws IOException { + 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, "", CcaAsmHelper.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, "", CcaAsmHelper.IMMUTABLE_COMPONENT_WRAPPER_CTOR_DESC, false); + init.visitInsn(Opcodes.RETURN); + init.visitEnd(); + + writer.visitEnd(); + return (Class) CcaAsmHelper.generateClass(writer, false, null); + } + + 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..826f79e9 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,10 @@ 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.ImmutableComponent; +import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponentFactory; +import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponentKey; +import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponentWrapper; import java.util.function.Predicate; @@ -98,6 +102,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 +157,12 @@ interface Registration { */ void end(ComponentFactory factory); } + + + interface ImmutableRegistration { + ImmutableRegistration filter(Predicate> test); + ImmutableRegistration after(ComponentKey dependency); + ImmutableRegistration respawnStrategy(RespawnCopyStrategy> strategy); + 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..797031c9 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,21 @@ 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.ImmutableComponent; +import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponentFactory; +import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponentKey; +import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponentWrapper; 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.LazyDispatcher; import org.ladysnake.cca.internal.base.QualifiedComponentFactory; +import org.ladysnake.cca.internal.base.asm.CcaImmutableBootstrap; 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 +130,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 +239,65 @@ public void end(ComponentFactory factory) { } } } + + private final class ImmutableRegistrationImpl implements ImmutableRegistration { + private final Class target; + private final ImmutableComponentKey key; + private final Set> dependencies; + private Predicate> test; + + ImmutableRegistrationImpl(Class target, ImmutableComponentKey key) { + this.target = target; + this.dependencies = new LinkedHashSet<>(); + 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 void end(ImmutableComponentFactory factory) { + try { + StaticEntityComponentPlugin.this.checkLoading(Registration.class, "end"); + Class> componentClass = CcaImmutableBootstrap.makeWrapper(this.key, this.target); + ComponentFactory> componentFactory = CcaImmutableBootstrap.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 + ) + )); + } + } 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..b2d717f9 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 From b8303f4615ad8b0b7ebe5e4164ce6d26f5a083db Mon Sep 17 00:00:00 2001 From: williambl Date: Sun, 17 Nov 2024 23:39:47 +0000 Subject: [PATCH 2/5] feat: immutable components sync / persist --- .../immutable/ImmutableComponentWrapper.java | 16 +++++++ .../cca/internal/base/ImmutableInternals.java | 36 ++++++++++++++++ .../cca/internal/base/asm/CcaAsmHelper.java | 24 +++++++++++ .../base/asm/CcaImmutableBootstrap.java | 42 ++++++++++++++++++- 4 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/ImmutableInternals.java 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 index 393eb45e..caf9158a 100644 --- 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 @@ -35,4 +35,20 @@ public void readFromNbt(NbtCompound tag, RegistryWrapper.WrapperLookup registryL 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; + } } 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..799be147 --- /dev/null +++ b/cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/ImmutableInternals.java @@ -0,0 +1,36 @@ +package org.ladysnake.cca.internal.base; + +import com.mojang.serialization.Dynamic; +import com.mojang.serialization.MapCodec; +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 org.ladysnake.cca.api.v3.component.immutable.ImmutableComponent; +import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponentWrapper; + +public class ImmutableInternals { + 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 5d346e39..60acd8b3 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; @@ -35,7 +39,9 @@ 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.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; @@ -77,6 +83,8 @@ public final class CcaAsmHelper { 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 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); @@ -89,6 +97,14 @@ public final class CcaAsmHelper { public static final String STATIC_IMMUTABLE_COMPONENT_WRAPPER = createClassName("GeneratedImmutableComponentWrapper"); public static final String ABSTRACT_COMPONENT_CONTAINER_CTOR_DESC; 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; private static final List asmGeneratedCallbacks = findAsmComponentCallbacks(); @@ -98,6 +114,14 @@ record AsmGeneratedCallbackInfo(String containerCallbackName, Class> Class makeWrapper( ImmutableComponentKey key, Class targetClass - ) throws IOException { + ) 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); @@ -58,6 +62,42 @@ public static ) CcaAsmHelper.generateClass(writer, false, null); } From 4ce63057c81588b3a0cf05d2d724def816c7fc40 Mon Sep 17 00:00:00 2001 From: williambl Date: Mon, 18 Nov 2024 02:16:58 +0000 Subject: [PATCH 3/5] feat(wip): immutable components tickers --- .../immutable/ImmutableComponent.java | 16 ++-- .../immutable/ImmutableComponentWrapper.java | 1 + .../cca/internal/base/ImmutableInternals.java | 74 +++++++++++++++++++ .../cca/internal/base/asm/CcaAsmHelper.java | 12 +++ .../base/asm/CcaImmutableBootstrap.java | 48 +++++++++--- .../EntityComponentFactoryRegistry.java | 5 ++ .../entity/StaticEntityComponentPlugin.java | 38 +++++++++- .../cca/test/entity/CcaEntityTestMod.java | 4 +- .../cca/test/entity/PlayerEnergy.java | 24 ++++++ 9 files changed, 204 insertions(+), 18 deletions(-) create mode 100644 cardinal-components-entity/src/testmod/java/org/ladysnake/cca/test/entity/PlayerEnergy.java 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 index 126eace0..dd90a307 100644 --- 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 @@ -1,11 +1,15 @@ package org.ladysnake.cca.api.v3.component.immutable; -import net.minecraft.nbt.NbtCompound; -import net.minecraft.registry.RegistryWrapper; -import org.ladysnake.cca.api.v3.component.Component; - public interface ImmutableComponent { - interface Ticker { - C onTick(C component, O attachedTo); + interface Modifier { + C modify(C component, O attachedTo); + } + 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/ImmutableComponentWrapper.java b/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponentWrapper.java index caf9158a..5b55e751 100644 --- 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 @@ -48,6 +48,7 @@ public C getData() { return this.data; } + //TODO more methods for this. syncing, unary operators, etc. public void setData(C data) { this.data = data; } 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 index 799be147..096b9d45 100644 --- 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 @@ -1,5 +1,6 @@ package org.ladysnake.cca.internal.base; +import com.mojang.datafixers.util.Pair; import com.mojang.serialization.Dynamic; import com.mojang.serialization.MapCodec; import net.minecraft.nbt.NbtCompound; @@ -7,10 +8,83 @@ 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.ImmutableComponentWrapper; +import org.ladysnake.cca.internal.base.asm.CcaAsmHelper; + +import java.lang.invoke.*; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; public class ImmutableInternals { + private static final MethodHandle SET; + private static final MethodHandle DECONSTRUCT_WRAPPER; + + static { + try { + DECONSTRUCT_WRAPPER = MethodHandles.lookup().findStatic(ImmutableInternals.class, "deconstructWrapper", MethodType.methodType(Object.class.arrayType(), ImmutableComponentWrapper.class)); + SET = MethodHandles.lookup().findVirtual(ImmutableComponentWrapper.class, "setData", MethodType.methodType(void.class, ImmutableComponent.class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + //TODO id and type do not uniquely describe a component implementation - e.g. predicates in entity component registration + public static final Map, ImmutableComponent.Modifier> serverTickHandlers = new HashMap<>(); + public static final Map, ImmutableComponent.Modifier> clientTickHandlers = new HashMap<>(); + public static final Map, ImmutableComponent.Modifier> loadServersideHandlers = new HashMap<>(); + public static final Map, ImmutableComponent.Modifier> loadClientsideHandlers = new HashMap<>(); + public static final Map, ImmutableComponent.Modifier> unloadServersideHandlers = new HashMap<>(); + public static final Map, ImmutableComponent.Modifier> unloadClientsideHandlers = new HashMap<>(); + + public static Object bootstrap(MethodHandles.Lookup lookup, String methodName, TypeDescriptor type, + String id, + Type targetClass) throws Throwable { + MethodType methodType; + if (type instanceof MethodType mt) + methodType = mt; + else { + methodType = null; + if (!MethodHandle.class.equals(type)) + throw new IllegalArgumentException(type.toString()); + } + MethodHandle handle = switch (methodName) { + case "serverTick" -> makeModifierHandler(lookup, id, targetClass, serverTickHandlers); + case "clientTick" -> makeModifierHandler(lookup, id, targetClass, clientTickHandlers); + case "loadServerside" -> makeModifierHandler(lookup, id, targetClass, loadServersideHandlers); + case "loadClientside" -> makeModifierHandler(lookup, id, targetClass, loadClientsideHandlers); + case "unloadServerside" -> makeModifierHandler(lookup, id, targetClass, unloadServersideHandlers); + case "unloadClientside" -> makeModifierHandler(lookup, id, targetClass, unloadClientsideHandlers); + default -> throw new IllegalArgumentException(methodName); + }; + return methodType != null ? new ConstantCallSite(handle) : 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)); + } + // todo for tomorrow: work out how to get a MethodHandle from a lambda / anonymous class + // so that this doesn't happen: + // java.lang.IllegalAccessException: symbolic reference class is not accessible: class org.ladysnake.cca.test.entity.CcaEntityTestMod$$Lambda/0x0000000601c90428 + + // (c, o) -> c + var transform = lookup.bind(modifier, "modify", CcaAsmHelper.MODIFIER_MODIFY_TYPE); + // (c, o) -> void + var tfAndSet = MethodHandles.filterReturnValue(transform, SET); + // ([c,o]) -> void + var spreadTfAndSet = MethodHandles.spreadInvoker(tfAndSet.type(), 0).bindTo(tfAndSet); + // w -> void + return MethodHandles.filterArguments(spreadTfAndSet, 0, DECONSTRUCT_WRAPPER); + } + + public static Object[] deconstructWrapper(ImmutableComponentWrapper wrapper) { + return new Object[] {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()) 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 60acd8b3..8c1c4710 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 @@ -40,6 +40,8 @@ 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; @@ -56,6 +58,7 @@ import java.io.IOException; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; +import java.lang.invoke.TypeDescriptor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.nio.file.Files; @@ -85,9 +88,12 @@ public final class CcaAsmHelper { 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 + ";"; @@ -105,6 +111,9 @@ public final class CcaAsmHelper { 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 SERVER_TICK_DESC; + public static final String CLIENT_TICK_DESC; + public static final String IMMUTABLE_BSM_DESC; private static final List asmGeneratedCallbacks = findAsmComponentCallbacks(); @@ -122,6 +131,9 @@ record AsmGeneratedCallbackInfo(String containerCallbackName, Class> Class makeWrapper( ImmutableComponentKey key, - Class targetClass + Class targetClass, + ImmutableComponent.Modifier serverTicker, + ImmutableComponent.Modifier clientTicker, + ImmutableComponent.Modifier serverOnLoad, + ImmutableComponent.Modifier clientOnLoad ) 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); @@ -98,10 +99,39 @@ public static ) CcaAsmHelper.generateClass(writer, false, null); } + private static void makeTicker(ImmutableComponentKey key, Class targetClass, ClassNode writer, String interfaceName, String methodName, String methodDesc) { + writer.interfaces.add(interfaceName); + MethodVisitor onTick = writer.visitMethod(Opcodes.ACC_PUBLIC, methodName, methodDesc, null, null); + onTick.visitVarInsn(Opcodes.ALOAD, 0); // this + onTick.visitInvokeDynamicInsn( + methodName, + methodDesc, + new Handle( + H_INVOKESTATIC, + CcaAsmHelper.IMMUTABLE_INTERNALS, + "bootstrap", + CcaAsmHelper.IMMUTABLE_BSM_DESC, + false), + key.getId().toString(), + Type.getType(targetClass)); + onTick.visitInsn(Opcodes.RETURN); + onTick.visitEnd(); + } + public static > ComponentFactory makeFactory( ImmutableComponentKey key, Class targetClass, 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 826f79e9..dc9890d5 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 @@ -163,6 +163,11 @@ interface ImmutableRegistration ImmutableRegistration filter(Predicate> test); ImmutableRegistration after(ComponentKey dependency); ImmutableRegistration respawnStrategy(RespawnCopyStrategy> strategy); + ImmutableRegistration onServerTick(ImmutableComponent.Modifier modifier); + ImmutableRegistration onClientTick(ImmutableComponent.Modifier modifier); + ImmutableRegistration onServerLoad(ImmutableComponent.Modifier modifier); + ImmutableRegistration onClientLoad(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 797031c9..0cc8a144 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 @@ -24,6 +24,7 @@ import net.minecraft.entity.Entity; import net.minecraft.entity.player.PlayerEntity; +import org.jetbrains.annotations.Nullable; import org.ladysnake.cca.api.v3.component.Component; import org.ladysnake.cca.api.v3.component.ComponentContainer; import org.ladysnake.cca.api.v3.component.ComponentFactory; @@ -244,6 +245,10 @@ private final class ImmutableRegistrationImpl target; private final ImmutableComponentKey key; private final Set> dependencies; + private @Nullable ImmutableComponent.Modifier serverTicker; + private @Nullable ImmutableComponent.Modifier clientTicker; + private @Nullable ImmutableComponent.Modifier serverOnLoad; + private @Nullable ImmutableComponent.Modifier clientOnLoad; private Predicate> test; ImmutableRegistrationImpl(Class target, ImmutableComponentKey key) { @@ -272,11 +277,42 @@ public ImmutableRegistration respawnStrategy(RespawnCopyStrategy onServerTick(ImmutableComponent.Modifier modifier) { + this.serverTicker = modifier; + return this; + } + + @Override + public ImmutableRegistration onClientTick(ImmutableComponent.Modifier modifier) { + this.clientTicker = modifier; + return this; + } + + @Override + public ImmutableRegistration onServerLoad(ImmutableComponent.Modifier modifier) { + this.serverOnLoad = modifier; + return this; + } + + @Override + public ImmutableRegistration onClientLoad(ImmutableComponent.Modifier modifier) { + this.clientOnLoad = modifier; + return this; + } + @Override public void end(ImmutableComponentFactory factory) { try { StaticEntityComponentPlugin.this.checkLoading(Registration.class, "end"); - Class> componentClass = CcaImmutableBootstrap.makeWrapper(this.key, this.target); + Class> componentClass = CcaImmutableBootstrap.makeWrapper( + this.key, + this.target, + this.serverTicker, + this.clientTicker, + this.serverOnLoad, + this.clientOnLoad + ); ComponentFactory> componentFactory = CcaImmutableBootstrap.makeFactory(this.key, this.target, componentClass, factory); if (this.test == null) { StaticEntityComponentPlugin.this.register0( 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 b2d717f9..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 @@ -59,10 +59,10 @@ public void registerEntityComponentFactories(EntityComponentFactoryRegistry regi 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) + .onServerTick(PlayerEnergy::onServerTick) .onClientTick(PlayerEnergy::onClientTick) .onServerLoad(PlayerEnergy::onServerLoad) - .onClientLoad(PlayerEnergy::onClientLoad)*/ + .onClientLoad(PlayerEnergy::onClientLoad) .respawnStrategy(RespawnCopyStrategy.INVENTORY) .end($ -> new Energy(0)); } 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; + } +} From b23fd3eae5413a5d1049e31970610b34ff355095 Mon Sep 17 00:00:00 2001 From: williambl Date: Mon, 18 Nov 2024 09:58:47 +0000 Subject: [PATCH 4/5] feat: immutable components tickers --- .../cca/internal/base/ImmutableInternals.java | 18 +++++++----------- .../cca/internal/base/asm/CcaAsmHelper.java | 4 ++++ .../base/asm/CcaImmutableBootstrap.java | 8 ++++---- 3 files changed, 15 insertions(+), 15 deletions(-) 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 index 096b9d45..192106bb 100644 --- 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 @@ -21,11 +21,13 @@ public class ImmutableInternals { private static final MethodHandle SET; private static final MethodHandle DECONSTRUCT_WRAPPER; + private static final MethodHandle RUN_TRANSFORMER; static { try { DECONSTRUCT_WRAPPER = MethodHandles.lookup().findStatic(ImmutableInternals.class, "deconstructWrapper", MethodType.methodType(Object.class.arrayType(), ImmutableComponentWrapper.class)); SET = MethodHandles.lookup().findVirtual(ImmutableComponentWrapper.class, "setData", MethodType.methodType(void.class, ImmutableComponent.class)); + RUN_TRANSFORMER = MethodHandles.lookup().findStatic(ImmutableInternals.class, "runTransformer", MethodType.methodType(void.class, ImmutableComponent.Modifier.class, ImmutableComponentWrapper.class)); } catch (NoSuchMethodException | IllegalAccessException e) { throw new RuntimeException(e); } @@ -67,18 +69,12 @@ private static MethodHandle makeModifierHandler(MethodHandles.Lookup lookup, Str if (modifier == null) { return MethodHandles.empty(MethodType.methodType(void.class, ImmutableComponentWrapper.class)); } - // todo for tomorrow: work out how to get a MethodHandle from a lambda / anonymous class - // so that this doesn't happen: - // java.lang.IllegalAccessException: symbolic reference class is not accessible: class org.ladysnake.cca.test.entity.CcaEntityTestMod$$Lambda/0x0000000601c90428 - // (c, o) -> c - var transform = lookup.bind(modifier, "modify", CcaAsmHelper.MODIFIER_MODIFY_TYPE); - // (c, o) -> void - var tfAndSet = MethodHandles.filterReturnValue(transform, SET); - // ([c,o]) -> void - var spreadTfAndSet = MethodHandles.spreadInvoker(tfAndSet.type(), 0).bindTo(tfAndSet); - // w -> void - return MethodHandles.filterArguments(spreadTfAndSet, 0, DECONSTRUCT_WRAPPER); + return RUN_TRANSFORMER.bindTo(modifier); + } + + public static void runTransformer(ImmutableComponent.Modifier transformer, ImmutableComponentWrapper wrapper) { + wrapper.setData(transformer.modify(wrapper.getData(), wrapper.getOwner())); } public static Object[] deconstructWrapper(ImmutableComponentWrapper wrapper) { 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 8c1c4710..15a257ff 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 @@ -113,6 +113,8 @@ public final class CcaAsmHelper { public static final String IMMUTABLE_WRAPPER_WRITE_SYNC_DESC; public static final String SERVER_TICK_DESC; public static final String CLIENT_TICK_DESC; + public static final String IMMUTABLE_WRAPPER_SERVER_TICK_DESC; + public static final String IMMUTABLE_WRAPPER_CLIENT_TICK_DESC; public static final String IMMUTABLE_BSM_DESC; private static final List asmGeneratedCallbacks = findAsmComponentCallbacks(); @@ -131,6 +133,8 @@ record AsmGeneratedCallbackInfo(String containerCallbackName, Class) CcaAsmHelper.generateClass(writer, false, null); } - private static void makeTicker(ImmutableComponentKey key, Class targetClass, ClassNode writer, String interfaceName, String methodName, String methodDesc) { + private static void makeTicker(ImmutableComponentKey key, Class targetClass, ClassNode writer, String interfaceName, String methodName, String methodDesc, String dynMethodDesc) { writer.interfaces.add(interfaceName); MethodVisitor onTick = writer.visitMethod(Opcodes.ACC_PUBLIC, methodName, methodDesc, null, null); onTick.visitVarInsn(Opcodes.ALOAD, 0); // this onTick.visitInvokeDynamicInsn( methodName, - methodDesc, + dynMethodDesc, new Handle( H_INVOKESTATIC, CcaAsmHelper.IMMUTABLE_INTERNALS, From 2a36e4f3c6050aed265b616bd6fe0eb6ba001c02 Mon Sep 17 00:00:00 2001 From: williambl Date: Mon, 18 Nov 2024 16:51:40 +0000 Subject: [PATCH 5/5] feat: immutable components general listeners --- .../ImmutableComponentCallbackType.java | 44 ++++++++++++++++ .../cca/internal/base/ImmutableInternals.java | 48 +++++++++++------- .../base/asm/CcaImmutableBootstrap.java | 36 ++++++------- .../EntityComponentFactoryRegistry.java | 8 +-- .../entity/StaticEntityComponentPlugin.java | 50 +++++++++++-------- 5 files changed, 123 insertions(+), 63 deletions(-) create mode 100644 cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponentCallbackType.java diff --git a/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponentCallbackType.java b/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponentCallbackType.java new file mode 100644 index 00000000..6afbfe61 --- /dev/null +++ b/cardinal-components-base/src/main/java/org/ladysnake/cca/api/v3/component/immutable/ImmutableComponentCallbackType.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 ImmutableComponentCallbackType(Class itf, + String methodName, + MethodType exposedType, + MethodType implType) { + public ImmutableComponentCallbackType { + if (!itf.isInterface() + || Arrays.stream(itf.getDeclaredMethods()) + .filter(m -> Modifier.isAbstract(m.getModifiers())) + .count() != 1) { + throw new IllegalArgumentException("ImmutableComponentCallbackType accepts only functional interfaces"); + } + } + + public static ImmutableComponentCallbackType fromFunctionalInterface(Class itf) { + var method = Arrays.stream(itf.getDeclaredMethods()) + .filter(m -> Modifier.isAbstract(m.getModifiers())) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("ImmutableComponentCallbackType 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 ImmutableComponentCallbackType<>(itf, name, type, implType); + } + + public static final ImmutableComponentCallbackType SERVER_TICK = fromFunctionalInterface(ServerTickingComponent.class); + public static final ImmutableComponentCallbackType CLIENT_TICK = fromFunctionalInterface(ClientTickingComponent.class); + public static final ImmutableComponentCallbackType SERVER_LOAD = fromFunctionalInterface(ServerLoadAwareComponent.class); + public static final ImmutableComponentCallbackType CLIENT_LOAD = fromFunctionalInterface(ClientLoadAwareComponent.class); + public static final ImmutableComponentCallbackType SERVER_UNLOAD = fromFunctionalInterface(ServerUnloadAwareComponent.class); + public static final ImmutableComponentCallbackType CLIENT_UNLOAD = fromFunctionalInterface(ClientUnloadAwareComponent.class); +} 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 index 192106bb..44d21f1f 100644 --- 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 @@ -3,6 +3,7 @@ import com.mojang.datafixers.util.Pair; import com.mojang.serialization.Dynamic; import com.mojang.serialization.MapCodec; +import net.minecraft.entity.Entity; import net.minecraft.nbt.NbtCompound; import net.minecraft.nbt.NbtOps; import net.minecraft.network.RegistryByteBuf; @@ -10,13 +11,22 @@ 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.ImmutableComponentCallbackType; +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.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 org.ladysnake.cca.internal.base.asm.CcaAsmHelper; 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 SET; @@ -34,33 +44,37 @@ public class ImmutableInternals { } //TODO id and type do not uniquely describe a component implementation - e.g. predicates in entity component registration - public static final Map, ImmutableComponent.Modifier> serverTickHandlers = new HashMap<>(); - public static final Map, ImmutableComponent.Modifier> clientTickHandlers = new HashMap<>(); - public static final Map, ImmutableComponent.Modifier> loadServersideHandlers = new HashMap<>(); - public static final Map, ImmutableComponent.Modifier> loadClientsideHandlers = new HashMap<>(); - public static final Map, ImmutableComponent.Modifier> unloadServersideHandlers = new HashMap<>(); - public static final Map, ImmutableComponent.Modifier> unloadClientsideHandlers = new HashMap<>(); + public static final Map, Map, ImmutableComponent.Modifier>> listeners = new HashMap<>(); + public static final Set> CALLBACK_TYPES = Set.of( + ImmutableComponentCallbackType.SERVER_TICK, + ImmutableComponentCallbackType.CLIENT_TICK, + ImmutableComponentCallbackType.SERVER_LOAD, + ImmutableComponentCallbackType.CLIENT_LOAD, + ImmutableComponentCallbackType.SERVER_UNLOAD, + ImmutableComponentCallbackType.CLIENT_UNLOAD + ); + + public static void addListener(ImmutableComponentKey key, Class target, ImmutableComponentCallbackType type, ImmutableComponent.Modifier modifier) { + listeners.computeIfAbsent(type, $ -> new HashMap<>()).put(Pair.of(key.getId(), target), modifier); + } public static Object bootstrap(MethodHandles.Lookup lookup, String methodName, TypeDescriptor type, String id, Type targetClass) throws Throwable { MethodType methodType; - if (type instanceof MethodType mt) + if (type instanceof MethodType mt) { methodType = mt; - else { + } else { methodType = null; if (!MethodHandle.class.equals(type)) throw new IllegalArgumentException(type.toString()); } - MethodHandle handle = switch (methodName) { - case "serverTick" -> makeModifierHandler(lookup, id, targetClass, serverTickHandlers); - case "clientTick" -> makeModifierHandler(lookup, id, targetClass, clientTickHandlers); - case "loadServerside" -> makeModifierHandler(lookup, id, targetClass, loadServersideHandlers); - case "loadClientside" -> makeModifierHandler(lookup, id, targetClass, loadClientsideHandlers); - case "unloadServerside" -> makeModifierHandler(lookup, id, targetClass, unloadServersideHandlers); - case "unloadClientside" -> makeModifierHandler(lookup, id, targetClass, unloadClientsideHandlers); - default -> throw new IllegalArgumentException(methodName); - }; + ImmutableComponentCallbackType callbackType = CALLBACK_TYPES.stream() + .filter(t -> t.methodName().equals(methodName)) + .filter(t -> methodType == null || t.implType().equals(methodType)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Invalid method name/type: %s:%s".formatted(methodName, methodType == null ? "?" : methodType.descriptorString()))); + MethodHandle handle = makeModifierHandler(lookup, id, targetClass, listeners.getOrDefault(callbackType, Map.of())); return methodType != null ? new ConstantCallSite(handle) : handle; } diff --git a/cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/asm/CcaImmutableBootstrap.java b/cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/asm/CcaImmutableBootstrap.java index a312ff7e..88ea99f5 100644 --- a/cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/asm/CcaImmutableBootstrap.java +++ b/cardinal-components-base/src/main/java/org/ladysnake/cca/internal/base/asm/CcaImmutableBootstrap.java @@ -22,13 +22,9 @@ */ package org.ladysnake.cca.internal.base.asm; -import com.mojang.datafixers.util.Pair; +import com.google.common.collect.Iterables; import org.ladysnake.cca.api.v3.component.ComponentFactory; -import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponent; -import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponentFactory; -import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponentKey; -import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponentWrapper; -import org.ladysnake.cca.internal.base.ImmutableInternals; +import org.ladysnake.cca.api.v3.component.immutable.*; import org.objectweb.asm.Handle; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; @@ -38,6 +34,8 @@ import java.io.IOException; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; +import java.util.Iterator; +import java.util.List; import static org.objectweb.asm.Opcodes.H_INVOKESTATIC; @@ -45,10 +43,7 @@ public final class CcaImmutableBootstrap { public static > Class makeWrapper( ImmutableComponentKey key, Class targetClass, - ImmutableComponent.Modifier serverTicker, - ImmutableComponent.Modifier clientTicker, - ImmutableComponent.Modifier serverOnLoad, - ImmutableComponent.Modifier clientOnLoad + Iterable> callbackTypes ) 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); @@ -99,24 +94,25 @@ public static ) CcaAsmHelper.generateClass(writer, false, null); } - private static void makeTicker(ImmutableComponentKey key, Class targetClass, ClassNode writer, String interfaceName, String methodName, String methodDesc, String dynMethodDesc) { + private static void implementCallbackItf(ImmutableComponentKey key, Class targetClass, ClassNode writer, ImmutableComponentCallbackType callbackType) { + String interfaceName = Type.getInternalName(callbackType.itf()); writer.interfaces.add(interfaceName); + String methodName = callbackType.methodName(); + String methodDesc = callbackType.exposedType().toMethodDescriptorString(); + var dynMethodType = callbackType.exposedType().insertParameterTypes(0, ImmutableComponentWrapper.class); + String dynMethodDesc = dynMethodType.toMethodDescriptorString(); MethodVisitor onTick = writer.visitMethod(Opcodes.ACC_PUBLIC, methodName, methodDesc, null, null); - onTick.visitVarInsn(Opcodes.ALOAD, 0); // this + for (int i = 0; i < dynMethodType.parameterCount(); i++) { + onTick.visitVarInsn(Opcodes.ALOAD, i); + } onTick.visitInvokeDynamicInsn( methodName, dynMethodDesc, 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 dc9890d5..c0284e1f 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,10 +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.ImmutableComponent; -import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponentFactory; -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.immutable.*; import java.util.function.Predicate; @@ -163,10 +160,13 @@ interface ImmutableRegistration ImmutableRegistration filter(Predicate> test); ImmutableRegistration after(ComponentKey dependency); ImmutableRegistration respawnStrategy(RespawnCopyStrategy> strategy); + ImmutableRegistration listen(ImmutableComponentCallbackType 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 0cc8a144..26e00a65 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 @@ -24,19 +24,16 @@ import net.minecraft.entity.Entity; import net.minecraft.entity.player.PlayerEntity; -import org.jetbrains.annotations.Nullable; import org.ladysnake.cca.api.v3.component.Component; 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.ImmutableComponent; -import org.ladysnake.cca.api.v3.component.immutable.ImmutableComponentFactory; -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.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.CcaImmutableBootstrap; @@ -245,15 +242,13 @@ private final class ImmutableRegistrationImpl target; private final ImmutableComponentKey key; private final Set> dependencies; - private @Nullable ImmutableComponent.Modifier serverTicker; - private @Nullable ImmutableComponent.Modifier clientTicker; - private @Nullable ImmutableComponent.Modifier serverOnLoad; - private @Nullable ImmutableComponent.Modifier clientOnLoad; + private final Map, ImmutableComponent.Modifier> callbacks; private Predicate> test; ImmutableRegistrationImpl(Class target, ImmutableComponentKey key) { this.target = target; this.dependencies = new LinkedHashSet<>(); + this.callbacks = new HashMap<>(); this.test = null; this.key = key; } @@ -270,7 +265,6 @@ public ImmutableRegistration after(ComponentKey dependency) { return this; } - @Override public ImmutableRegistration respawnStrategy(RespawnCopyStrategy> strategy) { CardinalEntityInternals.registerRespawnCopyStrat(this.key, this.target, strategy); @@ -278,27 +272,39 @@ public ImmutableRegistration respawnStrategy(RespawnCopyStrategy onServerTick(ImmutableComponent.Modifier modifier) { - this.serverTicker = modifier; + public ImmutableRegistration listen(ImmutableComponentCallbackType type, ImmutableComponent.Modifier modifier) { + this.callbacks.put(type, modifier); return this; } + @Override + public ImmutableRegistration onServerTick(ImmutableComponent.Modifier modifier) { + return this.listen(ImmutableComponentCallbackType.SERVER_TICK, modifier); + } + @Override public ImmutableRegistration onClientTick(ImmutableComponent.Modifier modifier) { - this.clientTicker = modifier; - return this; + return this.listen(ImmutableComponentCallbackType.CLIENT_TICK, modifier); } @Override public ImmutableRegistration onServerLoad(ImmutableComponent.Modifier modifier) { - this.serverOnLoad = modifier; - return this; + return this.listen(ImmutableComponentCallbackType.SERVER_LOAD, modifier); } @Override public ImmutableRegistration onClientLoad(ImmutableComponent.Modifier modifier) { - this.clientOnLoad = modifier; - return this; + return this.listen(ImmutableComponentCallbackType.CLIENT_LOAD, modifier); + } + + @Override + public ImmutableRegistration onServerUnload(ImmutableComponent.Modifier modifier) { + return this.listen(ImmutableComponentCallbackType.SERVER_UNLOAD, modifier); + } + + @Override + public ImmutableRegistration onClientUnload(ImmutableComponent.Modifier modifier) { + return this.listen(ImmutableComponentCallbackType.CLIENT_UNLOAD, modifier); } @Override @@ -308,10 +314,7 @@ public void end(ImmutableComponentFactory factory) { Class> componentClass = CcaImmutableBootstrap.makeWrapper( this.key, this.target, - this.serverTicker, - this.clientTicker, - this.serverOnLoad, - this.clientOnLoad + this.callbacks.keySet() ); ComponentFactory> componentFactory = CcaImmutableBootstrap.makeFactory(this.key, this.target, componentClass, factory); if (this.test == null) { @@ -331,6 +334,9 @@ public void end(ImmutableComponentFactory factory) { ) )); } + for (var callback : callbacks.entrySet()) { + ImmutableInternals.addListener(this.key, this.target, callback.getKey(), callback.getValue()); + } } catch (IOException | NoSuchMethodException | IllegalAccessException e) { throw new RuntimeException(e); }