diff --git a/doc/Modding.md b/doc/Modding.md index 6d7045b99..20a13ea4a 100644 --- a/doc/Modding.md +++ b/doc/Modding.md @@ -12,6 +12,7 @@ In the root folder of your jar file (the mod) must be a file called `mod.json`. "Example dev" ], "name": "Example Mod", + "moddingAPIVersion": 1, "identifier": "example", "mainClass": "de.example.mod.Main", "loading": { @@ -43,7 +44,8 @@ In the root folder of your jar file (the mod) must be a file called `mod.json`. - `uuid` is a unique id for the mod. Generate 1 and keep it in all versions (used for dependencies, etc). **Required** - `versionId` like in android there is a numeric version id. It is used to compare between versions (and as identifier). **Required** - `versionName`, `authors`, `name` is the classic implementation of metadata. Can be anything, will be displayed in the mod list. **Required** -- `identifier` is the prefix of items (for Minecraft it is `minecraft`). Aka the thing before the ``. **Required** +- `moddingAPIVersion` Modding API version of minosoft. Currently `1` **Required** +- `identifier` is the prefix of items (for Minecraft it is `minecraft`). Aka the thing before the `:`. **Required** - `mainClass` the Main class of your mod (self explaining). The main class needs to extent the abstract class `MinosoftMod`. **Required** - `loading` Loading attributes. **Optional** - `priority` should the mod be loaded at the beginning or at the end. Possible values are `LOWEST`, `LOW`, `NORMAL`, `HIGH`, `HIGHEST` **Optional** @@ -52,8 +54,8 @@ In the root folder of your jar file (the mod) must be a file called `mod.json`. - `soft` These mods are **optional** to work. Both use the following format: **Optional** - `uuid` the uuid of the mod to load. **Required** - `version` Specifies the version you need to load. **Optional** - - `minimum` Minimum versionId required. **Maximum, minimum or both** - - `maximum` Maximum versionId required. **Maximum, minimum or both** + - `minimum` Minimum versionId required. **Maximum, minimum, both or none** + - `maximum` Maximum versionId required. **Maximum, minimum, both or none** ## Mod loading (aka Main class) Your main class must extend the following class: `de.bixilon.minosoft.MinosoftMod`. diff --git a/src/main/java/de/bixilon/minosoft/config/Configuration.java b/src/main/java/de/bixilon/minosoft/config/Configuration.java index 68662e13d..8a42d054e 100644 --- a/src/main/java/de/bixilon/minosoft/config/Configuration.java +++ b/src/main/java/de/bixilon/minosoft/config/Configuration.java @@ -33,19 +33,19 @@ public class Configuration { private final Object lock = new Object(); public Configuration() throws IOException { - File file = new File(StaticConfiguration.homeDir + "config/" + StaticConfiguration.CONFIG_FILENAME); + File file = new File(StaticConfiguration.HOME_DIR + "config/" + StaticConfiguration.CONFIG_FILENAME); if (!file.exists()) { // no configuration file InputStream input = getClass().getResourceAsStream("/config/" + StaticConfiguration.CONFIG_FILENAME); if (input == null) { throw new FileNotFoundException(String.format("[Config] Missing default config: %s!", StaticConfiguration.CONFIG_FILENAME)); } - File folder = new File(StaticConfiguration.homeDir + "config/"); + File folder = new File(StaticConfiguration.HOME_DIR + "config/"); if (!folder.exists() && !folder.mkdirs()) { throw new IOException("[Config] Could not create config folder!"); } Files.copy(input, Paths.get(file.getAbsolutePath())); - file = new File(StaticConfiguration.homeDir + "config/" + StaticConfiguration.CONFIG_FILENAME); + file = new File(StaticConfiguration.HOME_DIR + "config/" + StaticConfiguration.CONFIG_FILENAME); } config = Util.readJsonFromFile(file.getAbsolutePath()); int configVersion = getInt(ConfigurationPaths.CONFIG_VERSION); @@ -67,7 +67,7 @@ public class Configuration { } } // write config to temp file, delete original config, rename temp file to original file to avoid conflicts if minosoft gets closed while saving the config - File tempFile = new File(StaticConfiguration.homeDir + "config/" + StaticConfiguration.CONFIG_FILENAME + ".tmp"); + File tempFile = new File(StaticConfiguration.HOME_DIR + "config/" + StaticConfiguration.CONFIG_FILENAME + ".tmp"); Gson gson = new GsonBuilder().setPrettyPrinting().create(); FileWriter writer; try { diff --git a/src/main/java/de/bixilon/minosoft/config/StaticConfiguration.java b/src/main/java/de/bixilon/minosoft/config/StaticConfiguration.java index 6f13401e1..05c7caf05 100644 --- a/src/main/java/de/bixilon/minosoft/config/StaticConfiguration.java +++ b/src/main/java/de/bixilon/minosoft/config/StaticConfiguration.java @@ -23,10 +23,11 @@ public class StaticConfiguration { public static final boolean COLORED_LOG = true; // the log should be colored with ANSI (does not affect base components) public static final boolean LOG_RELATIVE_TIME = false; // prefix all log messages with the relative start time in milliseconds instead of the formatted time - public static String homeDir; + public static final String HOME_DIR; static { // Sets Config.homeDir to the correct folder per OS + String homeDir; homeDir = System.getProperty("user.home"); if (!homeDir.endsWith(File.separator)) { homeDir += "/"; @@ -42,5 +43,6 @@ public class StaticConfiguration { // failed creating folder throw new RuntimeException(String.format("Could not create home folder (%s)!", homeDir)); } + HOME_DIR = folder.getAbsolutePath() + "/"; } } diff --git a/src/main/java/de/bixilon/minosoft/data/assets/AssetsManager.java b/src/main/java/de/bixilon/minosoft/data/assets/AssetsManager.java index 56dab0ce4..f7ca83439 100644 --- a/src/main/java/de/bixilon/minosoft/data/assets/AssetsManager.java +++ b/src/main/java/de/bixilon/minosoft/data/assets/AssetsManager.java @@ -241,6 +241,6 @@ public class AssetsManager { } private static String getAssetDiskPath(String hash) { - return StaticConfiguration.homeDir + String.format("assets/objects/%s/%s.gz", hash.substring(0, 2), hash); + return StaticConfiguration.HOME_DIR + String.format("assets/objects/%s/%s.gz", hash.substring(0, 2), hash); } } diff --git a/src/main/java/de/bixilon/minosoft/data/mappings/versions/Versions.java b/src/main/java/de/bixilon/minosoft/data/mappings/versions/Versions.java index 8c668d922..9fd2f28fb 100644 --- a/src/main/java/de/bixilon/minosoft/data/mappings/versions/Versions.java +++ b/src/main/java/de/bixilon/minosoft/data/mappings/versions/Versions.java @@ -128,7 +128,7 @@ public class Versions { long startTime = System.currentTimeMillis(); // check if mapping folder exist - File mappingFolder = new File(StaticConfiguration.homeDir + "assets/mapping"); + File mappingFolder = new File(StaticConfiguration.HOME_DIR + "assets/mapping"); if (!mappingFolder.exists()) { if (mappingFolder.mkdirs()) { Log.verbose("Created mappings folder."); @@ -138,7 +138,7 @@ public class Versions { } } - String fileName = StaticConfiguration.homeDir + String.format("assets/mapping/%s.tar.gz", version.getVersionName()); + String fileName = StaticConfiguration.HOME_DIR + String.format("assets/mapping/%s.tar.gz", version.getVersionName()); HashMap files; try { files = Util.readJsonTarGzFile(fileName); diff --git a/src/main/java/de/bixilon/minosoft/modding/loading/ModDependency.java b/src/main/java/de/bixilon/minosoft/modding/loading/ModDependency.java new file mode 100644 index 000000000..fae1699cd --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/modding/loading/ModDependency.java @@ -0,0 +1,110 @@ +/* + * Minosoft + * Copyright (C) 2020 Moritz Zwerger + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If not, see . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.modding.loading; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import de.bixilon.minosoft.util.Util; + +import java.util.HashSet; +import java.util.UUID; + +public class ModDependency { + + private final UUID uuid; + private Integer versionMinimum; + private Integer versionMaximum; + + public ModDependency(UUID uuid, Integer versionMinimum, Integer versionMaximum) { + this.uuid = uuid; + this.versionMinimum = versionMinimum; + this.versionMaximum = versionMaximum; + } + + public ModDependency(UUID uuid) { + this.uuid = uuid; + } + + public static ModDependency serialize(JsonObject json) { + UUID uuid = Util.getUUIDFromString(json.get("uuid").getAsString()); + Integer versionMinimum = null; + Integer versionMaximum = null; + + if (json.has("version")) { + JsonObject version = json.getAsJsonObject("version"); + if (version.has("minimum")) { + versionMinimum = version.get("minimum").getAsInt(); + } + if (version.has("maximum")) { + versionMaximum = version.get("maximum").getAsInt(); + } + } + return new ModDependency(uuid, versionMinimum, versionMaximum); + } + + public static HashSet serializeDependencyArray(JsonArray json) { + HashSet result = new HashSet<>(); + json.forEach((jsonElement -> result.add(serialize(jsonElement.getAsJsonObject())))); + return result; + } + + public UUID getUUID() { + return uuid; + } + + public Integer getVersionMinimum() { + return versionMinimum; + } + + public Integer getVersionMaximum() { + return versionMaximum; + } + + @Override + public int hashCode() { + int result = uuid.hashCode(); + if (versionMinimum != null && versionMinimum > 0) { + result *= versionMinimum; + } + + if (versionMaximum != null && versionMaximum > 0) { + result *= versionMaximum; + } + return result; + } + + @Override + public boolean equals(Object obj) { + if (super.equals(obj)) { + return true; + } + if (hashCode() != obj.hashCode()) { + return false; + } + ModDependency their = (ModDependency) obj; + return getUUID().equals(their.getUUID()) && getVersionMaximum().equals(their.getVersionMaximum()) && getVersionMinimum().equals(their.getVersionMinimum()); + } + + @Override + public String toString() { + String result = uuid.toString(); + if (versionMinimum != null) { + result += " >" + versionMinimum; + } + if (versionMaximum != null) { + result += " <" + versionMaximum; + } + return result; + } +} diff --git a/src/main/java/de/bixilon/minosoft/modding/loading/ModInfo.java b/src/main/java/de/bixilon/minosoft/modding/loading/ModInfo.java index 33684ad59..142ed5f69 100644 --- a/src/main/java/de/bixilon/minosoft/modding/loading/ModInfo.java +++ b/src/main/java/de/bixilon/minosoft/modding/loading/ModInfo.java @@ -17,6 +17,7 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; import de.bixilon.minosoft.util.Util; +import java.util.HashSet; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; @@ -26,11 +27,14 @@ public class ModInfo { final String versionName; final String name; final String[] authors; + final int moddingAPIVersion; final String identifier; final String mainClass; + final HashSet hardDependencies = new HashSet<>(); + final HashSet softDependencies = new HashSet<>(); LoadingInfo loadingInfo; - public ModInfo(JsonObject json) { + public ModInfo(JsonObject json) throws ModLoadingException { this.uuid = Util.getUUIDFromString(json.get("uuid").getAsString()); this.versionId = json.get("versionId").getAsInt(); this.versionName = json.get("versionName").getAsString(); @@ -39,6 +43,10 @@ public class ModInfo { this.authors = new String[authors.size()]; AtomicInteger i = new AtomicInteger(); authors.forEach((authorElement) -> this.authors[i.getAndIncrement()] = authorElement.getAsString()); + moddingAPIVersion = json.get("moddingAPIVersion").getAsInt(); + if (moddingAPIVersion > ModLoader.CURRENT_MODDING_API_VERSION) { + throw new ModLoadingException(String.format("Mod was written with for a newer version of minosoft (moddingAPIVersion=%d, expected=%d)", moddingAPIVersion, ModLoader.CURRENT_MODDING_API_VERSION)); + } this.identifier = json.get("identifier").getAsString(); this.mainClass = json.get("mainClass").getAsString(); if (json.has("loading")) { @@ -48,6 +56,15 @@ public class ModInfo { this.loadingInfo.setLoadingPriority(Priorities.valueOf(loading.get("priority").getAsString())); } } + if (json.has("dependencies")) { + JsonObject dependencies = json.getAsJsonObject("dependencies"); + if (dependencies.has("hard")) { + hardDependencies.addAll(ModDependency.serializeDependencyArray(dependencies.getAsJsonArray("hard"))); + } + if (dependencies.has("soft")) { + softDependencies.addAll(ModDependency.serializeDependencyArray(dependencies.getAsJsonArray("soft"))); + } + } } public String[] getAuthors() { diff --git a/src/main/java/de/bixilon/minosoft/modding/loading/ModLoader.java b/src/main/java/de/bixilon/minosoft/modding/loading/ModLoader.java index fc588147d..bcfdac0e8 100644 --- a/src/main/java/de/bixilon/minosoft/modding/loading/ModLoader.java +++ b/src/main/java/de/bixilon/minosoft/modding/loading/ModLoader.java @@ -24,63 +24,78 @@ import org.xeustechnologies.jcl.JclObjectFactory; import java.io.File; import java.io.IOException; -import java.util.HashSet; import java.util.LinkedList; -import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.zip.ZipFile; public class ModLoader { - static final LinkedList mods = new LinkedList<>(); + public static final int CURRENT_MODDING_API_VERSION = 1; + public static final LinkedList mods = new LinkedList<>(); public static void loadMods(CountUpAndDownLatch progress) throws Exception { Log.verbose("Start loading mods..."); + ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), Util.getThreadFactory("ModLoader")); + // load all jars, parse the mod.json // sort the list and prioritize // load all lists and dependencies async - HashSet> callables = new HashSet<>(); - File[] files = new File(StaticConfiguration.homeDir + "mods").listFiles(); + File[] files = new File(StaticConfiguration.HOME_DIR + "mods").listFiles(); if (files == null) { // no mods to load return; } + CountDownLatch latch = new CountDownLatch(files.length); for (File modFile : files) { if (modFile.isDirectory()) { continue; } - callables.add(() -> { + executor.execute(() -> { MinosoftMod mod = loadMod(progress, modFile); if (mod != null) { mods.add(mod); } - return mod; + latch.countDown(); }); } + latch.await(); - Util.executeInThreadPool("ModLoader", callables); + if (mods.size() == 0) { + Log.info("No mods to load."); + return; + } progress.addCount(mods.size() * ModPhases.values().length); // count * mod phases + // sort for priority mods.sort((a, b) -> { if (a == null || b == null) { return 0; } return -(getLoadingPriorityOrDefault(b.getInfo()).ordinal() - getLoadingPriorityOrDefault(a.getInfo()).ordinal()); }); + // ToDo: check dependencies + for (ModPhases phase : ModPhases.values()) { Log.verbose(String.format("Map loading phase changed: %s", phase)); - HashSet> phaseLoaderCallables = new HashSet<>(); - mods.forEach((instance) -> phaseLoaderCallables.add(() -> { - if (!instance.isEnabled()) { - return instance; - } - if (!instance.start(phase)) { - Log.warn(String.format("An error occurred while loading %s", instance.getInfo())); - instance.setEnabled(false); - } - progress.countDown(); - return instance; - })); - Util.executeInThreadPool("ModLoader", phaseLoaderCallables); + CountDownLatch modLatch = new CountDownLatch(mods.size()); + mods.forEach((instance) -> { + executor.execute(() -> { + if (!instance.isEnabled()) { + modLatch.countDown(); + progress.countDown(); + return; + } + if (!instance.start(phase)) { + Log.warn(String.format("An error occurred while loading %s", instance.getInfo())); + instance.setEnabled(false); + } + modLatch.countDown(); + progress.countDown(); + }); + }); + modLatch.await(); } mods.forEach((instance) -> { if (instance.isEnabled()) { @@ -94,6 +109,7 @@ public class ModLoader { } public static MinosoftMod loadMod(CountUpAndDownLatch progress, File file) { + MinosoftMod instance; try { Log.verbose(String.format("[MOD] Loading file %s", file.getAbsolutePath())); progress.countUp(); @@ -107,18 +123,17 @@ public class ModLoader { jcl.add(file.getAbsolutePath()); JclObjectFactory factory = JclObjectFactory.getInstance(); - MinosoftMod instance = (MinosoftMod) factory.create(jcl, modInfo.getMainClass()); + instance = (MinosoftMod) factory.create(jcl, modInfo.getMainClass()); instance.setInfo(modInfo); Log.verbose(String.format("[MOD] Mod file loaded and added to classpath (%s)", modInfo)); zipFile.close(); - progress.countDown(); - return instance; - } catch (IOException e) { + } catch (IOException | ModLoadingException | NullPointerException e) { + instance = null; e.printStackTrace(); Log.warn(String.format("Could not load mod: %s", file.getAbsolutePath())); } progress.countDown(); // failed - return null; + return instance; } private static Priorities getLoadingPriorityOrDefault(ModInfo info) { diff --git a/src/main/java/de/bixilon/minosoft/modding/loading/ModLoadingException.java b/src/main/java/de/bixilon/minosoft/modding/loading/ModLoadingException.java new file mode 100644 index 000000000..81655b8ff --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/modding/loading/ModLoadingException.java @@ -0,0 +1,35 @@ +/* + * Minosoft + * Copyright (C) 2020 Moritz Zwerger + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If not, see . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.modding.loading; + +public class ModLoadingException extends Exception { + public ModLoadingException() { + } + + public ModLoadingException(String message) { + super(message); + } + + public ModLoadingException(String message, Throwable cause) { + super(message, cause); + } + + public ModLoadingException(Throwable cause) { + super(cause); + } + + public ModLoadingException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/de/bixilon/minosoft/util/Util.java b/src/main/java/de/bixilon/minosoft/util/Util.java index 2e33a3c1c..82512c82d 100644 --- a/src/main/java/de/bixilon/minosoft/util/Util.java +++ b/src/main/java/de/bixilon/minosoft/util/Util.java @@ -26,10 +26,9 @@ import java.net.URL; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.Collection; import java.util.HashMap; import java.util.UUID; -import java.util.concurrent.*; +import java.util.concurrent.ThreadFactory; import java.util.regex.Pattern; import java.util.zip.*; @@ -246,17 +245,6 @@ public final class Util { return new BufferedInputStream(new URL(url).openStream()); } - public static void executeInThreadPool(String name, Collection> callables) throws InterruptedException { - ExecutorService phaseLoader = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), getThreadFactory(name)); - phaseLoader.invokeAll(callables).forEach((tFuture -> { - try { - tFuture.get(); - } catch (ExecutionException | InterruptedException ex) { - ex.getCause().printStackTrace(); - } - })); - } - public static ThreadFactory getThreadFactory(String threadName) { return new ThreadFactoryBuilder().setNameFormat(threadName + "#%d").build(); }