Separate plugin types #10

Merged
owlsys merged 14 commits from ecorous/plugin-types into main 2024-06-16 17:13:54 -04:00
8 changed files with 392 additions and 370 deletions
Showing only changes of commit f6715e36a2 - Show all commits

View file

@ -0,0 +1,15 @@
package dev.frogmc.frogloader.api.plugin;
import dev.frogmc.frogloader.api.FrogLoader;
public interface FrogGamePlugin {
default void run() {
}
default boolean isApplicable() {
return false;
}
default void init(FrogLoader loader) throws Exception {
}
}

View file

@ -0,0 +1,24 @@
package dev.frogmc.frogloader.api.plugin;
import dev.frogmc.frogloader.api.mod.ModProperties;
import java.io.File;
import java.nio.file.Path;
public interface FrogModPlugin {
default String loadDirectory() {
return "mods";
}
default boolean isApplicable() {
return false;
}
default boolean isFileApplicable(Path path) {
return false;
}
default ModProperties loadMod(Path path) {
return null;
}
}

View file

@ -2,6 +2,7 @@ package dev.frogmc.frogloader.api.plugin;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import dev.frogmc.frogloader.api.FrogLoader;
import dev.frogmc.frogloader.api.mod.ModProperties;

View file

@ -1,19 +1,11 @@
package dev.frogmc.frogloader.impl;
import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
import com.google.gson.Gson;
import dev.frogmc.frogloader.api.FrogLoader;
import dev.frogmc.frogloader.api.env.Env;
import dev.frogmc.frogloader.api.mod.ModProperties;
import dev.frogmc.frogloader.api.plugin.FrogGamePlugin;
import dev.frogmc.frogloader.api.plugin.FrogModPlugin;
import dev.frogmc.frogloader.api.plugin.FrogPlugin;
import dev.frogmc.frogloader.impl.gui.LoaderGui;
import dev.frogmc.frogloader.impl.launch.MixinClassLoader;
@ -25,6 +17,16 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongepowered.asm.mixin.MixinEnvironment;
import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
public class FrogLoaderImpl implements FrogLoader {
public static final String MOD_FILE_EXTENSION = ".frogmod";
private static final boolean DEV_ENV = Boolean.getBoolean(SystemProperties.DEVELOPMENT);
@ -39,6 +41,12 @@ public class FrogLoaderImpl implements FrogLoader {
@Getter
private final List<FrogPlugin> plugins = new ArrayList<>();
@Getter
private final List<FrogGamePlugin> gamePlugins = new ArrayList<>();
@Getter
private final List<FrogModPlugin> modPlugins = new ArrayList<>();
@Getter
private final Path gameDir, configDir, modsDir;
@ -102,6 +110,61 @@ public class FrogLoaderImpl implements FrogLoader {
}
}
private void discoverModPlugins() {
FrogModPlugin[] applicablePlugins = ServiceLoader
.load(FrogModPlugin.class)
.stream()
.map(ServiceLoader.Provider::get)
.filter(FrogModPlugin::isApplicable)
.toArray(FrogModPlugin[]::new);
for (FrogModPlugin plugin : applicablePlugins) {
try {
Collection<Path> paths = Discovery.find(gameDir.resolve(plugin.loadDirectory()), p->false, plugin::isFileApplicable);
paths.forEach(p -> {
try {
ModProperties mod = plugin.loadMod(p);
mods.put(mod.id(), mod);
modIds.add(mod.id());
} catch (Throwable e) {
LOGGER.error("Error during mod initialisation: ", e);
throw new RuntimeException(e);
}
});
modPlugins.add(plugin);
} catch (Throwable e) {
LOGGER.error("Error during plugin initialisation: ", e);
throw new RuntimeException(e);
}
}
}
private void discoverGamePlugins() {
// this cast is safe
FrogGamePlugin[] applicablePlugins = ServiceLoader
.load(FrogGamePlugin.class)
.stream()
.map(ServiceLoader.Provider::get)
.filter(FrogGamePlugin::isApplicable)
.toArray(FrogGamePlugin[]::new);
if (applicablePlugins.length > 1) {
throw new IllegalStateException("Multiple applicable game plugins found!");
} else if (applicablePlugins.length == 0) {
throw new IllegalStateException("No applicable game plugin found!");
}
for (FrogGamePlugin plugin : applicablePlugins) {
try {
plugin.init(this);
gamePlugins.add(plugin);
} catch (Throwable e) {
LOGGER.error("Error during plugin initialisation: ", e);
throw new RuntimeException(e);
}
}
}
private void discoverPlugins() {
ServiceLoader.load(FrogPlugin.class).forEach(plugin -> {
try {
@ -153,8 +216,7 @@ public class FrogLoaderImpl implements FrogLoader {
}
private Map<String, ModProperties> collectMods() {
return plugins.stream().map(FrogPlugin::getMods).flatMap(Collection::stream)
.collect(Collectors.toMap(ModProperties::id, m -> m));
return plugins.stream().map(FrogPlugin::getMods).flatMap(Collection::stream).collect(Collectors.toMap(ModProperties::id, m -> m));
}
private Collection<String> collectModIds() {

View file

@ -1,244 +0,0 @@
package dev.frogmc.frogloader.impl.plugin.game.minecraft;
import java.io.File;
import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.net.MalformedURLException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import com.electronwill.nightconfig.core.UnmodifiableConfig;
import com.google.gson.JsonObject;
import dev.frogmc.frogloader.api.FrogLoader;
import dev.frogmc.frogloader.api.extensions.PreLaunchExtension;
import dev.frogmc.frogloader.api.mod.ModDependencies;
import dev.frogmc.frogloader.api.mod.ModExtensions;
import dev.frogmc.frogloader.api.mod.ModProperties;
import dev.frogmc.frogloader.api.plugin.FrogPlugin;
import dev.frogmc.frogloader.impl.Discovery;
import dev.frogmc.frogloader.impl.FrogLoaderImpl;
import dev.frogmc.frogloader.impl.gui.LoaderGui;
import dev.frogmc.frogloader.impl.mixin.AWProcessor;
import dev.frogmc.frogloader.impl.mod.*;
import dev.frogmc.frogloader.impl.util.CrashReportGenerator;
import dev.frogmc.frogloader.impl.util.SystemProperties;
import dev.frogmc.thyroxine.Thyroxine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongepowered.asm.mixin.Mixins;
public class Minecraft implements FrogPlugin {
protected static final String[] MINECRAFT_CLASSES = new String[]{
"net/minecraft/client/main/Main.class",
"net/minecraft/client/MinecraftApplet.class",
"net/minecraft/server/Main.class"
};
private static final Logger LOGGER = LoggerFactory.getLogger("Plugin/Minecraft");
protected final Collection<ModProperties> modProperties = new ArrayList<>();
protected Path gamePath;
protected String foundMainClass;
private String version;
@Override
public boolean isApplicable() {
gamePath = findGame();
return gamePath != null;
}
@SuppressWarnings({"rawtypes", "unchecked"})
@Override
public void init(FrogLoader loader) throws Exception {
if (gamePath == null) {
throw new IllegalStateException("Game not found yet!");
}
Path remappedGamePath = loader.getGameDir().resolve(".frogmc/remappedJars").resolve(version).resolve("game-" + version + "-remapped.jar");
if (!Files.exists(remappedGamePath.getParent())) {
try {
Files.createDirectories(remappedGamePath.getParent());
} catch (IOException e) {
LOGGER.error("Failed to create directory", e);
}
}
modProperties.add(JavaModProperties.get());
modProperties.add(new ModPropertiesImpl("minecraft", "Minecraft", "/assets/minecraft/textures/block/grass_block_side.png",
MinecraftSemVerImpl.get(version), "MC-EULA",
Map.of("Mojang AB", Collections.singleton("Author")),
new ModDependencies(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet()),
ModExtensions.of(Collections.emptyMap()), Collections.emptySet()));
Collection<Path> mods = Discovery.find(loader.getModsDir(), path ->
version.equals(path.getFileName().toString()), path ->
path.getFileName().toString().endsWith(FrogLoaderImpl.MOD_FILE_EXTENSION));
this.getClass().getClassLoader().resources(ModPropertiesReader.PROPERTIES_FILE_NAME).map(ModPropertiesReader::readFile)
.map(o -> o.orElse(null)).filter(Objects::nonNull).forEach(modProperties::add);
Map<Path, ModProperties> modPaths = new HashMap<>();
Path jijCache = getJijCacheDir();
for (Path mod : new HashSet<>(mods)) {
findJiJMods(mod, mods, modPaths, jijCache);
}
try {
modProperties.retainAll(new ModDependencyResolver(modProperties).solve());
} catch (ModDependencyResolver.BreakingModException e) {
LoaderGui.execBreakingDep(CrashReportGenerator.writeReport(e, modProperties), e, false);
} catch (ModDependencyResolver.UnfulfilledDependencyException e) {
LoaderGui.execUnfulfilledDep(CrashReportGenerator.writeReport(e, modProperties), e, false);
}
mods.stream().filter(p -> modProperties.contains(modPaths.get(p))).map(path -> {
try {
return path.toUri().toURL();
} catch (MalformedURLException e) {
LOGGER.warn("Failed to resolve url for {}", path, e);
return null;
}
}).filter(Objects::nonNull).forEach(FrogLoaderImpl.getInstance().getClassloader()::addURL);
modProperties.forEach(props -> {
Object o = props.extensions().get(BuiltinExtensions.MIXIN_CONFIG);
if (o instanceof String name) {
Mixins.addConfiguration(name);
} else if (o instanceof Collection l) {
((Collection<String>) l).forEach(Mixins::addConfiguration);
}
});
if (!loader.isDevelopment()) {
if (!Files.exists(remappedGamePath)) {
Thyroxine.run(version, gamePath, remappedGamePath, true, false);
}
}
AWProcessor.load(modProperties);
var runtimePath = loader.isDevelopment() ? gamePath : remappedGamePath;
FrogLoaderImpl.getInstance().getClassloader().addURL(runtimePath.toUri().toURL());
}
protected void findJiJMods(Path mod, Collection<Path> mods, Map<Path, ModProperties> modPaths, Path jijCache) throws IOException {
Optional<ModProperties> opt = ModPropertiesReader.read(mod);
if (opt.isPresent()) {
ModProperties p = opt.get();
modProperties.add(p);
modPaths.put(mod, p);
List<List<UnmodifiableConfig>> entries = p.extensions().getOrDefault(BuiltinExtensions.INCLUDED_JARS, Collections.emptyList());
if (entries.isEmpty()) {
return;
}
try (FileSystem fs = FileSystems.newFileSystem(mod)) {
for (List<UnmodifiableConfig> jars : entries) {
for (UnmodifiableConfig jar : jars) {
Path path = fs.getPath(jar.get("path")).toAbsolutePath();
Path extracted = jijCache.resolve((String) jar.get("id"));
if (!Files.exists(extracted)){
Files.createDirectories(jijCache);
Files.copy(path, extracted);
}
mods.add(extracted);
findJiJMods(extracted, mods, modPaths, jijCache);
}
}
}
}
}
private Path getJijCacheDir(){
Path dir = FrogLoader.getInstance().getGameDir().resolve(".frogmc").resolve("jijcache");
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
Files.walkFileTree(dir, new SimpleFileVisitor<>(){
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return super.visitFile(file, attrs);
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return super.postVisitDirectory(dir, exc);
}
});
} catch (IOException e) {
LOGGER.error("Failed to clear extracted jij mods!", e);
}
}));
return dir;
}
@Override
public Collection<ModProperties> getMods() {
return modProperties;
}
protected Path findGame() {
LOGGER.info("Locating game..");
String jar = System.getProperty(SystemProperties.MINECRAFT_GAME_JAR);
if (jar != null) {
Path p = Paths.get(jar);
if (checkLocation(p)) {
return p;
}
}
for (String s : System.getProperty("java.class.path", "").split(File.pathSeparator)) {
Path p = Paths.get(s);
if (checkLocation(p)) {
return p;
}
}
LOGGER.warn("Could not locate game!");
return null;
}
protected boolean checkLocation(Path jar) {
if (!Files.exists(jar) || Files.isDirectory(jar)) {
return false;
}
try (FileSystem fs = FileSystems.newFileSystem(jar)) {
for (String n : MINECRAFT_CLASSES) {
if (Files.exists(fs.getPath(n)) && n.contains(FrogLoaderImpl.getInstance().getEnv().getIdentifier())) {
LOGGER.info("Found game: {}", jar);
foundMainClass = n.substring(0, n.length() - 6).replace("/", ".");
try {
version = FrogLoaderImpl.getInstance().getGson().fromJson(Files.readString(fs.getPath("version.json")), JsonObject.class).get("id").getAsString();
} catch (Exception e){
version = FrogLoaderImpl.getInstance().getArgument("version");
}
return true;
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return false;
}
@Override
public void run() {
try {
if (foundMainClass != null) {
modProperties.forEach(props ->
props.extensions().runIfPresent(PreLaunchExtension.ID,
PreLaunchExtension.class, PreLaunchExtension::onPreLaunch));
LOGGER.info("Launching main class: {}", foundMainClass);
Class<?> mainClass = Class.forName(foundMainClass);
MethodHandle main = MethodHandles.publicLookup().findStatic(mainClass, "main", MethodType.methodType(void.class, String[].class));
main.invoke((Object) FrogLoaderImpl.getInstance().getArgs());
} else {
LOGGER.warn("Failed to locate main class!");
}
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}

View file

@ -0,0 +1,121 @@
package dev.frogmc.frogloader.impl.plugin.game.minecraft;
import com.google.gson.JsonObject;
import dev.frogmc.frogloader.api.FrogLoader;
import dev.frogmc.frogloader.api.plugin.FrogGamePlugin;
import dev.frogmc.frogloader.impl.FrogLoaderImpl;
import dev.frogmc.frogloader.impl.util.SystemProperties;
import dev.frogmc.thyroxine.Thyroxine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.nio.file.*;
public class MinecraftGamePlugin implements FrogGamePlugin {
protected final String[] MINECRAFT_CLASSES = new String[]{
"net/minecraft/client/main/Main.class",
"net/minecraft/client/MinecraftApplet.class",
"net/minecraft/server/Main.class"
};
private static final Logger LOGGER = LoggerFactory.getLogger("Plugin/Minecraft");
protected Path gamePath;
protected String foundMainClass;
private String version;
protected boolean checkLocation(Path jar) {
if (!Files.exists(jar) || Files.isDirectory(jar)) {
return false;
}
try (FileSystem fs = FileSystems.newFileSystem(jar)) {
for (String n : MINECRAFT_CLASSES) {
if (Files.exists(fs.getPath(n)) && n.contains(FrogLoaderImpl.getInstance().getEnv().getIdentifier())) {
LOGGER.info("Found game: {}", jar);
foundMainClass = n.substring(0, n.length() - 6).replace("/", ".");
try {
version = FrogLoaderImpl.getInstance().getGson().fromJson(Files.readString(fs.getPath("version.json")), JsonObject.class).get("id").getAsString();
} catch (Exception e){
version = FrogLoaderImpl.getInstance().getArgument("version");
}
return true;
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return false;
}
protected Path findGame() {
LOGGER.info("Locating game..");
String jar = System.getProperty(SystemProperties.MINECRAFT_GAME_JAR);
if (jar != null) {
Path p = Paths.get(jar);
if (checkLocation(p)) {
return p;
}
}
for (String s : System.getProperty("java.class.path", "").split(File.pathSeparator)) {
Path p = Paths.get(s);
if (checkLocation(p)) {
return p;
}
}
LOGGER.warn("Could not locate game!");
return null;
}
@Override
public boolean isApplicable() {
gamePath = findGame();
return gamePath != null;
}
@Override
public void run() {
try {
if (foundMainClass != null) {
LOGGER.info("Launching main class: {}", foundMainClass);
Class<?> mainClass = Class.forName(foundMainClass);
MethodHandle main = MethodHandles.publicLookup().findStatic(mainClass, "main", MethodType.methodType(void.class, String[].class));
main.invoke((Object) FrogLoaderImpl.getInstance().getArgs());
} else {
LOGGER.warn("Failed to locate main class!");
}
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
@Override
public void init(FrogLoader loader) throws Exception {
if (gamePath == null) {
throw new IllegalStateException("Game not found!");
}
Path remappedGamePath = loader.getGameDir().resolve(".frogmc/remappedJars").resolve(version).resolve("game-" + version + "-remapped.jar");
if (!Files.exists(remappedGamePath.getParent())) {
try {
Files.createDirectories(remappedGamePath.getParent());
} catch (IOException e) {
LOGGER.error("Failed to create directory", e);
}
}
if (!loader.isDevelopment()) {
if (!Files.exists(remappedGamePath)) {
Thyroxine.run(version, gamePath, remappedGamePath, true, false);
}
}
var runtimePath = loader.isDevelopment() ? gamePath : remappedGamePath;
FrogLoaderImpl.getInstance().getClassloader().addURL(runtimePath.toUri().toURL());
}
}

View file

@ -0,0 +1,43 @@
package dev.frogmc.frogloader.impl.plugin.mod;
import dev.frogmc.frogloader.api.extensions.PreLaunchExtension;
import dev.frogmc.frogloader.api.mod.ModProperties;
import dev.frogmc.frogloader.api.plugin.FrogModPlugin;
import dev.frogmc.frogloader.impl.mod.ModPropertiesImpl;
import dev.frogmc.frogloader.impl.mod.ModPropertiesReader;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Path;
public class FrogmodModPlugin implements FrogModPlugin {
@Override
public boolean isApplicable() {
return true;
}
@Override
public boolean isFileApplicable(Path path) {
if (!path.endsWith(".frogmod")) {
return false;
}
try (FileSystem fs = FileSystems.newFileSystem(path)) {
return fs.getPath("frog.mod.toml").toFile().exists();
} catch (Exception e) {
return false;
}
}
@Override
public ModProperties loadMod(Path path) {
try (FileSystem fs = FileSystems.newFileSystem(path)) {
ModProperties prop = ModPropertiesReader.readFile(fs.getPath("frog.mod.toml").toUri().toURL()).orElseThrow(IOException::new);
prop.extensions().runIfPresent(PreLaunchExtension.ID, PreLaunchExtension.class, PreLaunchExtension::onPreLaunch);
return prop;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View file

@ -1 +1 @@
dev.frogmc.frogloader.impl.plugin.game.minecraft.Minecraft
dev.frogmc.frogloader.impl.plugin.game.minecraft.MinecraftOld