From e2f5817d1efeb3169503e139c3e1dfa51657c2aa Mon Sep 17 00:00:00 2001 From: Lixfel Date: Sat, 18 May 2024 12:52:49 +0200 Subject: [PATCH] WIP --- .../comphenix/tinyprotocol/TinyProtocol.java | 2 +- .../src/de/steamwar/core/Core.java | 15 +- .../src/de/steamwar/plugin/Reflection.java | 198 ++++++++++++++++++ .../src/de/steamwar/plugin/SWPlugin.java | 139 ++++++++++++ .../src/de/steamwar/plugin/SpigotCore.java | 98 +++++++++ .../steamwar/plugin/SteamWarClassLoader.java | 87 ++++++++ 6 files changed, 534 insertions(+), 5 deletions(-) create mode 100644 SpigotCore_Main/src/de/steamwar/plugin/Reflection.java create mode 100644 SpigotCore_Main/src/de/steamwar/plugin/SWPlugin.java create mode 100644 SpigotCore_Main/src/de/steamwar/plugin/SpigotCore.java create mode 100644 SpigotCore_Main/src/de/steamwar/plugin/SteamWarClassLoader.java diff --git a/SpigotCore_Main/src/com/comphenix/tinyprotocol/TinyProtocol.java b/SpigotCore_Main/src/com/comphenix/tinyprotocol/TinyProtocol.java index caa2bb8..0777c6c 100644 --- a/SpigotCore_Main/src/com/comphenix/tinyprotocol/TinyProtocol.java +++ b/SpigotCore_Main/src/com/comphenix/tinyprotocol/TinyProtocol.java @@ -194,7 +194,7 @@ public class TinyProtocol implements Listener { private Object filterPacket(Player player, Object packet) { List> filters = packetFilters.getOrDefault(packet.getClass(), Collections.emptyList()); - for(BiFunction filter : filters) { + for(BiFunction filter : filters) { //TODO spurious concurrentModificationException packet = filter.apply(player, packet); if(packet == null) diff --git a/SpigotCore_Main/src/de/steamwar/core/Core.java b/SpigotCore_Main/src/de/steamwar/core/Core.java index 7e0b371..6e2b186 100644 --- a/SpigotCore_Main/src/de/steamwar/core/Core.java +++ b/SpigotCore_Main/src/de/steamwar/core/Core.java @@ -26,6 +26,7 @@ import de.steamwar.core.events.*; import de.steamwar.message.Message; import de.steamwar.network.NetworkReceiver; import de.steamwar.network.handlers.ServerDataHandler; +import de.steamwar.plugin.SteamWarClassLoader; import de.steamwar.sql.SchematicNode; import de.steamwar.sql.SteamwarUser; import de.steamwar.sql.internal.Statement; @@ -44,10 +45,9 @@ public class Core extends JavaPlugin{ public static final Message MESSAGE = new Message("SpigotCore", Core.class.getClassLoader()); - private static final int VERSION = Integer.parseInt(Bukkit.getServer().getClass().getPackage().getName().split("_", 3)[1]); - + @Deprecated public static int getVersion(){ - return VERSION; + return SteamWarClassLoader.MINECAFT_MAJOR_VERSION; } private static JavaPlugin instance; @@ -68,7 +68,14 @@ public class Core extends JavaPlugin{ @Override public void onEnable() { - errorHandler = new ErrorHandler(); + try { + System.out.println(new SteamWarClassLoader(this.getClassLoader()).loadClass("org.bukkit.craftbukkit.CraftServer").getName()); + new SteamWarClassLoader(this.getClassLoader()).loadClass("de.steamwar.SteamWarPlugin"); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + + errorHandler = new ErrorHandler(); crashDetector = new CrashDetector(); SWCommandUtils.init((SWTypeMapperCreator, CommandSender, Object>) (mapper, tabCompleter) -> new TypeMapper() { diff --git a/SpigotCore_Main/src/de/steamwar/plugin/Reflection.java b/SpigotCore_Main/src/de/steamwar/plugin/Reflection.java new file mode 100644 index 0000000..461c704 --- /dev/null +++ b/SpigotCore_Main/src/de/steamwar/plugin/Reflection.java @@ -0,0 +1,198 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.plugin; + +import de.steamwar.core.Core; +import jdk.internal.misc.Unsafe; +import lombok.AllArgsConstructor; +import lombok.experimental.UtilityClass; + +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Arrays; + + +@UtilityClass +public final class Reflection { + @AllArgsConstructor + public static class Constructor { + private final java.lang.reflect.Constructor constructor; + public Object newInstance(Object... arguments) { + try { + return constructor.newInstance(arguments); + } catch (Exception e) { + throw new IllegalArgumentException("Cannot invoke constructor " + constructor, e); + } + } + } + + @AllArgsConstructor + public static class Method { + private final java.lang.reflect.Method method; + public Object invoke(Object target, Object... arguments) { + try { + return method.invoke(target, arguments); + } catch (Exception e) { + throw new IllegalArgumentException("Cannot invoke method " + method, e); + } + } + public Object invokeStatic(Object... arguments) { + return invoke(null, arguments); + } + } + + @AllArgsConstructor + public static class FieldAccessor { + private final Field field; + @SuppressWarnings("unchecked") + public T get(Object target) { + try { + return (T) field.get(target); + } catch (IllegalAccessException e) { + throw new IllegalArgumentException("Cannot access reflection.", e); + } + } + public void set(Object target, Object value) { + try { + field.set(target, value); + } catch (IllegalAccessException e) { + throw new IllegalArgumentException("Cannot access reflection.", e); + } + } + } + + public static FieldAccessor getField(Class target, String name, Class fieldType) { + return getField(target, name, fieldType, 0); + } + + public static FieldAccessor getField(String className, String name, Class fieldType) { + return getField(getClass(className), name, fieldType, 0); + } + + public static FieldAccessor getField(Class target, Class fieldType, int index) { + return getField(target, null, fieldType, index); + } + + public static FieldAccessor getField(String className, Class fieldType, int index) { + return getField(getClass(className), fieldType, index); + } + + public static FieldAccessor getField(Class target, Class fieldType, int index, Class... parameters) { + return getField(target, null, fieldType, index, parameters); + } + + private static FieldAccessor getField(Class target, String name, Class fieldType, int index, Class... parameters) { + for (final Field field : target.getDeclaredFields()) { + if(matching(field, name, fieldType, parameters) && index-- <= 0) { + field.setAccessible(true); + return new FieldAccessor(field); + } + } + + // Search in parent classes + if (target.getSuperclass() != null) + return getField(target.getSuperclass(), name, fieldType, index); + + throw new IllegalArgumentException("Cannot find field with type " + fieldType); + } + + private static boolean matching(Field field, String name, Class fieldType, Class... parameters) { + if(name != null && !field.getName().equals(name)) + return false; + + if(!fieldType.isAssignableFrom(field.getType())) + return false; + + if(parameters.length > 0) { + Type[] arguments = ((ParameterizedType)field.getGenericType()).getActualTypeArguments(); + + for(int i = 0; i < parameters.length; i++) { + if(arguments[i] instanceof ParameterizedType ? ((ParameterizedType) arguments[i]).getRawType() != parameters[i] : arguments[i] != parameters[i]) + return false; + } + } + return true; + } + + public static Method getMethod(String className, String methodName, Class... params) { + return getTypedMethod(getClass(className), methodName, null, params); + } + + public static Method getMethod(Class clazz, String methodName, Class... params) { + return getTypedMethod(clazz, methodName, null, params); + } + + public static Method getTypedMethod(Class clazz, Class returnType, Class... params) { + return getTypedMethod(clazz, null, returnType, params); + } + + public static Method getTypedMethod(Class clazz, String methodName, Class returnType, Class... params) { + for (final java.lang.reflect.Method method : clazz.getDeclaredMethods()) { + if ((methodName == null || method.getName().equals(methodName)) + && (returnType == null || method.getReturnType().equals(returnType)) + && Arrays.equals(method.getParameterTypes(), params)) { + method.setAccessible(true); + return new Method(method); + } + } + + // Search in every superclass + if (clazz.getSuperclass() != null) + return getTypedMethod(clazz.getSuperclass(), methodName, returnType, params); + + throw new IllegalStateException(String.format("Unable to find method %s (%s).", methodName, Arrays.asList(params))); + } + + public static Constructor getConstructor(String className, Class... params) { + return getConstructor(getClass(className), params); + } + + public static Constructor getConstructor(Class clazz, Class... params) { + for (final java.lang.reflect.Constructor constructor : clazz.getDeclaredConstructors()) { + if (Arrays.equals(constructor.getParameterTypes(), params)) { + constructor.setAccessible(true); + return new Constructor(constructor); + } + } + + throw new IllegalStateException(String.format("Unable to find constructor for %s (%s).", clazz, Arrays.asList(params))); + } + + public static Class getClass(String name) { + try { + return Class.forName(name); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Cannot find " + name, e); + } + } + + public static Object newInstance(Class clazz) { + try { + if (Core.getVersion() > 15) { + return Unsafe.getUnsafe().allocateInstance(clazz); + } else { + return clazz.newInstance(); + } + } catch (InstantiationException | IllegalAccessException e) { + throw new SecurityException("Could not create object", e); + } + } +} diff --git a/SpigotCore_Main/src/de/steamwar/plugin/SWPlugin.java b/SpigotCore_Main/src/de/steamwar/plugin/SWPlugin.java new file mode 100644 index 0000000..47bfcdd --- /dev/null +++ b/SpigotCore_Main/src/de/steamwar/plugin/SWPlugin.java @@ -0,0 +1,139 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.plugin; + +import com.comphenix.tinyprotocol.Reflection; +import de.steamwar.core.CommandRemover; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.SimpleCommandMap; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; +import org.bukkit.plugin.PluginLogger; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.plugin.messaging.PluginMessageListener; +import org.bukkit.scheduler.BukkitScheduler; +import org.bukkit.scheduler.BukkitTask; + +import java.io.File; +import java.util.*; + +@Getter +@AllArgsConstructor +public class SWPlugin { + private final JavaPlugin bukkit; + private final PluginLogger logger; + private final File dataFolder; + + private final Map tasks = new HashMap<>(); + private final List commands = new ArrayList<>(); + private final Set listeners = new HashSet<>(); + private final List outgoingChannels = new ArrayList<>(); + private final List incomingChannels = new ArrayList<>(); + //TODO commands + + public void load() { + + } + + public void unload() { + tasks.values().forEach(BukkitTask::cancel); + tasks.clear(); + outgoingChannels.forEach(channel -> bukkit.getServer().getMessenger().unregisterOutgoingPluginChannel(bukkit, channel)); + outgoingChannels.clear(); + incomingChannels.forEach(channel -> bukkit.getServer().getMessenger().unregisterIncomingPluginChannel(bukkit, channel)); + incomingChannels.clear(); + listeners.forEach(HandlerList::unregisterAll); + listeners.clear(); + commands.forEach(CommandRemover::removeAll); + commands.clear(); + } + + public YamlConfiguration getConfig() { + return YamlConfiguration.loadConfiguration(new File(dataFolder, "config.yml")); + } + + public void runTask(Runnable runnable) { + getScheduler().runTask(bukkit, runnable); + } + + public Runnable runTaskLater(Runnable runnable, long delay) { + return makeCancellable(runnable, getScheduler().runTaskLater(bukkit, runnable, delay)); + } + + public Runnable runTaskTimer(Runnable runnable, long delay, long frequency) { + return makeCancellable(runnable, getScheduler().runTaskTimer(bukkit, runnable, delay, frequency)); + } + + public void registerOutgoingPluginChannel(String channel) { + outgoingChannels.add(channel); + bukkit.getServer().getMessenger().registerOutgoingPluginChannel(bukkit, channel); + } + + public void registerIncomingPluginChannel(String channel, PluginMessageListener listener) { + incomingChannels.add(channel); + bukkit.getServer().getMessenger().registerIncomingPluginChannel(bukkit, channel, listener); + } + + public void registerEvents(Listener listener) { + listeners.add(listener); + bukkit.getServer().getPluginManager().registerEvents(listener, bukkit); + } + + public void unregisterEvents(Listener listener) { + HandlerList.unregisterAll(listener); + listeners.remove(listener); + } + + public void callEvent(Event event) { + bukkit.getServer().getPluginManager().callEvent(event); + } + + + private static final com.comphenix.tinyprotocol.Reflection.FieldAccessor craftServerCommandMap = Reflection.getField("{obc}.CraftServer", "commandMap", SimpleCommandMap.class); + public void setExecutor(String name, CommandExecutor executor) { + //TODO tab completes + SimpleCommandMap commandMap = craftServerCommandMap.get(bukkit.getServer()); + commandMap.register(bukkit.getName(), new Command(name) { + @Override + public boolean execute(CommandSender commandSender, String s, String[] strings) { + return executor.onCommand(commandSender, this, s, strings); + } + }); + } + + + private BukkitScheduler getScheduler() { + return bukkit.getServer().getScheduler(); + } + + private Runnable makeCancellable(Runnable runnable, BukkitTask task) { + tasks.put(runnable, task); + return () -> { + task.cancel(); + tasks.remove(runnable); + }; + } +} diff --git a/SpigotCore_Main/src/de/steamwar/plugin/SpigotCore.java b/SpigotCore_Main/src/de/steamwar/plugin/SpigotCore.java new file mode 100644 index 0000000..091d296 --- /dev/null +++ b/SpigotCore_Main/src/de/steamwar/plugin/SpigotCore.java @@ -0,0 +1,98 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.plugin; + + +import org.bukkit.plugin.InvalidDescriptionException; +import org.bukkit.plugin.InvalidPluginException; +import org.bukkit.plugin.PluginLogger; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.*; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; + +public class SpigotCore extends JavaPlugin { + + public static SpigotCore getInstance() { + return JavaPlugin.getPlugin(SpigotCore.class); + } + + private static final String BUKKIT_INDICATOR = "bukkit:"; + private static final String STEAMWAR_INDICATOR = "steamwar:"; + + private final SteamWarClassLoader loader; + + private final List loaded = new ArrayList<>(); + private final List enabled = new ArrayList<>(); + + public SpigotCore() { + loader = new SteamWarClassLoader(this.getClassLoader()); + } + + @Override + public void onLoad() { + //TODO more dynamic than file + File file = new File("plugins.properties"); + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + while(true) { + String plugin = reader.readLine(); + if(plugin == null) + break; + + if(plugin.startsWith(BUKKIT_INDICATOR)) + loadBukkit(plugin.substring(BUKKIT_INDICATOR.length())); + else if(plugin.startsWith(STEAMWAR_INDICATOR)) + loadSteamWar(plugin.substring(STEAMWAR_INDICATOR.length())); + } + } catch (FileNotFoundException e) { + getLogger().log(Level.WARNING, "Did not load any plugins due to missing plugins.properties file."); + } catch (IOException e) { + getLogger().log(Level.WARNING, "Unhandled error reading plugins.properties file.", e); + } + } + + @Override + public void onEnable() { + + } + + @Override + public void onDisable() { + + } + + private void loadSteamWar(String path) { + loader.loadPlugin(new File(path)); + loaded.add(new SWPlugin(this, plugin, new PluginLogger(todo), )); + //TODO multiple plugins per file + + } + + private void loadBukkit(String path) { + try { + //Default plugin config folder is generated from parent file folder + getServer().getPluginManager().loadPlugin(new File(path)); + } catch (InvalidPluginException | InvalidDescriptionException e) { + getLogger().log(Level.SEVERE, e, () -> "Unhandled error loading plugin: " + path); + } + } +} diff --git a/SpigotCore_Main/src/de/steamwar/plugin/SteamWarClassLoader.java b/SpigotCore_Main/src/de/steamwar/plugin/SteamWarClassLoader.java new file mode 100644 index 0000000..eb07488 --- /dev/null +++ b/SpigotCore_Main/src/de/steamwar/plugin/SteamWarClassLoader.java @@ -0,0 +1,87 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.plugin; + +import org.bukkit.Bukkit; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; + +public class SteamWarClassLoader extends URLClassLoader { + static { + registerAsParallelCapable(); + } + + public SteamWarClassLoader(ClassLoader parent) { + super(new URL[0], parent); + /*new URL(); + ZipFile zipFile = new ZipFile("string"); + addURL(); + getResources("").nextElement().openStream();*/ + } + + public void loadPlugin(File file, String name, SWPlugin context) { + //TODO SteamWarPlugin as Class with constructor taking Context (unregister methods for disable) + try { + addURL(file.toURI().toURL()); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + + public static final int MINECAFT_MAJOR_VERSION; + public static final int MINECAFT_MINOR_VERSION; + static { + String[] version = Bukkit.getServer().getBukkitVersion().split("-")[0].split("\\."); + MINECAFT_MAJOR_VERSION = Integer.parseInt(version[1]); + MINECAFT_MINOR_VERSION = version.length > 2 ? Integer.parseInt(version[2]) : 0; + } + + private static final String CRAFTBUKKIT_PREFIX = "org.bukkit.craftbukkit"; + private static final String CRAFTBUKKIT_RELOCATED_PREFIX = Bukkit.getServer().getClass().getPackage().getName(); + private static final boolean CRAFTBUKKIT_RELOCATED = !CRAFTBUKKIT_RELOCATED_PREFIX.substring(CRAFTBUKKIT_PREFIX.length()).isEmpty(); + + private static final boolean NET_MINECRAFT_RELOCATED = MINECAFT_MAJOR_VERSION < 17; + private static final String NET_MINECRAFT_RELOCATED_PREFIX = "net.minecraft.server" + CRAFTBUKKIT_RELOCATED_PREFIX.substring(CRAFTBUKKIT_PREFIX.length()); + public static Class getClass(String name) { + //TODO running on mojang mapped servers (mojang mappings only available starting with 1.14.4) + if(CRAFTBUKKIT_RELOCATED && name.startsWith(CRAFTBUKKIT_PREFIX)) { + return Class.forName(CRAFTBUKKIT_RELOCATED_PREFIX + name.substring(CRAFTBUKKIT_PREFIX.length())); + }else if(NET_MINECRAFT_RELOCATED && name.startsWith("net.minecraft")) { + return Class.forName(NET_MINECRAFT_RELOCATED_PREFIX + "." + name.substring(name.lastIndexOf('.'))); + } + + throw new ClassNotFoundException(name); + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + //TODO running on mojang mapped servers + if(CRAFTBUKKIT_RELOCATED && name.startsWith(CRAFTBUKKIT_PREFIX)) { + return super.loadClass(CRAFTBUKKIT_RELOCATED_PREFIX + name.substring(CRAFTBUKKIT_PREFIX.length())); + }else if(NET_MINECRAFT_RELOCATED && name.startsWith("net.minecraft")) { + return super.loadClass(NET_MINECRAFT_RELOCATED_PREFIX + "." + name.substring(name.lastIndexOf('.'))); + } + + throw new ClassNotFoundException(name); + } +}