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 new file mode 100644 index 00000000..bbf3dcb7 --- /dev/null +++ b/OrchEngine/src/main/java/cc/abro/orchengine/util/ClassCache.java @@ -0,0 +1,82 @@ +package cc.abro.orchengine.util; + +import java.util.*; + +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..b49a92c3 --- /dev/null +++ b/OrchEngine/src/main/java/cc/abro/orchengine/util/Loader.java @@ -0,0 +1,69 @@ +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); + } +} 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); } }