From f13042d3d2f5a1618b0eaa4d48446f0ff53d1e8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20N=C3=BCcke?= Date: Mon, 30 Nov 2020 18:34:06 +0100 Subject: [PATCH] API for adding device providers and custom parameter serializers. --- src/main/java/li/cil/oc2/api/API.java | 32 +++++ .../li/cil/oc2/api/device/DeviceMethod.java | 4 + .../cil/oc2/api/device/object/Callback.java | 4 + .../imc/DeviceMethodParameterTypeAdapter.java | 11 ++ .../java/li/cil/oc2/common/CommonSetup.java | 13 +- src/main/java/li/cil/oc2/common/IMC.java | 65 ++++++++++ .../li/cil/oc2/common/ServerScheduler.java | 111 ++++++++++++++++++ 7 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 src/main/java/li/cil/oc2/api/imc/DeviceMethodParameterTypeAdapter.java create mode 100644 src/main/java/li/cil/oc2/common/IMC.java create mode 100644 src/main/java/li/cil/oc2/common/ServerScheduler.java diff --git a/src/main/java/li/cil/oc2/api/API.java b/src/main/java/li/cil/oc2/api/API.java index e0f100ea..ccdc819c 100644 --- a/src/main/java/li/cil/oc2/api/API.java +++ b/src/main/java/li/cil/oc2/api/API.java @@ -1,8 +1,40 @@ package li.cil.oc2.api; +import com.google.gson.GsonBuilder; +import li.cil.oc2.api.device.DeviceMethod; +import li.cil.oc2.api.device.object.Callback; +import li.cil.oc2.api.imc.DeviceMethodParameterTypeAdapter; + +import java.lang.reflect.Type; + public final class API { public static final String MOD_ID = "oc2"; + /** + * IMC message for registering a {@link li.cil.oc2.api.device.provider.DeviceProvider}. + *

+ * Example: + *

+     * InterModComms.sendTo(API.MOD_ID, API.IMC_ADD_DEVICE_PROVIDER, () -> new DeviceProvider() { ... });
+     * 
+ */ + public static final String IMC_ADD_DEVICE_PROVIDER = "addDeviceProvider"; + + /** + * IMC message for registering Gson type adapters for method parameter serialization and + * deserialization. + *

+ * Must be called with a supplier that provides an instance of {@link DeviceMethodParameterTypeAdapter}. + *

+ * It can be necessary to register additional serializers when implementing {@link DeviceMethod}s + * that use custom parameter types. + * + * @see GsonBuilder#registerTypeAdapter(Type, Object) + * @see DeviceMethod + * @see Callback + */ + public static final String IMC_ADD_DEVICE_METHOD_PARAMETER_TYPE_ADAPTER = "addDeviceMethodParameterTypeAdapter"; + private API() { } } diff --git a/src/main/java/li/cil/oc2/api/device/DeviceMethod.java b/src/main/java/li/cil/oc2/api/device/DeviceMethod.java index d662b50e..3396b947 100644 --- a/src/main/java/li/cil/oc2/api/device/DeviceMethod.java +++ b/src/main/java/li/cil/oc2/api/device/DeviceMethod.java @@ -11,6 +11,10 @@ import java.util.Optional; *

* The easiest and hence recommended way of implementing this interface is to use * the {@link ObjectDevice} class. + *

