From 73301f032db560ee742ee0b58fc7681bf555f8fe Mon Sep 17 00:00:00 2001 From: Bronuh Date: Tue, 14 Nov 2023 11:45:56 +0300 Subject: [PATCH 1/3] Add custom class loader for ModLoader prototype --- .../cc/abro/orchengine/util/ClassCache.java | 83 +++++++++++++++++++ .../java/cc/abro/orchengine/util/Loader.java | 70 ++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 OrchEngine/src/main/java/cc/abro/orchengine/util/ClassCache.java create mode 100644 OrchEngine/src/main/java/cc/abro/orchengine/util/Loader.java diff --git a/OrchEngine/src/main/java/cc/abro/orchengine/util/ClassCache.java b/OrchEngine/src/main/java/cc/abro/orchengine/util/ClassCache.java new file mode 100644 index 00000000..e66727b4 --- /dev/null +++ b/OrchEngine/src/main/java/cc/abro/orchengine/util/ClassCache.java @@ -0,0 +1,83 @@ +package cc.abro.orchengine.util; + +import java.util.*; +import java.util.stream.Collectors; + +public class ClassCache { + private static final Set loadedJarPaths = new HashSet<>(); + private static final HashMap>> loadedJars = new HashMap<>(); + private static final Set knownClasses = new HashSet<>(); + + + /** + * @return список зарегистрированных .jar файлов. + */ + public static Set getLoadedJarPaths(){ + return Collections.unmodifiableSet(loadedJarPaths); + } + + /** + * + * @param path - путь к .jar файлу. + * @return список классов, загруженных из .jar файла по указанному пути. + */ + public static List> getLoadedJarClasses(String path){ + return Collections.unmodifiableList(loadedJars.get(path)); + } + + /** + * @return список всех загруженных Loader'ом классов. + */ + public static List> getAllLoadedClasses(){ + return loadedJars.values().stream() + .flatMap(List::stream).toList(); + } + + /** + * @param clazz - базовый класс. + * @return список всех закэшированных классов, унаследованных от указанного класса. + */ + public static List> getAssignableTo(Class clazz){ + return getAllLoadedClasses().stream().filter(clazz::isAssignableFrom).toList(); + } + + /** + * @param path - путь к .jar файлу. + * @return true, если .jar файл был загружен в кэш. + */ + public static boolean isLoadedJar(String path){ + return loadedJarPaths.contains(path); + } + + /** + * Добавляет класс в кэш. + * @param jar - .jar файл, в котором находится класс. + * @param clazz - сам класс. + */ + public static void addClass(String jar, Class clazz){ + var classname = clazz.getCanonicalName(); + var loadedClasses = getModifiableLoadedJarClasses(jar); + loadedClasses.add(clazz); + knownClasses.add(classname); + } + + /** + * @param name - полное имя класса. + * @return true, если такой класс имеется в кэше. + */ + public static boolean isClassKnown(String name){ + return knownClasses.contains(name); + } + + /** + * Добавляет .jar файл в список зарегистированных. Зарегистрированные .jar файлы будут игрнорироваться при попытке загрузки в {@link Loader#Load(String)}. + * @param path - путь к .jar файлу. + */ + public static void registerLoadedJar(String path){ + loadedJarPaths.add(path); + } + + private static List> getModifiableLoadedJarClasses(String path){ + return loadedJars.computeIfAbsent(path, k -> new ArrayList<>()); + } +} diff --git a/OrchEngine/src/main/java/cc/abro/orchengine/util/Loader.java b/OrchEngine/src/main/java/cc/abro/orchengine/util/Loader.java new file mode 100644 index 00000000..7322e4e3 --- /dev/null +++ b/OrchEngine/src/main/java/cc/abro/orchengine/util/Loader.java @@ -0,0 +1,70 @@ +package cc.abro.orchengine.util; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; +import java.util.jar.JarFile; + +public class Loader { + + + /** + * Загружает классы в указанном .jar файле в {@link cc.abro.orchengine.util.ClassCache}, если он еще не был загружен. + * @param jarPath - путь к .jar файлу. + * @return Список классов загруженнх в ClassCache из этого .jar файла. + */ + public static List> Load(String jarPath) throws IOException { + return Load(jarPath, false); + } + + /** + * Загружает классы в указанном .jar файле в {@link cc.abro.orchengine.util.ClassCache} + * @param jarPath - путь к .jar файлу. + * @param forceLoad - попытается загрузить .jar файл, даже если он уже был загружен. + * @return Список классов загруженнх в ClassCache из этого .jar файла. + */ + public static List> Load(String jarPath, boolean forceLoad) throws IOException { + // Если .jar уже загружен, вернуть закэшированные классы. + if(ClassCache.isLoadedJar(jarPath) && !forceLoad){ + return ClassCache.getLoadedJarClasses(jarPath); + } + + File jarFile = new File(jarPath); + URL jarUrl = jarFile.toURI().toURL(); + ArrayList classNames = new ArrayList<>(); + ArrayList> classes = new ArrayList<>(); + + // Находим все классы в джарнике для использования их в URLClassLoader + try(JarFile jar = new JarFile(jarFile)){ + jar.stream().forEach(jarEntry -> { + if(jarEntry.getName().endsWith(".class")) + { + classNames.add(jarEntry.getName()); + } + }); + } + + // Пытаемся загрузить классы из джарника + try(URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{jarUrl}, ClassLoader.getSystemClassLoader())){ + classNames.forEach(classPath -> { + try{ + String className = classPath.replaceAll("/",".").replace(".class",""); + if(ClassCache.isClassKnown(className)){ + return; // не нужно грузить уже загруженный класс. + } + Class cls = urlClassLoader.loadClass(className); + ClassCache.addClass(jarPath, cls); + }catch(Exception e){ + System.out.println(e); + } + }); + // Регистрируем .jar файл, если удалось его открыть. + ClassCache.registerLoadedJar(jarPath); + } + + return ClassCache.getLoadedJarClasses(jarPath); + } +} From 9daa83a5d167f0ffcd22db62ab54eb7c2bf4c785 Mon Sep 17 00:00:00 2001 From: Bronuh Date: Sat, 18 Nov 2023 11:17:20 +0300 Subject: [PATCH 2/3] Some code style fixes --- .../cc/abro/orchengine/util/ClassCache.java | 3 +-- .../java/cc/abro/orchengine/util/Loader.java | 21 +++++++++---------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/OrchEngine/src/main/java/cc/abro/orchengine/util/ClassCache.java b/OrchEngine/src/main/java/cc/abro/orchengine/util/ClassCache.java index e66727b4..d9746994 100644 --- a/OrchEngine/src/main/java/cc/abro/orchengine/util/ClassCache.java +++ b/OrchEngine/src/main/java/cc/abro/orchengine/util/ClassCache.java @@ -1,7 +1,6 @@ package cc.abro.orchengine.util; import java.util.*; -import java.util.stream.Collectors; public class ClassCache { private static final Set loadedJarPaths = new HashSet<>(); @@ -70,7 +69,7 @@ public static boolean isClassKnown(String name){ } /** - * Добавляет .jar файл в список зарегистированных. Зарегистрированные .jar файлы будут игрнорироваться при попытке загрузки в {@link Loader#Load(String)}. + * Добавляет .jar файл в список зарегистированных. Зарегистрированные .jar файлы будут игрнорироваться при попытке загрузки в {@link Loader#load(String)}. * @param path - путь к .jar файлу. */ public static void registerLoadedJar(String path){ diff --git a/OrchEngine/src/main/java/cc/abro/orchengine/util/Loader.java b/OrchEngine/src/main/java/cc/abro/orchengine/util/Loader.java index 7322e4e3..b49a92c3 100644 --- a/OrchEngine/src/main/java/cc/abro/orchengine/util/Loader.java +++ b/OrchEngine/src/main/java/cc/abro/orchengine/util/Loader.java @@ -16,8 +16,8 @@ public class Loader { * @param jarPath - путь к .jar файлу. * @return Список классов загруженнх в ClassCache из этого .jar файла. */ - public static List> Load(String jarPath) throws IOException { - return Load(jarPath, false); + public static List> load(String jarPath) throws IOException { + return load(jarPath, false); } /** @@ -26,9 +26,9 @@ public static List> Load(String jarPath) throws IOException { * @param forceLoad - попытается загрузить .jar файл, даже если он уже был загружен. * @return Список классов загруженнх в ClassCache из этого .jar файла. */ - public static List> Load(String jarPath, boolean forceLoad) throws IOException { + public static List> load(String jarPath, boolean forceLoad) throws IOException { // Если .jar уже загружен, вернуть закэшированные классы. - if(ClassCache.isLoadedJar(jarPath) && !forceLoad){ + if (ClassCache.isLoadedJar(jarPath) && !forceLoad) { return ClassCache.getLoadedJarClasses(jarPath); } @@ -38,26 +38,25 @@ public static List> Load(String jarPath, boolean forceLoad) throws IOEx ArrayList> classes = new ArrayList<>(); // Находим все классы в джарнике для использования их в URLClassLoader - try(JarFile jar = new JarFile(jarFile)){ + try (JarFile jar = new JarFile(jarFile)) { jar.stream().forEach(jarEntry -> { - if(jarEntry.getName().endsWith(".class")) - { + if (jarEntry.getName().endsWith(".class")){ classNames.add(jarEntry.getName()); } }); } // Пытаемся загрузить классы из джарника - try(URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{jarUrl}, ClassLoader.getSystemClassLoader())){ + try (URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{jarUrl}, ClassLoader.getSystemClassLoader())) { classNames.forEach(classPath -> { - try{ + try { String className = classPath.replaceAll("/",".").replace(".class",""); - if(ClassCache.isClassKnown(className)){ + if (ClassCache.isClassKnown(className)) { return; // не нужно грузить уже загруженный класс. } Class cls = urlClassLoader.loadClass(className); ClassCache.addClass(jarPath, cls); - }catch(Exception e){ + } catch (Exception e) { System.out.println(e); } }); From 342c8e2258c8fc93ed1c6a6d37a11a2630f7bc81 Mon Sep 17 00:00:00 2001 From: Bronuh Date: Sat, 18 Nov 2023 14:58:25 +0300 Subject: [PATCH 3/3] Some cursed mod loading tests --- DemoMod/build.gradle | 20 ++++++++++++++ .../main/java/cc/abro/demomod/DemoMod.java | 12 +++++++++ .../cc/abro/orchengine/util/ClassCache.java | 12 ++++----- .../java/cc/abro/orchengine/util/Mod.java | 5 ++++ .../abro/orchengine/util/ReflectionUtils.java | 11 ++++++++ settings.gradle | 3 ++- src/main/java/cc/abro/tow/GameStart.java | 27 ++++++++++++++++--- 7 files changed, 80 insertions(+), 10 deletions(-) create mode 100644 DemoMod/build.gradle create mode 100644 DemoMod/src/main/java/cc/abro/demomod/DemoMod.java create mode 100644 OrchEngine/src/main/java/cc/abro/orchengine/util/Mod.java diff --git a/DemoMod/build.gradle b/DemoMod/build.gradle new file mode 100644 index 00000000..45cb9e14 --- /dev/null +++ b/DemoMod/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'java' +} + +group = 'cc.abro' +version = '4.3.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + implementation project(path: ':OrchEngine') + testImplementation platform('org.junit:junit-bom:5.9.1') + testImplementation 'org.junit.jupiter:junit-jupiter' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/DemoMod/src/main/java/cc/abro/demomod/DemoMod.java b/DemoMod/src/main/java/cc/abro/demomod/DemoMod.java new file mode 100644 index 00000000..22152180 --- /dev/null +++ b/DemoMod/src/main/java/cc/abro/demomod/DemoMod.java @@ -0,0 +1,12 @@ +package cc.abro.demomod; + +import cc.abro.orchengine.util.Mod; + + +public class DemoMod implements Mod { + + @Override + public void run() { + System.out.println("Running Demo Mod"); + } +} diff --git a/OrchEngine/src/main/java/cc/abro/orchengine/util/ClassCache.java b/OrchEngine/src/main/java/cc/abro/orchengine/util/ClassCache.java index d9746994..bbf3dcb7 100644 --- a/OrchEngine/src/main/java/cc/abro/orchengine/util/ClassCache.java +++ b/OrchEngine/src/main/java/cc/abro/orchengine/util/ClassCache.java @@ -36,7 +36,7 @@ public static List> getAllLoadedClasses(){ * @param clazz - базовый класс. * @return список всех закэшированных классов, унаследованных от указанного класса. */ - public static List> getAssignableTo(Class clazz){ + public static List> getAssignableTo(Class clazz) { return getAllLoadedClasses().stream().filter(clazz::isAssignableFrom).toList(); } @@ -44,7 +44,7 @@ public static List> getAssignableTo(Class clazz){ * @param path - путь к .jar файлу. * @return true, если .jar файл был загружен в кэш. */ - public static boolean isLoadedJar(String path){ + public static boolean isLoadedJar(String path) { return loadedJarPaths.contains(path); } @@ -53,7 +53,7 @@ public static boolean isLoadedJar(String path){ * @param jar - .jar файл, в котором находится класс. * @param clazz - сам класс. */ - public static void addClass(String jar, Class clazz){ + public static void addClass(String jar, Class clazz) { var classname = clazz.getCanonicalName(); var loadedClasses = getModifiableLoadedJarClasses(jar); loadedClasses.add(clazz); @@ -64,7 +64,7 @@ public static void addClass(String jar, Class clazz){ * @param name - полное имя класса. * @return true, если такой класс имеется в кэше. */ - public static boolean isClassKnown(String name){ + public static boolean isClassKnown(String name) { return knownClasses.contains(name); } @@ -72,11 +72,11 @@ public static boolean isClassKnown(String name){ * Добавляет .jar файл в список зарегистированных. Зарегистрированные .jar файлы будут игрнорироваться при попытке загрузки в {@link Loader#load(String)}. * @param path - путь к .jar файлу. */ - public static void registerLoadedJar(String path){ + public static void registerLoadedJar(String path) { loadedJarPaths.add(path); } - private static List> getModifiableLoadedJarClasses(String path){ + private static List> getModifiableLoadedJarClasses(String path) { return loadedJars.computeIfAbsent(path, k -> new ArrayList<>()); } } diff --git a/OrchEngine/src/main/java/cc/abro/orchengine/util/Mod.java b/OrchEngine/src/main/java/cc/abro/orchengine/util/Mod.java new file mode 100644 index 00000000..bce80d9d --- /dev/null +++ b/OrchEngine/src/main/java/cc/abro/orchengine/util/Mod.java @@ -0,0 +1,5 @@ +package cc.abro.orchengine.util; + +public interface Mod { + void run(); +} diff --git a/OrchEngine/src/main/java/cc/abro/orchengine/util/ReflectionUtils.java b/OrchEngine/src/main/java/cc/abro/orchengine/util/ReflectionUtils.java index df1de4cf..01601a7f 100644 --- a/OrchEngine/src/main/java/cc/abro/orchengine/util/ReflectionUtils.java +++ b/OrchEngine/src/main/java/cc/abro/orchengine/util/ReflectionUtils.java @@ -7,6 +7,7 @@ import java.lang.annotation.Annotation; import java.util.Collection; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -49,4 +50,14 @@ public Set createInstances(Set> types){ return instances; } + + + /** + * @param classes + * @param clazz + * @return все классы из списка, унаследованные от указанного класса + */ + public static List> getAssignableTo(List> classes, Class clazz) { + return classes.stream().filter(clazz::isAssignableFrom).toList(); + } } diff --git a/settings.gradle b/settings.gradle index cd8a49a1..703826f4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -8,4 +8,5 @@ */ rootProject.name = 'TOW' -include 'OrchEngine' \ No newline at end of file +include 'OrchEngine' +include 'DemoMod' \ No newline at end of file diff --git a/src/main/java/cc/abro/tow/GameStart.java b/src/main/java/cc/abro/tow/GameStart.java index 4ba02546..16a59a97 100644 --- a/src/main/java/cc/abro/tow/GameStart.java +++ b/src/main/java/cc/abro/tow/GameStart.java @@ -1,17 +1,38 @@ package cc.abro.tow; import cc.abro.orchengine.OrchEngine; +import cc.abro.orchengine.util.Loader; +import cc.abro.orchengine.util.Mod; +import cc.abro.orchengine.util.ReflectionUtils; +import java.io.IOException; import java.util.Arrays; +import java.util.List; import java.util.Set; import java.util.stream.Collectors; public class GameStart { public static void main(String[] args) { - Set activeProfiles = Arrays.stream(args).collect(Collectors.toSet()); - Set packagesForScan = Set.of(GameStart.class.getPackage().getName()); - OrchEngine.start(activeProfiles, packagesForScan); + // Нах не надо, загружать моды можно и без самой игры + //Set activeProfiles = Arrays.stream(args).collect(Collectors.toSet()); + //Set packagesForScan = Set.of(GameStart.class.getPackage().getName()); + //OrchEngine.start(activeProfiles, packagesForScan); + + // Сюда нужно передать путь к джарнику с модом. Хз как собирать его, помогите! + List> classes = null; + try { + classes = Loader.load("./mods/DemoMod.jar"); + } catch (IOException e) { + throw new RuntimeException(e); + } + + // Попытка запустить все моды + ReflectionUtils.getAssignableTo(classes, Mod.class).forEach((c) -> { + var instance = ReflectionUtils.createInstance(c); + ((Mod)instance).run(); + }); + System.exit(0); } }