API for adding device providers and custom parameter serializers.

This commit is contained in:
Florian Nücke
2020-11-30 18:34:06 +01:00
parent 7a0f0ecc0f
commit f13042d3d2
7 changed files with 239 additions and 1 deletions

View File

@@ -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}.
* <p>
* Example:
* <pre>
* InterModComms.sendTo(API.MOD_ID, API.IMC_ADD_DEVICE_PROVIDER, () -> new DeviceProvider() { ... });
* </pre>
*/
public static final String IMC_ADD_DEVICE_PROVIDER = "addDeviceProvider";
/**
* IMC message for registering Gson type adapters for method parameter serialization and
* deserialization.
* <p>
* Must be called with a supplier that provides an instance of {@link DeviceMethodParameterTypeAdapter}.
* <p>
* 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() {
}
}

View File

@@ -11,6 +11,10 @@ import java.util.Optional;
* <p>
* The easiest and hence recommended way of implementing this interface is to use
* the {@link ObjectDevice} class.
* <p>
* 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
*/

View File

@@ -13,6 +13,10 @@ import java.lang.annotation.Target;
* <p>
* Intended to be used in classes instances of which are used in combination with
* {@link ObjectDevice} and subclasses of {@link ObjectDevice}.
* <p>
* 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)

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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<String, Consumer<InterModComms.IMCMessage>> METHODS = Util.make(() -> {
HashMap<String, Consumer<InterModComms.IMCMessage>> 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<InterModComms.IMCMessage> 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 <T> Optional<T> getMessageParameter(final InterModComms.IMCMessage message, final Class<T> 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();
}
}
}

View File

@@ -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<IWorld, Scheduler> 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<ScheduledRunnable> 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<ScheduledRunnable> {
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);
}
}
}