diff --git a/fabric-component-api-v1/build.gradle b/fabric-component-api-v1/build.gradle new file mode 100644 index 0000000000..7d2ad2384b --- /dev/null +++ b/fabric-component-api-v1/build.gradle @@ -0,0 +1,7 @@ +version = getSubprojectVersion(project) + +moduleDependencies(project, [ + 'fabric-api-base', + 'fabric-data-attachment-api-v1', + 'fabric-lifecycle-events-v1' +]) diff --git a/fabric-component-api-v1/src/main/java/net/fabricmc/fabric/api/component/v1/api/Component.java b/fabric-component-api-v1/src/main/java/net/fabricmc/fabric/api/component/v1/api/Component.java new file mode 100644 index 0000000000..f3ca450bc0 --- /dev/null +++ b/fabric-component-api-v1/src/main/java/net/fabricmc/fabric/api/component/v1/api/Component.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.api.component.v1.api; + +import net.fabricmc.fabric.api.attachment.v1.AttachmentTarget; + +public interface Component { + interface EventHandler, A extends AttachmentTarget, E> { + void handle(C component, A target, E eventPayload); + } +} diff --git a/fabric-component-api-v1/src/main/java/net/fabricmc/fabric/api/component/v1/api/ComponentEvents.java b/fabric-component-api-v1/src/main/java/net/fabricmc/fabric/api/component/v1/api/ComponentEvents.java new file mode 100644 index 0000000000..5604c080fa --- /dev/null +++ b/fabric-component-api-v1/src/main/java/net/fabricmc/fabric/api/component/v1/api/ComponentEvents.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.api.component.v1.api; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.EquipmentSlot; +import net.minecraft.entity.LivingEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.server.world.ServerWorld; + +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerEntityEvents; + +public final class ComponentEvents { + public static final TargetedEvent ENTITY_LOAD = TargetedEvent.create(ServerEntityEvents.ENTITY_LOAD, + event -> event::accept); + + public static final TargetedEvent ENTITY_UNLOAD = TargetedEvent.create(ServerEntityEvents.ENTITY_UNLOAD, + event -> event::accept); + + public static final TargetedEvent EQUIPMENT_CHANGE = TargetedEvent.create(ServerEntityEvents.EQUIPMENT_CHANGE, + event -> (livingEntity, equipmentSlot, previousStack, currentStack) -> + event.accept(livingEntity, new EquipmentChangeEvent(equipmentSlot, previousStack, currentStack))); + + private ComponentEvents() { + } + + public record EquipmentChangeEvent(EquipmentSlot equipmentSlot, ItemStack previousStack, ItemStack currentStack) { + } +} diff --git a/fabric-component-api-v1/src/main/java/net/fabricmc/fabric/api/component/v1/api/ComponentType.java b/fabric-component-api-v1/src/main/java/net/fabricmc/fabric/api/component/v1/api/ComponentType.java new file mode 100644 index 0000000000..b1033c6a4e --- /dev/null +++ b/fabric-component-api-v1/src/main/java/net/fabricmc/fabric/api/component/v1/api/ComponentType.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.api.component.v1.api; + +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import net.minecraft.util.Identifier; + +import net.fabricmc.fabric.api.attachment.v1.AttachmentRegistry; +import net.fabricmc.fabric.api.attachment.v1.AttachmentTarget; +import net.fabricmc.fabric.api.attachment.v1.AttachmentType; +import net.fabricmc.fabric.api.component.v1.impl.ComponentTypeImpl; + +public interface ComponentType> extends AttachmentType { + static > ComponentType create(Identifier identifier) { + return create(identifier, builder -> { }); + } + + static > ComponentType create(Identifier identifier, Consumer> consumer) { + var builder = new ComponentTypeImpl.BuilderImpl(); + + consumer.accept(builder); + + return builder.buildAndRegister(identifier); + } + + List> getEventHandlers(TargetedEvent event); + + interface Builder> extends AttachmentRegistry.Builder { + // We probably don't absolutely need all three of + Builder listen(TargetedEvent event, Class targetClass, Component.EventHandler handler); + Builder listen(TargetedEvent event, Class targetClass, BiConsumer handler); + Builder listen(TargetedEvent event, Class targetClass, Consumer handler); + + Builder listen(TargetedEvent event, Component.EventHandler handler); + Builder listen(TargetedEvent event, BiConsumer handler); + Builder listen(TargetedEvent event, Consumer handler); + } +} diff --git a/fabric-component-api-v1/src/main/java/net/fabricmc/fabric/api/component/v1/api/TargetedEvent.java b/fabric-component-api-v1/src/main/java/net/fabricmc/fabric/api/component/v1/api/TargetedEvent.java new file mode 100644 index 0000000000..379c7aab4c --- /dev/null +++ b/fabric-component-api-v1/src/main/java/net/fabricmc/fabric/api/component/v1/api/TargetedEvent.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.api.component.v1.api; + +import java.util.function.BiConsumer; +import java.util.function.Function; + +import net.fabricmc.fabric.api.attachment.v1.AttachmentTarget; +import net.fabricmc.fabric.api.component.v1.impl.TargetedEventImpl; +import net.fabricmc.fabric.api.event.Event; + +public interface TargetedEvent { + static TargetedEvent create(Event event, Function, T> invokerFactory) { + TargetedEventImpl targetedEvent = TargetedEventImpl.create(event); + T invoker = invokerFactory.apply(targetedEvent::invoke); + + event.register(invoker); + + return targetedEvent; + } + + void invoke(A attachmentTarget, E eventContext); +} diff --git a/fabric-component-api-v1/src/main/java/net/fabricmc/fabric/api/component/v1/impl/ComponentTypeImpl.java b/fabric-component-api-v1/src/main/java/net/fabricmc/fabric/api/component/v1/impl/ComponentTypeImpl.java new file mode 100644 index 0000000000..eca787c87f --- /dev/null +++ b/fabric-component-api-v1/src/main/java/net/fabricmc/fabric/api/component/v1/impl/ComponentTypeImpl.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.api.component.v1.impl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import com.mojang.serialization.Codec; +import org.jetbrains.annotations.Nullable; + +import net.minecraft.util.Identifier; + +import net.fabricmc.fabric.api.attachment.v1.AttachmentRegistry; +import net.fabricmc.fabric.api.attachment.v1.AttachmentTarget; +import net.fabricmc.fabric.api.attachment.v1.AttachmentType; +import net.fabricmc.fabric.api.component.v1.api.Component; +import net.fabricmc.fabric.api.component.v1.api.ComponentType; +import net.fabricmc.fabric.api.component.v1.api.TargetedEvent; + +public final class ComponentTypeImpl> implements ComponentType { + private final AttachmentType attachmentType; + private final Map, List>> eventHandlers = new IdentityHashMap<>(); + + public ComponentTypeImpl(AttachmentType attachmentType) { + this.attachmentType = attachmentType; + } + + @Override + public List> getEventHandlers(TargetedEvent event) { + //noinspection unchecked + return (List>) (Object) this.eventHandlers.getOrDefault(event, Collections.emptyList()); + } + + @Override + public Identifier identifier() { + return this.attachmentType.identifier(); + } + + @Override + public @Nullable Codec persistenceCodec() { + return this.attachmentType.persistenceCodec(); + } + + @Override + public boolean isPersistent() { + return this.attachmentType.isPersistent(); + } + + @Override + public @Nullable Supplier initializer() { + return this.attachmentType.initializer(); + } + + @Override + public boolean copyOnDeath() { + return this.attachmentType.copyOnDeath(); + } + + public static final class BuilderImpl> implements Builder, AttachmentRegistry.Builder { + private final AttachmentRegistry.Builder attachmentBuilder = AttachmentRegistry.builder(); + private final Map, List>> eventHandlers = new IdentityHashMap<>(); + + @Override + public Builder listen(TargetedEvent event, Class targetClass, Component.EventHandler handler) { + List> handlers = this.eventHandlers.computeIfAbsent(event, e -> new ArrayList<>()); + + handlers.add(new Component.EventHandler() { + @Override + public void handle(C component, AttachmentTarget target, E eventPayload) { + if (targetClass.isAssignableFrom(target.getClass())) { + //noinspection unchecked + handler.handle(component, (A) target, eventPayload); + } + } + }); + + return this; + } + + @Override + public Builder listen(TargetedEvent event, Class targetClass, BiConsumer handler) { + return this.listen(event, targetClass, (component, attachmentTarget, eventPayload) -> + handler.accept(component, attachmentTarget) + ); + } + + @Override + public Builder listen(TargetedEvent event, Class targetClass, Consumer handler) { + return this.listen(event, targetClass, (component, attachmentTarget, eventPayload) -> + handler.accept(component) + ); + } + + @Override + public Builder listen(TargetedEvent event, Component.EventHandler handler) { + this.eventHandlers.computeIfAbsent(event, e -> new ArrayList<>()).add(handler); + + return this; + } + + @Override + public Builder listen(TargetedEvent event, BiConsumer handler) { + return this.listen(event, (component, attachmentTarget, eventPayload) -> + handler.accept(component, attachmentTarget) + ); + } + + @Override + public Builder listen(TargetedEvent event, Consumer handler) { + return this.listen(event, (component, attachmentTarget, eventPayload) -> + handler.accept(component) + ); + } + + public ComponentTypeImpl buildAndRegister(Identifier id) { + AttachmentType attachmentType = this.attachmentBuilder.buildAndRegister(id); + ComponentTypeImpl type = new ComponentTypeImpl<>(attachmentType); + + this.eventHandlers.forEach((event, list) -> { + type.eventHandlers.put(event, List.copyOf(list)); + ((TargetedEventImpl) event).addListener(type); + }); + + return type; + } + + @Override + public AttachmentRegistry.Builder persistent(Codec codec) { + return attachmentBuilder.persistent(codec); + } + + @Override + public AttachmentRegistry.Builder copyOnDeath() { + return attachmentBuilder.copyOnDeath(); + } + + @Override + public AttachmentRegistry.Builder initializer(Supplier initializer) { + return attachmentBuilder.initializer(initializer); + } + } +} diff --git a/fabric-component-api-v1/src/main/java/net/fabricmc/fabric/api/component/v1/impl/TargetedEventImpl.java b/fabric-component-api-v1/src/main/java/net/fabricmc/fabric/api/component/v1/impl/TargetedEventImpl.java new file mode 100644 index 0000000000..fa0adc3260 --- /dev/null +++ b/fabric-component-api-v1/src/main/java/net/fabricmc/fabric/api/component/v1/impl/TargetedEventImpl.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.api.component.v1.impl; + +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +import net.fabricmc.fabric.api.attachment.v1.AttachmentTarget; +import net.fabricmc.fabric.api.component.v1.api.Component; +import net.fabricmc.fabric.api.component.v1.api.ComponentType; +import net.fabricmc.fabric.api.component.v1.api.TargetedEvent; +import net.fabricmc.fabric.api.event.Event; + +public class TargetedEventImpl implements TargetedEvent { + private static final Map, TargetedEvent> INSTANCES = new IdentityHashMap<>(); + + private final List> componentTypes = new ArrayList<>(); + + private TargetedEventImpl() { + } + + public void addListener(ComponentType componentType) { + this.componentTypes.add(componentType); + } + + public static TargetedEventImpl create(Event event) { + //noinspection unchecked + return (TargetedEventImpl) INSTANCES.computeIfAbsent(event, e -> new TargetedEventImpl<>()); + } + + private > void invokeListenersForComponentType(ComponentType componentType, AttachmentTarget attachmentTarget, E eventPayload) { + C component = attachmentTarget.getAttached(componentType); + + if (component == null && componentType.initializer() != null) { + component = attachmentTarget.getAttachedOrCreate(componentType); + } + + if (component != null) { + for (Component.EventHandler handler : componentType.getEventHandlers(this)) { + //noinspection unchecked,rawtypes + ((Component.EventHandler) handler).handle(component, attachmentTarget, eventPayload); + } + } + } + + @Override + public void invoke(A attachmentTarget, E eventContext) { + for (ComponentType componentType : this.componentTypes) { + this.invokeListenersForComponentType(componentType, attachmentTarget, eventContext); + } + } +} diff --git a/fabric-component-api-v1/src/main/resources/assets/fabric-component-api-v1/icon.png b/fabric-component-api-v1/src/main/resources/assets/fabric-component-api-v1/icon.png new file mode 100644 index 0000000000..2931efbf61 Binary files /dev/null and b/fabric-component-api-v1/src/main/resources/assets/fabric-component-api-v1/icon.png differ diff --git a/fabric-component-api-v1/src/main/resources/fabric.mod.json b/fabric-component-api-v1/src/main/resources/fabric.mod.json new file mode 100644 index 0000000000..2eb7f81fa9 --- /dev/null +++ b/fabric-component-api-v1/src/main/resources/fabric.mod.json @@ -0,0 +1,30 @@ +{ + "schemaVersion": 1, + "id": "fabric-components-api-v1", + "name": "Fabric Components API (v1)", + "version": "${version}", + "environment": "*", + "license": "Apache-2.0", + "icon": "assets/fabric-components-api-v1/icon.png", + "contact": { + "homepage": "https://fabricmc.net", + "irc": "irc://irc.esper.net:6667/fabric", + "issues": "https://github.com/FabricMC/fabric/issues", + "sources": "https://github.com/FabricMC/fabric" + }, + "authors": [ + "FabricMC" + ], + "depends": { + "fabricloader": ">=0.15.11" + }, + "entrypoints": { + }, + "description": "Hooks for components", + "mixins": [ + "fabric-components-api-v1.mixins.json" + ], + "custom": { + "fabric-api:module-lifecycle": "experimental" + } +} diff --git a/fabric-component-api-v1/src/testmod/java/net/fabricmc/fabric/test/component/ComponentTestMod.java b/fabric-component-api-v1/src/testmod/java/net/fabricmc/fabric/test/component/ComponentTestMod.java new file mode 100644 index 0000000000..b8f814ffc1 --- /dev/null +++ b/fabric-component-api-v1/src/testmod/java/net/fabricmc/fabric/test/component/ComponentTestMod.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.test.component; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.minecraft.entity.mob.CreeperEntity; +import net.minecraft.util.Identifier; + +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.component.v1.api.ComponentEvents; +import net.fabricmc.fabric.api.component.v1.api.ComponentType; + +public class ComponentTestMod implements ModInitializer { + public static final String MOD_ID = "fabric-components-api-v1-testmod"; + public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + + public static ComponentType CREEPER_COLOR = ComponentType.create(Identifier.of(ComponentTestMod.MOD_ID, "creeper_color"), builder -> { + // Using the context object + builder.listen(ComponentEvents.ENTITY_LOAD, CreeperEntity.class, CreeperColor::onLoad); + // Not using the context object + builder.listen(ComponentEvents.ENTITY_UNLOAD, CreeperEntity.class, CreeperColor::onUnload); + // Not using the context object or the attachment target + builder.listen(ComponentEvents.ENTITY_UNLOAD, CreeperEntity.class, CreeperColor::onLoad2); + builder.initializer(CreeperColor::new); + }); + + @Override + public void onInitialize() { + LOGGER.info("Started components API test mod"); + } +} diff --git a/fabric-component-api-v1/src/testmod/java/net/fabricmc/fabric/test/component/CreeperColor.java b/fabric-component-api-v1/src/testmod/java/net/fabricmc/fabric/test/component/CreeperColor.java new file mode 100644 index 0000000000..dddc1d592c --- /dev/null +++ b/fabric-component-api-v1/src/testmod/java/net/fabricmc/fabric/test/component/CreeperColor.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.test.component; + +import net.minecraft.entity.mob.CreeperEntity; +import net.minecraft.server.world.ServerWorld; + +import net.fabricmc.fabric.api.component.v1.api.Component; + +public record CreeperColor(int r, int g, int b) implements Component { + public CreeperColor() { + this(9, 166, 3); + } + + void onLoad(CreeperEntity creeper, ServerWorld world) { + ComponentTestMod.LOGGER.info("Creeper with color {} loaded at {}", this, creeper.getBlockPos()); + } + + void onUnload(CreeperEntity creeper) { + ComponentTestMod.LOGGER.info("Creeper with color {} unloaded at {}", this, creeper.getBlockPos()); + } + + void onLoad2() { + ComponentTestMod.LOGGER.info("Creeper with color {} loaded", this); + } +} diff --git a/fabric-component-api-v1/src/testmod/resources/fabric.mod.json b/fabric-component-api-v1/src/testmod/resources/fabric.mod.json new file mode 100644 index 0000000000..3b05f5e217 --- /dev/null +++ b/fabric-component-api-v1/src/testmod/resources/fabric.mod.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": 1, + "id": "fabric-component-api-v1-testmod", + "name": "Fabric Components API (v1) Test Mod", + "version": "1.0.0", + "environment": "*", + "license": "Apache-2.0", + "depends": { + "fabric-data-attachment-api-v1": "*", + "fabric-component-api-v1": "*", + "fabric-lifecycle-events-v1": "*" + }, + "entrypoints": { + "main": [ + "net.fabricmc.fabric.test.component.ComponentTestMod" + ] + } +} diff --git a/gradle.properties b/gradle.properties index 625887d00a..291e4b7c03 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,6 +21,7 @@ fabric-blockrenderlayer-v1-version=1.1.52 fabric-command-api-v1-version=1.2.49 fabric-command-api-v2-version=2.2.28 fabric-commands-v0-version=0.2.66 +fabric-component-api-v1-version=0.1.0 fabric-content-registries-v0-version=8.0.16 fabric-crash-report-info-v1-version=0.2.29 fabric-data-attachment-api-v1-version=1.1.28 diff --git a/settings.gradle b/settings.gradle index 540cbccdbf..ba288ab7fc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -22,6 +22,7 @@ include 'fabric-block-view-api-v2' include 'fabric-blockrenderlayer-v1' include 'fabric-client-tags-api-v1' include 'fabric-command-api-v2' +include 'fabric-component-api-v1' include 'fabric-content-registries-v0' include 'fabric-convention-tags-v2' include 'fabric-crash-report-info-v1'