diff --git a/build.gradle b/build.gradle index d64efcb..2029097 100644 --- a/build.gradle +++ b/build.gradle @@ -64,6 +64,7 @@ dependencies { modImplementation include("xyz.nucleoid:server-translations-api:${project.server_translations_version}") implementation include("com.electronwill.night-config:toml:${project.night_config_version}") + compileOnly("io.netty:netty-codec-http:${project.netty_version}.Final") modCompileOnly "maven.modrinth:universal-graves:${project.universal_graves_version}" } diff --git a/gradle.properties b/gradle.properties index c2d996d..552ca1c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,5 +21,6 @@ org.gradle.jvmargs=-Xmx1G server_translations_version=2.5.1+1.21.5 night_config_version=3.8.3 + netty_version=4.1.130 universal_graves_version=3.8.0+1.21.6 diff --git a/src/main/java/io/github/skippyall/minions/gui/InstructionGui.java b/src/main/java/io/github/skippyall/minions/gui/InstructionGui.java index d94da2f..1c6a272 100644 --- a/src/main/java/io/github/skippyall/minions/gui/InstructionGui.java +++ b/src/main/java/io/github/skippyall/minions/gui/InstructionGui.java @@ -2,6 +2,7 @@ package io.github.skippyall.minions.gui; import eu.pb4.sgui.api.elements.GuiElementBuilder; import eu.pb4.sgui.api.gui.SimpleGui; +import io.github.skippyall.minions.program.value.ValueType; import io.github.skippyall.minions.registration.MinionComponentTypes; import io.github.skippyall.minions.registration.MinionRegistries; import io.github.skippyall.minions.gui.input.Result; @@ -129,20 +130,22 @@ public class InstructionGui { gui.open(); } - public static CompletableFuture> selectArgumentType(ServerPlayerEntity player, MinionFakePlayer minion, ConfiguredInstruction instruction) { + public static CompletableFuture> selectArgumentType(ServerPlayerEntity player, MinionFakePlayer minion, ValueType valueType, ConfiguredInstruction instruction) { CompletableFuture> future = new CompletableFuture<>(); SimpleGui gui = new InstructionBoundSimpleGui(ScreenHandlerType.GENERIC_9X3, player, minion, instruction); for (ValueSupplierType type : MinionRegistries.VALUE_SUPPLIER_TYPES) { - gui.addSlot(new GuiElementBuilder(GuiDisplay.getDisplayStackWithName(MinionRegistries.VALUE_SUPPLIER_TYPES, type, player.getRegistryManager())) - .setCallback(() -> future.complete(type)) - ); + if(type.isConfigurable(player, valueType, minion)) { + gui.addSlot(new GuiElementBuilder(GuiDisplay.getDisplayStackWithName(MinionRegistries.VALUE_SUPPLIER_TYPES, type, player.getRegistryManager())) + .setCallback(() -> future.complete(type)) + ); + } } gui.open(); return future; } public static void configureTypeAndValue(String name, ConfiguredInstruction instruction, Parameter parameter, MinionFakePlayer minion, ServerPlayerEntity player) { - selectArgumentType(player, minion, instruction) + selectArgumentType(player, minion, parameter.type(), instruction) .thenApply(type -> type.openConfiguration(player, parameter.type(), null) .thenAccept(newArgument -> { instruction.getArguments().setArgument(parameter, newArgument); diff --git a/src/main/java/io/github/skippyall/minions/listener/ListenerManager.java b/src/main/java/io/github/skippyall/minions/listener/ListenerManager.java index 6d33f3d..1e1eba8 100644 --- a/src/main/java/io/github/skippyall/minions/listener/ListenerManager.java +++ b/src/main/java/io/github/skippyall/minions/listener/ListenerManager.java @@ -3,17 +3,17 @@ package io.github.skippyall.minions.listener; import org.jetbrains.annotations.NotNull; import java.util.Iterator; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; public class ListenerManager implements Iterable { - protected final List listeners; + protected final Set listeners; public ListenerManager() { - this(new CopyOnWriteArrayList<>()); + this(new CopyOnWriteArraySet<>()); } - protected ListenerManager(List listeners) { + protected ListenerManager(Set listeners) { this.listeners = listeners; } @@ -23,34 +23,15 @@ public class ListenerManager implements Iterable { public void removeListener(T listener) { listeners.remove(listener); - onRemove(listener); } - protected void onRemove(T listener) {} + public boolean hasListener(T listener) { + return listeners.contains(listener); + } @Override public @NotNull Iterator iterator() { - return new Iterator<>() { - final Iterator backing = listeners.iterator(); - T last; - - @Override - public boolean hasNext() { - return backing.hasNext(); - } - - @Override - public T next() { - last = backing.next(); - return last; - } - - @Override - public void remove() { - backing.remove(); - onRemove(last); - } - }; + return listeners.iterator(); } @Override diff --git a/src/main/java/io/github/skippyall/minions/listener/SerializableListenerManager.java b/src/main/java/io/github/skippyall/minions/listener/SerializableListenerManager.java index 48d6aaa..1330c37 100644 --- a/src/main/java/io/github/skippyall/minions/listener/SerializableListenerManager.java +++ b/src/main/java/io/github/skippyall/minions/listener/SerializableListenerManager.java @@ -9,7 +9,8 @@ import net.minecraft.util.Identifier; import java.util.ArrayList; import java.util.List; import java.util.Optional; -import java.util.concurrent.CopyOnWriteArrayList; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; public class SerializableListenerManager extends ListenerManager { private final Registry> registry; @@ -18,7 +19,7 @@ public class SerializableListenerManager> registry, List listeners) { + private SerializableListenerManager(Registry> registry, Set listeners) { super(listeners); this.registry = registry; } @@ -28,7 +29,7 @@ public class SerializableListenerManager listener.getCodecId().map(registry::get).orElse(Codec.unit(null)), codec -> codec.fieldOf("data") ).listOf().xmap( - list -> new SerializableListenerManager<>(registry, new CopyOnWriteArrayList<>(list)), + list -> new SerializableListenerManager<>(registry, new CopyOnWriteArraySet<>(list)), manager -> { List serializableListeners = new ArrayList<>(); for(T listener : manager.listeners) { diff --git a/src/main/java/io/github/skippyall/minions/minion/MinionRuntime.java b/src/main/java/io/github/skippyall/minions/minion/MinionRuntime.java index 70ed434..3c73aae 100644 --- a/src/main/java/io/github/skippyall/minions/minion/MinionRuntime.java +++ b/src/main/java/io/github/skippyall/minions/minion/MinionRuntime.java @@ -1,5 +1,6 @@ package io.github.skippyall.minions.minion; +import io.github.skippyall.minions.program.instruction.ConfiguredInstructionListener; import io.github.skippyall.minions.registration.MinionRegistries; import io.github.skippyall.minions.Minions; import io.github.skippyall.minions.minion.fakeplayer.MinionFakePlayer; @@ -16,10 +17,12 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; public class MinionRuntime implements InstructionRuntime { private final MinionFakePlayer minion; private final Map> configuredInstructions = new HashMap<>(); + private final Map> webSocketInstructions = new ConcurrentHashMap<>(); public MinionRuntime(MinionFakePlayer minion) { this.minion = minion; @@ -33,6 +36,10 @@ public class MinionRuntime implements InstructionRuntime { for (ConfiguredInstruction instruction : configuredInstructions.values()) { instruction.tick(this); } + + for(ConfiguredInstruction instruction : webSocketInstructions.values()) { + instruction.tick(this); + } } public void disableInstructionType(InstructionType instructionType) { @@ -49,6 +56,11 @@ public class MinionRuntime implements InstructionRuntime { instruction.updatePauseStatus(this); } } + for(ConfiguredInstruction instruction : webSocketInstructions.values()) { + if(instruction.getInstruction() == instructionType) { + instruction.updatePauseStatus(this); + } + } } @Override @@ -102,6 +114,16 @@ public class MinionRuntime implements InstructionRuntime { } } + public void addWebSocketInstruction(int id, ConfiguredInstruction instruction) { + webSocketInstructions.put(id, instruction); + instruction.addListener(new ConfiguredInstructionListener() { + @Override + public void onStop(ConfiguredInstruction instruction) { + webSocketInstructions.remove(id); + } + }); + } + public void save(WriteView view) { WriteView.ListView list = view.getList("configuredInstructions"); for (Map.Entry> instruction : configuredInstructions.entrySet()) { diff --git a/src/main/java/io/github/skippyall/minions/program/consumer/ValueConsumerType.java b/src/main/java/io/github/skippyall/minions/program/consumer/ValueConsumerType.java index ca470b7..e7d327a 100644 --- a/src/main/java/io/github/skippyall/minions/program/consumer/ValueConsumerType.java +++ b/src/main/java/io/github/skippyall/minions/program/consumer/ValueConsumerType.java @@ -1,6 +1,7 @@ package io.github.skippyall.minions.program.consumer; import com.mojang.serialization.Codec; +import io.github.skippyall.minions.minion.fakeplayer.MinionFakePlayer; import io.github.skippyall.minions.program.InstructionRuntime; import io.github.skippyall.minions.program.value.ValueType; import net.minecraft.server.network.ServerPlayerEntity; @@ -8,8 +9,12 @@ import org.jetbrains.annotations.Nullable; import java.util.concurrent.CompletableFuture; -public abstract class ValueConsumerType> { - public abstract Codec> getCodec(ValueType type); +public interface ValueConsumerType> { + Codec> getCodec(ValueType type); - public abstract CompletableFuture> openConfiguration(ServerPlayerEntity player, ValueType valueType, @Nullable ValueConsumer previous); + default boolean isConfigurable(ServerPlayerEntity player, ValueType valueType, MinionFakePlayer minion) { + return true; + } + + CompletableFuture> openConfiguration(ServerPlayerEntity player, ValueType valueType, @Nullable ValueConsumer previous); } diff --git a/src/main/java/io/github/skippyall/minions/program/instruction/ConfiguredInstruction.java b/src/main/java/io/github/skippyall/minions/program/instruction/ConfiguredInstruction.java index dd33b30..13edeb7 100644 --- a/src/main/java/io/github/skippyall/minions/program/instruction/ConfiguredInstruction.java +++ b/src/main/java/io/github/skippyall/minions/program/instruction/ConfiguredInstruction.java @@ -26,7 +26,7 @@ public class ConfiguredInstruction> { this.paused = paused; } - private ConfiguredInstruction(InstructionType instruction, ValueSupplierList arguments, ValueConsumerList valueConsumers, @Nullable InstructionExecution execution) { + public ConfiguredInstruction(InstructionType instruction, ValueSupplierList arguments, ValueConsumerList valueConsumers, @Nullable InstructionExecution execution) { this.instruction = instruction; this.arguments = arguments; this.valueConsumers = valueConsumers; diff --git a/src/main/java/io/github/skippyall/minions/program/instruction/InstructionExecution.java b/src/main/java/io/github/skippyall/minions/program/instruction/InstructionExecution.java index c3ee90e..e8a5791 100644 --- a/src/main/java/io/github/skippyall/minions/program/instruction/InstructionExecution.java +++ b/src/main/java/io/github/skippyall/minions/program/instruction/InstructionExecution.java @@ -8,10 +8,6 @@ import net.minecraft.storage.WriteView; /** * Responsible for executing instructions. - * When an instruction is executed: - *
  • A new instance is created using the factory
  • - *
  • {@link InstructionExecution#readArguments(ValueSupplierList, R) readFromParameters} is called
  • - *
  • {@link InstructionExecution#start(R) start} is called
  • */ public interface InstructionExecution> { /** @@ -26,18 +22,21 @@ public interface InstructionExecution> { default void tick(R runtime) {} /** - * Called every tick to determine if the execution of this instruction should be stopped. + * Called before and after {@code tick} to determine if the execution of this instruction should be stopped. * @return true if the instruction is done, false otherwise. */ boolean isDone(R runtime); + /** + * Called when the instruction is paused. This freezes the instruction in its current state. + */ default void pause(R runtime) {} default void resume(R runtime) {} /** * Stops this execution. Is called when isDone returns true, but it may also be called before that. - * This should undo changes to the minion unless they are supposed to be permanent. + * This should undo temporary changes to the minion. * * @param runtime The runtime that was executing this instruction. */ diff --git a/src/main/java/io/github/skippyall/minions/program/supplier/FixedValueSupplierType.java b/src/main/java/io/github/skippyall/minions/program/supplier/FixedValueSupplierType.java index 8908367..349ceb9 100644 --- a/src/main/java/io/github/skippyall/minions/program/supplier/FixedValueSupplierType.java +++ b/src/main/java/io/github/skippyall/minions/program/supplier/FixedValueSupplierType.java @@ -8,7 +8,7 @@ import org.jetbrains.annotations.Nullable; import java.util.concurrent.CompletableFuture; -public class FixedValueSupplierType> extends ValueSupplierType { +public class FixedValueSupplierType> implements ValueSupplierType { @Override public Codec> getCodec(ValueType valueType) { return valueType.codec().xmap(value -> new FixedValueSupplier<>(this, valueType, value), FixedValueSupplier::getValue); diff --git a/src/main/java/io/github/skippyall/minions/program/supplier/ValueSupplierType.java b/src/main/java/io/github/skippyall/minions/program/supplier/ValueSupplierType.java index eb50215..17e63fd 100644 --- a/src/main/java/io/github/skippyall/minions/program/supplier/ValueSupplierType.java +++ b/src/main/java/io/github/skippyall/minions/program/supplier/ValueSupplierType.java @@ -1,6 +1,7 @@ package io.github.skippyall.minions.program.supplier; import com.mojang.serialization.Codec; +import io.github.skippyall.minions.minion.fakeplayer.MinionFakePlayer; import io.github.skippyall.minions.program.InstructionRuntime; import io.github.skippyall.minions.program.value.ValueType; import net.minecraft.server.network.ServerPlayerEntity; @@ -8,8 +9,12 @@ import org.jetbrains.annotations.Nullable; import java.util.concurrent.CompletableFuture; -public abstract class ValueSupplierType> { - public abstract Codec> getCodec(ValueType type); +public interface ValueSupplierType> { + Codec> getCodec(ValueType type); - public abstract CompletableFuture> openConfiguration(ServerPlayerEntity player, ValueType valueType, @Nullable ValueSupplier previous); + default boolean isConfigurable(ServerPlayerEntity player, ValueType valueType, MinionFakePlayer minion) { + return true; + } + + CompletableFuture> openConfiguration(ServerPlayerEntity player, ValueType valueType, @Nullable ValueSupplier previous); } diff --git a/src/main/java/io/github/skippyall/minions/registration/MinionRegistration.java b/src/main/java/io/github/skippyall/minions/registration/MinionRegistration.java index 62de12b..b28127e 100644 --- a/src/main/java/io/github/skippyall/minions/registration/MinionRegistration.java +++ b/src/main/java/io/github/skippyall/minions/registration/MinionRegistration.java @@ -13,6 +13,7 @@ public class MinionRegistration { MinionListeners.register(); SkinProviders.register(); SpecialAbilities.register(); + ValueConsumers.register(); ValueSuppliers.register(); ValueTypes.register(); diff --git a/src/main/java/io/github/skippyall/minions/registration/ValueConsumers.java b/src/main/java/io/github/skippyall/minions/registration/ValueConsumers.java new file mode 100644 index 0000000..fb8eac2 --- /dev/null +++ b/src/main/java/io/github/skippyall/minions/registration/ValueConsumers.java @@ -0,0 +1,18 @@ +package io.github.skippyall.minions.registration; + +import io.github.skippyall.minions.Minions; +import io.github.skippyall.minions.minion.MinionRuntime; +import io.github.skippyall.minions.program.consumer.ValueConsumerType; +import io.github.skippyall.minions.websocket.WebsocketValueConsumer; +import net.minecraft.registry.Registry; +import net.minecraft.util.Identifier; + +public class ValueConsumers { + public static final WebsocketValueConsumer.Type WEBSOCKET = register("websocket", new WebsocketValueConsumer.Type()); + + public static > T register(String id, T type) { + return Registry.register(MinionRegistries.VALUE_CONSUMER_TYPES, Identifier.of(Minions.MOD_ID, id), type); + } + + public static void register() {} +} diff --git a/src/main/java/io/github/skippyall/minions/websocket/Authenticator.java b/src/main/java/io/github/skippyall/minions/websocket/Authenticator.java new file mode 100644 index 0000000..def4afe --- /dev/null +++ b/src/main/java/io/github/skippyall/minions/websocket/Authenticator.java @@ -0,0 +1,38 @@ +package io.github.skippyall.minions.websocket; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.util.AttributeKey; + +import java.util.UUID; + +public class Authenticator extends ChannelInboundHandlerAdapter { + public static final AttributeKey AUTHENTICATED = AttributeKey.newInstance("authenticated"); + public static final AttributeKey MINION = AttributeKey.newInstance("minion"); + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if(!ctx.channel().hasAttr(AUTHENTICATED) && msg instanceof HttpMessage message) { + String header = message.headers().get(HttpHeaderNames.AUTHORIZATION); + if (header.startsWith("Bearer ")) { + String key = header.substring("Bearer ".length()).trim(); + if (WebsocketServer.keyToMinion.containsKey(key)) { + ctx.channel().attr(AUTHENTICATED).set(true); + ctx.channel().attr(MINION).set(WebsocketServer.keyToMinion.get(key)); + } else { + ctx.channel().attr(AUTHENTICATED).set(false); + } + } else { + ctx.channel().attr(AUTHENTICATED).set(false); + } + } + + if(ctx.channel().hasAttr(AUTHENTICATED) && ctx.channel().attr(AUTHENTICATED).get() == Boolean.TRUE) { + super.channelRead(ctx, msg); + } else { + ctx.close(); + } + } +} diff --git a/src/main/java/io/github/skippyall/minions/websocket/JsonDecoder.java b/src/main/java/io/github/skippyall/minions/websocket/JsonDecoder.java new file mode 100644 index 0000000..d2643f7 --- /dev/null +++ b/src/main/java/io/github/skippyall/minions/websocket/JsonDecoder.java @@ -0,0 +1,18 @@ +package io.github.skippyall.minions.websocket; + +import com.google.gson.JsonParser; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageDecoder; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; + +import java.util.List; + +public class JsonDecoder extends MessageToMessageDecoder { + @Override + protected void decode(ChannelHandlerContext ctx, WebSocketFrame msg, List out) { + if(msg instanceof TextWebSocketFrame text) { + out.add(JsonParser.parseString(text.text())); + } + } +} diff --git a/src/main/java/io/github/skippyall/minions/websocket/JsonEncoder.java b/src/main/java/io/github/skippyall/minions/websocket/JsonEncoder.java new file mode 100644 index 0000000..b482050 --- /dev/null +++ b/src/main/java/io/github/skippyall/minions/websocket/JsonEncoder.java @@ -0,0 +1,14 @@ +package io.github.skippyall.minions.websocket; + +import com.google.gson.JsonObject; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageEncoder; + +import java.util.List; + +public class JsonEncoder extends MessageToMessageEncoder { + @Override + protected void encode(ChannelHandlerContext ctx, JsonObject msg, List out) { + out.add(msg.toString()); + } +} diff --git a/src/main/java/io/github/skippyall/minions/websocket/MessageHandler.java b/src/main/java/io/github/skippyall/minions/websocket/MessageHandler.java new file mode 100644 index 0000000..5855af6 --- /dev/null +++ b/src/main/java/io/github/skippyall/minions/websocket/MessageHandler.java @@ -0,0 +1,43 @@ +package io.github.skippyall.minions.websocket; + +import com.google.gson.JsonObject; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; + +import java.util.UUID; + +public class MessageHandler extends SimpleChannelInboundHandler { + private final WebsocketServer server; + + public MessageHandler(WebsocketServer server) { + this.server = server; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + super.channelActive(ctx); + server.handlers.add(this); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + super.channelInactive(ctx); + server.handlers.remove(this); + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, JsonObject msg) throws Exception { + UUID minion = ctx.channel().attr(Authenticator.MINION).get(); + MinionWebsocketManager.get(minion).handleMessage(msg); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) { + + } else { + super.userEventTriggered(ctx, evt); + } + } +} diff --git a/src/main/java/io/github/skippyall/minions/websocket/MinionWebsocketManager.java b/src/main/java/io/github/skippyall/minions/websocket/MinionWebsocketManager.java new file mode 100644 index 0000000..f70c2fe --- /dev/null +++ b/src/main/java/io/github/skippyall/minions/websocket/MinionWebsocketManager.java @@ -0,0 +1,111 @@ +package io.github.skippyall.minions.websocket; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.JsonOps; +import io.github.skippyall.minions.minion.MinionRuntime; +import io.github.skippyall.minions.minion.fakeplayer.MinionFakePlayer; +import io.github.skippyall.minions.program.consumer.ValueConsumerList; +import io.github.skippyall.minions.program.instruction.ConfiguredInstruction; +import io.github.skippyall.minions.program.instruction.ConfiguredInstructionListener; +import io.github.skippyall.minions.program.instruction.InstructionType; +import io.github.skippyall.minions.program.supplier.FixedValueSupplier; +import io.github.skippyall.minions.program.supplier.Parameter; +import io.github.skippyall.minions.program.supplier.ValueSupplierList; +import io.github.skippyall.minions.registration.MinionRegistries; +import io.github.skippyall.minions.registration.ValueSuppliers; +import net.minecraft.util.Identifier; + +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class MinionWebsocketManager { + private static Map managers; + + private int currentRequestId = 0; + private final Map requests = new ConcurrentHashMap<>(); + private MinionFakePlayer minion; + + public MinionWebsocketManager(MinionFakePlayer minion) { + this.minion = minion; + } + + public static MinionWebsocketManager get(UUID minion) { + return managers.get(minion); + } + + public WebsocketRequest getRequest(int id) { + return requests.get(id); + } + + public void handleMessage(JsonObject msg) { + String type = msg.get("type").getAsString(); + if(type.equals("run_instruction")) { + onRunInstruction(msg); + } + } + + private void onRunInstruction(JsonObject msg) { + Identifier id = Identifier.tryParse(msg.get("instruction").getAsString()); + InstructionType instructionType = MinionRegistries.INSTRUCTION_TYPES.get(id); + + if(instructionType == null) { + return; + } + + ValueSupplierList valueSuppliers = new ValueSupplierList<>(); + JsonObject arguments = msg.get("arguments").getAsJsonObject(); + for(String argumentName : arguments.keySet()) { + JsonElement argument = arguments.get(argumentName); + + Parameter parameter = null; + for(Parameter otherParameter : instructionType.getParameters()) { + if(otherParameter.name().equals(argumentName)) { + parameter = otherParameter; + break; + } + } + if(parameter != null) { + addArgument(valueSuppliers, argument, parameter); + } + } + currentRequestId++; + int requestId = currentRequestId; + + ValueConsumerList valueConsumers = new ValueConsumerList<>(); + for(Parameter parameter : instructionType.getReturnParameters()) { + valueConsumers.setValueConsumer(parameter, new WebsocketValueConsumer<>(parameter.type(), parameter.name(), requestId)); + } + + ConfiguredInstruction instruction = new ConfiguredInstruction<>(instructionType, valueSuppliers, new ValueConsumerList<>(), null); + } + + public void addArgument(ValueSupplierList valueSuppliers, JsonElement element, Parameter parameter) { + if(parameter.type() != null) { + Optional value = parameter.type().codec().decode(JsonOps.INSTANCE, element).map(Pair::getFirst).result(); + if(value.isPresent()) { + valueSuppliers.setArgument(parameter, new FixedValueSupplier<>(ValueSuppliers.FIXED_VALUE_SUPPLIER_TYPE, parameter.type(), value.get())); + } + } + } + + public class WebsocketRequest implements ConfiguredInstructionListener { + private JsonObject returnObject = null; + private ConfiguredInstruction instruction; + + public WebsocketRequest(ConfiguredInstruction instruction) { + this.instruction = instruction; + instruction.addListener(this); + } + + public void acceptReturnValue(String key, JsonElement value) { + if(returnObject == null) { + returnObject = new JsonObject(); + } + returnObject.add(key, value); + } + } +} diff --git a/src/main/java/io/github/skippyall/minions/websocket/WebsocketServer.java b/src/main/java/io/github/skippyall/minions/websocket/WebsocketServer.java new file mode 100644 index 0000000..965774e --- /dev/null +++ b/src/main/java/io/github/skippyall/minions/websocket/WebsocketServer.java @@ -0,0 +1,79 @@ +package io.github.skippyall.minions.websocket; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; +import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class WebsocketServer { + public static final Map keyToMinion = new HashMap<>(); + + private final NioEventLoopGroup group = new NioEventLoopGroup(); + private Channel channel; + List handlers; + + private String host; + private int port; + private SslContext sslCtx; + + public void start() { + channel = new ServerBootstrap() + .group(group) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new WebSocketServerInitializer(sslCtx, this)) + .bind(host, port) + .syncUninterruptibly() + .channel(); + } + + public void stop() throws InterruptedException { + if(channel != null) { + channel.close().sync(); + } + group.shutdownGracefully(); + } + + public static class WebSocketServerInitializer extends ChannelInitializer { + private static final int MAX_CONTENT_LENGTH = 65536; + + private final SslContext sslCtx; + private final WebsocketServer server; + + public WebSocketServerInitializer(SslContext sslCtx, WebsocketServer server) { + this.sslCtx = sslCtx; + this.server = server; + } + + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + if (sslCtx != null) { + pipeline.addLast(sslCtx.newHandler(ch.alloc())); + } + pipeline.addLast(new HttpServerCodec()) + .addLast(new HttpObjectAggregator(MAX_CONTENT_LENGTH)) + .addLast(new Authenticator()) + .addLast(new WebSocketServerCompressionHandler(MAX_CONTENT_LENGTH)) + .addLast(new WebSocketServerProtocolHandler("/", null, true)) + .addLast(new JsonDecoder()) + .addLast(new JsonEncoder()) + .addLast(new MessageHandler(server)); + } + } +} diff --git a/src/main/java/io/github/skippyall/minions/websocket/WebsocketValueConsumer.java b/src/main/java/io/github/skippyall/minions/websocket/WebsocketValueConsumer.java new file mode 100644 index 0000000..9f1f9ae --- /dev/null +++ b/src/main/java/io/github/skippyall/minions/websocket/WebsocketValueConsumer.java @@ -0,0 +1,65 @@ +package io.github.skippyall.minions.websocket; + +import com.google.gson.JsonElement; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.JsonOps; +import io.github.skippyall.minions.Minions; +import io.github.skippyall.minions.minion.MinionRuntime; +import io.github.skippyall.minions.minion.fakeplayer.MinionFakePlayer; +import io.github.skippyall.minions.program.consumer.ValueConsumer; +import io.github.skippyall.minions.program.consumer.ValueConsumerType; +import io.github.skippyall.minions.program.value.ValueType; +import io.github.skippyall.minions.registration.ValueConsumers; +import net.minecraft.server.network.ServerPlayerEntity; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.CompletableFuture; + +public class WebsocketValueConsumer implements ValueConsumer { + private final ValueType valueType; + private final String key; + private final int id; + + public WebsocketValueConsumer(ValueType valueType, String key, int id) { + this.valueType = valueType; + this.key = key; + this.id = id; + } + + @Override + public void consume(T value, MinionRuntime runtime) { + DataResult encoded = valueType.codec().encodeStart(JsonOps.INSTANCE, value); + if(encoded.hasResultOrPartial()) { + MinionWebsocketManager.get(runtime.getMinion().getUuid()).getRequest(id).acceptReturnValue(key, encoded.getPartialOrThrow()); + } + encoded.ifError(error -> Minions.LOGGER.error("Error while encoding value for websocket: {}", error.message())); + } + + @Override + public ValueType getValueType() { + return valueType; + } + + @Override + public ValueConsumerType getType() { + return ValueConsumers.WEBSOCKET; + } + + public static class Type implements ValueConsumerType { + @Override + public Codec> getCodec(ValueType type) { + return null; + } + + @Override + public boolean isConfigurable(ServerPlayerEntity player, ValueType valueType, MinionFakePlayer minion) { + return false; + } + + @Override + public CompletableFuture> openConfiguration(ServerPlayerEntity player, ValueType valueType, @Nullable ValueConsumer previous) { + return CompletableFuture.failedFuture(new UnsupportedOperationException()); + } + } +}