+ * Method parameters are serialized and deserialized using Gson. When using custom + * parameter types it may be necessary to register a custom type adapter for them + * via {@link li.cil.oc2.api.API#IMC_ADD_DEVICE_METHOD_PARAMETER_TYPE_ADAPTER}. * * @see ObjectDevice */ diff --git a/src/main/java/li/cil/oc2/api/device/object/Callback.java b/src/main/java/li/cil/oc2/api/device/object/Callback.java index bd879006..b5a17ad4 100644 --- a/src/main/java/li/cil/oc2/api/device/object/Callback.java +++ b/src/main/java/li/cil/oc2/api/device/object/Callback.java @@ -13,6 +13,10 @@ import java.lang.annotation.Target; *

* Intended to be used in classes instances of which are used in combination with * {@link ObjectDevice} and subclasses of {@link ObjectDevice}. + *

+ * Method parameters are serialized and deserialized using Gson. When using custom + * parameter types it may be necessary to register a custom type adapter for them + * via {@link li.cil.oc2.api.API#IMC_ADD_DEVICE_METHOD_PARAMETER_TYPE_ADAPTER}. */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) diff --git a/src/main/java/li/cil/oc2/api/imc/DeviceMethodParameterTypeAdapter.java b/src/main/java/li/cil/oc2/api/imc/DeviceMethodParameterTypeAdapter.java new file mode 100644 index 00000000..d4f98c47 --- /dev/null +++ b/src/main/java/li/cil/oc2/api/imc/DeviceMethodParameterTypeAdapter.java @@ -0,0 +1,11 @@ +package li.cil.oc2.api.imc; + +public final class DeviceMethodParameterTypeAdapter { + public final Class type; + public final Object typeAdapter; + + public DeviceMethodParameterTypeAdapter(final Class type, final Object typeAdapter) { + this.type = type; + this.typeAdapter = typeAdapter; + } +} diff --git a/src/main/java/li/cil/oc2/common/CommonSetup.java b/src/main/java/li/cil/oc2/common/CommonSetup.java index 29ae9bb3..57ee8076 100644 --- a/src/main/java/li/cil/oc2/common/CommonSetup.java +++ b/src/main/java/li/cil/oc2/common/CommonSetup.java @@ -4,19 +4,26 @@ import li.cil.oc2.common.capabilities.DeviceBusElementCapability; import li.cil.oc2.common.network.Network; import li.cil.oc2.common.vm.Allocator; import li.cil.oc2.serialization.BlobStorage; +import li.cil.oc2.serialization.serializers.ItemStackJsonSerializer; +import net.minecraft.item.ItemStack; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; import net.minecraftforge.fml.event.server.FMLServerAboutToStartEvent; import net.minecraftforge.fml.event.server.FMLServerStoppedEvent; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; public final class CommonSetup { public static void run(final FMLCommonSetupEvent event) { DeviceBusElementCapability.register(); - + Providers.initialize(); Network.setup(); + FMLJavaModLoadingContext.get().getModEventBus().addListener(IMC::handleIMCMessages); MinecraftForge.EVENT_BUS.addListener(CommonSetup::handleServerAboutToStart); MinecraftForge.EVENT_BUS.addListener(CommonSetup::handleServerStoppedEvent); + ServerScheduler.register(); + + addBuiltinDeviceMethodParameterTypeAdapters(); } public static void handleServerAboutToStart(final FMLServerAboutToStartEvent event) { @@ -27,4 +34,8 @@ public final class CommonSetup { BlobStorage.synchronize(); Allocator.resetAndCheckLeaks(); } + + private static void addBuiltinDeviceMethodParameterTypeAdapters() { + DeviceMethodParameterTypeAdapters.addTypeAdapter(ItemStack.class, new ItemStackJsonSerializer()); + } } diff --git a/src/main/java/li/cil/oc2/common/IMC.java b/src/main/java/li/cil/oc2/common/IMC.java new file mode 100644 index 00000000..b91a6f90 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/IMC.java @@ -0,0 +1,65 @@ +package li.cil.oc2.common; + +import li.cil.oc2.api.API; +import li.cil.oc2.api.device.provider.DeviceProvider; +import li.cil.oc2.api.imc.DeviceMethodParameterTypeAdapter; +import li.cil.oc2.common.device.DeviceMethodParameterTypeAdapters; +import li.cil.oc2.common.device.Providers; +import net.minecraft.util.Util; +import net.minecraftforge.fml.InterModComms; +import net.minecraftforge.fml.event.lifecycle.InterModProcessEvent; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.HashMap; +import java.util.Optional; +import java.util.function.Consumer; + +public final class IMC { + private static final Logger LOGGER = LogManager.getLogger(); + + private static final HashMap> METHODS = Util.make(() -> { + HashMap> map = new HashMap<>(); + + map.put(API.IMC_ADD_DEVICE_PROVIDER, IMC::addDeviceProvider); + map.put(API.IMC_ADD_DEVICE_METHOD_PARAMETER_TYPE_ADAPTER, IMC::addDeviceMethodParameterTypeAdapter); + + return map; + }); + + public static void handleIMCMessages(final InterModProcessEvent event) { + event.getIMCStream().forEach(message -> { + final Consumer method = METHODS.get(message.getMethod()); + if (method != null) { + method.accept(message); + } else { + LOGGER.error("Received unknown IMC message [{}] from mod [{}], ignoring.", message.getMethod(), message.getSenderModId()); + } + }); + } + + private static void addDeviceProvider(final InterModComms.IMCMessage message) { + getMessageParameter(message, DeviceProvider.class).ifPresent(Providers::addProvider); + } + + private static void addDeviceMethodParameterTypeAdapter(final InterModComms.IMCMessage message) { + getMessageParameter(message, DeviceMethodParameterTypeAdapter.class).ifPresent(value -> { + try { + DeviceMethodParameterTypeAdapters.addTypeAdapter(value); + } catch (final IllegalArgumentException e) { + LOGGER.error("Received invalid type adapter registration [{}] for type [{}] from mod [{}].", value.typeAdapter, value.type, message.getSenderModId()); + } + }); + } + + @SuppressWarnings("unchecked") + private static Optional getMessageParameter(final InterModComms.IMCMessage message, final Class type) { + final Object value = message.getMessageSupplier().get(); + if (type.isInstance(value)) { + return Optional.of((T) value); + } else { + LOGGER.error("Received incompatible parameter [{}] for IMC message [{}] from mod [{}]. Expected type is [{}].", message.getMessageSupplier().get(), message.getMethod(), message.getSenderModId(), type); + return Optional.empty(); + } + } +} diff --git a/src/main/java/li/cil/oc2/common/ServerScheduler.java b/src/main/java/li/cil/oc2/common/ServerScheduler.java new file mode 100644 index 00000000..f5c94d09 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/ServerScheduler.java @@ -0,0 +1,111 @@ +package li.cil.oc2.common; + +import net.minecraft.world.IWorld; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.event.world.WorldEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.event.server.FMLServerStoppedEvent; +import org.jetbrains.annotations.NotNull; + +import java.util.PriorityQueue; +import java.util.WeakHashMap; + +public final class ServerScheduler { + public static void register() { + MinecraftForge.EVENT_BUS.register(EventHandler.class); + } + + private static final Scheduler serverScheduler = new Scheduler(); + private static final WeakHashMap worldSchedulers = new WeakHashMap<>(); + + public static void schedule(final Runnable runnable) { + schedule(runnable, 0); + } + + public static void schedule(final Runnable runnable, final int afterTicks) { + serverScheduler.schedule(runnable, afterTicks); + } + + public static void schedule(final IWorld world, final Runnable runnable) { + schedule(world, runnable, 0); + } + + public static void schedule(final IWorld world, final Runnable runnable, final int afterTicks) { + final Scheduler scheduler = worldSchedulers.computeIfAbsent(world, w -> new Scheduler()); + scheduler.schedule(runnable, afterTicks); + } + + private static final class EventHandler { + @SubscribeEvent + public static void handleServerStoppedEvent(final FMLServerStoppedEvent event) { + serverScheduler.clear(); + worldSchedulers.clear(); + } + + @SubscribeEvent + public static void handleWorldUnload(final WorldEvent.Unload event) { + worldSchedulers.remove(event.getWorld()); + } + + @SubscribeEvent + public static void handleServerTick(final TickEvent.ServerTickEvent event) { + if (event.phase == TickEvent.Phase.START) { + for (final Scheduler scheduler : worldSchedulers.values()) { + scheduler.tick(); + } + } + } + + @SubscribeEvent + public static void handleWorldTick(final TickEvent.WorldTickEvent event) { + if (event.phase != TickEvent.Phase.START) { + return; + } + + final Scheduler scheduler = worldSchedulers.get(event.world); + if (scheduler != null) { + scheduler.processQueue(); + } + } + } + + private static final class Scheduler { + private final PriorityQueue queue = new PriorityQueue<>(); + private int currentTick; + + public void schedule(final Runnable runnable, final int afterTicks) { + queue.add(new ScheduledRunnable(currentTick + afterTicks, runnable)); + } + + public void processQueue() { + while (!queue.isEmpty() && queue.peek().tick <= currentTick) { + queue.poll().runnable.run(); + } + } + + public void tick() { + currentTick++; + } + + public void clear() { + currentTick = 0; + queue.clear(); + } + } + + private static final class ScheduledRunnable implements Comparable { + public final int tick; + public final Runnable runnable; + + private ScheduledRunnable(final int tick, final Runnable runnable) { + this.tick = tick; + this.runnable = runnable; + } + + @Override + public int compareTo(@NotNull final ServerScheduler.ScheduledRunnable o) { + return Integer.compare(tick, o.tick); + } + } +}