Start Websocket

This commit is contained in:
skippyall
2026-02-17 23:15:16 +01:00
parent 9b61dba4c7
commit bc6bbc448a
20 changed files with 455 additions and 50 deletions
+1
View File
@@ -64,6 +64,7 @@ dependencies {
modImplementation include("xyz.nucleoid:server-translations-api:${project.server_translations_version}") modImplementation include("xyz.nucleoid:server-translations-api:${project.server_translations_version}")
implementation include("com.electronwill.night-config:toml:${project.night_config_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}" modCompileOnly "maven.modrinth:universal-graves:${project.universal_graves_version}"
} }
+1
View File
@@ -21,5 +21,6 @@ org.gradle.jvmargs=-Xmx1G
server_translations_version=2.5.1+1.21.5 server_translations_version=2.5.1+1.21.5
night_config_version=3.8.3 night_config_version=3.8.3
netty_version=4.1.130
universal_graves_version=3.8.0+1.21.6 universal_graves_version=3.8.0+1.21.6
@@ -2,6 +2,7 @@ package io.github.skippyall.minions.gui;
import eu.pb4.sgui.api.elements.GuiElementBuilder; import eu.pb4.sgui.api.elements.GuiElementBuilder;
import eu.pb4.sgui.api.gui.SimpleGui; 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.MinionComponentTypes;
import io.github.skippyall.minions.registration.MinionRegistries; import io.github.skippyall.minions.registration.MinionRegistries;
import io.github.skippyall.minions.gui.input.Result; import io.github.skippyall.minions.gui.input.Result;
@@ -129,20 +130,22 @@ public class InstructionGui {
gui.open(); gui.open();
} }
public static CompletableFuture<ValueSupplierType<MinionRuntime>> selectArgumentType(ServerPlayerEntity player, MinionFakePlayer minion, ConfiguredInstruction<MinionRuntime> instruction) { public static CompletableFuture<ValueSupplierType<MinionRuntime>> selectArgumentType(ServerPlayerEntity player, MinionFakePlayer minion, ValueType<?> valueType, ConfiguredInstruction<MinionRuntime> instruction) {
CompletableFuture<ValueSupplierType<MinionRuntime>> future = new CompletableFuture<>(); CompletableFuture<ValueSupplierType<MinionRuntime>> future = new CompletableFuture<>();
SimpleGui gui = new InstructionBoundSimpleGui(ScreenHandlerType.GENERIC_9X3, player, minion, instruction); SimpleGui gui = new InstructionBoundSimpleGui(ScreenHandlerType.GENERIC_9X3, player, minion, instruction);
for (ValueSupplierType<MinionRuntime> type : MinionRegistries.VALUE_SUPPLIER_TYPES) { for (ValueSupplierType<MinionRuntime> type : MinionRegistries.VALUE_SUPPLIER_TYPES) {
gui.addSlot(new GuiElementBuilder(GuiDisplay.getDisplayStackWithName(MinionRegistries.VALUE_SUPPLIER_TYPES, type, player.getRegistryManager())) if(type.isConfigurable(player, valueType, minion)) {
.setCallback(() -> future.complete(type)) gui.addSlot(new GuiElementBuilder(GuiDisplay.getDisplayStackWithName(MinionRegistries.VALUE_SUPPLIER_TYPES, type, player.getRegistryManager()))
); .setCallback(() -> future.complete(type))
);
}
} }
gui.open(); gui.open();
return future; return future;
} }
public static <T> void configureTypeAndValue(String name, ConfiguredInstruction<MinionRuntime> instruction, Parameter<T> parameter, MinionFakePlayer minion, ServerPlayerEntity player) { public static <T> void configureTypeAndValue(String name, ConfiguredInstruction<MinionRuntime> instruction, Parameter<T> parameter, MinionFakePlayer minion, ServerPlayerEntity player) {
selectArgumentType(player, minion, instruction) selectArgumentType(player, minion, parameter.type(), instruction)
.thenApply(type -> type.openConfiguration(player, parameter.type(), null) .thenApply(type -> type.openConfiguration(player, parameter.type(), null)
.thenAccept(newArgument -> { .thenAccept(newArgument -> {
instruction.getArguments().setArgument(parameter, newArgument); instruction.getArguments().setArgument(parameter, newArgument);
@@ -3,17 +3,17 @@ package io.github.skippyall.minions.listener;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet;
public class ListenerManager<T> implements Iterable<T> { public class ListenerManager<T> implements Iterable<T> {
protected final List<T> listeners; protected final Set<T> listeners;
public ListenerManager() { public ListenerManager() {
this(new CopyOnWriteArrayList<>()); this(new CopyOnWriteArraySet<>());
} }
protected ListenerManager(List<T> listeners) { protected ListenerManager(Set<T> listeners) {
this.listeners = listeners; this.listeners = listeners;
} }
@@ -23,34 +23,15 @@ public class ListenerManager<T> implements Iterable<T> {
public void removeListener(T listener) { public void removeListener(T listener) {
listeners.remove(listener); listeners.remove(listener);
onRemove(listener);
} }
protected void onRemove(T listener) {} public boolean hasListener(T listener) {
return listeners.contains(listener);
}
@Override @Override
public @NotNull Iterator<T> iterator() { public @NotNull Iterator<T> iterator() {
return new Iterator<>() { return listeners.iterator();
final Iterator<T> 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);
}
};
} }
@Override @Override
@@ -9,7 +9,8 @@ import net.minecraft.util.Identifier;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
public class SerializableListenerManager<T extends SerializableListenerManager.SerializableListener> extends ListenerManager<T> { public class SerializableListenerManager<T extends SerializableListenerManager.SerializableListener> extends ListenerManager<T> {
private final Registry<Codec<? extends T>> registry; private final Registry<Codec<? extends T>> registry;
@@ -18,7 +19,7 @@ public class SerializableListenerManager<T extends SerializableListenerManager.S
this.registry = registry; this.registry = registry;
} }
public SerializableListenerManager(Registry<Codec<? extends T>> registry, List<T> listeners) { private SerializableListenerManager(Registry<Codec<? extends T>> registry, Set<T> listeners) {
super(listeners); super(listeners);
this.registry = registry; this.registry = registry;
} }
@@ -28,7 +29,7 @@ public class SerializableListenerManager<T extends SerializableListenerManager.S
listener -> listener.getCodecId().map(registry::get).orElse(Codec.unit(null)), listener -> listener.getCodecId().map(registry::get).orElse(Codec.unit(null)),
codec -> codec.fieldOf("data") codec -> codec.fieldOf("data")
).listOf().xmap( ).listOf().xmap(
list -> new SerializableListenerManager<>(registry, new CopyOnWriteArrayList<>(list)), list -> new SerializableListenerManager<>(registry, new CopyOnWriteArraySet<>(list)),
manager -> { manager -> {
List<T> serializableListeners = new ArrayList<>(); List<T> serializableListeners = new ArrayList<>();
for(T listener : manager.listeners) { for(T listener : manager.listeners) {
@@ -1,5 +1,6 @@
package io.github.skippyall.minions.minion; 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.registration.MinionRegistries;
import io.github.skippyall.minions.Minions; import io.github.skippyall.minions.Minions;
import io.github.skippyall.minions.minion.fakeplayer.MinionFakePlayer; import io.github.skippyall.minions.minion.fakeplayer.MinionFakePlayer;
@@ -16,10 +17,12 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class MinionRuntime implements InstructionRuntime<MinionRuntime> { public class MinionRuntime implements InstructionRuntime<MinionRuntime> {
private final MinionFakePlayer minion; private final MinionFakePlayer minion;
private final Map<String, ConfiguredInstruction<MinionRuntime>> configuredInstructions = new HashMap<>(); private final Map<String, ConfiguredInstruction<MinionRuntime>> configuredInstructions = new HashMap<>();
private final Map<Integer, ConfiguredInstruction<MinionRuntime>> webSocketInstructions = new ConcurrentHashMap<>();
public MinionRuntime(MinionFakePlayer minion) { public MinionRuntime(MinionFakePlayer minion) {
this.minion = minion; this.minion = minion;
@@ -33,6 +36,10 @@ public class MinionRuntime implements InstructionRuntime<MinionRuntime> {
for (ConfiguredInstruction<MinionRuntime> instruction : configuredInstructions.values()) { for (ConfiguredInstruction<MinionRuntime> instruction : configuredInstructions.values()) {
instruction.tick(this); instruction.tick(this);
} }
for(ConfiguredInstruction<MinionRuntime> instruction : webSocketInstructions.values()) {
instruction.tick(this);
}
} }
public void disableInstructionType(InstructionType<MinionRuntime> instructionType) { public void disableInstructionType(InstructionType<MinionRuntime> instructionType) {
@@ -49,6 +56,11 @@ public class MinionRuntime implements InstructionRuntime<MinionRuntime> {
instruction.updatePauseStatus(this); instruction.updatePauseStatus(this);
} }
} }
for(ConfiguredInstruction<MinionRuntime> instruction : webSocketInstructions.values()) {
if(instruction.getInstruction() == instructionType) {
instruction.updatePauseStatus(this);
}
}
} }
@Override @Override
@@ -102,6 +114,16 @@ public class MinionRuntime implements InstructionRuntime<MinionRuntime> {
} }
} }
public void addWebSocketInstruction(int id, ConfiguredInstruction<MinionRuntime> instruction) {
webSocketInstructions.put(id, instruction);
instruction.addListener(new ConfiguredInstructionListener() {
@Override
public void onStop(ConfiguredInstruction<?> instruction) {
webSocketInstructions.remove(id);
}
});
}
public void save(WriteView view) { public void save(WriteView view) {
WriteView.ListView list = view.getList("configuredInstructions"); WriteView.ListView list = view.getList("configuredInstructions");
for (Map.Entry<String, ConfiguredInstruction<MinionRuntime>> instruction : configuredInstructions.entrySet()) { for (Map.Entry<String, ConfiguredInstruction<MinionRuntime>> instruction : configuredInstructions.entrySet()) {
@@ -1,6 +1,7 @@
package io.github.skippyall.minions.program.consumer; package io.github.skippyall.minions.program.consumer;
import com.mojang.serialization.Codec; 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.InstructionRuntime;
import io.github.skippyall.minions.program.value.ValueType; import io.github.skippyall.minions.program.value.ValueType;
import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.network.ServerPlayerEntity;
@@ -8,8 +9,12 @@ import org.jetbrains.annotations.Nullable;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
public abstract class ValueConsumerType<R extends InstructionRuntime<R>> { public interface ValueConsumerType<R extends InstructionRuntime<R>> {
public abstract <T> Codec<? extends ValueConsumer<T,R>> getCodec(ValueType<T> type); <T> Codec<? extends ValueConsumer<T,R>> getCodec(ValueType<T> type);
public abstract <T> CompletableFuture<? extends ValueConsumer<T,R>> openConfiguration(ServerPlayerEntity player, ValueType<T> valueType, @Nullable ValueConsumer<T,R> previous); default boolean isConfigurable(ServerPlayerEntity player, ValueType<?> valueType, MinionFakePlayer minion) {
return true;
}
<T> CompletableFuture<? extends ValueConsumer<T,R>> openConfiguration(ServerPlayerEntity player, ValueType<T> valueType, @Nullable ValueConsumer<T,R> previous);
} }
@@ -26,7 +26,7 @@ public class ConfiguredInstruction<R extends InstructionRuntime<R>> {
this.paused = paused; this.paused = paused;
} }
private ConfiguredInstruction(InstructionType<R> instruction, ValueSupplierList<R> arguments, ValueConsumerList<R> valueConsumers, @Nullable InstructionExecution<R> execution) { public ConfiguredInstruction(InstructionType<R> instruction, ValueSupplierList<R> arguments, ValueConsumerList<R> valueConsumers, @Nullable InstructionExecution<R> execution) {
this.instruction = instruction; this.instruction = instruction;
this.arguments = arguments; this.arguments = arguments;
this.valueConsumers = valueConsumers; this.valueConsumers = valueConsumers;
@@ -8,10 +8,6 @@ import net.minecraft.storage.WriteView;
/** /**
* Responsible for executing instructions. * Responsible for executing instructions.
* When an instruction is executed:
* <li>A new instance is created using the factory</li>
* <li>{@link InstructionExecution#readArguments(ValueSupplierList, R) readFromParameters} is called</li>
* <li>{@link InstructionExecution#start(R) start} is called</li>
*/ */
public interface InstructionExecution<R extends InstructionRuntime<R>> { public interface InstructionExecution<R extends InstructionRuntime<R>> {
/** /**
@@ -26,18 +22,21 @@ public interface InstructionExecution<R extends InstructionRuntime<R>> {
default void tick(R runtime) {} 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 <code>true</code> if the instruction is done, <code>false</code> otherwise. * @return <code>true</code> if the instruction is done, <code>false</code> otherwise.
*/ */
boolean isDone(R runtime); 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 pause(R runtime) {}
default void resume(R runtime) {} default void resume(R runtime) {}
/** /**
* Stops this execution. Is called when isDone returns true, but it may also be called before that. * 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. * @param runtime The runtime that was executing this instruction.
*/ */
@@ -8,7 +8,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
public class FixedValueSupplierType<R extends InstructionRuntime<R>> extends ValueSupplierType<R> { public class FixedValueSupplierType<R extends InstructionRuntime<R>> implements ValueSupplierType<R> {
@Override @Override
public <T> Codec<FixedValueSupplier<T,R>> getCodec(ValueType<T> valueType) { public <T> Codec<FixedValueSupplier<T,R>> getCodec(ValueType<T> valueType) {
return valueType.codec().xmap(value -> new FixedValueSupplier<>(this, valueType, value), FixedValueSupplier::getValue); return valueType.codec().xmap(value -> new FixedValueSupplier<>(this, valueType, value), FixedValueSupplier::getValue);
@@ -1,6 +1,7 @@
package io.github.skippyall.minions.program.supplier; package io.github.skippyall.minions.program.supplier;
import com.mojang.serialization.Codec; 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.InstructionRuntime;
import io.github.skippyall.minions.program.value.ValueType; import io.github.skippyall.minions.program.value.ValueType;
import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.network.ServerPlayerEntity;
@@ -8,8 +9,12 @@ import org.jetbrains.annotations.Nullable;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
public abstract class ValueSupplierType<R extends InstructionRuntime<R>> { public interface ValueSupplierType<R extends InstructionRuntime<R>> {
public abstract <T> Codec<? extends ValueSupplier<T,R>> getCodec(ValueType<T> type); <T> Codec<? extends ValueSupplier<T,R>> getCodec(ValueType<T> type);
public abstract <T> CompletableFuture<? extends ValueSupplier<T,R>> openConfiguration(ServerPlayerEntity player, ValueType<T> valueType, @Nullable ValueSupplier<T,R> previous); default boolean isConfigurable(ServerPlayerEntity player, ValueType<?> valueType, MinionFakePlayer minion) {
return true;
}
<T> CompletableFuture<? extends ValueSupplier<T,R>> openConfiguration(ServerPlayerEntity player, ValueType<T> valueType, @Nullable ValueSupplier<T,R> previous);
} }
@@ -13,6 +13,7 @@ public class MinionRegistration {
MinionListeners.register(); MinionListeners.register();
SkinProviders.register(); SkinProviders.register();
SpecialAbilities.register(); SpecialAbilities.register();
ValueConsumers.register();
ValueSuppliers.register(); ValueSuppliers.register();
ValueTypes.register(); ValueTypes.register();
@@ -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 extends ValueConsumerType<MinionRuntime>> T register(String id, T type) {
return Registry.register(MinionRegistries.VALUE_CONSUMER_TYPES, Identifier.of(Minions.MOD_ID, id), type);
}
public static void register() {}
}
@@ -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<Boolean> AUTHENTICATED = AttributeKey.newInstance("authenticated");
public static final AttributeKey<UUID> 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();
}
}
}
@@ -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<WebSocketFrame> {
@Override
protected void decode(ChannelHandlerContext ctx, WebSocketFrame msg, List<Object> out) {
if(msg instanceof TextWebSocketFrame text) {
out.add(JsonParser.parseString(text.text()));
}
}
}
@@ -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<JsonObject> {
@Override
protected void encode(ChannelHandlerContext ctx, JsonObject msg, List<Object> out) {
out.add(msg.toString());
}
}
@@ -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<JsonObject> {
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);
}
}
}
@@ -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<UUID, MinionWebsocketManager> managers;
private int currentRequestId = 0;
private final Map<Integer, WebsocketRequest> 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<MinionRuntime> instructionType = MinionRegistries.INSTRUCTION_TYPES.get(id);
if(instructionType == null) {
return;
}
ValueSupplierList<MinionRuntime> 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<MinionRuntime> valueConsumers = new ValueConsumerList<>();
for(Parameter<?> parameter : instructionType.getReturnParameters()) {
valueConsumers.setValueConsumer(parameter, new WebsocketValueConsumer<>(parameter.type(), parameter.name(), requestId));
}
ConfiguredInstruction<MinionRuntime> instruction = new ConfiguredInstruction<>(instructionType, valueSuppliers, new ValueConsumerList<>(), null);
}
public <T> void addArgument(ValueSupplierList<MinionRuntime> valueSuppliers, JsonElement element, Parameter<T> parameter) {
if(parameter.type() != null) {
Optional<T> 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<MinionRuntime> instruction;
public WebsocketRequest(ConfiguredInstruction<MinionRuntime> instruction) {
this.instruction = instruction;
instruction.addListener(this);
}
public void acceptReturnValue(String key, JsonElement value) {
if(returnObject == null) {
returnObject = new JsonObject();
}
returnObject.add(key, value);
}
}
}
@@ -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<String, UUID> keyToMinion = new HashMap<>();
private final NioEventLoopGroup group = new NioEventLoopGroup();
private Channel channel;
List<MessageHandler> 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<SocketChannel> {
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));
}
}
}
@@ -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<T> implements ValueConsumer<T, MinionRuntime> {
private final ValueType<T> valueType;
private final String key;
private final int id;
public WebsocketValueConsumer(ValueType<T> valueType, String key, int id) {
this.valueType = valueType;
this.key = key;
this.id = id;
}
@Override
public void consume(T value, MinionRuntime runtime) {
DataResult<JsonElement> 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<T> getValueType() {
return valueType;
}
@Override
public ValueConsumerType<MinionRuntime> getType() {
return ValueConsumers.WEBSOCKET;
}
public static class Type implements ValueConsumerType<MinionRuntime> {
@Override
public <T> Codec<? extends ValueConsumer<T, MinionRuntime>> getCodec(ValueType<T> type) {
return null;
}
@Override
public boolean isConfigurable(ServerPlayerEntity player, ValueType<?> valueType, MinionFakePlayer minion) {
return false;
}
@Override
public <T> CompletableFuture<? extends ValueConsumer<T, MinionRuntime>> openConfiguration(ServerPlayerEntity player, ValueType<T> valueType, @Nullable ValueConsumer<T, MinionRuntime> previous) {
return CompletableFuture.failedFuture(new UnsupportedOperationException());
}
}
}