diff --git a/build.gradle b/build.gradle index a91444d6439..07afe32021d 100644 --- a/build.gradle +++ b/build.gradle @@ -617,6 +617,7 @@ void setupRepositories(RepositoryHandler repositories) { def devOnlyModules = [ "fabric-client-gametest-api-v1", "fabric-gametest-api-v1", + "fabric-test-api-v1" ] dependencies { diff --git a/fabric-api-base/src/main/java/net/fabricmc/fabric/api/event/EventFactory.java b/fabric-api-base/src/main/java/net/fabricmc/fabric/api/event/EventFactory.java index 4e452ca6b34..c998276bddd 100644 --- a/fabric-api-base/src/main/java/net/fabricmc/fabric/api/event/EventFactory.java +++ b/fabric-api-base/src/main/java/net/fabricmc/fabric/api/event/EventFactory.java @@ -41,7 +41,7 @@ private EventFactory() { } * @return The Event instance. */ public static Event createArrayBacked(Class type, Function invokerFactory) { - return EventFactoryImpl.createArrayBacked(type, invokerFactory); + return EventFactoryImpl.INSTANCE.createArrayBacked(type, invokerFactory); } /** diff --git a/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/ArrayBackedEvent.java b/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/ArrayBackedEvent.java index e3c338f46b9..2f3829a696f 100644 --- a/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/ArrayBackedEvent.java +++ b/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/ArrayBackedEvent.java @@ -30,10 +30,10 @@ import net.fabricmc.fabric.api.event.Event; import net.fabricmc.fabric.impl.base.toposort.NodeSorting; -class ArrayBackedEvent extends Event { +public class ArrayBackedEvent extends Event { private final Function invokerFactory; - private final Object lock = new Object(); - private T[] handlers; + protected final Object lock = new Object(); + protected T[] handlers; /** * Registered event phases. */ @@ -44,7 +44,7 @@ class ArrayBackedEvent extends Event { private final List> sortedPhases = new ArrayList<>(); @SuppressWarnings("unchecked") - ArrayBackedEvent(Class type, Function invokerFactory) { + protected ArrayBackedEvent(Class type, Function invokerFactory) { this.invokerFactory = invokerFactory; this.handlers = (T[]) Array.newInstance(type, 0); update(); @@ -70,7 +70,7 @@ public void register(Identifier phaseIdentifier, T listener) { } } - private EventPhaseData getOrCreatePhase(Identifier id, boolean sortIfCreate) { + protected final EventPhaseData getOrCreatePhase(Identifier id, boolean sortIfCreate) { EventPhaseData phase = phases.get(id); if (phase == null) { @@ -86,7 +86,7 @@ private EventPhaseData getOrCreatePhase(Identifier id, boolean sortIfCreate) return phase; } - private void rebuildInvoker(int newLength) { + protected final void rebuildInvoker(int newLength) { // Rebuild handlers. if (sortedPhases.size() == 1) { // Special case with a single phase: use the array of the phase directly. diff --git a/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/EventFactoryImpl.java b/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/EventFactoryImpl.java index 1d0be4aa530..a5af6c54ffd 100644 --- a/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/EventFactoryImpl.java +++ b/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/EventFactoryImpl.java @@ -32,19 +32,49 @@ import net.minecraft.resources.Identifier; import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.loader.api.FabricLoader; -public final class EventFactoryImpl { +public class EventFactoryImpl { + public static final EventFactoryImpl INSTANCE; private static final Set> ARRAY_BACKED_EVENTS = Collections.newSetFromMap(new MapMaker().weakKeys().makeMap()); - private EventFactoryImpl() { } + static { + final String eventScopeModule = "fabric-test-api-v1"; + + if (FabricLoader.getInstance().isModLoaded(eventScopeModule)) { + String clazzName = FabricLoader.getInstance() + .getModContainer(eventScopeModule) + .orElseThrow() + .getMetadata() + .getCustomValue("fabric-api-base:event_factory_impl") + .getAsString(); + try { + Class clazz = Class.forName(clazzName); + INSTANCE = (EventFactoryImpl) MethodHandles.publicLookup() + .findConstructor(clazz, MethodType.methodType(void.class)) + .invoke(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } else { + INSTANCE = new EventFactoryImpl(); + } + } + + protected EventFactoryImpl() { + } public static void invalidate() { ARRAY_BACKED_EVENTS.forEach(ArrayBackedEvent::update); } - public static Event createArrayBacked(Class type, Function invokerFactory) { - ArrayBackedEvent event = new ArrayBackedEvent<>(type, invokerFactory); + protected ArrayBackedEvent doCreateArrayBacked(Class type, Function invokerFactory) { + return new ArrayBackedEvent<>(type, invokerFactory); + } + + public final Event createArrayBacked(Class type, Function invokerFactory) { + ArrayBackedEvent event = doCreateArrayBacked(type, invokerFactory); ARRAY_BACKED_EVENTS.add(event); return event; } diff --git a/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/EventPhaseData.java b/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/EventPhaseData.java index c11454e4ef3..1b6cbbcab8a 100644 --- a/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/EventPhaseData.java +++ b/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/EventPhaseData.java @@ -26,7 +26,7 @@ /** * Data of an {@link ArrayBackedEvent} phase. */ -class EventPhaseData extends SortableNode> { +public class EventPhaseData extends SortableNode> { final Identifier id; T[] listeners; @@ -42,6 +42,25 @@ void addListener(T listener) { listeners[oldLength] = listener; } + public boolean removeListener(T listener) { + int indexToRemove; + + for (indexToRemove = listeners.length - 1; indexToRemove >= 0; indexToRemove--) { + if (listeners[indexToRemove] == listener) { + break; + } + } + + if (indexToRemove == -1) { + return false; + } + + T[] newListeners = Arrays.copyOf(listeners, listeners.length - 1); + System.arraycopy(listeners, indexToRemove + 1, newListeners, indexToRemove, newListeners.length - indexToRemove); + listeners = newListeners; + return true; + } + @Override protected String getDescription() { return id.toString(); diff --git a/fabric-debug-api-v1/src/main/resources/fabric.mod.json b/fabric-debug-api-v1/src/main/resources/fabric.mod.json index 4b76263097f..6be29e7c74a 100644 --- a/fabric-debug-api-v1/src/main/resources/fabric.mod.json +++ b/fabric-debug-api-v1/src/main/resources/fabric.mod.json @@ -17,8 +17,7 @@ ], "depends": { "fabricloader": ">=0.18.4", - "fabric-api-base": "*", - "fabric-registry-sync-v0": "*" + "fabric-api-base": "*" }, "description": "A toolkit for registering and using debug subscriptions and other debug tools Mojang have created.", "mixins": [ diff --git a/fabric-test-api-v1/build.gradle b/fabric-test-api-v1/build.gradle new file mode 100644 index 00000000000..b93723665d3 --- /dev/null +++ b/fabric-test-api-v1/build.gradle @@ -0,0 +1,9 @@ +version = getSubprojectVersion(project) + +moduleDependencies(project, [ + 'fabric-api-base' +]) + +testDependencies(project, [ + 'fabric-gametest-api-v1' +]) diff --git a/fabric-test-api-v1/src/main/java/net/fabricmc/fabric/api/test/v1/EventScope.java b/fabric-test-api-v1/src/main/java/net/fabricmc/fabric/api/test/v1/EventScope.java new file mode 100644 index 00000000000..11a0e2c902a --- /dev/null +++ b/fabric-test-api-v1/src/main/java/net/fabricmc/fabric/api/test/v1/EventScope.java @@ -0,0 +1,61 @@ +/* + * 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.test.v1; + +import java.util.function.Function; + +import org.jetbrains.annotations.ApiStatus; + +import net.minecraft.resources.Identifier; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.fabricmc.fabric.impl.test.EventScopeImpl; + +/** + * Represents a wrapper around a short-lived {@link Event}. + * This class implements {@link AutoCloseable} and is intended to be used in a try-with-resources block. + * When closed, the event will be unregistered. + */ +@ApiStatus.NonExtendable +@ApiStatus.Experimental +public interface EventScope extends AutoCloseable { + @Override + void close(); + + /** + * @param event The {@link Event} to be registered in an {@link EventScope} + * @param listener The event listener + * @param is the type parameter for the event's listener + * @return a new {@link EventScope} instance holding the event, its listener and the event phase {@link Event#DEFAULT_PHASE} + */ + static EventScope register(Event event, T listener) { + return register(event, Event.DEFAULT_PHASE, listener); + } + + /** + * @param event The {@link Event} to be registered in an {@link EventScope} + * @param phase The {@linkplain EventFactory#createWithPhases(Class, Function, Identifier...) event phase} + * @param listener The event listener + * @param is the type parameter for the event's listener + * @return a new {@link EventScope} instance holding the event, its listener and the event phase + * @see EventFactory#createWithPhases(Class, Function, Identifier...) + */ + static EventScope register(Event event, Identifier phase, T listener) { + return EventScopeImpl.register(event, phase, listener); + } +} diff --git a/fabric-test-api-v1/src/main/java/net/fabricmc/fabric/impl/test/EventScopeImpl.java b/fabric-test-api-v1/src/main/java/net/fabricmc/fabric/impl/test/EventScopeImpl.java new file mode 100644 index 00000000000..adcc545d8ea --- /dev/null +++ b/fabric-test-api-v1/src/main/java/net/fabricmc/fabric/impl/test/EventScopeImpl.java @@ -0,0 +1,48 @@ +/* + * 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.impl.test; + +import net.minecraft.resources.Identifier; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.test.v1.EventScope; + +public class EventScopeImpl implements EventScope { + private final TestableArrayBackedEvent event; + private final Identifier phase; + private final T listener; + + public EventScopeImpl(TestableArrayBackedEvent event, Identifier phase, T listener) { + this.event = event; + this.phase = phase; + this.listener = listener; + } + + public static EventScope register(Event event, Identifier phase, T listener) { + if (!(event instanceof TestableArrayBackedEvent testableEvent)) { + throw new IllegalArgumentException("Event is not testable, something has gone very wrong!"); + } + + event.register(phase, listener); + return new EventScopeImpl<>(testableEvent, phase, listener); + } + + @Override + public void close() { + event.unregister(phase, listener); + } +} diff --git a/fabric-test-api-v1/src/main/java/net/fabricmc/fabric/impl/test/TestableArrayBackedEvent.java b/fabric-test-api-v1/src/main/java/net/fabricmc/fabric/impl/test/TestableArrayBackedEvent.java new file mode 100644 index 00000000000..454cb5cf199 --- /dev/null +++ b/fabric-test-api-v1/src/main/java/net/fabricmc/fabric/impl/test/TestableArrayBackedEvent.java @@ -0,0 +1,41 @@ +/* + * 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.impl.test; + +import java.util.Objects; +import java.util.function.Function; + +import net.minecraft.resources.Identifier; + +import net.fabricmc.fabric.impl.base.event.ArrayBackedEvent; + +public class TestableArrayBackedEvent extends ArrayBackedEvent { + TestableArrayBackedEvent(Class type, Function invokerFactory) { + super(type, invokerFactory); + } + + public void unregister(Identifier phaseIdentifier, T listener) { + Objects.requireNonNull(phaseIdentifier, "Tried to unregister a listener for a null phase!"); + Objects.requireNonNull(listener, "Tried to unregister a null listener!"); + + synchronized (lock) { + if (getOrCreatePhase(phaseIdentifier, false).removeListener(listener)) { + rebuildInvoker(handlers.length - 1); + } + } + } +} diff --git a/fabric-test-api-v1/src/main/java/net/fabricmc/fabric/impl/test/TestableEventFactoryImpl.java b/fabric-test-api-v1/src/main/java/net/fabricmc/fabric/impl/test/TestableEventFactoryImpl.java new file mode 100644 index 00000000000..09dae87f261 --- /dev/null +++ b/fabric-test-api-v1/src/main/java/net/fabricmc/fabric/impl/test/TestableEventFactoryImpl.java @@ -0,0 +1,29 @@ +/* + * 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.impl.test; + +import java.util.function.Function; + +import net.fabricmc.fabric.impl.base.event.ArrayBackedEvent; +import net.fabricmc.fabric.impl.base.event.EventFactoryImpl; + +public class TestableEventFactoryImpl extends EventFactoryImpl { + @Override + protected ArrayBackedEvent doCreateArrayBacked(Class type, Function invokerFactory) { + return new TestableArrayBackedEvent<>(type, invokerFactory); + } +} diff --git a/fabric-test-api-v1/src/main/resources/assets/fabric-test-api-v1/icon.png b/fabric-test-api-v1/src/main/resources/assets/fabric-test-api-v1/icon.png new file mode 100644 index 00000000000..12c4531de92 Binary files /dev/null and b/fabric-test-api-v1/src/main/resources/assets/fabric-test-api-v1/icon.png differ diff --git a/fabric-test-api-v1/src/main/resources/fabric.mod.json b/fabric-test-api-v1/src/main/resources/fabric.mod.json new file mode 100644 index 00000000000..b04da2f566b --- /dev/null +++ b/fabric-test-api-v1/src/main/resources/fabric.mod.json @@ -0,0 +1,21 @@ +{ + "schemaVersion": 1, + "id": "fabric-test-api-v1", + "name": "Fabric Test API (v1)", + "version": "${version}", + "environment": "*", + "license": "Apache-2.0", + "icon": "assets/fabric-test-api-v1/icon.png", + "authors": [ + "FabricMC" + ], + "depends": { + "fabricloader": ">=0.18.4", + "fabric-api-base": "*" + }, + "description": "General utilities for tests and development environments.", + "custom": { + "fabric-api:module-lifecycle": "experimental", + "fabric-api-base:event_factory_impl": "net.fabricmc.fabric.impl.test.TestableEventFactoryImpl" + } +} diff --git a/fabric-test-api-v1/src/test/java/net/fabricmc/fabric/test/test/EventScopeTest.java b/fabric-test-api-v1/src/test/java/net/fabricmc/fabric/test/test/EventScopeTest.java new file mode 100644 index 00000000000..cd8f9ee4619 --- /dev/null +++ b/fabric-test-api-v1/src/test/java/net/fabricmc/fabric/test/test/EventScopeTest.java @@ -0,0 +1,66 @@ +/* + * 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.test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import net.minecraft.SharedConstants; +import net.minecraft.server.Bootstrap; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.fabricmc.fabric.api.test.v1.EventScope; + +public class EventScopeTest { + private static final Event EVENT = EventFactory.createArrayBacked( + Foo.class, + listeners -> () -> { + for (Foo listener : listeners) { + if (!listener.doSomething()) { + return false; + } + } + + return true; + } + ); + + @BeforeAll + static void bootstrap() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + + @Test + void testEventScope() { + Foo foo = () -> false; + + try (EventScope _ = EventScope.register(EVENT, foo)) { + assertFalse(EVENT.invoker().doSomething(), "Event Foo in EventScope was not registered."); + } + + assertTrue(EVENT.invoker().doSomething(), "EventScope did not unregister event Foo after closing."); + } + + private interface Foo { + boolean doSomething(); + } +} diff --git a/fabric-test-api-v1/src/testmod/resources/fabric.mod.json b/fabric-test-api-v1/src/testmod/resources/fabric.mod.json new file mode 100644 index 00000000000..ffbfee655f6 --- /dev/null +++ b/fabric-test-api-v1/src/testmod/resources/fabric.mod.json @@ -0,0 +1,8 @@ +{ + "schemaVersion": 1, + "id": "fabric-test-api-v1-testmod", + "name": "Fabric Test API (v1) Test Mod", + "version": "1.0.0", + "environment": "*", + "license": "Apache-2.0" +} diff --git a/gradle.properties b/gradle.properties index efa7bf160df..6bdceee25e1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -53,6 +53,7 @@ fabric-menu-api-v1-version=2.0.12 fabric-serialization-api-v1-version=2.0.3 fabric-sound-api-v1-version=2.0.4 fabric-tag-api-v1-version=2.0.9 +fabric-test-api-v1-version=1.0.0 fabric-transfer-api-v1-version=8.0.2 fabric-transitive-access-wideners-v1-version=8.0.11 fabric-convention-tags-v2-version=4.3.2 diff --git a/settings.gradle b/settings.gradle index 8e249715bc4..1b3bc601aaf 100644 --- a/settings.gradle +++ b/settings.gradle @@ -65,6 +65,7 @@ include 'fabric-screen-api-v1' include 'fabric-serialization-api-v1' include 'fabric-sound-api-v1' include 'fabric-tag-api-v1' +include 'fabric-test-api-v1' include 'fabric-transfer-api-v1' include 'fabric-transitive-access-wideners-v1' include 'deprecated'