From a49aee259a88d15400eabaacc08c1344294d76ca Mon Sep 17 00:00:00 2001 From: LadyCailin Date: Sun, 3 Mar 2024 22:43:48 +0100 Subject: [PATCH] PacketJumper completed --- .../PureUtilities/Common/ReflectionUtils.java | 16 +- .../bukkit/BukkitMCWorldBorder.java | 2 +- .../events/BukkitAbstractEventMixin.java | 6 +- .../commandhelper/CommandHelperPlugin.java | 3 +- .../java/com/laytonsmith/core/AliasCore.java | 3 + .../java/com/laytonsmith/core/Static.java | 13 +- .../laytonsmith/core/constructs/CPacket.java | 81 +++- .../core/events/AbstractEvent.java | 333 +---------------- .../core/events/AbstractGenericEvent.java | 352 ++++++++++++++++++ .../com/laytonsmith/core/events/Driver.java | 5 + .../com/laytonsmith/core/events/Event.java | 35 +- .../laytonsmith/core/events/EventUtils.java | 25 +- .../core/events/drivers/PacketEvents.java | 336 +++++++++++++++++ .../prefilters/AbstractPrefilterMatcher.java | 5 + .../prefilters/BooleanPrefilterMatcher.java | 6 + .../ExpressionPrefilterMatcher.java | 5 + .../prefilters/ItemStackPrefilterMatcher.java | 5 + .../prefilters/LocationPrefilterMatcher.java | 5 + .../prefilters/MacroICPrefilterMatcher.java | 5 + .../prefilters/MacroPrefilterMatcher.java | 5 + .../prefilters/MaterialPrefilterMatcher.java | 5 + .../prefilters/MathPrefilterMatcher.java | 5 + .../core/events/prefilters/Prefilter.java | 23 +- .../events/prefilters/PrefilterBuilder.java | 39 +- .../events/prefilters/PrefilterMatcher.java | 6 + .../prefilters/RegexPrefilterMatcher.java | 5 + .../prefilters/StringICPrefilterMatcher.java | 5 + .../prefilters/StringPrefilterMatcher.java | 5 + .../prefilters/WorldPrefilterMatcher.java | 5 + .../exceptions/ConfigRuntimeException.java | 13 +- .../core/extensions/ExtensionTracker.java | 10 +- .../core/functions/EventBinding.java | 18 +- .../core/functions/XPacketJumper.java | 183 ++++++++- .../packetjumper/BindablePacketEvent.java | 19 + .../core/packetjumper/Comparisons.java | 29 ++ .../core/packetjumper/Conversions.java | 196 ++++++++++ .../core/packetjumper/PacketDirection.java | 19 + .../packetjumper/PacketEventNotifier.java | 67 ++++ .../core/packetjumper/PacketJumper.java | 254 +++++++------ .../PacketKind.java | 2 +- .../PacketUtils.java | 83 ++++- .../packetjumper/ProtocolLibPacketEvent.java | 49 +++ .../core/protocollib/Conversions.java | 127 ------- .../core/protocollib/PacketJumper.java | 157 -------- .../laytonsmith/tools/ShellEventMixin.java | 6 +- src/main/resources/docs/Dev | 9 + src/main/resources/docs/Developer_Guide | 24 +- src/main/resources/docs/Extension_Development | 14 +- src/main/resources/docs/Packet_Jumper | 120 +++++- .../core/functions/EnchantmentsTest.java | 77 ++++ .../com/laytonsmith/testing/StaticTest.java | 4 +- 51 files changed, 1980 insertions(+), 844 deletions(-) create mode 100644 src/main/java/com/laytonsmith/core/events/AbstractGenericEvent.java create mode 100644 src/main/java/com/laytonsmith/core/events/drivers/PacketEvents.java create mode 100644 src/main/java/com/laytonsmith/core/packetjumper/BindablePacketEvent.java create mode 100644 src/main/java/com/laytonsmith/core/packetjumper/Comparisons.java create mode 100644 src/main/java/com/laytonsmith/core/packetjumper/Conversions.java create mode 100644 src/main/java/com/laytonsmith/core/packetjumper/PacketDirection.java create mode 100644 src/main/java/com/laytonsmith/core/packetjumper/PacketEventNotifier.java rename src/main/java/com/laytonsmith/core/{protocollib => packetjumper}/PacketKind.java (96%) rename src/main/java/com/laytonsmith/core/{protocollib => packetjumper}/PacketUtils.java (81%) create mode 100644 src/main/java/com/laytonsmith/core/packetjumper/ProtocolLibPacketEvent.java delete mode 100644 src/main/java/com/laytonsmith/core/protocollib/Conversions.java delete mode 100644 src/main/java/com/laytonsmith/core/protocollib/PacketJumper.java create mode 100644 src/main/resources/docs/Dev create mode 100644 src/test/java/com/laytonsmith/core/functions/EnchantmentsTest.java diff --git a/src/main/java/com/laytonsmith/PureUtilities/Common/ReflectionUtils.java b/src/main/java/com/laytonsmith/PureUtilities/Common/ReflectionUtils.java index cebedf0796..c9a54d453e 100644 --- a/src/main/java/com/laytonsmith/PureUtilities/Common/ReflectionUtils.java +++ b/src/main/java/com/laytonsmith/PureUtilities/Common/ReflectionUtils.java @@ -189,7 +189,7 @@ public static void set(Class clazz, Object instance, String variableName, Object * @param methodName The name of the method. * @return The invocation result, null if void. */ - public static Object invokeMethod(Class clazz, Object instance, String methodName) throws ReflectionException { + public static T invokeMethod(Class clazz, Object instance, String methodName) throws ReflectionException { return invokeMethod(clazz, instance, methodName, new Class[]{}, new Object[]{}); } @@ -204,7 +204,7 @@ public static Object invokeMethod(Class clazz, Object instance, String methodNam * @throws ReflectionException */ @SuppressWarnings({"ThrowableInstanceNotThrown", "ThrowableInstanceNeverThrown"}) - public static Object invokeMethod(Object instance, String methodName, Object... params) throws ReflectionException { + public static T invokeMethod(Object instance, String methodName, Object... params) throws ReflectionException { Class c = instance.getClass(); Class[] argTypes; { @@ -237,7 +237,7 @@ public static Object invokeMethod(Object instance, String methodName, Object... } } m.setAccessible(true); - return m.invoke(instance, params); + return (T) m.invoke(instance, params); } } catch(IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { throw new ReflectionException(ex); @@ -260,13 +260,13 @@ public static Object invokeMethod(Object instance, String methodName, Object... * @throws ReflectionException */ @SuppressWarnings({"ThrowableInstanceNotThrown", "ThrowableInstanceNeverThrown"}) - public static Object invokeMethod(Object instance, String methodName) throws ReflectionException { + public static T invokeMethod(Object instance, String methodName) throws ReflectionException { Class c = instance.getClass(); while(c != null) { for(Method m : c.getDeclaredMethods()) { - if(methodName.equals(m.getName())) { + if(methodName.equals(m.getName()) && m.getParameterCount() == 0) { try { - return m.invoke(instance); + return (T) m.invoke(instance); } catch(IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { throw new ReflectionException(ex); } @@ -290,12 +290,12 @@ public static Object invokeMethod(Object instance, String methodName) throws Ref * @param args The arguments. * @return The invocation result, null if void. */ - public static Object invokeMethod(Class clazz, Object instance, String methodName, Class[] argTypes, Object[] args) + public static T invokeMethod(Class clazz, Object instance, String methodName, Class[] argTypes, Object[] args) throws ReflectionException { try { Method m = clazz.getDeclaredMethod(methodName, argTypes); m.setAccessible(true); - return m.invoke(instance, args); + return (T) m.invoke(instance, args); } catch(InvocationTargetException | NoSuchMethodException | IllegalArgumentException | IllegalAccessException | SecurityException ex) { throw new ReflectionException(ex); diff --git a/src/main/java/com/laytonsmith/abstraction/bukkit/BukkitMCWorldBorder.java b/src/main/java/com/laytonsmith/abstraction/bukkit/BukkitMCWorldBorder.java index 89c4cf8284..40b53f13e0 100644 --- a/src/main/java/com/laytonsmith/abstraction/bukkit/BukkitMCWorldBorder.java +++ b/src/main/java/com/laytonsmith/abstraction/bukkit/BukkitMCWorldBorder.java @@ -102,4 +102,4 @@ public boolean equals(Object obj) { public int hashCode() { return wb.hashCode(); } -} +} \ No newline at end of file diff --git a/src/main/java/com/laytonsmith/abstraction/bukkit/events/BukkitAbstractEventMixin.java b/src/main/java/com/laytonsmith/abstraction/bukkit/events/BukkitAbstractEventMixin.java index af409366e8..525495d832 100644 --- a/src/main/java/com/laytonsmith/abstraction/bukkit/events/BukkitAbstractEventMixin.java +++ b/src/main/java/com/laytonsmith/abstraction/bukkit/events/BukkitAbstractEventMixin.java @@ -4,7 +4,7 @@ import com.laytonsmith.core.Static; import com.laytonsmith.core.constructs.CString; import com.laytonsmith.core.constructs.Target; -import com.laytonsmith.core.events.AbstractEvent; +import com.laytonsmith.core.events.AbstractGenericEvent; import com.laytonsmith.core.events.BindableEvent; import com.laytonsmith.core.events.EventMixinInterface; import com.laytonsmith.core.exceptions.EventException; @@ -26,9 +26,9 @@ public class BukkitAbstractEventMixin implements EventMixinInterface { - AbstractEvent mySuper; + AbstractGenericEvent mySuper; - public BukkitAbstractEventMixin(AbstractEvent mySuper) { + public BukkitAbstractEventMixin(AbstractGenericEvent mySuper) { this.mySuper = mySuper; } diff --git a/src/main/java/com/laytonsmith/commandhelper/CommandHelperPlugin.java b/src/main/java/com/laytonsmith/commandhelper/CommandHelperPlugin.java index ce3cb33eb2..f18208b3c7 100644 --- a/src/main/java/com/laytonsmith/commandhelper/CommandHelperPlugin.java +++ b/src/main/java/com/laytonsmith/commandhelper/CommandHelperPlugin.java @@ -62,7 +62,7 @@ import com.laytonsmith.core.apps.AppsApiUtil; import com.laytonsmith.core.constructs.Target; import com.laytonsmith.core.extensions.ExtensionManager; -import com.laytonsmith.core.protocollib.PacketJumper; +import com.laytonsmith.core.packetjumper.PacketJumper; import com.laytonsmith.core.telemetry.DefaultTelemetry; import com.laytonsmith.core.telemetry.Telemetry; import org.bukkit.Bukkit; @@ -443,6 +443,7 @@ public void onDisable() { } ExtensionManager.Cleanup(); + PacketJumper.Shutdown(); ac = null; } diff --git a/src/main/java/com/laytonsmith/core/AliasCore.java b/src/main/java/com/laytonsmith/core/AliasCore.java index 8ce955878e..ff80e69f52 100644 --- a/src/main/java/com/laytonsmith/core/AliasCore.java +++ b/src/main/java/com/laytonsmith/core/AliasCore.java @@ -44,6 +44,7 @@ import com.laytonsmith.core.functions.Scheduling; import com.laytonsmith.core.natives.interfaces.MEnumType; import com.laytonsmith.core.natives.interfaces.Mixed; +import com.laytonsmith.core.packetjumper.PacketJumper; import com.laytonsmith.core.profiler.ProfilePoint; import com.laytonsmith.core.profiler.Profiler; import com.laytonsmith.core.taskmanager.TaskManagerImpl; @@ -261,6 +262,8 @@ public final void reload(MCPlayer player, String[] settings, final boolean first } } + PacketJumper.Shutdown(); + MSLog.initialize(fileLocations.getConfigDirectory()); Profiler profiler; diff --git a/src/main/java/com/laytonsmith/core/Static.java b/src/main/java/com/laytonsmith/core/Static.java index 597cbbf79d..6cb190a5a1 100644 --- a/src/main/java/com/laytonsmith/core/Static.java +++ b/src/main/java/com/laytonsmith/core/Static.java @@ -1487,9 +1487,18 @@ public static Construct getMSObject(Object object, Target t) { return CNull.NULL; } else if(object instanceof Boolean) { return CBoolean.get((boolean) object); - } else if((object instanceof Byte) || (object instanceof Short) || (object instanceof Integer) || (object instanceof Long)) { + } else if(object instanceof Byte b) { + // These are required due to unboxing + return new CInt(b.longValue(), t); + } else if(object instanceof Short s) { + return new CInt(s.longValue(), t); + } else if(object instanceof Integer i) { + return new CInt(i.longValue(), t); + } else if(object instanceof Long) { return new CInt((long) object, t); - } else if((object instanceof Float) || (object instanceof Double)) { + } else if(object instanceof Float f) { + return new CDouble(f.doubleValue(), t); + } else if(object instanceof Double) { return new CDouble((double) object, t); } else if(object instanceof Character) { return new CString((char) object, t); diff --git a/src/main/java/com/laytonsmith/core/constructs/CPacket.java b/src/main/java/com/laytonsmith/core/constructs/CPacket.java index 8423e408a3..424975f019 100644 --- a/src/main/java/com/laytonsmith/core/constructs/CPacket.java +++ b/src/main/java/com/laytonsmith/core/constructs/CPacket.java @@ -9,12 +9,15 @@ import com.laytonsmith.abstraction.bukkit.entities.BukkitMCPlayer; import com.laytonsmith.annotations.typeof; import com.laytonsmith.core.MSVersion; +import com.laytonsmith.core.exceptions.CRE.CREPluginInternalException; import com.laytonsmith.core.natives.interfaces.ArrayAccess; import com.laytonsmith.core.natives.interfaces.Mixed; -import com.laytonsmith.core.protocollib.Conversions; -import com.laytonsmith.core.protocollib.PacketKind; -import com.laytonsmith.core.protocollib.PacketUtils; +import com.laytonsmith.core.packetjumper.Conversions; +import com.laytonsmith.core.packetjumper.PacketJumper; +import com.laytonsmith.core.packetjumper.PacketKind; +import com.laytonsmith.core.packetjumper.PacketUtils; import java.lang.reflect.Field; +import net.fabricmc.mappingio.tree.MappingTree; /** * Created by JunHyung Im on 2020-07-05 @@ -82,32 +85,40 @@ public CClassType[] getInterfaces() { return CClassType.EMPTY_CLASS_ARRAY; } + public int indexFromString(String field, Target t) { + CArray packetData = (CArray)((ArrayAccess)((ArrayAccess)PacketUtils.getAllPackets() + .get(protocol.name(), t)) + .get(sender == PacketType.Sender.CLIENT ? "IN" : "OUT", t)) + .get(packetType, t); + CArray fieldData = (CArray)((ArrayAccess)packetData.get("fields", t)).get(field, t); + int index = (int)((CInt)fieldData.get("field", t)).getInt(); + return index; + } + public Object read(int index) { return packet.getModifier().read(index); } public Mixed readMixed(int index, Target target) { - return Conversions.convertObjectToMixed(read(index), target); + return Conversions.convertObjectToMixed(read(index)); + } + + public Mixed readMixed(String index, Target target) { + return readMixed(indexFromString(index, target), target); } public void write(int index, Object object) { packet.getModifier().write(index, object); } - public void writeMixed(int index, Mixed mixed) { + public void writeMixed(int index, Mixed mixed, Target t) { Field field = packet.getModifier().getField(index); Class type = field.getType(); - write(index, Conversions.adjustObject(Conversions.convertMixedToObject(mixed, type), type)); + write(index, Conversions.adjustObject(Conversions.convertMixedToObject(mixed, type, t), type)); } public void writeMixed(String field, Mixed mixed, Target t) { - CArray packetData = (CArray)((ArrayAccess)((ArrayAccess)PacketUtils.getAllPackets() - .get(protocol.name(), t)) - .get(sender == PacketType.Sender.CLIENT ? "IN" : "OUT", t)) - .get(packetType, t); - CArray fieldData = (CArray)((CArray)packetData.get("fields", t)).get(field, t); - int index = (int)((CInt)fieldData.get("field", t)).getInt(); - writeMixed(index, mixed); + writeMixed(indexFromString(field, t), mixed, t); } public CArray getFields(Target target) { @@ -129,4 +140,48 @@ public PacketKind getKind() { return PacketUtils.getPacketKind(packet.getType()); } + public CArray toCArray() { + CArray array = new CArray(Target.UNKNOWN); + MappingTree tree = PacketJumper.GetMappingTree(); + for(FieldAccessor field : packet.getModifier().getFields()) { + Mixed value; + Object instance = field.get(packet.getHandle()); + String type = field.getField().getType().getSimpleName(); + String name; + Class clazz = packet.getHandle().getClass(); + MappingTree.ClassMapping classMapping + = tree.getClass(clazz.getName().replace(".", "/"), PacketJumper.GetServerNamespace()); + MappingTree.FieldMapping fm; + do { + fm = classMapping.getField(field.getField().getName(), null); + if(fm != null) { + break; + } + clazz = clazz.getSuperclass(); + if(clazz == Object.class || clazz == Record.class) { + break; + } + classMapping = tree.getClass(clazz.getName().replace(".", "/"), + PacketJumper.GetServerNamespace()); + if(classMapping == null) { + throw new CREPluginInternalException("Cannot find packet superclass.", Target.UNKNOWN); + } + } while(true); + name = fm.getDstName(PacketJumper.GetMojangNamespace()); + if(instance == null) { + value = CNull.NULL; + } else if(Conversions.getTypeConversion(field.getField().getType()) == null) { + value = new CString("", Target.UNKNOWN); + } else { + value = Conversions.convertObjectToMixed(instance); + } + CArray descriptor = new CArray(Target.UNKNOWN); + descriptor.set("name", name); + descriptor.set("type", type); + descriptor.set("value", value, Target.UNKNOWN); + array.set(name, descriptor, Target.UNKNOWN); + } + return array; + } + } diff --git a/src/main/java/com/laytonsmith/core/events/AbstractEvent.java b/src/main/java/com/laytonsmith/core/events/AbstractEvent.java index 4326ebe537..d04df086cc 100644 --- a/src/main/java/com/laytonsmith/core/events/AbstractEvent.java +++ b/src/main/java/com/laytonsmith/core/events/AbstractEvent.java @@ -1,334 +1,11 @@ package com.laytonsmith.core.events; -import com.laytonsmith.PureUtilities.ClassLoading.ClassDiscovery; -import com.laytonsmith.PureUtilities.Common.StreamUtils; -import com.laytonsmith.abstraction.MCCommandSender; -import com.laytonsmith.abstraction.MCPlayer; -import com.laytonsmith.abstraction.events.MCPlayerEvent; -import com.laytonsmith.annotations.core; -import com.laytonsmith.annotations.hide; -import com.laytonsmith.core.Documentation; -import com.laytonsmith.core.LogLevel; -import com.laytonsmith.core.MSLog; -import com.laytonsmith.core.MSLog.Tags; -import com.laytonsmith.core.MethodScriptCompiler; -import com.laytonsmith.core.ParseTree; -import com.laytonsmith.core.Static; -import com.laytonsmith.core.constructs.CArray; -import com.laytonsmith.core.constructs.CNull; -import com.laytonsmith.core.constructs.Target; -import com.laytonsmith.core.environments.CommandHelperEnvironment; -import com.laytonsmith.core.environments.Environment; -import com.laytonsmith.core.environments.StaticRuntimeEnv; -import com.laytonsmith.core.events.prefilters.Prefilter; -import com.laytonsmith.core.events.prefilters.PrefilterBuilder; -import com.laytonsmith.core.exceptions.CRE.CREFormatException; -import com.laytonsmith.core.exceptions.CancelCommandException; -import com.laytonsmith.core.exceptions.ConfigRuntimeException; -import com.laytonsmith.core.exceptions.EventException; -import com.laytonsmith.core.exceptions.FunctionReturnException; -import com.laytonsmith.core.exceptions.PrefilterNonMatchException; -import com.laytonsmith.core.exceptions.ProgramFlowManipulationException; -import com.laytonsmith.core.natives.interfaces.Mixed; -import com.laytonsmith.core.profiler.ProfilePoint; - -import java.net.URL; -import java.util.HashMap; -import java.util.Map; - /** - * This helper class implements a few of the common functions in event, and most (all?) Events should extend this class. - * + * This class is deprecated, and events should extend AbstractGenericEvent directly instead. + * @deprecated Use {@link AbstractGenericEvent} instead. */ -public abstract class AbstractEvent implements Event, Comparable { - - private EventMixinInterface mixin; -// protected EventHandlerInterface handler; -// -// protected AbstractEvent(EventHandlerInterface handler){ -// this.handler = handler; -// } -// - - public final void setAbstractEventMixin(EventMixinInterface mixin) { - this.mixin = mixin; - } - - /** - * This function should return true if the event code should be run, based on implementation specific conditions - * for the BindableEvent. - * - * @param e The bindable event itself - * @return {@code true} if the event code should be run - */ - public boolean shouldFire(BindableEvent e) { - return this.mixin.shouldFire(e); - } - - /** - * If the event needs to run special code when a player binds the event, it can be done here. By default, an - * UnsupportedOperationException is thrown, but is caught and ignored. - */ - @Override - public void bind(BoundEvent event) { - - } - - /** - * If the event needs to run special code when a player unbinds the event, it can be done here. By default, an - * UnsupportedOperationException is thrown, but is caught and ignored. - */ - @Override - public void unbind(BoundEvent event) { - - } - - /** - * If the event needs to run special code at server startup, it can be done here. By default, nothing happens. - */ - @Override - public void hook() { - - } - - @Override - public boolean matches(Map prefilter, BindableEvent e) throws PrefilterNonMatchException { - throw new UnsupportedOperationException(); - } - - /** - * This function is run when the actual event occurs. - * - * @param tree The compiled parse tree - * @param b The bound event - * @param env The operating environment - * @param activeEvent The active event being executed - */ - @Override - public final void execute(ParseTree tree, BoundEvent b, Environment env, BoundEvent.ActiveEvent activeEvent) throws ConfigRuntimeException { - preExecution(env, activeEvent); - - // Events can have a player to put into the CommandHelperEnvironment. - if(activeEvent.getUnderlyingEvent() instanceof MCPlayerEvent playerEvent) { - env.getEnv(CommandHelperEnvironment.class).SetPlayer(playerEvent.getPlayer()); - } else { - // Probably not a player event, but might still have a player context. - Mixed c = activeEvent.getParsedEvent().get("player"); - if(c != null) { - if(c instanceof CNull) { - // This is a CNull "player", likely from an entity event, so we need to ensure player() does - // not return a player inherited from the bind's parent environment. - if(env.getEnv(CommandHelperEnvironment.class).GetPlayer() != null) { - env.getEnv(CommandHelperEnvironment.class).SetPlayer(null); - } - } else { - MCCommandSender p = Static.getServer().getPlayer(c.val()); - if(p == null) { - // Check if event (possibly from an extension) injected the player but didn't extend MCPlayerEvent - p = Static.GetInjectedPlayer(c.val()); - } - if(p != null) { - env.getEnv(CommandHelperEnvironment.class).SetPlayer((MCPlayer) p); - } else { - MSLog.GetLogger().w(Tags.GENERAL, c.val() + " offline for " + b.getEventName(), tree.getTarget()); - // Set env CommandSender to prevent incorrect inherited player from being used in a player event. - if(env.getEnv(CommandHelperEnvironment.class).GetPlayer() != null) { - env.getEnv(CommandHelperEnvironment.class).SetPlayer(null); - } - } - } - } - } - - ProfilePoint event = null; - if(env.getEnv(StaticRuntimeEnv.class).GetProfiler() != null) { - event = env.getEnv(StaticRuntimeEnv.class).GetProfiler().start( - "Event " + b.getEventName() + " (defined at " + b.getTarget().toString() + ")", LogLevel.ERROR); - } - try { - try { - MethodScriptCompiler.execute(tree, env, null, null); - } catch (CancelCommandException ex) { - if(ex.getMessage() != null && !ex.getMessage().isEmpty()) { - StreamUtils.GetSystemOut().println(ex.getMessage()); - } - } catch (FunctionReturnException ex) { - //We simply allow this to end the event execution - } catch (ProgramFlowManipulationException ex) { - ConfigRuntimeException.HandleUncaughtException(new CREFormatException("Unexpected control flow operation used.", ex.getTarget()), env); - } - } finally { - if(event != null) { - event.stop(); - } - // Finally, among other things, we need to clean-up injected players and entities - postExecution(env, activeEvent); - } - } - - /** - * This method is called before the event handling code is run, and provides a place for the event code itself to - * modify the environment or active event data. - * - * @param env The environment, at the time just before the event handler is called. - * @param activeEvent The event handler code. - */ - public void preExecution(Environment env, BoundEvent.ActiveEvent activeEvent) { - - } - - /** - * This method is called after the event handling code is run, and provides a place for the event code itself to - * modify or cleanup the environment or active event data. - * - * @param env The environment, at the time just before the event handler is called. - * @param activeEvent The event handler code. - * @throws UnsupportedOperationException If the preExecution isn't supported, this may be thrown, and it will be - * ignored. - */ - public void postExecution(Environment env, BoundEvent.ActiveEvent activeEvent) { - - } - - /** - * For sorting and optimizing events, we need a comparison operation. By default it is compared by looking at the - * event name. - * - * @param o - * @return - */ - @Override - public int compareTo(Event o) { - return this.getName().compareTo(o.getName()); - } - - /** - * Since most events are minecraft events, we return true by default. - * - * @return - */ - @Override - public boolean supportsExternal() { - return true; - } - - /** - * If it is ok to by default do a simple conversion from a CArray to a Map, this method can do it for you. Likely - * this is not acceptable, so hard-coding the conversion will be necessary. - * - * @param manualObject - * @return - */ - public static Object DoConvert(CArray manualObject) { - Map map = new HashMap<>(); - for(String key : manualObject.stringKeySet()) { - map.put(key, manualObject.get(key, Target.UNKNOWN)); - } - return map; - } - - public Map evaluate_helper(BindableEvent e) throws EventException { - return mixin.evaluate_helper(e); - } - - /** - * By default, this function triggers the event by calling the mixin handler. If this is not the desired behavior, - * this method can be overridden in the actual event (if it's an external event, for instance) - * - * @param o - */ - @Override - public void manualTrigger(BindableEvent o) { - mixin.manualTrigger(o); - } - - @Override - public void cancel(BindableEvent o, boolean state) { - mixin.cancel(o, state); - } - - @Override - public boolean isCancellable(BindableEvent o) { - return mixin.isCancellable(o); - } - - @Override - public boolean isCancelled(BindableEvent o) { - return mixin.isCancelled(o); - } - - @Override - public URL getSourceJar() { - return ClassDiscovery.GetClassContainer(this.getClass()); - } - - /** - * Returns true if the event is annotated with @hide - * - * @return - */ - @Override - public final boolean appearInDocumentation() { - return this.getClass().getAnnotation(hide.class) != null; - } - - private static final Class[] EMPTY_CLASS = new Class[0]; - - @Override - public Class[] seeAlso() { - return EMPTY_CLASS; - } - - /** - * Most events should return true for this, but passive events may override this to return null. - * - * @return - */ - @Override - public boolean addCounter() { - return true; - } - - @Override - public final boolean isCore() { - Class c = this.getClass(); - do { - if(c.getAnnotation(core.class) != null) { - return true; - } - c = c.getDeclaringClass(); - } while(c != null); - return false; - } - - private Map> prefilterCache = null; - private volatile boolean isCacheSaturated = false; - - @Override - public final Map> getPrefilters() { - if(!isCacheSaturated) { - synchronized(this) { - if(!isCacheSaturated) { - PrefilterBuilder builder = getPrefilterBuilder(); - if(builder != null) { - prefilterCache = builder.build(); - } - isCacheSaturated = true; - } - } - } - return prefilterCache; - } - - /** - * Returns the prefilter builder for this subclass. This is built by AbstractEvent and cached, so that calls - * to getPrefilters will be faster for future calls. - * @return - */ - // TODO: Once everything has this, this should be re-added, since everything should have its own version. -// @ForceImplementation - protected PrefilterBuilder getPrefilterBuilder() { - return null; - } +@Deprecated +public abstract class AbstractEvent + extends AbstractGenericEvent { } diff --git a/src/main/java/com/laytonsmith/core/events/AbstractGenericEvent.java b/src/main/java/com/laytonsmith/core/events/AbstractGenericEvent.java new file mode 100644 index 0000000000..37c220d0a7 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/events/AbstractGenericEvent.java @@ -0,0 +1,352 @@ +package com.laytonsmith.core.events; + +import com.laytonsmith.PureUtilities.ClassLoading.ClassDiscovery; +import com.laytonsmith.PureUtilities.Common.StreamUtils; +import com.laytonsmith.abstraction.MCCommandSender; +import com.laytonsmith.abstraction.MCPlayer; +import com.laytonsmith.abstraction.events.MCPlayerEvent; +import com.laytonsmith.annotations.core; +import com.laytonsmith.annotations.hide; +import com.laytonsmith.core.Documentation; +import com.laytonsmith.core.LogLevel; +import com.laytonsmith.core.MSLog; +import com.laytonsmith.core.MSLog.Tags; +import com.laytonsmith.core.MethodScriptCompiler; +import com.laytonsmith.core.ParseTree; +import com.laytonsmith.core.Static; +import com.laytonsmith.core.constructs.CArray; +import com.laytonsmith.core.constructs.CNull; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.environments.CommandHelperEnvironment; +import com.laytonsmith.core.environments.Environment; +import com.laytonsmith.core.environments.StaticRuntimeEnv; +import com.laytonsmith.core.events.prefilters.Prefilter; +import com.laytonsmith.core.events.prefilters.PrefilterBuilder; +import com.laytonsmith.core.exceptions.CRE.CREFormatException; +import com.laytonsmith.core.exceptions.CancelCommandException; +import com.laytonsmith.core.exceptions.ConfigCompileException; +import com.laytonsmith.core.exceptions.ConfigCompileGroupException; +import com.laytonsmith.core.exceptions.ConfigRuntimeException; +import com.laytonsmith.core.exceptions.EventException; +import com.laytonsmith.core.exceptions.FunctionReturnException; +import com.laytonsmith.core.exceptions.PrefilterNonMatchException; +import com.laytonsmith.core.exceptions.ProgramFlowManipulationException; +import com.laytonsmith.core.natives.interfaces.Mixed; +import com.laytonsmith.core.profiler.ProfilePoint; + +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +/** + * This helper class implements a few of the common functions in event, and most (all?) Events should extend this class. + * + * @param The underlying event type. + */ +public abstract class AbstractGenericEvent + implements Event, Comparable> { + + private EventMixinInterface mixin; +// protected EventHandlerInterface handler; +// +// protected AbstractEvent(EventHandlerInterface handler){ +// this.handler = handler; +// } +// + + public final void setAbstractEventMixin(EventMixinInterface mixin) { + this.mixin = mixin; + } + + /** + * This function should return true if the event code should be run, based on implementation specific conditions + * for the BindableEvent. + * + * @param e The bindable event itself + * @return {@code true} if the event code should be run + */ + public boolean shouldFire(TBindableEvent e) { + return this.mixin.shouldFire(e); + } + + /** + * If the event needs to run special code when a player binds the event, it can be done here. By default, an + * UnsupportedOperationException is thrown, but is caught and ignored. + */ + @Override + public void bind(BoundEvent event) { + + } + + /** + * If the event needs to run special code when a player unbinds the event, it can be done here. By default, an + * UnsupportedOperationException is thrown, but is caught and ignored. + */ + @Override + public void unbind(BoundEvent event) { + + } + + /** + * If the event needs to run special code at server startup, it can be done here. By default, nothing happens. + */ + @Override + public void hook() { + + } + + @Override + public boolean matches(Map prefilter, TBindableEvent e) throws PrefilterNonMatchException { + throw new UnsupportedOperationException(); + } + + /** + * This function is run when the actual event occurs. + * + * @param tree The compiled parse tree + * @param b The bound event + * @param env The operating environment + * @param activeEvent The active event being executed + */ + @Override + public final void execute(ParseTree tree, BoundEvent b, Environment env, BoundEvent.ActiveEvent activeEvent) throws ConfigRuntimeException { + preExecution(env, activeEvent); + + // Events can have a player to put into the CommandHelperEnvironment. + if(activeEvent.getUnderlyingEvent() instanceof MCPlayerEvent playerEvent) { + env.getEnv(CommandHelperEnvironment.class).SetPlayer(playerEvent.getPlayer()); + } else { + // Probably not a player event, but might still have a player context. + Mixed c = activeEvent.getParsedEvent().get("player"); + if(c != null) { + if(c instanceof CNull) { + // This is a CNull "player", likely from an entity event, so we need to ensure player() does + // not return a player inherited from the bind's parent environment. + if(env.getEnv(CommandHelperEnvironment.class).GetPlayer() != null) { + env.getEnv(CommandHelperEnvironment.class).SetPlayer(null); + } + } else { + MCCommandSender p = Static.getServer().getPlayer(c.val()); + if(p == null) { + // Check if event (possibly from an extension) injected the player but didn't extend MCPlayerEvent + p = Static.GetInjectedPlayer(c.val()); + } + if(p != null) { + env.getEnv(CommandHelperEnvironment.class).SetPlayer((MCPlayer) p); + } else { + MSLog.GetLogger().w(Tags.GENERAL, c.val() + " offline for " + b.getEventName(), tree.getTarget()); + // Set env CommandSender to prevent incorrect inherited player from being used in a player event. + if(env.getEnv(CommandHelperEnvironment.class).GetPlayer() != null) { + env.getEnv(CommandHelperEnvironment.class).SetPlayer(null); + } + } + } + } + } + + ProfilePoint event = null; + if(env.getEnv(StaticRuntimeEnv.class).GetProfiler() != null) { + event = env.getEnv(StaticRuntimeEnv.class).GetProfiler().start( + "Event " + b.getEventName() + " (defined at " + b.getTarget().toString() + ")", LogLevel.ERROR); + } + try { + try { + MethodScriptCompiler.execute(tree, env, null, null); + } catch (CancelCommandException ex) { + if(ex.getMessage() != null && !ex.getMessage().isEmpty()) { + StreamUtils.GetSystemOut().println(ex.getMessage()); + } + } catch (FunctionReturnException ex) { + //We simply allow this to end the event execution + } catch (ProgramFlowManipulationException ex) { + ConfigRuntimeException.HandleUncaughtException(new CREFormatException("Unexpected control flow operation used.", ex.getTarget()), env); + } + } finally { + if(event != null) { + event.stop(); + } + // Finally, among other things, we need to clean-up injected players and entities + postExecution(env, activeEvent); + } + } + + /** + * This method is called before the event handling code is run, and provides a place for the event code itself to + * modify the environment or active event data. + * + * @param env The environment, at the time just before the event handler is called. + * @param activeEvent The event handler code. + */ + public void preExecution(Environment env, BoundEvent.ActiveEvent activeEvent) { + + } + + /** + * This method is called after the event handling code is run, and provides a place for the event code itself to + * modify or cleanup the environment or active event data. + * + * @param env The environment, at the time just before the event handler is called. + * @param activeEvent The event handler code. + * @throws UnsupportedOperationException If the preExecution isn't supported, this may be thrown, and it will be + * ignored. + */ + public void postExecution(Environment env, BoundEvent.ActiveEvent activeEvent) { + + } + + /** + * For sorting and optimizing events, we need a comparison operation. By default it is compared by looking at the + * event name. + * + * @param o + * @return + */ + @Override + public int compareTo(Event o) { + return this.getName().compareTo(o.getName()); + } + + /** + * Since most events are minecraft events, we return true by default. + * + * @return + */ + @Override + public boolean supportsExternal() { + return true; + } + + /** + * If it is ok to by default do a simple conversion from a CArray to a Map, this method can do it for you. Likely + * this is not acceptable, so hard-coding the conversion will be necessary. + * + * @param manualObject + * @return + */ + public static Object DoConvert(CArray manualObject) { + Map map = new HashMap<>(); + for(String key : manualObject.stringKeySet()) { + map.put(key, manualObject.get(key, Target.UNKNOWN)); + } + return map; + } + + public Map evaluate_helper(TBindableEvent e) throws EventException { + return mixin.evaluate_helper(e); + } + + /** + * By default, this function triggers the event by calling the mixin handler. If this is not the desired behavior, + * this method can be overridden in the actual event (if it's an external event, for instance) + * + * @param o + */ + @Override + public void manualTrigger(TBindableEvent o) { + mixin.manualTrigger(o); + } + + @Override + public void cancel(TBindableEvent o, boolean state) { + mixin.cancel(o, state); + } + + @Override + public boolean isCancellable(TBindableEvent o) { + return mixin.isCancellable(o); + } + + @Override + public boolean isCancelled(TBindableEvent o) { + return mixin.isCancelled(o); + } + + @Override + public URL getSourceJar() { + return ClassDiscovery.GetClassContainer(this.getClass()); + } + + /** + * Returns true if the event is annotated with @hide + * + * @return + */ + @Override + public final boolean appearInDocumentation() { + return this.getClass().getAnnotation(hide.class) != null; + } + + private static final Class[] EMPTY_CLASS = new Class[0]; + + @Override + public Class[] seeAlso() { + return EMPTY_CLASS; + } + + /** + * Most events should return true for this, but passive events may override this to return null. + * + * @return + */ + @Override + public boolean addCounter() { + return true; + } + + @Override + public final boolean isCore() { + Class c = this.getClass(); + do { + if(c.getAnnotation(core.class) != null) { + return true; + } + c = c.getDeclaringClass(); + } while(c != null); + return false; + } + + private Map> prefilterCache = null; + private volatile boolean isCacheSaturated = false; + + @Override + public final Map> getPrefilters() { + if(!isCacheSaturated) { + synchronized(this) { + if(!isCacheSaturated) { + PrefilterBuilder builder = getPrefilterBuilder(); + if(builder != null) { + prefilterCache = builder.build(); + } + isCacheSaturated = true; + } + } + } + return prefilterCache; + } + + /** + * Returns the prefilter builder for this subclass. This is built by AbstractEvent and cached, so that calls + * to getPrefilters will be faster for future calls. Subclasses should implement this in order to provide + * declarative prefilter support, which is more efficient and provides more features to end users. + * @return + */ + // TODO: Once everything has this, this should be re-added, since everything should have its own version. +// @ForceImplementation + protected PrefilterBuilder getPrefilterBuilder() { + return null; + } + + /** + * By default, we do nothing. + * @param prefilters + * @param env + */ + @Override + public void validatePrefilters(Map, ParseTree> prefilters, Environment env) + throws ConfigCompileException, ConfigCompileGroupException { + // + } + + + +} diff --git a/src/main/java/com/laytonsmith/core/events/Driver.java b/src/main/java/com/laytonsmith/core/events/Driver.java index bd9f5d63dd..d5552ca400 100644 --- a/src/main/java/com/laytonsmith/core/events/Driver.java +++ b/src/main/java/com/laytonsmith/core/events/Driver.java @@ -134,6 +134,11 @@ public enum Driver { */ CMDLINE_PROMPT_INPUT, SHUTDOWN, + /** + * Packet events + */ + PACKET_RECEIVED, + PACKET_SENT, /** * Extension events, used by events fired from the extension system. */ diff --git a/src/main/java/com/laytonsmith/core/events/Event.java b/src/main/java/com/laytonsmith/core/events/Event.java index 0e75baead9..ba363f0628 100644 --- a/src/main/java/com/laytonsmith/core/events/Event.java +++ b/src/main/java/com/laytonsmith/core/events/Event.java @@ -8,6 +8,8 @@ import com.laytonsmith.core.environments.Environment; import com.laytonsmith.core.events.BoundEvent.ActiveEvent; import com.laytonsmith.core.events.prefilters.Prefilter; +import com.laytonsmith.core.exceptions.ConfigCompileException; +import com.laytonsmith.core.exceptions.ConfigCompileGroupException; import com.laytonsmith.core.exceptions.EventException; import com.laytonsmith.core.exceptions.PrefilterNonMatchException; import com.laytonsmith.core.natives.interfaces.Mixed; @@ -16,8 +18,9 @@ /** * This interface should be implemented to allow the bind() function to bind to a particular event type. To be * recognized as an event type, it should also tag itself with @api, and it will be included in the EventList. + * @param The underlying event type. */ -public interface Event extends Comparable, Documentation { +public interface Event extends Comparable>, Documentation { /** * This should return the name of the event. @@ -59,7 +62,7 @@ public interface Event extends Comparable, Documentation { * Error. */ @Deprecated - public boolean matches(Map prefilter, BindableEvent e) throws PrefilterNonMatchException; + public boolean matches(Map prefilter, TBindableEvent e) throws PrefilterNonMatchException; /** * If an event is manually triggered, then it may be required for an event object to be faked, so the rest of the @@ -69,7 +72,7 @@ public interface Event extends Comparable, Documentation { * @param t * @return */ - public BindableEvent convert(CArray manualObject, Target t); + public TBindableEvent convert(CArray manualObject, Target t); /** * This function is called when an event is triggered. It passes the event, and expects back a Map, which will be @@ -80,7 +83,7 @@ public interface Event extends Comparable, Documentation { * @return The map build from the event * @throws com.laytonsmith.core.exceptions.EventException If some exception occurs during map building */ - public Map evaluate(BindableEvent e) throws EventException; + public Map evaluate(TBindableEvent e) throws EventException; /** * This is called to determine if an event is cancellable in the first place @@ -88,7 +91,7 @@ public interface Event extends Comparable, Documentation { * @param e * @return */ - public boolean isCancellable(BindableEvent e); + public boolean isCancellable(TBindableEvent e); /** * This is called if the script attempts to cancel the event, so the underlying event can also be cancelled. If the @@ -99,7 +102,7 @@ public interface Event extends Comparable, Documentation { * @param state True, if the event should be cancelled, false if it should be uncancelled. * @throws com.laytonsmith.core.exceptions.EventException If the event isn't cancellable */ - public void cancel(BindableEvent e, boolean state) throws EventException; + public void cancel(TBindableEvent e, boolean state) throws EventException; /** * This function returns the "driver" class of the event needed to trigger it. Though not strictly needed, this @@ -156,7 +159,7 @@ public interface Event extends Comparable, Documentation { * * @param e */ - public void manualTrigger(BindableEvent e); + public void manualTrigger(TBindableEvent e); /** * If the event is an external event, and there is no reason to attempt a serverwide manual triggering, this @@ -176,7 +179,7 @@ public interface Event extends Comparable, Documentation { * @param event * @return */ - public boolean modifyEvent(String key, Mixed value, BindableEvent event); + public boolean modifyEvent(String key, Mixed value, TBindableEvent event); /** * Returns if this event is cancelled. If the event is not cancellable, false should be returned, though this case @@ -185,7 +188,7 @@ public interface Event extends Comparable, Documentation { * @param underlyingEvent * @return */ - public boolean isCancelled(BindableEvent underlyingEvent); + public boolean isCancelled(TBindableEvent underlyingEvent); /** * Some events don't need to show up in documentation. Maybe they are experimental, or magic functions. If they @@ -229,6 +232,18 @@ public interface Event extends Comparable, Documentation { * * @return */ - public Map> getPrefilters(); + public Map> getPrefilters(); + + /** + * Called after prefilters are individually validated, and can be used when there is additional validation + * that can be done across multiple prefilters, i.e. when the value of one prefilter is as such, then + * another prefilter may have different behavior. + * @param prefilters The full set of prefilters. + * @param env The environment. + * @throws com.laytonsmith.core.exceptions.ConfigCompileException + * @throws com.laytonsmith.core.exceptions.ConfigCompileGroupException + */ + public void validatePrefilters(Map, ParseTree> prefilters, Environment env) + throws ConfigCompileException, ConfigCompileGroupException; } diff --git a/src/main/java/com/laytonsmith/core/events/EventUtils.java b/src/main/java/com/laytonsmith/core/events/EventUtils.java index eb2c594b65..d33445008d 100644 --- a/src/main/java/com/laytonsmith/core/events/EventUtils.java +++ b/src/main/java/com/laytonsmith/core/events/EventUtils.java @@ -23,8 +23,11 @@ import com.laytonsmith.core.natives.interfaces.Mixed; import java.io.File; +import java.util.ArrayList; import java.util.EnumMap; import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; @@ -199,15 +202,25 @@ public static boolean PrefilterMatches(BoundEvent b, BindableEvent e, Event driv return false; } } else { - for(Map.Entry prefilter : userPrefilters.entrySet()) { - if(!prefilters.containsKey(prefilter.getKey())) { + // Sort our prefilters based on prefilter priority + List>> list = new ArrayList<>(prefilters.entrySet()); + list.sort(Map.Entry.comparingByValue()); + Map> sortedPrefilters = new LinkedHashMap<>(userPrefilters.size()); + for(Map.Entry> entry : list) { + sortedPrefilters.put(entry.getKey(), entry.getValue()); + } + + + for(Map.Entry> prefilter : sortedPrefilters.entrySet()) { + String key = prefilter.getKey(); + if(!userPrefilters.containsKey(key)) { // The compiler should have already warned about this continue; } - Prefilter pf = prefilters.get(prefilter.getKey()); + Prefilter pf = prefilter.getValue(); PrefilterMatcher matcher = pf.getMatcher(); - Mixed value = prefilter.getValue(); - if(!matcher.matches(prefilter.getKey(), value, e, b.getTarget())) { + Mixed value = userPrefilters.get(key); + if(!matcher.matches(key, value, e, b.getTarget())) { return false; } } @@ -323,7 +336,7 @@ public static void TriggerListener(Driver type, String eventName, BindableEvent if(driver == null) { throw ConfigRuntimeException.CreateUncatchableException("Tried to fire an unknown event: " + eventName, Target.UNKNOWN); } - if(!(driver instanceof AbstractEvent) || ((AbstractEvent) driver).shouldFire(e)) { + if(!(driver instanceof AbstractGenericEvent) || ((AbstractGenericEvent) driver).shouldFire(e)) { FireListeners(GetMatchingEvents(bounded, eventName, e, driver), driver, e); } } diff --git a/src/main/java/com/laytonsmith/core/events/drivers/PacketEvents.java b/src/main/java/com/laytonsmith/core/events/drivers/PacketEvents.java new file mode 100644 index 0000000000..d7499d525a --- /dev/null +++ b/src/main/java/com/laytonsmith/core/events/drivers/PacketEvents.java @@ -0,0 +1,336 @@ +package com.laytonsmith.core.events.drivers; + +import com.comphenix.protocol.PacketType; +import com.laytonsmith.PureUtilities.Common.ReflectionUtils; +import com.laytonsmith.PureUtilities.Version; +import com.laytonsmith.annotations.api; +import com.laytonsmith.core.MSVersion; +import com.laytonsmith.core.ParseTree; +import com.laytonsmith.core.compiler.CompilerEnvironment; +import com.laytonsmith.core.compiler.CompilerWarning; +import com.laytonsmith.core.constructs.CArray; +import com.laytonsmith.core.constructs.CFunction; +import com.laytonsmith.core.constructs.CLabel; +import com.laytonsmith.core.constructs.CString; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.environments.Environment; +import com.laytonsmith.core.events.AbstractGenericEvent; +import com.laytonsmith.core.events.Driver; +import com.laytonsmith.core.events.prefilters.CustomPrefilterMatcher; +import com.laytonsmith.core.events.prefilters.EnumPrefilterMatcher; +import com.laytonsmith.core.events.prefilters.PlayerPrefilterMatcher; +import com.laytonsmith.core.events.prefilters.Prefilter; +import com.laytonsmith.core.events.prefilters.PrefilterBuilder; +import com.laytonsmith.core.events.prefilters.StringPrefilterMatcher; +import com.laytonsmith.core.exceptions.CRE.CREIllegalArgumentException; +import com.laytonsmith.core.exceptions.CRE.CREPluginInternalException; +import com.laytonsmith.core.exceptions.CRE.CREUnsupportedOperationException; +import com.laytonsmith.core.exceptions.ConfigCompileException; +import com.laytonsmith.core.exceptions.ConfigCompileGroupException; +import com.laytonsmith.core.exceptions.EventException; +import com.laytonsmith.core.functions.DataHandling; +import com.laytonsmith.core.natives.interfaces.ArrayAccess; +import com.laytonsmith.core.natives.interfaces.Mixed; +import com.laytonsmith.core.packetjumper.Comparisons; +import com.laytonsmith.core.packetjumper.Conversions; +import com.laytonsmith.core.packetjumper.PacketDirection; +import com.laytonsmith.core.packetjumper.PacketJumper; +import com.laytonsmith.core.packetjumper.PacketUtils; +import com.laytonsmith.core.packetjumper.ProtocolLibPacketEvent; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import net.fabricmc.mappingio.tree.MappingTree; + +/** + * + */ +public class PacketEvents { + + private PacketEvents() { + } + + public static String docs() { + return "Contains events related to packet management. PacketJumper must be enabled for these events to fire."; + } + + private static PrefilterBuilder GetPacketPrefilter() { + return new PrefilterBuilder() + .set("protocol", "The protocol of the packet.", new StringPrefilterMatcher() { + @Override + protected String getProperty(ProtocolLibPacketEvent event) { + return event.getPacketEvent().getPacketType().getProtocol().name(); + } + }) + .set("type", "The packet type.", new StringPrefilterMatcher() { + @Override + protected String getProperty(ProtocolLibPacketEvent event) { + return event.getPacketEvent().getPacketType().name(); + } + }) + .set("direction", "The packet direction.", new EnumPrefilterMatcher(PacketDirection.class) { + @Override + protected Enum getEnum(ProtocolLibPacketEvent event) { + return event.getPacketEvent().getPacketType().getSender() == PacketType.Sender.CLIENT + ? PacketDirection.IN : PacketDirection.OUT; + } + }) + .set("player", "The player that the packet is being sent to/from", new PlayerPrefilterMatcher()) + .set("values", "An associative array of value matches." + + " The key should be the field name to match, and the value should be the desired match." + + " This only supports exact matches.", new CustomPrefilterMatcher() { + @Override + public boolean matches(String key, Mixed value, ProtocolLibPacketEvent event, Target t) { + if(!(value instanceof CArray)) { + throw new CREIllegalArgumentException("\"values\" prefilter must be an array.", t); + } + Object packet = event.getInternalPacket().getHandle(); + MappingTree tree = PacketJumper.GetMappingTree(); + MappingTree.ClassMapping classMapping + = tree.getClass(event.getPacketEvent().getPacketType().getPacketClass() + .getName().replace(".", "/"), PacketJumper.GetServerNamespace()); + if(classMapping == null) { + throw new CREPluginInternalException("Cannot find packet class.", t); + } + for(Mixed k : ((ArrayAccess) value).keySet()) { + Class clazz = event.getPacketEvent().getPacketType().getPacketClass(); + MappingTree.FieldMapping fm = null; + do { + fm = classMapping.getField(k.val(), null, PacketJumper.GetMojangNamespace()); + if(fm != null) { + break; + } + clazz = clazz.getSuperclass(); + if(clazz == Object.class || clazz == Record.class) { + break; + } + classMapping = tree.getClass(clazz.getName().replace(".", "/"), + PacketJumper.GetServerNamespace()); + if(classMapping == null) { + throw new CREPluginInternalException("Cannot find packet superclass.", t); + } + } while(true); + if(fm == null) { + throw new CREIllegalArgumentException("Invalid property \"" + k.val() + "\"", t); + } + String tK = fm.getSrcName(); + Mixed v = ((ArrayAccess) value).get(k, t); + Object oV = Conversions.convertMixedToObject(v, clazz, t); + Object packetPropertyValue = ReflectionUtils.get(clazz, packet, tK); + if(packetPropertyValue == null) { + if(oV != null) { + return false; + } else { + continue; + } + } + if(!Comparisons.IsEqual(oV, packetPropertyValue)) { + return false; + } + } + return true; + } + + @Override + public int getPriority() { + return 100; + } + + }); + } + + private static void DoCrossPrefilterValidation(Map, ParseTree> prefilters, + Environment env) throws ConfigCompileException, ConfigCompileGroupException { + List prefilterTypes = new ArrayList<>(); + Prefilter valuesPrefilter = null; + String protocol = null; + String type = null; + for(Prefilter p : prefilters.keySet()) { + prefilterTypes.add(p.getPrefilterName()); + ParseTree parseTree = prefilters.get(p); + switch(p.getPrefilterName()) { + case "values": + valuesPrefilter = p; + break; + case "protocol": + if(parseTree.isConst()) { + protocol = parseTree.getData().val(); + } + break; + case "type": + if(parseTree.isConst()) { + type = parseTree.getData().val(); + } + break; + default: + break; + } + } + + if(prefilterTypes.contains("values") && (!prefilterTypes.contains("protocol") || !prefilterTypes.contains("type"))) { + ParseTree valuesParseTree = prefilters.get(valuesPrefilter); + env.getEnv(CompilerEnvironment.class).addCompilerWarning(valuesParseTree.getFileOptions(), + new CompilerWarning("\"values\" prefilter cannot be made to work reliably without" + + " specifying protocol and type values, as this will match multiple packet" + + " types, each with different values, so this cannot be made to work generically," + + " unless you happen to only be registering for a single packet type. Add a 'protocol'" + + " and 'type' prefilter also to get rid of this warning.", + valuesParseTree.getTarget(), null)); + return; + } + + if(prefilterTypes.contains("values") && protocol != null && type != null) { + ParseTree node = prefilters.get(valuesPrefilter); + if(node.getData() instanceof CFunction f) { + if(f.getFunction() instanceof DataHandling.array || f.getFunction() instanceof DataHandling.associative_array) { + Set labels = new HashSet<>(); + for(int i = 0; i < node.numberOfChildren(); i++) { + ParseTree child = node.getChildAt(i); + if(child.getData() instanceof CFunction centryFunction + && centryFunction.getFunction() instanceof com.laytonsmith.core.functions.Compiler.centry) { + labels.add(((CLabel) child.getChildAt(0).getData()).cVal().val()); + } + } + PacketType packetType = PacketUtils.findPacketTypeByCommonName(protocol, type, node.getTarget()); + Set errors = new HashSet<>(); + propertyLoop: + for(String property : labels) { + Class clazz = packetType.getPacketClass(); + MappingTree.FieldMapping fm = null; + MappingTree tree = PacketJumper.GetMappingTree(); + MappingTree.ClassMapping classMapping + = tree.getClass(clazz + .getName().replace(".", "/"), PacketJumper.GetServerNamespace()); + if(classMapping == null) { + throw new ConfigCompileException("Cannot find packet class.", node.getTarget()); + } + do { + fm = classMapping.getField(property, null, PacketJumper.GetMojangNamespace()); + if(fm != null) { + break; + } + clazz = clazz.getSuperclass(); + if(clazz == Object.class || clazz == Record.class) { + break; + } + classMapping = tree.getClass(clazz.getName().replace(".", "/"), + PacketJumper.GetServerNamespace()); + if(classMapping == null) { + errors.add(new ConfigCompileException("Cannot find packet superclass.", node.getTarget())); + continue propertyLoop; + } + } while(true); + if(fm == null) { + errors.add(new ConfigCompileException("Invalid property \"" + property + "\"", node.getTarget())); + } + } + if(!errors.isEmpty()) { + throw new ConfigCompileGroupException(errors); + } + } + } + } + + } + + public static abstract class PacketEvent extends AbstractGenericEvent { + @Override + public ProtocolLibPacketEvent convert(CArray manualObject, Target t) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Map evaluate(ProtocolLibPacketEvent e) throws EventException { + Map map = evaluate_helper(e); + map.put("macrotype", new CString("packet", Target.UNKNOWN)); + map.put("player", new CString(e.getPlayer().getName(), Target.UNKNOWN)); + map.put("packet", e.getPacket(Target.UNKNOWN)); + map.put("protocol", new CString(e.getPacketEvent().getPacketType().getProtocol().name(), Target.UNKNOWN)); + map.put("type", new CString(e.getPacketEvent().getPacketType().name(), Target.UNKNOWN)); + map.put("direction", new CString( + PacketDirection.FromSender(e.getPacketEvent().getPacketType().getSender()).name(), Target.UNKNOWN)); + return map; + } + + @Override + public boolean modifyEvent(String key, Mixed value, ProtocolLibPacketEvent event) { + throw new CREUnsupportedOperationException("Cannot modify the packet event, use packet_write to modify" + + " individual parameters.", value.getTarget()); + } + + @Override + public Version since() { + return MSVersion.V3_3_5; + } + + @Override + protected PrefilterBuilder getPrefilterBuilder() { + return GetPacketPrefilter(); + } + + @Override + public void validatePrefilters(Map, ParseTree> prefilters, Environment env) + throws ConfigCompileException, ConfigCompileGroupException { + DoCrossPrefilterValidation(prefilters, env); + } + + @Override + public void cancel(ProtocolLibPacketEvent o, boolean state) { + o.getPacketEvent().setCancelled(state); + } + } + + @api + public static class packet_received extends PacketEvent { + + @Override + public String getName() { + return "packet_received"; + } + + @Override + public String docs() { + return "{}" + + " Fires when a packet is received from a client." + + " Cancelling the event will cause the packet to not be processed by the server." + + " {player: the player the packet was received from | type: the packet type" + + " | fields: an array of fields that map to values which should be handled. Missing fields" + + " match any value.}" + + " {}" + + " {}"; + } + + @Override + public Driver driver() { + return Driver.PACKET_RECEIVED; + } + } + + @api + public static class packet_sent extends PacketEvent { + + @Override + public String getName() { + return "packet_sent"; + } + + @Override + public String docs() { + return "{}" + + " Fires when a packet is sent from the server." + + " Cancelling the event will cause the packet to not be sent to the client." + + " {player: the player the packet was received from | type: the packet type" + + " | fields: an array of fields that map to values which should be handled. Missing fields" + + " match any value.}" + + " {}" + + " {}"; + } + + @Override + public Driver driver() { + return Driver.PACKET_SENT; + } + } +} diff --git a/src/main/java/com/laytonsmith/core/events/prefilters/AbstractPrefilterMatcher.java b/src/main/java/com/laytonsmith/core/events/prefilters/AbstractPrefilterMatcher.java index f48d1a474d..c1381fa68a 100644 --- a/src/main/java/com/laytonsmith/core/events/prefilters/AbstractPrefilterMatcher.java +++ b/src/main/java/com/laytonsmith/core/events/prefilters/AbstractPrefilterMatcher.java @@ -58,4 +58,9 @@ public CClassType typecheck(StaticAnalysis analysis, } return prefilterValueType; } + + @Override + public int getPriority() { + return 0; + } } diff --git a/src/main/java/com/laytonsmith/core/events/prefilters/BooleanPrefilterMatcher.java b/src/main/java/com/laytonsmith/core/events/prefilters/BooleanPrefilterMatcher.java index c1d026f8da..41c6c2576f 100644 --- a/src/main/java/com/laytonsmith/core/events/prefilters/BooleanPrefilterMatcher.java +++ b/src/main/java/com/laytonsmith/core/events/prefilters/BooleanPrefilterMatcher.java @@ -70,4 +70,10 @@ public boolean matches(String key, Mixed value, T event, Target t) { } protected abstract boolean getProperty(T event); + + @Override + public int getPriority() { + return -1; + } + } diff --git a/src/main/java/com/laytonsmith/core/events/prefilters/ExpressionPrefilterMatcher.java b/src/main/java/com/laytonsmith/core/events/prefilters/ExpressionPrefilterMatcher.java index 3f3df246f0..516865fc99 100644 --- a/src/main/java/com/laytonsmith/core/events/prefilters/ExpressionPrefilterMatcher.java +++ b/src/main/java/com/laytonsmith/core/events/prefilters/ExpressionPrefilterMatcher.java @@ -106,4 +106,9 @@ public boolean matches(String key, Mixed value, T event, Target t) { protected abstract double getProperty(T event); + @Override + public int getPriority() { + return 1; + } + } diff --git a/src/main/java/com/laytonsmith/core/events/prefilters/ItemStackPrefilterMatcher.java b/src/main/java/com/laytonsmith/core/events/prefilters/ItemStackPrefilterMatcher.java index 44393bfa89..f267d4ccf3 100644 --- a/src/main/java/com/laytonsmith/core/events/prefilters/ItemStackPrefilterMatcher.java +++ b/src/main/java/com/laytonsmith/core/events/prefilters/ItemStackPrefilterMatcher.java @@ -26,4 +26,9 @@ public PrefilterDocs getDocsObject() { protected abstract MCItemStack getItemStack(T event); + + @Override + public int getPriority() { + return -1; + } } diff --git a/src/main/java/com/laytonsmith/core/events/prefilters/LocationPrefilterMatcher.java b/src/main/java/com/laytonsmith/core/events/prefilters/LocationPrefilterMatcher.java index f8f9d18f20..7be4e9b28e 100644 --- a/src/main/java/com/laytonsmith/core/events/prefilters/LocationPrefilterMatcher.java +++ b/src/main/java/com/laytonsmith/core/events/prefilters/LocationPrefilterMatcher.java @@ -143,4 +143,9 @@ protected double getDefaultTolerance() { return 1.0; } + @Override + public int getPriority() { + return 1; + } + } diff --git a/src/main/java/com/laytonsmith/core/events/prefilters/MacroICPrefilterMatcher.java b/src/main/java/com/laytonsmith/core/events/prefilters/MacroICPrefilterMatcher.java index 091293ac2b..a3365a5244 100644 --- a/src/main/java/com/laytonsmith/core/events/prefilters/MacroICPrefilterMatcher.java +++ b/src/main/java/com/laytonsmith/core/events/prefilters/MacroICPrefilterMatcher.java @@ -92,4 +92,9 @@ public boolean matches(String key, Mixed value, T event, Target t) { */ protected abstract Object getProperty(T event); + @Override + public int getPriority() { + return 1; + } + } diff --git a/src/main/java/com/laytonsmith/core/events/prefilters/MacroPrefilterMatcher.java b/src/main/java/com/laytonsmith/core/events/prefilters/MacroPrefilterMatcher.java index 8ef0639090..47d7439bb8 100644 --- a/src/main/java/com/laytonsmith/core/events/prefilters/MacroPrefilterMatcher.java +++ b/src/main/java/com/laytonsmith/core/events/prefilters/MacroPrefilterMatcher.java @@ -98,4 +98,9 @@ public boolean matches(String key, Mixed value, T event, Target t) { */ protected abstract Object getProperty(T event); + @Override + public int getPriority() { + return -1; + } + } diff --git a/src/main/java/com/laytonsmith/core/events/prefilters/MaterialPrefilterMatcher.java b/src/main/java/com/laytonsmith/core/events/prefilters/MaterialPrefilterMatcher.java index 1d2b37abdb..a366c03480 100644 --- a/src/main/java/com/laytonsmith/core/events/prefilters/MaterialPrefilterMatcher.java +++ b/src/main/java/com/laytonsmith/core/events/prefilters/MaterialPrefilterMatcher.java @@ -85,4 +85,9 @@ protected String getProperty(T event) { protected abstract MCMaterial getMaterial(T event); + @Override + public int getPriority() { + return -1; + } + } diff --git a/src/main/java/com/laytonsmith/core/events/prefilters/MathPrefilterMatcher.java b/src/main/java/com/laytonsmith/core/events/prefilters/MathPrefilterMatcher.java index 5d57f954cb..b7b12436f8 100644 --- a/src/main/java/com/laytonsmith/core/events/prefilters/MathPrefilterMatcher.java +++ b/src/main/java/com/laytonsmith/core/events/prefilters/MathPrefilterMatcher.java @@ -143,4 +143,9 @@ public boolean matches(String key, Mixed value, T event, Target t) { protected abstract double getTolerance(); + @Override + public int getPriority() { + return -1; + } + } diff --git a/src/main/java/com/laytonsmith/core/events/prefilters/Prefilter.java b/src/main/java/com/laytonsmith/core/events/prefilters/Prefilter.java index 1746d462b6..2490aa2a37 100644 --- a/src/main/java/com/laytonsmith/core/events/prefilters/Prefilter.java +++ b/src/main/java/com/laytonsmith/core/events/prefilters/Prefilter.java @@ -12,13 +12,14 @@ * @param */ @StandardField -public class Prefilter { +public class Prefilter implements Comparable>{ private final String prefilterName; private final String docs; private final PrefilterMatcher matcher; private final Set status; + private final int priority; - public Prefilter(String prefilterName, String docs, PrefilterMatcher matcher, Set status) { + public Prefilter(String prefilterName, String docs, PrefilterMatcher matcher, Set status, int priority) { Objects.requireNonNull(prefilterName); Objects.requireNonNull(docs); Objects.requireNonNull(matcher); @@ -26,6 +27,7 @@ public Prefilter(String prefilterName, String docs, PrefilterMatcher matcher, this.docs = docs; this.matcher = matcher; this.status = status == null ? EnumSet.noneOf(PrefilterStatus.class) : status; + this.priority = priority; } public String getDocs() { @@ -60,4 +62,21 @@ public String toString() { return ObjectHelpers.DoToString(this); } + /** + * The priority is the order in which prefilters should be matched against. Some prefilters are more expensive + * than others, and so should not be run if a cheaper prefilter doesn't match. Prefilter types can define + * their own default priority, but if they don't, the default is 0, where prefilters with a lower value (i.e. + * negative) are run after prefilters that have a higher priority. For prefilters with + * the same priority, the ordering is deterministic but undefined. + * @return + */ + public int getPriority() { + return priority; + } + + @Override + public int compareTo(Prefilter o) { + return Integer.compare(this.priority, o.priority); + } + } diff --git a/src/main/java/com/laytonsmith/core/events/prefilters/PrefilterBuilder.java b/src/main/java/com/laytonsmith/core/events/prefilters/PrefilterBuilder.java index f57b76837d..2292d8c534 100644 --- a/src/main/java/com/laytonsmith/core/events/prefilters/PrefilterBuilder.java +++ b/src/main/java/com/laytonsmith/core/events/prefilters/PrefilterBuilder.java @@ -35,6 +35,20 @@ public PrefilterBuilder set(String prefilterName, String docs, PrefilterMatch return set(prefilterName, docs, matcher, null); } + /** + * Adds another prefilter to the PrefilterBuilder. + * + * @param prefilterName The name of the prefilter. + * @param matcher The matcher object. This should override a single method, the one that corresponds to the + * specified type of this prefilter. + * @param docs The documentation for this prefilter. + * @param priority Sets the match ordering priority. + * @return {@code this} for easy chaining. + */ + public PrefilterBuilder set(String prefilterName, String docs, PrefilterMatcher matcher, int priority) { + return set(prefilterName, docs, matcher, null, priority); + } + /** * Adds another prefilter to the PrefilterBuilder. * @@ -47,13 +61,34 @@ public PrefilterBuilder set(String prefilterName, String docs, PrefilterMatch */ public PrefilterBuilder set(String prefilterName, String docs, PrefilterMatcher matcher, Set status) { if(builder == null) { - builder = MapBuilder.start(prefilterName, new Prefilter<>(prefilterName, docs, matcher, status)); + builder = MapBuilder.start(prefilterName, new Prefilter<>(prefilterName, docs, matcher, status, matcher.getPriority())); + } else { + builder.set(prefilterName, new Prefilter<>(prefilterName, docs, matcher, status, matcher.getPriority())); + } + return this; + } + + /** + * Adds another prefilter to the PrefilterBuilder. + * + * @param prefilterName The name of the prefilter. + * @param matcher The matcher object. This should override a single method, the one that corresponds to the + * specified type of this prefilter. + * @param docs The documentation for this prefilter. + * @param status Status flags that apply to this prefilter. + * @param priority Sets the match ordering priority. + * @return {@code this} for easy chaining. + */ + public PrefilterBuilder set(String prefilterName, String docs, PrefilterMatcher matcher, Set status, int priority) { + if(builder == null) { + builder = MapBuilder.start(prefilterName, new Prefilter<>(prefilterName, docs, matcher, status, priority)); } else { - builder.set(prefilterName, new Prefilter<>(prefilterName, docs, matcher, status)); + builder.set(prefilterName, new Prefilter<>(prefilterName, docs, matcher, status, priority)); } return this; } + /** * Builds a Map object based on the configured parameters. * diff --git a/src/main/java/com/laytonsmith/core/events/prefilters/PrefilterMatcher.java b/src/main/java/com/laytonsmith/core/events/prefilters/PrefilterMatcher.java index 7a9b52e881..b7745323de 100644 --- a/src/main/java/com/laytonsmith/core/events/prefilters/PrefilterMatcher.java +++ b/src/main/java/com/laytonsmith/core/events/prefilters/PrefilterMatcher.java @@ -94,4 +94,10 @@ CClassType typecheck(StaticAnalysis analysis, void validate(ParseTree node, CClassType nodeType, Environment env) throws ConfigCompileException, ConfigCompileGroupException, ConfigRuntimeException; + /** + * Returns the default priority for this matcher. This can be overridden on a per event basis regardless. + * @return + */ + int getPriority(); + } diff --git a/src/main/java/com/laytonsmith/core/events/prefilters/RegexPrefilterMatcher.java b/src/main/java/com/laytonsmith/core/events/prefilters/RegexPrefilterMatcher.java index 8ca6b45dc0..1b970cec97 100644 --- a/src/main/java/com/laytonsmith/core/events/prefilters/RegexPrefilterMatcher.java +++ b/src/main/java/com/laytonsmith/core/events/prefilters/RegexPrefilterMatcher.java @@ -77,4 +77,9 @@ public boolean matches(String key, Mixed value, T event, Target t) { protected abstract String getProperty(T event); + @Override + public int getPriority() { + return 0; + } + } diff --git a/src/main/java/com/laytonsmith/core/events/prefilters/StringICPrefilterMatcher.java b/src/main/java/com/laytonsmith/core/events/prefilters/StringICPrefilterMatcher.java index fd0dea811d..5f3181f1ac 100644 --- a/src/main/java/com/laytonsmith/core/events/prefilters/StringICPrefilterMatcher.java +++ b/src/main/java/com/laytonsmith/core/events/prefilters/StringICPrefilterMatcher.java @@ -73,4 +73,9 @@ public boolean matches(String key, Mixed value, T event, Target t) { protected abstract String getProperty(T event); + @Override + public int getPriority() { + return -1; + } + } diff --git a/src/main/java/com/laytonsmith/core/events/prefilters/StringPrefilterMatcher.java b/src/main/java/com/laytonsmith/core/events/prefilters/StringPrefilterMatcher.java index 960189173d..487935482b 100644 --- a/src/main/java/com/laytonsmith/core/events/prefilters/StringPrefilterMatcher.java +++ b/src/main/java/com/laytonsmith/core/events/prefilters/StringPrefilterMatcher.java @@ -73,4 +73,9 @@ public boolean matches(String key, Mixed value, T event, Target t) { protected abstract String getProperty(T event); + @Override + public int getPriority() { + return -1; + } + } diff --git a/src/main/java/com/laytonsmith/core/events/prefilters/WorldPrefilterMatcher.java b/src/main/java/com/laytonsmith/core/events/prefilters/WorldPrefilterMatcher.java index 2bd49b5f5f..d13f0f945d 100644 --- a/src/main/java/com/laytonsmith/core/events/prefilters/WorldPrefilterMatcher.java +++ b/src/main/java/com/laytonsmith/core/events/prefilters/WorldPrefilterMatcher.java @@ -48,4 +48,9 @@ protected String getProperty(T event) { } protected abstract MCWorld getWorld(T event); + + @Override + public int getPriority() { + return -1; + } } diff --git a/src/main/java/com/laytonsmith/core/exceptions/ConfigRuntimeException.java b/src/main/java/com/laytonsmith/core/exceptions/ConfigRuntimeException.java index d6e9945822..9d1dc09e2a 100644 --- a/src/main/java/com/laytonsmith/core/exceptions/ConfigRuntimeException.java +++ b/src/main/java/com/laytonsmith/core/exceptions/ConfigRuntimeException.java @@ -189,10 +189,13 @@ private static void HandleUncaughtException(ConfigRuntimeException e, Environmen } } - private static void PrintMessage(StringBuilder log, StringBuilder console, StringBuilder player, String type, String message, Throwable ex, List st) { + private static void PrintMessage(StringBuilder log, StringBuilder console, StringBuilder player, String type, String message, Throwable ex, List st, Target top) { log.append(type).append(message).append("\n"); console.append(TermColors.RED).append(type).append(TermColors.WHITE).append(message).append("\n"); player.append(MCChatColor.RED).append(type).append(MCChatColor.WHITE).append(message).append("\n"); + if(st.isEmpty()) { + st.add(new StackTraceElement("<
>", top)); + } for(StackTraceElement e : st) { Target t = e.getDefinedAt(); String proc = e.getProcedureName(); @@ -248,16 +251,20 @@ private static void DoReport(String message, String exceptionType, ConfigRuntime } Target top = Target.UNKNOWN; + if(ex != null) { + top = ex.getTarget(); + } for(StackTraceElement e : st) { Target t = e.getDefinedAt(); if(top == Target.UNKNOWN) { top = t; } } + StringBuilder log = new StringBuilder(); StringBuilder console = new StringBuilder(); StringBuilder player = new StringBuilder(); - PrintMessage(log, console, player, type, message, ex, st); + PrintMessage(log, console, player, type, message, ex, st, top); if(ex != null) { // Otherwise, a CCE if(ex.getCause() != null && ex.getCause() instanceof ConfigRuntimeException) { @@ -285,7 +292,7 @@ private static void DoReport(String message, String exceptionType, ConfigRuntime if(!"".equals(nMessage.trim())) { nMessage = ": " + nMessage; } - PrintMessage(log, console, player, nType, nMessage, ex, newSt); + PrintMessage(log, console, player, nType, nMessage, ex, newSt, top); ex = (ConfigRuntimeException) ex.getCause(); } } diff --git a/src/main/java/com/laytonsmith/core/extensions/ExtensionTracker.java b/src/main/java/com/laytonsmith/core/extensions/ExtensionTracker.java index 814cbdaf48..ba3368b9a0 100644 --- a/src/main/java/com/laytonsmith/core/extensions/ExtensionTracker.java +++ b/src/main/java/com/laytonsmith/core/extensions/ExtensionTracker.java @@ -6,7 +6,7 @@ import com.laytonsmith.abstraction.StaticLayer; import com.laytonsmith.annotations.api; import com.laytonsmith.core.MSVersion; -import com.laytonsmith.core.events.AbstractEvent; +import com.laytonsmith.core.events.AbstractGenericEvent; import com.laytonsmith.core.events.Driver; import com.laytonsmith.core.events.Event; import com.laytonsmith.core.events.EventMixinInterface; @@ -141,18 +141,18 @@ public Set getEvents(Driver type) { @SuppressWarnings("UseSpecificCatch") public synchronized void registerEvent(Event e) { - if(e instanceof AbstractEvent) { - AbstractEvent ae = (AbstractEvent) e; + if(e instanceof AbstractGenericEvent) { + AbstractGenericEvent ae = (AbstractGenericEvent) e; //Get the mixin for this server, and add it to e Class mixinClass = StaticLayer.GetServerEventMixin(); try { - Constructor mixinConstructor = mixinClass.getConstructor(AbstractEvent.class); + Constructor mixinConstructor = mixinClass.getConstructor(AbstractGenericEvent.class); EventMixinInterface mixin = (EventMixinInterface) mixinConstructor.newInstance(e); ae.setAbstractEventMixin(mixin); } catch (Exception ex) { //This is a serious problem, and it should kill the plugin, for fast failure detection. throw new Error("Could not properly instantiate the mixin class. " - + "The constructor with the signature \"public " + mixinClass.getSimpleName() + "(AbstractEvent e)\" is missing" + + "The constructor with the signature \"public " + mixinClass.getSimpleName() + "(AbstractGenericEvent e)\" is missing" + " from " + mixinClass.getName()); } } diff --git a/src/main/java/com/laytonsmith/core/functions/EventBinding.java b/src/main/java/com/laytonsmith/core/functions/EventBinding.java index c73a05baf3..e35efe281d 100644 --- a/src/main/java/com/laytonsmith/core/functions/EventBinding.java +++ b/src/main/java/com/laytonsmith/core/functions/EventBinding.java @@ -55,6 +55,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -219,8 +220,14 @@ public CClassType typecheck(StaticAnalysis analysis, // Perform prefilter validation for known events. if(i == 2 && eventName != null) { - argTypes.add(this.typecheckPrefilterParseTree( - analysis, eventName, child, env, ast.getFileOptions(), exceptions)); + try { + argTypes.add(this.typecheckPrefilterParseTree( + analysis, eventName, child, env, ast.getFileOptions(), exceptions)); + } catch(ConfigCompileException ex) { + exceptions.add(ex); + } catch(ConfigCompileGroupException ex) { + exceptions.addAll(ex.getList()); + } argTargets.add(child.getTarget()); } else { @@ -236,7 +243,8 @@ public CClassType typecheck(StaticAnalysis analysis, private CClassType typecheckPrefilterParseTree( StaticAnalysis analysis, String eventName, ParseTree prefilterParseTree, - Environment env, FileOptions fileOptions, Set exceptions) { + Environment env, FileOptions fileOptions, Set exceptions) + throws ConfigCompileException, ConfigCompileGroupException { // Return if the prefilter parse tree is not a hard-coded "array(...)" node. if(!(prefilterParseTree.getData() instanceof CFunction) @@ -258,6 +266,7 @@ private CClassType typecheckPrefilterParseTree( } // Validate prefilters. + Map, ParseTree> fullPrefilters = new HashMap<>(); for(ParseTree node : prefilterParseTree.getChildren()) { if(node.getData() instanceof CFunction && node.getData().val().equals(Compiler.centry.NAME)) { List children = node.getChildren(); @@ -266,6 +275,7 @@ private CClassType typecheckPrefilterParseTree( if(prefilters.containsKey(prefilterKey)) { Prefilter prefilter = prefilters.get(prefilterKey); prefilter.getMatcher().typecheck(analysis, prefilterEntryValParseTree, env, exceptions); + fullPrefilters.put(prefilter, prefilterEntryValParseTree); if(prefilter.getStatus().contains(PrefilterStatus.REMOVED)) { exceptions.add(new ConfigCompileException("This prefilter has been removed," + " and is no longer available.", prefilterEntryValParseTree.getTarget())); @@ -289,6 +299,8 @@ private CClassType typecheckPrefilterParseTree( } } + ev.validatePrefilters(fullPrefilters, env); + // All array entries have been typechecked, so we can just return the array type here. return CArray.TYPE; } diff --git a/src/main/java/com/laytonsmith/core/functions/XPacketJumper.java b/src/main/java/com/laytonsmith/core/functions/XPacketJumper.java index ff59a2aeac..11e2579aa2 100644 --- a/src/main/java/com/laytonsmith/core/functions/XPacketJumper.java +++ b/src/main/java/com/laytonsmith/core/functions/XPacketJumper.java @@ -1,6 +1,7 @@ package com.laytonsmith.core.functions; import com.comphenix.protocol.PacketType; +import com.comphenix.protocol.events.ListenerPriority; import com.laytonsmith.PureUtilities.Common.StringUtils; import com.laytonsmith.PureUtilities.Version; import com.laytonsmith.abstraction.MCPlayer; @@ -18,10 +19,15 @@ import com.laytonsmith.core.environments.Environment; import com.laytonsmith.core.exceptions.CRE.CRECastException; import com.laytonsmith.core.exceptions.CRE.CREIllegalArgumentException; +import com.laytonsmith.core.exceptions.CRE.CREPluginInternalException; +import com.laytonsmith.core.exceptions.CRE.CRERangeException; import com.laytonsmith.core.exceptions.CRE.CREThrowable; import com.laytonsmith.core.exceptions.ConfigRuntimeException; import com.laytonsmith.core.natives.interfaces.Mixed; -import com.laytonsmith.core.protocollib.PacketUtils; +import com.laytonsmith.core.packetjumper.PacketDirection; +import com.laytonsmith.core.packetjumper.PacketEventNotifier; +import com.laytonsmith.core.packetjumper.PacketJumper; +import com.laytonsmith.core.packetjumper.PacketUtils; /** * @@ -114,10 +120,7 @@ public Boolean runAsync() { @Override public Mixed exec(Target t, Environment env, Mixed... args) throws ConfigRuntimeException { - String direction = args[1].val().toUpperCase(); - if(!"IN".equals(direction) && !"OUT".equals(direction)) { - throw new CREIllegalArgumentException("Direction must be one of \"IN\" or \"OUT\".", t); - } + PacketDirection direction = ArgumentValidation.getEnum(args[1], PacketDirection.class, t); return PacketUtils.createPacket( args[0].val().toUpperCase(), direction, @@ -181,7 +184,7 @@ public Mixed exec(Target t, Environment env, Mixed... args) throws ConfigRuntime packet.writeMixed(field, value, t); } else if(args[1].isInstanceOf(CInt.TYPE)) { int field = ArgumentValidation.getInt32(args[1], t); - packet.writeMixed(field, value); + packet.writeMixed(field, value, t); } return CVoid.VOID; } @@ -261,4 +264,172 @@ public Version since() { } } + + @api + @hide("Experimental") + public static class register_packet extends AbstractFunction { + + @Override + public Class[] thrown() { + return new Class[]{ + CRECastException.class, + CREIllegalArgumentException.class, + CREPluginInternalException.class, + }; + } + + @Override + public boolean isRestricted() { + return true; + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public Mixed exec(Target t, Environment env, Mixed... args) throws ConfigRuntimeException { + String protocol = ArgumentValidation.getStringObject(args[0], t); + String type = ArgumentValidation.getStringObject(args[1], t); + PacketEventNotifier notifier = PacketJumper.GetPacketEventNotifier() + .orElseThrow(() -> new CREPluginInternalException("Packet Jumper not enabled.", t)); + for(PacketType p : PacketType.values()) { + if(p.getProtocol().name().equals(protocol) && p.name().equals(type)) { + notifier.register(ListenerPriority.NORMAL, p); + return CVoid.VOID; + } + } + throw new CREIllegalArgumentException("Cannot find a packet of type \"" + + protocol + "\":\"" + type + "\"", t); + } + + @Override + public String getName() { + return "register_packet"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{2}; + } + + @Override + public String docs() { + return "void {string protocol, string type} Registers the system to listen for packets of the given" + + " protocol and type. The packet_sent and packet_received events will only fire for packets" + + " that have been explicitely registered for. You can see the list of possible packets with" + + " all_packets(). Note that you do not register the direction in this function, but you can" + + " filter on direction within the event handlers themselves."; + } + + @Override + public Version since() { + return MSVersion.V3_3_5; + } + + } + + @api + public static class packet_read extends AbstractFunction { + + @Override + public Class[] thrown() { + return new Class[]{ + CRECastException.class, + CRERangeException.class, + CREIllegalArgumentException.class, + }; + } + + @Override + public boolean isRestricted() { + return true; + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public Mixed exec(Target t, Environment env, Mixed... args) throws ConfigRuntimeException { + CPacket packet = ArgumentValidation.getObject(args[0], t, CPacket.class); + if(args[1].isInstanceOf(CInt.TYPE)) { + int index = ArgumentValidation.getInt32(args[1], t); + return packet.readMixed(index, t); + } else { + return packet.readMixed(args[1].val(), t); + } + } + + @Override + public String getName() { + return "packet_read"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{2}; + } + + @Override + public String docs() { + return "mixed {packet, index} Returns the value at the index. The index should be either a string or an" + + "int. The string version uses the mojang mappings, and the int version passes the value" + + " directly through to ProtocolLib."; + } + + @Override + public Version since() { + return MSVersion.V3_3_5; + } + + } + + @api + public static class packet_info extends AbstractFunction { + + @Override + public Class[] thrown() { + return new Class[] { CRECastException.class }; + } + + @Override + public boolean isRestricted() { + return true; + } + + @Override + public Boolean runAsync() { + return null; + } + + @Override + public Mixed exec(Target t, Environment env, Mixed... args) throws ConfigRuntimeException { + CPacket packet = ArgumentValidation.getObject(args[0], t, CPacket.class); + return packet.toCArray(); + } + + @Override + public String getName() { + return "packet_info"; + } + + @Override + public Integer[] numArgs() { + return new Integer[]{1}; + } + + @Override + public String docs() { + return "array {packet} Returns data about the given packet."; + } + + @Override + public Version since() { + return MSVersion.V3_3_5; + } + + } } diff --git a/src/main/java/com/laytonsmith/core/packetjumper/BindablePacketEvent.java b/src/main/java/com/laytonsmith/core/packetjumper/BindablePacketEvent.java new file mode 100644 index 0000000000..a57289606f --- /dev/null +++ b/src/main/java/com/laytonsmith/core/packetjumper/BindablePacketEvent.java @@ -0,0 +1,19 @@ +package com.laytonsmith.core.packetjumper; + +import com.laytonsmith.abstraction.MCPlayer; +import com.laytonsmith.core.constructs.CPacket; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.events.BindableEvent; + +/** + * Created by JunHyung Im on 2020-07-05 + */ +public interface BindablePacketEvent extends BindableEvent { + MCPlayer getPlayer(); + + PacketKind getKind(); + + CPacket getPacket(Target target); + + Object getInternalPacket(); +} diff --git a/src/main/java/com/laytonsmith/core/packetjumper/Comparisons.java b/src/main/java/com/laytonsmith/core/packetjumper/Comparisons.java new file mode 100644 index 0000000000..f118e249e4 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/packetjumper/Comparisons.java @@ -0,0 +1,29 @@ +package com.laytonsmith.core.packetjumper; + +/** + * + */ +public class Comparisons { + private Comparisons() {} + + /** + * Returns true if the methodscript object is effectively equal to the packet object. This is not necessarily + * a completely straightforward equals check as the values are both boxed and sometimes of disparate types, + * such as Long and Integer, which requires more detailed handling. + * @param msObject + * @param packetObject + * @return + */ + public static boolean IsEqual(Object msObject, Object packetObject) { + if(msObject instanceof Long msNumber && packetObject instanceof Number packetNumber) { + return msNumber == packetNumber.longValue(); + } + if(msObject instanceof Double msDouble && packetObject instanceof Number packetNumber) { + return java.lang.Math.abs(msDouble - packetNumber.doubleValue()) < 0.0001; + } + if(packetObject == null || msObject == null) { + return msObject == packetObject; + } + return msObject == packetObject || msObject.equals(packetObject); + } +} diff --git a/src/main/java/com/laytonsmith/core/packetjumper/Conversions.java b/src/main/java/com/laytonsmith/core/packetjumper/Conversions.java new file mode 100644 index 0000000000..eda94fb609 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/packetjumper/Conversions.java @@ -0,0 +1,196 @@ +package com.laytonsmith.core.packetjumper; + +import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.wrappers.WrappedChatComponent; +import com.laytonsmith.PureUtilities.Common.ClassUtils; +import com.laytonsmith.PureUtilities.Common.ReflectionUtils; +import com.laytonsmith.PureUtilities.Common.StringUtils; +import com.laytonsmith.abstraction.bukkit.BukkitMCItemStack; +import com.laytonsmith.core.ArgumentValidation; +import com.laytonsmith.core.ObjectGenerator; +import com.laytonsmith.core.Static; +import com.laytonsmith.core.constructs.CArray; +import com.laytonsmith.core.constructs.CBoolean; +import com.laytonsmith.core.constructs.CByteArray; +import com.laytonsmith.core.constructs.CClassType; +import com.laytonsmith.core.constructs.CDouble; +import com.laytonsmith.core.constructs.CInt; +import com.laytonsmith.core.constructs.CNull; +import com.laytonsmith.core.constructs.CString; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.natives.interfaces.Mixed; +import net.fabricmc.mappingio.tree.MappingTree; +import org.bukkit.inventory.ItemStack; + +/** + * Created by JunHyung Im on 2020-07-05 + */ +public final class Conversions { + + private Conversions() {} + + public static Object convertMixedToObject(Mixed mixed, Class type, Target t) { + // When adding to this list, ensure you update the Packet_Jumper docs, + // as well as the getTypeConversion method. + if(Enum.class.isAssignableFrom(type)) { + return getEnum(mixed.val(), type); +// } else if(Optional.class.isAssignableFrom(type)) { +// CArray array = ArgumentValidation.getArray(mixed, t); +// switch((int) array.size()) { +// case 0: +// return Optional.empty(); +// case 1: +// // TODO: Need the generic type to get this working. It's available, at least in all_packets. +// throw new Error(); +// default: +// throw new CRELengthException("Arrays representing Optionals must only be of length 0 or 1.", t); +// } + } else if(MinecraftReflection.getIChatBaseComponentClass().isAssignableFrom(type)) { + String contents = mixed.val(); + return contents.startsWith("{") && contents.endsWith("}") + ? WrappedChatComponent.fromJson(contents).getHandle() + : WrappedChatComponent.fromText(contents).getHandle(); + } else if(MinecraftReflection.getItemStackClass().isAssignableFrom(type)) { + return ObjectGenerator.GetGenerator().item(mixed, t); + } else if(MinecraftReflection.getBlockPositionClass().isAssignableFrom(type)) { + CArray array = ArgumentValidation.getArray(mixed, t); + int x = ArgumentValidation.getInt32(array.get("x", t), t); + int y = ArgumentValidation.getInt32(array.get("y", t), t); + int z = ArgumentValidation.getInt32(array.get("z", t), t); + return ReflectionUtils.newInstance(MinecraftReflection.getBlockPositionClass(), + new Class[]{int.class, int.class, int.class}, + new Object[]{x, y, z}); + } + return Static.getJavaObject(mixed); + } + + /** + * Returns the CClassType which the given java class would be converted to. For instance, + * java.lang.String returns CString.TYPE. Null is returned if the object type is not supported by the conversion + * methods in this class. + * + * @param clazz + * @return + */ + public static CClassType getTypeConversion(Class clazz) { + CArray enumTypes = new CArray(Target.UNKNOWN); + String desc = ClassUtils.getJVMName(clazz); + // Enums + if(clazz.isEnum()) { + for(Object o : clazz.getEnumConstants()) { + enumTypes.push(new CString(o.toString(), Target.UNKNOWN), Target.UNKNOWN); + } + return CString.TYPE; + } + // Basic types + switch(desc) { + case "I": + return CInt.TYPE; + case "Z": + return CBoolean.TYPE; + case "B": + case "J": + case "S": + return CInt.TYPE; + case "D": + case "F": + return CDouble.TYPE; + case "Ljava/lang/String;": + case "Ljava/util/UUID;": + return CString.TYPE; + case "Ljava/util/List;": + case "Ljava/util/Collection;": + case "Ljava/util/Set;": + case "Ljava/util/EnumSet;": + case "Ljava/util/Map;": + case "[Ljava/lang/String;": + case "[I": + case "[S": + return CArray.TYPE; + case "[B": + return CByteArray.TYPE; + } + // When adding to this list, ensure you update the Packet_Jumper docs, + // as well as the convertObject/MixedToMixed/Object methods. + // Minecraft Types +// if(Optional.class.isAssignableFrom(clazz)) { +// return CArray.TYPE; +// } + if(MinecraftReflection.getIChatBaseComponentClass().isAssignableFrom(clazz)) { + return CString.TYPE; + } else if(MinecraftReflection.getItemStackClass().isAssignableFrom(clazz)) { + return CArray.TYPE; + } else if(MinecraftReflection.getBlockPositionClass().isAssignableFrom(clazz)) { + return CArray.TYPE; + } + return null; + } + + public static Object adjustObject(Object object, Class type) { + if(type == int.class && object instanceof Number n) { + return n.intValue(); + } + if(type == short.class && object instanceof Number n) { + return n.shortValue(); + } + if(type == byte.class && object instanceof Number n) { + return n.byteValue(); + } + + return object; + } + + public static Mixed convertObjectToMixed(Object object) { + // When adding to this list, ensure you update the Packet_Jumper docs, + // as well as the getTypeConversion method. + if(object == null) { + return CNull.NULL; + } +// if(object instanceof Optional optional) { +// CArray a = new CArray(Target.UNKNOWN); +// if(optional.isPresent()) { +// a.push(convertObjectToMixed(optional.get()), Target.UNKNOWN); +// } +// return a; +// } + // Useful for finding the methods +// java.util.List list = java.util.stream.Stream.of(object.getClass().getDeclaredMethods()).map(x -> x.getName()) +// .toList(); +// System.out.println(list); + Class clazz = object.getClass(); + if(object instanceof Enum aEnum) { + return new CString(aEnum.name(), Target.UNKNOWN); + } else if(MinecraftReflection.getIChatBaseComponentClass().isAssignableFrom(clazz)) { + return new CString(object.toString(), Target.UNKNOWN); + } else if(MinecraftReflection.getItemStackClass().isAssignableFrom(clazz)) { + Object itemStack = ReflectionUtils.invokeMethod(object, "getBukkitStack"); + return ObjectGenerator.GetGenerator().item(new BukkitMCItemStack((ItemStack) itemStack), Target.UNKNOWN); + } else if(MinecraftReflection.getBlockPositionClass().isAssignableFrom(clazz)) { + java.util.List list = java.util.stream.Stream.of(object.getClass().getDeclaredMethods()).map(x -> x.getName()) + .toList(); + System.out.println(list); + MappingTree tree = PacketJumper.GetMappingTree(); + MappingTree.ClassMapping vec3i + = tree.getClass(object.getClass().getSuperclass().getName().replace(".", "/"), PacketJumper.GetServerNamespace()); + int x = ReflectionUtils.invokeMethod(object, vec3i.getMethod("getX", null, PacketJumper.GetMojangNamespace()).getSrcName()); + int y = ReflectionUtils.invokeMethod(object, vec3i.getMethod("getY", null, PacketJumper.GetMojangNamespace()).getSrcName()); + int z = ReflectionUtils.invokeMethod(object, vec3i.getMethod("getZ", null, PacketJumper.GetMojangNamespace()).getSrcName()); + CArray array = new CArray(Target.UNKNOWN); + array.set("x", x); + array.set("y", y); + array.set("z", z); + return array; + } + return Static.getMSObject(object, Target.UNKNOWN); + } + + public static Object getEnum(String name, Class enumType) { + try { + return Enum.valueOf(enumType, name); + } catch(IllegalArgumentException ex) { + Enum[] enums = (Enum[]) ReflectionUtils.invokeMethod(enumType, null, "values"); + throw new IllegalArgumentException(String.format("%s has elements [%s]", + enumType.getSimpleName(), StringUtils.Join(enums, ", "))); + } + } +} diff --git a/src/main/java/com/laytonsmith/core/packetjumper/PacketDirection.java b/src/main/java/com/laytonsmith/core/packetjumper/PacketDirection.java new file mode 100644 index 0000000000..be4cd7bbed --- /dev/null +++ b/src/main/java/com/laytonsmith/core/packetjumper/PacketDirection.java @@ -0,0 +1,19 @@ +package com.laytonsmith.core.packetjumper; + +import com.comphenix.protocol.PacketType; +import com.laytonsmith.annotations.MEnum; + +/** + * + */ +@MEnum("com.commandhelper.PacketDirection") +public enum PacketDirection { + IN, OUT; + + private PacketDirection() { + } + + public static PacketDirection FromSender(PacketType.Sender sender) { + return sender == PacketType.Sender.CLIENT ? IN : OUT; + } +} diff --git a/src/main/java/com/laytonsmith/core/packetjumper/PacketEventNotifier.java b/src/main/java/com/laytonsmith/core/packetjumper/PacketEventNotifier.java new file mode 100644 index 0000000000..59f6034541 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/packetjumper/PacketEventNotifier.java @@ -0,0 +1,67 @@ +package com.laytonsmith.core.packetjumper; + +import com.comphenix.protocol.PacketType; +import com.comphenix.protocol.ProtocolManager; +import com.comphenix.protocol.events.ListenerPriority; +import com.comphenix.protocol.events.PacketAdapter; +import com.comphenix.protocol.events.PacketEvent; +import com.laytonsmith.core.events.Driver; +import com.laytonsmith.core.events.EventUtils; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import org.bukkit.plugin.Plugin; + +/** + * Created by JunHyung Im on 2020-07-05 + */ +public class PacketEventNotifier { + private final Plugin plugin; + private final ProtocolManager protocolManager; + private final Collection packetAdapters = new ArrayList<>(); + private final Set registeredTypes = new HashSet<>(); + + public PacketEventNotifier(Plugin plugin, ProtocolManager protocolManager) { + this.plugin = plugin; + this.protocolManager = protocolManager; + } + + public void onPacketReceiving(PacketEvent event) { + ProtocolLibPacketEvent packetEvent = new ProtocolLibPacketEvent(event); + EventUtils.TriggerListener(Driver.PACKET_RECEIVED, "packet_received", packetEvent); + } + + public void onPacketSending(PacketEvent event) { + ProtocolLibPacketEvent packetEvent = new ProtocolLibPacketEvent(event); + EventUtils.TriggerListener(Driver.PACKET_SENT, "packet_sent", packetEvent); + } + + public void register(ListenerPriority priority, PacketType type) { + if(this.registeredTypes.contains(type)) { + return; + } + PacketAdapter adapter = new PacketAdapter(plugin, priority, type) { + @Override + public void onPacketReceiving(PacketEvent event) { + PacketEventNotifier.this.onPacketReceiving(event); + } + + @Override + public void onPacketSending(PacketEvent event) { + PacketEventNotifier.this.onPacketSending(event); + } + }; + this.packetAdapters.add(adapter); + this.protocolManager.addPacketListener(adapter); + this.registeredTypes.add(type); + } + + public void unregister() { + for(PacketAdapter adapter : this.packetAdapters) { + this.protocolManager.removePacketListener(adapter); + } + this.packetAdapters.clear(); + this.registeredTypes.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/com/laytonsmith/core/packetjumper/PacketJumper.java b/src/main/java/com/laytonsmith/core/packetjumper/PacketJumper.java index ea84e84db2..b5f2566a37 100644 --- a/src/main/java/com/laytonsmith/core/packetjumper/PacketJumper.java +++ b/src/main/java/com/laytonsmith/core/packetjumper/PacketJumper.java @@ -1,155 +1,171 @@ package com.laytonsmith.core.packetjumper; +import com.comphenix.protocol.ProtocolLibrary; +import com.laytonsmith.PureUtilities.Common.FileWriteMode; import com.laytonsmith.PureUtilities.Common.ReflectionUtils; -import com.laytonsmith.PureUtilities.Common.StreamUtils; -import com.laytonsmith.abstraction.MCPlayer; -import com.laytonsmith.core.constructs.Construct; -import org.bukkit.Bukkit; - -import java.util.Set; -import java.util.SortedSet; -import java.util.TreeSet; +import com.laytonsmith.PureUtilities.Version; +import com.laytonsmith.PureUtilities.Web.RequestSettings; +import com.laytonsmith.PureUtilities.Web.WebUtility; +import com.laytonsmith.PureUtilities.ZipReader; +import com.laytonsmith.abstraction.enums.MCVersion; +import com.laytonsmith.commandhelper.CommandHelperFileLocations; +import com.laytonsmith.commandhelper.CommandHelperPlugin; +import com.laytonsmith.core.Prefs; +import com.laytonsmith.core.Static; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; import java.util.logging.Level; -import java.util.logging.Logger; +import net.fabricmc.mappingio.format.tiny.Tiny2FileReader; +import net.fabricmc.mappingio.tree.MappingTree; +import net.fabricmc.mappingio.tree.MemoryMappingTree; /** - * * */ -public class PacketJumper { +public final class PacketJumper { - @SuppressWarnings("FieldMayBeFinal") - private static boolean started = false; - private static SortedSet packetInfo; - @SuppressWarnings("FieldMayBeFinal") - private static Thread initializingThread = null; - private static final String PROTOCOL_DOCS = ""; + private PacketJumper() { + } - public static void startup() { - if(true) { - return; //TODO: - } - if(started) { - return; - } - packetInfo = new TreeSet<>(); - initializingThread = new Thread(new Runnable() { + /** + * The current server mapping type. + */ + private static int ServerType = -1; - @Override - public void run() { -// try { -// protocolDocs = WebUtility.GetPageContents("http://mc.kev009.com/Protocol"); -// } catch (IOException ex) { -// Logger.getLogger(PacketJumper.class.getName()).log(Level.SEVERE, null, ex); -// } - String version = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3]; - try { - Class packetClass = Class.forName("net.minecraft.server." + version + ".Packet"); - Set packets = ReflectionUtils.getAllExtensions(packetClass); - for(Class packet : packets) { - packetInfo.add(new PacketInfo(packet)); - } - started = true; - } catch (ClassNotFoundException ex) { - StreamUtils.GetSystemOut().println("[CommandHelper] Failed to find Minecraft Packet classes."); - } - } - }, "PacketJumperInitializer"); - initializingThread.start(); - for(PacketInfo p : getPacketInfo()) { - StreamUtils.GetSystemOut().println(p); + /** + * A mapping from minecraft versions to takenaka urls. + */ + private static final Map VERSION_MAP = new HashMap<>() { + { + put(MCVersion.MC1_20_4, "https://repo.screamingsandals.org/releases/me/kcra/takenaka/mappings/1.8.8+1.20.4/mappings-1.8.8+1.20.4.jar"); } - } + }; - public static boolean started() { - return started; - } + private static final MemoryMappingTree MINECRAFT_MAPPINGS = new MemoryMappingTree(); + private static int SpigotNamespace; + private static int MojangNamespace; + + private static Optional packetEventNotifier = Optional.empty(); - private static void waitForInitialization() throws InterruptedException { - if(initializingThread == null) { - startup(); + /** + * Returns the best url for the current version of minecraft, or null if this version is not supported. Reads from + * the settings if applicable. + * + * @return + */ + private static String GetUrl() { + String pref = Prefs.TakenakaMapping(); + if(!"".equals(pref)) { + return pref; } - if(initializingThread.isAlive()) { - //Wait for the startup thread, if it's running - initializingThread.join(); + Version currentVersion = Static.getServer().getMinecraftVersion(); + Version bestCandidate = null; + for(Version v : VERSION_MAP.keySet()) { + // Get the highest version that supports the current version. + if(v.lt(bestCandidate)) { + continue; + } + if(v.gte(currentVersion)) { + bestCandidate = v; + } } + return VERSION_MAP.get(bestCandidate); } - public static Set getPacketInfo() { - try { - waitForInitialization(); - } catch (InterruptedException ex) { - Logger.getLogger(PacketJumper.class.getName()).log(Level.SEVERE, null, ex); + public static void Startup() throws MalformedURLException, IOException { + if(!Prefs.UseProtocolLib()) { + return; } - return new TreeSet<>(packetInfo); - } - - public static void fakePacketToPlayer(MCPlayer player, PacketInstance packet) { - try { - waitForInitialization(); - } catch (InterruptedException ex) { - Logger.getLogger(PacketJumper.class.getName()).log(Level.SEVERE, null, ex); + long time = System.currentTimeMillis(); + boolean downloading = false; + File protocolLibCache = new File(CommandHelperFileLocations.getDefault().getCacheDirectory(), "ProtocolLib"); + URL url = new URL(GetUrl()); + String[] parts = url.getPath().split("/"); + String filename = parts[parts.length - 1]; + protocolLibCache.mkdirs(); + File mappingsJar = new File(protocolLibCache, filename); + if(!mappingsJar.exists()) { + downloading = true; + RequestSettings requestSettings = new RequestSettings() + .setDownloadTo(mappingsJar) + .setDownloadStrategy(FileWriteMode.SAFE_WRITE) + .setBlocking(true); + WebUtility.GetPage(url, requestSettings); } - //TODO: - throw new UnsupportedOperationException("Not supported yet."); + Version currentVersion = Static.getServer().getMinecraftVersion(); + String mcVersion = currentVersion.getMajor() + "." + currentVersion.getMinor() + "." + currentVersion.getSupplemental(); + ZipReader tinyFileReader = new ZipReader(new File(mappingsJar, mcVersion + ".tiny")); + Reader reader = new BufferedReader(new InputStreamReader(tinyFileReader.getInputStream(), StandardCharsets.UTF_8)); + Tiny2FileReader.read(reader, MINECRAFT_MAPPINGS); + SpigotNamespace = MINECRAFT_MAPPINGS.getNamespaceId("spigot"); + MojangNamespace = MINECRAFT_MAPPINGS.getNamespaceId("mojang"); + long ms = System.currentTimeMillis() - time; + Static.getLogger().log(Level.INFO, + "Loading {0}mappings took {1}ms", + new Object[]{ + downloading ? "and downloading " : "", + ms + }); + packetEventNotifier = Optional.of(new PacketEventNotifier(CommandHelperPlugin.self, ProtocolLibrary.getProtocolManager())); } - public static void fakePacketFromPlayer(MCPlayer player, PacketInstance packet) { - try { - waitForInitialization(); - } catch (InterruptedException ex) { - Logger.getLogger(PacketJumper.class.getName()).log(Level.SEVERE, null, ex); - } - //TODO: - throw new UnsupportedOperationException("Not supported yet."); + public static void Shutdown() { + packetEventNotifier.ifPresent(e -> e.unregister()); } - //TODO: Add interceptor listeners, and make this support more than one. It should - //probably support binds even. - public static void setPacketReceivedInterceptor(int id, PacketHandler handler) { - try { - waitForInitialization(); - } catch (InterruptedException ex) { - Logger.getLogger(PacketJumper.class.getName()).log(Level.SEVERE, null, ex); - } - //TODO: - throw new UnsupportedOperationException("Not supported yet."); + public static Optional GetPacketEventNotifier() { + return packetEventNotifier; } - public static void setPacketSentInterceptor(int id, PacketHandler handler) { - try { - waitForInitialization(); - } catch (InterruptedException ex) { - Logger.getLogger(PacketJumper.class.getName()).log(Level.SEVERE, null, ex); - } - //TODO: - throw new UnsupportedOperationException("Not supported yet."); + /** + * Returns the mapping tree, if loaded, null otherwise. + * + * @return + */ + public static MappingTree GetMappingTree() { + return MINECRAFT_MAPPINGS; } - public static PacketInstance getPacket(int id, Construct... args) { - try { - waitForInitialization(); - } catch (InterruptedException ex) { - Logger.getLogger(PacketJumper.class.getName()).log(Level.SEVERE, null, ex); + /** + * Returns the server namespace. This value can be sent to the MappingTree to get the actual class that + * is running. + * @return + */ + public static int GetServerNamespace() { + if(ServerType == -1) { + // Paper has 2 versions, "moj-mapped" and "spigot" mapped. As we support paper and also spigot, we need + // to check which mapping version the server is running, and select the correct mapping segment. + ServerType = PacketJumper.GetMojangNamespace(); + // This class is named net.minecraft.network.protocol.status.ServerboundStatusRequestPacket in mojang mappings + if(ReflectionUtils.classExists("net.minecraft.network.protocol.status.PacketStatusInStart")) { + ServerType = PacketJumper.GetSpigotNamespace(); + } } - //TODO: - throw new UnsupportedOperationException("Not supported yet."); + return ServerType; } /** - * Used for packet interceptors, this allows an opportunity for a class to manipulate a packet before it is - * processed/sent + * Returns the Spigot namespace. Generally, one shouls use GetServerNamespace() instead. + * @return */ - public static interface PacketHandler { + public static int GetSpigotNamespace() { + return SpigotNamespace; + } - /** - * The packet to be processed/sent is passed to this method, and it is expected that this method returns a - * packet (which is actually going to be sent) or null, which cancels the packet send entirely. - * - * @param player The player sending/receiving the packet - * @param packet The packet in question - * @return - */ - PacketInstance Handle(MCPlayer player, PacketInstance packet); + /** + * Returns the Mojang namespace. This should be used when sending and receiving names from users. + * @return + */ + public static int GetMojangNamespace() { + return MojangNamespace; } } diff --git a/src/main/java/com/laytonsmith/core/protocollib/PacketKind.java b/src/main/java/com/laytonsmith/core/packetjumper/PacketKind.java similarity index 96% rename from src/main/java/com/laytonsmith/core/protocollib/PacketKind.java rename to src/main/java/com/laytonsmith/core/packetjumper/PacketKind.java index 19a7efcce4..9a7b31d5b8 100644 --- a/src/main/java/com/laytonsmith/core/protocollib/PacketKind.java +++ b/src/main/java/com/laytonsmith/core/packetjumper/PacketKind.java @@ -1,4 +1,4 @@ -package com.laytonsmith.core.protocollib; +package com.laytonsmith.core.packetjumper; import com.laytonsmith.core.constructs.CArray; import com.laytonsmith.core.constructs.CString; diff --git a/src/main/java/com/laytonsmith/core/protocollib/PacketUtils.java b/src/main/java/com/laytonsmith/core/packetjumper/PacketUtils.java similarity index 81% rename from src/main/java/com/laytonsmith/core/protocollib/PacketUtils.java rename to src/main/java/com/laytonsmith/core/packetjumper/PacketUtils.java index 6335ca71f2..de0db2314b 100644 --- a/src/main/java/com/laytonsmith/core/protocollib/PacketUtils.java +++ b/src/main/java/com/laytonsmith/core/packetjumper/PacketUtils.java @@ -1,4 +1,4 @@ -package com.laytonsmith.core.protocollib; +package com.laytonsmith.core.packetjumper; import com.comphenix.protocol.PacketType; import com.comphenix.protocol.ProtocolLibrary; @@ -19,6 +19,7 @@ import com.laytonsmith.core.natives.interfaces.ArrayAccess; import com.laytonsmith.core.natives.interfaces.Mixed; import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.WildcardType; @@ -50,6 +51,27 @@ public static CArray getAllPackets() { return getAllPacketsInternal().clone(); } + /** + * Returns a list of PacketTypes that are supported by the system. + * @return + */ + public static List SupportedPackets() { + List output = new ArrayList<>(); + for(Set set : new Set[]{PacketRegistry.getServerPacketTypes(), PacketRegistry.getClientPacketTypes()}) { + for(PacketType type : set) { + if(type.name() == null) { + // Not sure how to deal with these types of packets. + continue; + } + if(type.isDynamic()) { + continue; + } + output.add(type); + } + } + return output; + } + /** * Gets the packet structure, but does not clone it. Don't change the structure. * @return @@ -68,18 +90,9 @@ private static CArray getAllPacketsInternal() { subtypes.set("OUT", new CArray(Target.UNKNOWN), Target.UNKNOWN); packetTypeArray.set(protocol.name(), subtypes, Target.UNKNOWN); } - List output = new ArrayList<>(); - output.addAll(PacketRegistry.getServerPacketTypes()); - output.addAll(PacketRegistry.getClientPacketTypes()); + List output = SupportedPackets(); Set unsupportedTypes = new HashSet<>(); for(PacketType type : output) { - if(type.name() == null) { - // Not sure how to deal with these types of packets. - continue; - } - if(type.isDynamic()) { - continue; - } try { CArray array = (CArray)((ArrayAccess)packetTypeArray.get(type.getProtocol().name(), Target.UNKNOWN)) .get(type.getSender() == PacketType.Sender.CLIENT ? "IN" : "OUT", Target.UNKNOWN); @@ -112,10 +125,14 @@ private static CArray getPacketInfo(PacketType type, Set unsupportedType Class clazz = ClassUtils.forCanonicalName(mapping.getName(serverType).replace("/", ".")); arr.set("class", mapping.getName(PacketJumper.GetMojangNamespace()).replace("/", ".")); arr.set("superclass", clazz.getSuperclass().getName()); + arr.set("comment", mapping.getComment()); CArray fields = new CArray(Target.UNKNOWN); int index = 0; do { for(MappingTree.FieldMapping fieldMapping : mapping.getFields()) { + if((getJavaField(fieldMapping, clazz).getModifiers() & Modifier.STATIC) > 0) { + continue; + } CArray field = new CArray(Target.UNKNOWN); field.set("name", fieldMapping.getName(PacketJumper.GetMojangNamespace())); Mixed typeData = CNull.NULL; @@ -131,6 +148,7 @@ private static CArray getPacketInfo(PacketType type, Set unsupportedType field.set("type", typeData, Target.UNKNOWN); int currentId = index++; field.set("field", currentId); + field.set("comment", fieldMapping.getComment()); fields.set(fieldMapping.getName(PacketJumper.GetMojangNamespace()), field, Target.UNKNOWN); } clazz = clazz.getSuperclass(); @@ -213,6 +231,21 @@ private static CArray parseSubtype(Type[] types, Class clazz) { return array; } + public static Field getJavaField(MappingTree.FieldMapping fm, Class clazz) { + String field = fm.getSrcName(); + do { + try { + return clazz.getDeclaredField(field); + } catch (NoSuchFieldException ex) { + clazz = clazz.getSuperclass(); + if(clazz == Object.class || clazz == Record.class) { + break; + } + } + } while(true); + return null; + } + public static PacketType.Sender getSide(String name, Target target) { switch(name) { case "IN": @@ -223,33 +256,45 @@ public static PacketType.Sender getSide(String name, Target target) { throw new CREIllegalArgumentException("Unknown sender type: " + name, target); } - public static PacketType findPacketType(String protocol, String name, Target target) { + public static PacketType findPacketTypeByCommonName(String protocol, String name, Target target) { + for(PacketType type : SupportedPackets()) { + if(type.getProtocol().name().equals(protocol) && type.name().equals(name)) { + return type; + } + } + throw new CREIllegalArgumentException("Error while finding the packet of type" + + " \"" + protocol + "\":\"" + name + "\"." + + " Check the results of all_packets() for information about valid packet types.", target); + } + + public static PacketType findPacketTypeByClassName(String protocol, String className, Target target) { try { // Convert the packet class name into the current server version name, since this will always // be the mojang version, not necessarily the current server version. MappingTree tree = PacketJumper.GetMappingTree(); - name = tree.getClass(name.replace(".", "/"), PacketJumper.GetMojangNamespace()) + className = tree.getClass(className.replace(".", "/"), PacketJumper.GetMojangNamespace()) .getDstName(PacketJumper.GetServerNamespace()).replace("/", "."); - return PacketRegistry.getPacketType(PacketType.Protocol.valueOf(protocol), Class.forName(name)); + return PacketRegistry.getPacketType(PacketType.Protocol.valueOf(protocol), Class.forName(className)); } catch(Exception exception) { - throw new CREIllegalArgumentException("Error while finding the packet of type \"" + name + "\"." + throw new CREIllegalArgumentException("Error while finding the packet of type" + + " \"" + protocol + "\":\"" + className + "\"." + " Check the results of all_packets() for information about valid packet types.", target, exception); } } - public static CPacket createPacket(String protocol, String side, String name, Target target) { + public static CPacket createPacket(String protocol, PacketDirection side, String name, Target target) { try { CArray packetData = (CArray)((ArrayAccess)((ArrayAccess)getAllPacketsInternal().get(protocol, target)) - .get(side, target)) + .get(side.name(), target)) .get(name, target); String clazz = packetData.get("class", target).val(); name = clazz; } catch(CREIndexOverflowException ioe) { // Do nothing, use the name exactly as is. } - PacketContainer container = ProtocolLibrary.getProtocolManager().createPacket(findPacketType(protocol, name, target)); + PacketContainer container = ProtocolLibrary.getProtocolManager().createPacket(findPacketTypeByClassName(protocol, name, target)); return CPacket.create(container, name, target, PacketType.Protocol.valueOf(protocol), - (side.equals("IN") ? PacketType.Sender.CLIENT : PacketType.Sender.SERVER)); + (side == PacketDirection.IN ? PacketType.Sender.CLIENT : PacketType.Sender.SERVER)); } public static PacketKind getPacketKind(PacketType type) { diff --git a/src/main/java/com/laytonsmith/core/packetjumper/ProtocolLibPacketEvent.java b/src/main/java/com/laytonsmith/core/packetjumper/ProtocolLibPacketEvent.java new file mode 100644 index 0000000000..0f7215cf30 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/packetjumper/ProtocolLibPacketEvent.java @@ -0,0 +1,49 @@ +package com.laytonsmith.core.packetjumper; + +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.events.PacketEvent; +import com.laytonsmith.abstraction.MCPlayer; +import com.laytonsmith.abstraction.bukkit.entities.BukkitMCPlayer; +import com.laytonsmith.core.constructs.CPacket; +import com.laytonsmith.core.constructs.Target; + +/** + * Created by JunHyung Im on 2020-07-05 + */ +public class ProtocolLibPacketEvent implements BindablePacketEvent { + private final PacketEvent packetEvent; + + public ProtocolLibPacketEvent(PacketEvent packetEvent) { + this.packetEvent = packetEvent; + } + + @Override + public MCPlayer getPlayer() { + return new BukkitMCPlayer(packetEvent.getPlayer()); + } + + @Override + public PacketKind getKind() { + return PacketUtils.getPacketKind(packetEvent.getPacketType()); + } + + @Override + public CPacket getPacket(Target target) { + return CPacket.create(packetEvent.getPacket(), packetEvent.getPacketType().name(), target, + packetEvent.getPacketType().getProtocol(), packetEvent.getPacketType().getSender()); + } + + @Override + public PacketContainer getInternalPacket() { + return packetEvent.getPacket(); + } + + public PacketEvent getPacketEvent() { + return packetEvent; + } + + @Override + public Object _GetObject() { + return packetEvent; + } +} diff --git a/src/main/java/com/laytonsmith/core/protocollib/Conversions.java b/src/main/java/com/laytonsmith/core/protocollib/Conversions.java deleted file mode 100644 index a9ba3d404c..0000000000 --- a/src/main/java/com/laytonsmith/core/protocollib/Conversions.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.laytonsmith.core.protocollib; - -import com.comphenix.protocol.utility.MinecraftReflection; -import com.comphenix.protocol.wrappers.WrappedChatComponent; -import com.laytonsmith.PureUtilities.Common.ClassUtils; -import com.laytonsmith.PureUtilities.Common.ReflectionUtils; -import com.laytonsmith.PureUtilities.Common.StringUtils; -import com.laytonsmith.core.Static; -import com.laytonsmith.core.constructs.CArray; -import com.laytonsmith.core.constructs.CBoolean; -import com.laytonsmith.core.constructs.CByteArray; -import com.laytonsmith.core.constructs.CClassType; -import com.laytonsmith.core.constructs.CDouble; -import com.laytonsmith.core.constructs.CInt; -import com.laytonsmith.core.constructs.CString; -import com.laytonsmith.core.constructs.Target; -import com.laytonsmith.core.natives.interfaces.Mixed; - -/** - * Created by JunHyung Im on 2020-07-05 - */ -@SuppressWarnings({"rawtypes", "unchecked"}) -public final class Conversions { - - private Conversions() { - } - - public static Object convertMixedToObject(Mixed mixed) { - return Static.getJavaObject(mixed); - } - - public static Object convertMixedToObject(Mixed mixed, Class type) { - if(Enum.class.isAssignableFrom(type)) { - return getEnum(mixed.val(), type); - } else if(MinecraftReflection.getIChatBaseComponentClass().isAssignableFrom(type)) { - String contents = mixed.val(); - return contents.startsWith("{") && contents.endsWith("}") - ? WrappedChatComponent.fromJson(contents).getHandle() - : WrappedChatComponent.fromText(contents).getHandle(); - } - return convertMixedToObject(mixed); - } - - /** - * Returns the CClassType which the given java class would be converted to. For instance, - * java.lang.String returns CString.TYPE. Null is returned if the object type is not supported by the conversion - * methods in this class. - * - * @param clazz - * @return - */ - public static CClassType getTypeConversion(Class clazz) { - CArray enumTypes = new CArray(Target.UNKNOWN); - String desc = ClassUtils.getJVMName(clazz); - // Enums - if(clazz.isEnum()) { - for(Object o : clazz.getEnumConstants()) { - enumTypes.push(new CString(o.toString(), Target.UNKNOWN), Target.UNKNOWN); - } - return CString.TYPE; - } - // Basic types - switch(desc) { - case "I": - return CInt.TYPE; - case "Z": - return CBoolean.TYPE; - case "B": - case "J": - case "S": - return CInt.TYPE; - case "D": - case "F": - return CDouble.TYPE; - case "Ljava/lang/String;": - case "Ljava/util/UUID;": - return CString.TYPE; - case "Ljava/util/List;": - case "Ljava/util/Collection;": - case "Ljava/util/Set;": - case "Ljava/util/EnumSet;": - case "Ljava/util/Map;": - case "[Ljava/lang/String;": - case "[I": - case "[S": - return CArray.TYPE; - case "[B": - return CByteArray.TYPE; - } - // Minecraft Types - if(MinecraftReflection.getIChatBaseComponentClass().isAssignableFrom(clazz)) { - return CString.TYPE; - } - return null; - } - - public static Object adjustObject(Object object, Class type) { - if(type == int.class && object instanceof Number n) { - return n.intValue(); - } - if(type == short.class && object instanceof Number n) { - return n.shortValue(); - } - if(type == byte.class && object instanceof Number n) { - return n.byteValue(); - } - - return object; - } - - public static Mixed convertObjectToMixed(Object object, Target target) { - if(object instanceof Enum) { - return new CString(((Enum) object).name(), target); - } - return Static.getMSObject(object, target); - } - - public static Object getEnum(String name, Class enumType) { - try { - return Enum.valueOf(enumType, name); - } catch(IllegalArgumentException ex) { - Enum[] enums = (Enum[]) ReflectionUtils.invokeMethod(enumType, null, "values"); - throw new IllegalArgumentException(String.format("%s has elements [%s]", - enumType.getSimpleName(), StringUtils.Join(enums, ", "))); - } - } -} diff --git a/src/main/java/com/laytonsmith/core/protocollib/PacketJumper.java b/src/main/java/com/laytonsmith/core/protocollib/PacketJumper.java deleted file mode 100644 index 4d3c5b539e..0000000000 --- a/src/main/java/com/laytonsmith/core/protocollib/PacketJumper.java +++ /dev/null @@ -1,157 +0,0 @@ -package com.laytonsmith.core.protocollib; - -import com.laytonsmith.PureUtilities.Common.FileWriteMode; -import com.laytonsmith.PureUtilities.Common.ReflectionUtils; -import com.laytonsmith.PureUtilities.Version; -import com.laytonsmith.PureUtilities.Web.RequestSettings; -import com.laytonsmith.PureUtilities.Web.WebUtility; -import com.laytonsmith.PureUtilities.ZipReader; -import com.laytonsmith.abstraction.enums.MCVersion; -import com.laytonsmith.commandhelper.CommandHelperFileLocations; -import com.laytonsmith.core.Prefs; -import com.laytonsmith.core.Static; -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.Reader; -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Map; -import java.util.logging.Level; -import net.fabricmc.mappingio.format.tiny.Tiny2FileReader; -import net.fabricmc.mappingio.tree.MappingTree; -import net.fabricmc.mappingio.tree.MemoryMappingTree; - -/** - * - */ -public final class PacketJumper { - - private PacketJumper() { - } - - /** - * The current server mapping type. - */ - private static int ServerType = -1; - - /** - * A mapping from minecraft versions to takenaka urls. - */ - private static final Map VERSION_MAP = new HashMap<>() { - { - put(MCVersion.MC1_20_4, "https://repo.screamingsandals.org/releases/me/kcra/takenaka/mappings/1.8.8+1.20.4/mappings-1.8.8+1.20.4.jar"); - } - }; - - private static final MemoryMappingTree MINECRAFT_MAPPINGS = new MemoryMappingTree(); - private static int SpigotNamespace; - private static int MojangNamespace; - - /** - * Returns the best url for the current version of minecraft, or null if this version is not supported. Reads from - * the settings if applicable. - * - * @return - */ - private static String GetUrl() { - String pref = Prefs.TakenakaMapping(); - if(!"".equals(pref)) { - return pref; - } - Version currentVersion = Static.getServer().getMinecraftVersion(); - Version bestCandidate = null; - for(Version v : VERSION_MAP.keySet()) { - // Get the highest version that supports the current version. - if(v.lt(bestCandidate)) { - continue; - } - if(v.gte(currentVersion)) { - bestCandidate = v; - } - } - return VERSION_MAP.get(bestCandidate); - } - - public static void Startup() throws MalformedURLException, IOException { - if(!Prefs.UseProtocolLib()) { - return; - } - long time = System.currentTimeMillis(); - boolean downloading = false; - File protocolLibCache = new File(CommandHelperFileLocations.getDefault().getCacheDirectory(), "ProtocolLib"); - URL url = new URL(GetUrl()); - String[] parts = url.getPath().split("/"); - String filename = parts[parts.length - 1]; - protocolLibCache.mkdirs(); - File mappingsJar = new File(protocolLibCache, filename); - if(!mappingsJar.exists()) { - downloading = true; - RequestSettings requestSettings = new RequestSettings() - .setDownloadTo(mappingsJar) - .setDownloadStrategy(FileWriteMode.SAFE_WRITE) - .setBlocking(true); - WebUtility.GetPage(url, requestSettings); - } - Version currentVersion = Static.getServer().getMinecraftVersion(); - String mcVersion = currentVersion.getMajor() + "." + currentVersion.getMinor() + "." + currentVersion.getSupplemental(); - ZipReader tinyFileReader = new ZipReader(new File(mappingsJar, mcVersion + ".tiny")); - Reader reader = new BufferedReader(new InputStreamReader(tinyFileReader.getInputStream(), StandardCharsets.UTF_8)); - Tiny2FileReader.read(reader, MINECRAFT_MAPPINGS); - SpigotNamespace = MINECRAFT_MAPPINGS.getNamespaceId("spigot"); - MojangNamespace = MINECRAFT_MAPPINGS.getNamespaceId("mojang"); - long ms = System.currentTimeMillis() - time; - Static.getLogger().log(Level.INFO, - "Loading {0}mappings took {1}ms", - new Object[]{ - downloading ? "and downloading " : "", - ms - }); - } - - /** - * Returns the mapping tree, if loaded, null otherwise. - * - * @return - */ - public static MappingTree GetMappingTree() { - return MINECRAFT_MAPPINGS; - } - - /** - * Returns the server namespace. This value can be sent to the MappingTree to get the actual class that - * is running. - * @return - */ - public static int GetServerNamespace() { - if(ServerType == -1) { - // Paper has 2 versions, "moj-mapped" and "spigot" mapped. As we support paper and also spigot, we need - // to check which mapping version the server is running, and select the correct mapping segment. - ServerType = PacketJumper.GetMojangNamespace(); - // This class is named net.minecraft.network.protocol.status.ServerboundStatusRequestPacket in mojang mappings - if(ReflectionUtils.classExists("net.minecraft.network.protocol.status.PacketStatusInStart")) { - ServerType = PacketJumper.GetSpigotNamespace(); - } - } - return ServerType; - } - - /** - * Returns the Spigot namespace. Generally, one shouls use GetServerNamespace() instead. - * @return - */ - public static int GetSpigotNamespace() { - return SpigotNamespace; - } - - /** - * Returns the Mojang namespace. This should be used when sending and receiving names from users. - * @return - */ - public static int GetMojangNamespace() { - return MojangNamespace; - } -} diff --git a/src/main/java/com/laytonsmith/tools/ShellEventMixin.java b/src/main/java/com/laytonsmith/tools/ShellEventMixin.java index 3d968a95ef..fa1b51e485 100644 --- a/src/main/java/com/laytonsmith/tools/ShellEventMixin.java +++ b/src/main/java/com/laytonsmith/tools/ShellEventMixin.java @@ -1,6 +1,6 @@ package com.laytonsmith.tools; -import com.laytonsmith.core.events.AbstractEvent; +import com.laytonsmith.core.events.AbstractGenericEvent; import com.laytonsmith.core.events.BindableEvent; import com.laytonsmith.core.events.CancellableEvent; import com.laytonsmith.core.events.EventMixinInterface; @@ -14,9 +14,9 @@ */ public class ShellEventMixin implements EventMixinInterface { - AbstractEvent event; + AbstractGenericEvent event; - public ShellEventMixin(AbstractEvent e) { + public ShellEventMixin(AbstractGenericEvent e) { this.event = e; } diff --git a/src/main/resources/docs/Dev b/src/main/resources/docs/Dev new file mode 100644 index 0000000000..572ccf605b --- /dev/null +++ b/src/main/resources/docs/Dev @@ -0,0 +1,9 @@ +If you are interested in running the dev versions, you can find the latest build at the +[http://builds.enginehub.org/job/commandhelper/ Build] site. Dev builds are usually stable, however, should the light +next to the build be red or yellow instead of blue, you should probably avoid that build. In general, you should stay in +the IRC channel ([http://webchat.esper.net/?channels=CommandHelper irc.esper.net #CommandHelper]) in case there are +issues, so they can be quickly resolved, and you can be notified of potential problems, and new builds. The website +is for the dev build, so you can have documentation for the newest builds. IRC is the preferred method of bug reporting, +however the forum or dev.bukkit.org are also ok. + +{{LearningTrail}} diff --git a/src/main/resources/docs/Developer_Guide b/src/main/resources/docs/Developer_Guide index b2e8f30474..5a633a6d5a 100644 --- a/src/main/resources/docs/Developer_Guide +++ b/src/main/resources/docs/Developer_Guide @@ -20,7 +20,7 @@ related functions, and data handling functions. All implementations must accept that the compiler will use the default implementation of all these functions during compilation, for static code analysis. Most of these functions are deeply integrated with the compiler anyways, and cannot be generically separated from the compilation -process regardless. +process regardless. == Compilation == There are 3 distinct stages during compilation. Lexing, Compiling, and Optimizing/Linking. @@ -33,13 +33,13 @@ should the need arise. === Compiling === Compilation converts the token stream into a Abstract Syntax Tree (AST). This step -can emit only a few errors, (mostly mismatched parenthesis/braces) because the -lexer will have already halted if the syntax is wrong, and the optimizer is what +can emit only a few errors, (mostly mismatched parenthesis/braces) because the +lexer will have already halted if the syntax is wrong, and the optimizer is what actually causes linking errors. The compiler is a typical recursive decent parser, only it offloads the complexity of the infix notation parsing to the __autoconcat__ function, which runs separately later, during optimization. -=== Optimizing/Linking === +=== Optimizing/Linking === Once initial code compilation has occurred, linking happens. Since the linking process happens after control structures have been analysed, this allows @@ -67,7 +67,7 @@ from the AST, or by simply emitting warnings/errors for whatever conditions they check for. == Adding a function == -Adding a function is simple. Create a new class that implements +Adding a function is simple. Create a new class that implements %%GET_SIMPLE_CLASS|.*|Function%%, (or more probably extends %%GET_SIMPLE_CLASS|.*|AbstractFunction%% and tag it with @api. It is important to read and be very familiar with the methods in %%GET_SIMPLE_CLASS|.*|Function%%, so you know which methods that are optional should @@ -81,7 +81,7 @@ programmer to provide the data correctly. This makes it so that if the documenta for instance, but the restriction being missing is a bug, then the bug will be in both the documentation AND the code, and will therefore be much easier to catch. Besides restrictions, which are discussed below, the biggest thing this provides is type safety. Until strong typing is fully supported, the system will be mostly a runtime-only check, -though hard coded values that are incorrect will be able to be caught at runtime. Generics are also supported, +though hard coded values that are incorrect will be able to be caught at runtime. Generics are also supported, and are required for return types that return closures and arrays, and eventually all parameters that are closures or arrays. These restrictions will be caught via unit test, as Java has no way to make this a compile error. Additionally, several restrictions (used as MAnnotations) may be added, which will provide @@ -111,9 +111,9 @@ the MAnnotations available in Java will eventually be available to scripts, but functions. Regardless, the parameter restrictions will all work the same, they work to reduce the effort required to checking rote aspects of the parameter, and provide a way for the runtime to generically handle those cases. Additionally, the compiler is aware of many of these annotations, and where possible, will provide the same functionality, but at compile time, where possible, allowing errors -to be caught more quickly. +to be caught more quickly. -The annotations that are supported by either the compiler or runtime are listed below, but all of them must implement +The annotations that are supported by either the compiler or runtime are listed below, but all of them must implement GET_SIMPLE_CLASS|.*compiler|CompilerAwareAnnotation to be valid parameter annotations. Each annotation has it's own documentation in the normal API. @@ -130,7 +130,7 @@ Parameters tagged with this must match a regex. It is only taggable on CStrings. === Documentation and argument builder === Each function has embedded java doc and type information. This is extremely important to keep accurate, both for the sake of the user, and for a technical need. The docs() -method should return the plain text user readable documentation, which summarizes the +method should return the plain text user readable documentation, which summarizes the behavior of the function. This information isn't used programmatically in any way by the compiler, but it is used of course by the users, and should accurately reflect the behavior of the function. Previously, the docs() method needed to return a string @@ -149,11 +149,11 @@ provide the %%GET_SIMPLE_CLASS|.*Optimizable|OptimizationOption%%.TERMINAL optim The arguments() method returns the function's signature. For some functions, this will be quite complex, for others, it should be straightforward. By design, no arguments are passed to this method, because all arguments (and their defaults) should be constant, and should never vary -based on runtime parameters. (This complicates the design, but more importantly prevents +based on runtime parameters. (This complicates the design, but more importantly prevents certain optimizations from occurring.) Functions may have multiple signatures, that is, completely conflicting signature types, though this behavior is only provided for backwards compatibility, and should not be used for new functions. Disjoint types are recommended instead, -or general simplification of the function instead. See the methods in the +or general simplification of the function instead. See the methods in the %%GET_SIMPLE_CLASS|.*arguments|Argument%% class for more information on the various options available when creating arguments, and %%GET_SIMPLE_CLASS|.*|ArgumentBuilder%% for information about the ArgumentBuilder as a whole. @@ -164,7 +164,7 @@ the addition of platform specific abstraction layers may complicate the process some, both for events and functions. In general, adding an event only requires two steps, though in practice it may require additions in several places. The first step is to provide the event object via implementing %%GET_SIMPLE_CLASS|.*|Event%% -(or extending %%GET_SIMPLE_CLASS|.*|AbstractEvent%%) and tagging it with @api. The +(or extending %%GET_SIMPLE_CLASS|.*|AbstractGenericEvent%%) and tagging it with @api. The second step is to actually hook into whatever system there is for actually triggering the events, and calling %%GET_SIMPLE_CLASS|.*|EventUtils%% TriggerListener method, with the event name and event driver, and the actual event object that it will process. diff --git a/src/main/resources/docs/Extension_Development b/src/main/resources/docs/Extension_Development index 0e39a3d1c4..21bfa38754 100644 --- a/src/main/resources/docs/Extension_Development +++ b/src/main/resources/docs/Extension_Development @@ -69,7 +69,7 @@ public class EventGroup { } @api - public static class some_event extends AbstractEvent { + public static class some_event extends AbstractGenericEvent { } @@ -91,7 +91,7 @@ annotation. All of the built in tools use this mechanism, and extensions can pro ==== Persistence Network Data Sources ==== Implement the DataSource interface (or extend the AbstractDataSource class) and tag the class with the @datasource -annotation. +annotation. === Maven === @@ -155,13 +155,13 @@ Please include the following snippit under the <build><plugins> === Obfuscation/ProGuard === -Some extension devs have expressed a desire to obfuscate their code. Unfortunately, there's a gotcha: the caching -system we use runs before ProGuard obfuscates things, causing the original class names to be saved to the cache -instead of the obfuscated ones. The only way currently known to get around this is to tell ProGuard to not obfuscate -any of the extension points (lifecycle, function or event classes), via the -keep class option in the +Some extension devs have expressed a desire to obfuscate their code. Unfortunately, there's a gotcha: the caching +system we use runs before ProGuard obfuscates things, causing the original class names to be saved to the cache +instead of the obfuscated ones. The only way currently known to get around this is to tell ProGuard to not obfuscate +any of the extension points (lifecycle, function or event classes), via the -keep class option in the plugin's configuration section. -However, one should be able to use the annotated extension points as wrappers for the true code, which would reside +However, one should be able to use the annotated extension points as wrappers for the true code, which would reside in a different class. === Coding Standards === diff --git a/src/main/resources/docs/Packet_Jumper b/src/main/resources/docs/Packet_Jumper index 68b6c60b8b..7535157452 100644 --- a/src/main/resources/docs/Packet_Jumper +++ b/src/main/resources/docs/Packet_Jumper @@ -3,7 +3,8 @@ In general, in order to successfully use this system, you must have a basic unde of the actual Minecraft protocol, though this tutorial can help bridge any gaps you might have in your understanding. Code that uses this functionality is liable to break on every Minecraft version, and so should be avoided as best as possible. Some tooling exists to help mitigate this churn, but it is always better to use other, existing API -methods if they exist, and this should only be used as a last resort. +methods if they exist, and this should only be used as a last resort. Scoreboard operations, for instance, are fully +covered by the API, and therefore should generally use packets to accomplish goals. Also note that this guide has been written for 1.20.4, and while the overall procedures outlined will likely be relevant for all versions, the specifics (particularly packet ids and names) are likely to quickly become outdated. @@ -47,7 +48,7 @@ currently possible, as {{function|set_entity_loc}} actually moves the entity for however, the server is simply sending the "entity move" packet to each player individually. If we instead take over on tracking the entity (or indeed, not even creating a real entity, and just having a "ghost" entity that we manually track) we can instead send raw packets to whichever individual players we like. This has the effect of requiring us -to implement entity movement ourselves, in addition to using raw packets, but it is technically possible. +to implement entity movement and interaction ourselves, in addition to using raw packets, but it is technically possible. Before we can even begin writing code however, we need to understand exactly what task we are trying to accomplish, and which packets will need to be used to make the player see what we are wanting. The first step is to figure out @@ -56,7 +57,8 @@ which packet to use. In general, you'll want to familiarize yourself with protocol. The wiki here is hand written, so in general, this isn't guaranteed to be accurate, but the guide is good enough to give you an idea of what fields do what. Additionally, you can use [https://mappings.cephx.dev/ https://mappings.cephx.dev/] to cross check packet information across various mappings. -CommandHelper exclusively exposes the Mojang mappings. +CommandHelper exclusively exposes the Mojang mappings, regardless of what server you are running, and what mappings +it uses (it is dynamically determined and translated into the appropriate mapping for you). In our example of having entities move differently for different players, we are want to find something that can update an entity position. We're also wanting to run this during normal gameplay, and we want to send something @@ -102,11 +104,13 @@ There is a bunch of information in this object, which tells us specifics about t "superclass": "net.minecraft.network.protocol.game.PacketPlayOutEntity", "deprecated": false, "name": "REL_ENTITY_MOVE", + "comment": "", "id": 44, "fields": { "onGround": { "field": 6, "name": "onGround", + "comment": "", "type": { "originalType": "boolean", "type": "ms.lang.boolean" @@ -115,6 +119,7 @@ There is a bunch of information in this object, which tells us specifics about t "yRot": { "field": 4, "name": "yRot", + "comment": "", "type": { "originalType": "byte", "type": "ms.lang.int" @@ -123,6 +128,7 @@ There is a bunch of information in this object, which tells us specifics about t "za": { "field": 3, "name": "za", + "comment": "", "type": { "originalType": "short", "type": "ms.lang.int" @@ -131,6 +137,7 @@ There is a bunch of information in this object, which tells us specifics about t "xRot": { "field": 5, "name": "xRot", + "comment": "", "type": { "originalType": "byte", "type": "ms.lang.int" @@ -139,6 +146,7 @@ There is a bunch of information in this object, which tells us specifics about t "ya": { "field": 2, "name": "ya", + "comment": "", "type": { "originalType": "short", "type": "ms.lang.int" @@ -147,6 +155,7 @@ There is a bunch of information in this object, which tells us specifics about t "hasRot": { "field": 7, "name": "hasRot", + "comment": "", "type": { "originalType": "boolean", "type": "ms.lang.boolean" @@ -155,6 +164,7 @@ There is a bunch of information in this object, which tells us specifics about t "xa": { "field": 1, "name": "xa", + "comment": "", "type": { "originalType": "short", "type": "ms.lang.int" @@ -163,6 +173,7 @@ There is a bunch of information in this object, which tells us specifics about t "entityId": { "field": 0, "name": "entityId", + "comment": "", "type": { "originalType": "int", "type": "ms.lang.int" @@ -171,6 +182,7 @@ There is a bunch of information in this object, which tells us specifics about t "hasPos": { "field": 8, "name": "hasPos", + "comment": "", "type": { "originalType": "boolean", "type": "ms.lang.boolean" @@ -189,7 +201,8 @@ to find a better packet. Most often, these are packets which have just been repl effectively the same functionality, but you may be forced to use the deprecated class anyways if there is no equivalent. ''name'' is the name of the packet that you will use in code, so make note of this. ''id'' is only for reference, and should not be used except to compare with other references to the exact same version of the protocol. ''class'' denotes -the Java class of this packet, and is only used for reference. +the Java class of this packet, and is only used for reference. You'll also notice a blank ''comment'' field. This +is rarely populated, but when present, offers useful additional information about the field or class. ''fields'' is the most interesting property. This defines the different fields that are available in the packet. Each of the field objects contains the following properties: @@ -207,6 +220,7 @@ In this example, there are no generics, but let's look at a field where that is "profileIds": { "field": 0, "name": "profileIds", + "comment": "", "type": { "originalType": "java.util.List", "genericType": { @@ -230,6 +244,7 @@ Enums are another type that has special support: "source": { "field": 2, "name": "source", + "comment": "", "type": { "originalType": "net.minecraft.sounds.SoundSource", "enumType": "net.minecraft.sounds.SoundSource", @@ -263,6 +278,13 @@ unsupported types don't matter to your use case. The list of unsupported types is added to a top level ''__UnsupportedTypes'' array. This is only for reference. +You also generally need to understand how the entire server works together, in order to provide a complete solution +to the problem you're trying to solve. For instance, in our example, even if we successfully send the packet to +move an entity for the individual player, the server will occasionally try to reset the entity position. You may not +inherently know what packets the server is sending or receiving, so you may need to use a tool such as +[https://github.com/adepierre/SniffCraft SniffCraft] which works as a proxy between the client and server, and can +log packets in both directions, which can give you an idea of what kind of packets you need to emulate and/or modify. + == Implementation == Once all the necessary packet and field information has been determined, we can begin implementation. @@ -292,10 +314,100 @@ send_packet(@player, @packet); This will send this packet to the single player, which will cause the entity to move forward one block in the x direction. +However, after a few seconds, the entity will move back to its original position! + +This is because the server occasionally sends an ENTITY_TELEPORT packet to the client. We must intercept that packet, +and ideally overwrite the position in the packet to that of our "fake" entity position. In this case you could also +cancel the packet (all packet send and receive events are cancellable) but in this case it's probably a better idea to +overwrite the values, but for example's sake here's how simply cancelling it would look: + +<%CODE| +bind('packet_sent', null, array(protocol: 'PLAY', type: 'ENTITY_TELEPORT', values: + array(id: get_entity_transient_id(@entityUuid))), @event) { + cancel(); +} +// We also have to register for the events to start firing for these packet types. +register_packet('PLAY', 'ENTITY_TELEPORT'); +%> + +By default, CommandHelper doesn't actually register to listen to any packets, as this would be unnecessarily +inefficient. The {{function|register_packet}} function tells CommandHelper that it needs to start processing events +of the given type, and is required before the binds for these packets will start to be triggered. + +{{TakeNote=In the REL_ENTITY_MOVE event, the id was named "entityId" and in ENTITY_TELEPORT, it's just "id". Luckily, +CommandHelper offers some additional compile time checks on the values array, if you hardcode the protocol and type +parameters, which is highly recommended. This will make it easier to quickly see if you have errors in your +prefilters.}} + +We also need to handle the case where the player logs out and logs back in. In this case, the entity will be reset +back to the original location, because the server sends the SPAWN_ENTITY packet to cause the player to load in the +entities at first, but the trouble is, this includes the server-controlled position location, which is not where we +want the entity to be for this player. Also, in this case, we can't simply cancel the packet as we did for the +ENTITY_TELEPORT packet, if we did, the client wouldn't see the entity at all, we instead have to modify the +contents of the packet individually. In this case, we'll keep it simple, and only modify the x, y, and z parameters, +but there are others that you may need to modify as well, such as the velocity and angle, etc. + +Unlike most events, we can't use the {{function|modify_event}} function, but we can write directly to the packet with +{{function|packet_write}}. + +<%CODE| +bind('packet_sent', null, array(protocol: 'PLAY', type: 'SPAWN_ENTITY', + values: array(id: get_entity_transient_id(@entityId))), @event, @entityId) { + // This is up to whatever your logic is, to get the location for the entity on the per player basis + @pos = _getEntityPosition(@entityId, @event['player']); + packet_write(@event['packet'], 'x', @pos['x']); + packet_write(@event['packet'], 'y', @pos['y']); + packet_write(@event['packet'], 'z', @pos['z']); +} +// Don't forget, we have to register to get events for this packet type. +register_packet('PLAY', 'SPAWN_ENTITY'); +%> + +This is likely the best handling for the TELEPORT_ENTITY packet as well, rather than cancelling it, since this will +prevent desyncs from moving the entity without any way to recover. + +So, if you don't know which packet events you need to intercept, how can you find that? One of the easiest ways is to +use a packet sniffer, which will log incoming and outgoing packets to your client. This can be used to zero in on the +packets you need to care about, but will take a bit of sleuthing. Using +[https://github.com/adepierre/SniffCraft SniffCraft] for instance, we can intercept the packets, and print the packet +data for relevant looking packets, compare that to the wiki.vg and other docs, and see if that is the packet we're +interested in intercepting. A warning, this is not necessarily easy, and you may not find answers for what you're +looking to do, instead requiring reverse engineering to get there. Hopefully with the tools provided in this guide, +you will be able to get a head start on any detective work. + == Conversions == For some types, conversions are obvious, a field with a Java int type accepts a MethodScript int type. However, for more complex type conversions, it is not always obvious. This table lists the supported conversions. +Implicit conversions: All numeric types (floating point, integers, including bytes), Strings and UUIDs (to string), +List/Collection/Set/EnumSet/Map/arrays except byte arrays to array, and byte arrays to byte_array. + +Different servers have different class names, and so this list only gives you an indication of the specific class +name, the specific class name is obtained via ProtocolLib. More classes will be supported over time, see the +__UnsupportedTypes value in the return of {{function|all_packets}} for a list of types which CommandHelper does not +currently support. + +{| +|- +! scope="col" width="40%" | Java Type +! scope="col" width="60%" | Conversion +|- +| Optional +| NOT SUPPORTED YET. An array with 0 items if the item isn't present, otherwise an array with the item at index 0. When writing, \ +must be an array of length 0 or 1. +|- +| IChatBaseComponent +| A json string. +|- +| ItemStack +| The same item stack array used elsewhere +|- +| BlockPos +| An array containing 'x', 'y', and 'z' integers. +|} + +Also note that double comparisons are done with a delta of 0.0001. If you need something more or less precise, +you must do so inside the event handler, not inside the prefilter. == Unannounced Packets == Actually, the packet creation and packet logic doesn't strictly need to use known packets. Servers can dynamically diff --git a/src/test/java/com/laytonsmith/core/functions/EnchantmentsTest.java b/src/test/java/com/laytonsmith/core/functions/EnchantmentsTest.java new file mode 100644 index 0000000000..a8c6ff18e4 --- /dev/null +++ b/src/test/java/com/laytonsmith/core/functions/EnchantmentsTest.java @@ -0,0 +1,77 @@ +package com.laytonsmith.core.functions; + +import com.laytonsmith.abstraction.MCPlayer; +import com.laytonsmith.abstraction.MCServer; +import com.laytonsmith.core.Static; +import com.laytonsmith.core.environments.CommandHelperEnvironment; +import com.laytonsmith.testing.StaticTest; +import static com.laytonsmith.testing.StaticTest.GetFakeServer; +import static com.laytonsmith.testing.StaticTest.GetOnlinePlayer; +import static com.laytonsmith.testing.StaticTest.InstallFakeServerFrontend; +import static com.laytonsmith.testing.StaticTest.SRun; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import static org.mockito.Mockito.verify; +//import org.powermock.core.classloader.annotations.PowerMockIgnore; +//import org.powermock.core.classloader.annotations.PrepareForTest; +//import org.powermock.modules.junit4.PowerMockRunner; + +/** + * + * + */ +//@RunWith(PowerMockRunner.class) +//@PrepareForTest(Static.class) +//@PowerMockIgnore({"javax.xml.parsers.*", "com.sun.org.apache.xerces.internal.jaxp.*"}) +public class EnchantmentsTest { + + MCServer fakeServer; + MCPlayer fakePlayer; + com.laytonsmith.core.environments.Environment env; + + public EnchantmentsTest() throws Exception { + InstallFakeServerFrontend(); + env = Static.GenerateStandaloneEnvironment(); + env = env.cloneAndAdd(new CommandHelperEnvironment()); + } + + @BeforeClass + public static void setUpClass() throws Exception { + } + + @AfterClass + public static void tearDownClass() throws Exception { + } + + @Before + public void setUp() throws Exception { + fakeServer = GetFakeServer(); + fakePlayer = GetOnlinePlayer(fakeServer); + env.getEnv(CommandHelperEnvironment.class).SetPlayer(fakePlayer); + StaticTest.InstallFakeConvertor(fakePlayer); + Static.InjectPlayer(fakePlayer); + } + + @After + public void tearDown() { + } + + @Test + /** + * This is an interesting test. Because the server implementation has to implement the individual enchantments, they + * aren't implemented here, so everything returns an empty array. However, the test is more for testing array.clone + * than the enchantments themselves. + */ + public void testGetEnchants() throws Exception { + SRun("assign(@a, get_enchants(array('name': 'DIAMOND_SWORD')))\n" + + "array_push(@a, 'test')\n" + + "assign(@b, get_enchants(array('name': 'DIAMOND_SWORD')))\n" + + "msg(@a)\n" + + "msg(@b)\n", fakePlayer); + verify(fakePlayer).sendMessage("{test}"); + verify(fakePlayer).sendMessage("{}"); + } +} diff --git a/src/test/java/com/laytonsmith/testing/StaticTest.java b/src/test/java/com/laytonsmith/testing/StaticTest.java index 67643bc945..efc8f23e2a 100644 --- a/src/test/java/com/laytonsmith/testing/StaticTest.java +++ b/src/test/java/com/laytonsmith/testing/StaticTest.java @@ -64,7 +64,7 @@ import com.laytonsmith.core.environments.CommandHelperEnvironment; import com.laytonsmith.core.environments.Environment; import com.laytonsmith.core.environments.GlobalEnv; -import com.laytonsmith.core.events.AbstractEvent; +import com.laytonsmith.core.events.AbstractGenericEvent; import com.laytonsmith.core.events.BindableEvent; import com.laytonsmith.core.events.EventMixinInterface; import com.laytonsmith.core.exceptions.CRE.AbstractCREException; @@ -909,7 +909,7 @@ public static class FakeServerMixin implements EventMixinInterface { public static MCPlayer fakePlayer; public boolean cancelled = false; - public FakeServerMixin(AbstractEvent e) { + public FakeServerMixin(AbstractGenericEvent e) { }