wip mod dependencies, improve mod loading (reuse threads), rename StaticConfiguration.homeDir to HOME_DIR, remove old code

This commit is contained in:
Bixilon 2020-11-02 23:35:55 +01:00
parent 3d169d8e37
commit 4b4ae6903d
No known key found for this signature in database
GPG Key ID: 5CAD791931B09AC4
10 changed files with 220 additions and 51 deletions

View File

@ -12,6 +12,7 @@ In the root folder of your jar file (the mod) must be a file called `mod.json`.
"Example dev" "Example dev"
], ],
"name": "Example Mod", "name": "Example Mod",
"moddingAPIVersion": 1,
"identifier": "example", "identifier": "example",
"mainClass": "de.example.mod.Main", "mainClass": "de.example.mod.Main",
"loading": { "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** - `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** - `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** - `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** - `mainClass` the Main class of your mod (self explaining). The main class needs to extent the abstract class `MinosoftMod`. **Required**
- `loading` Loading attributes. **Optional** - `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** - `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** - `soft` These mods are **optional** to work. Both use the following format: **Optional**
- `uuid` the uuid of the mod to load. **Required** - `uuid` the uuid of the mod to load. **Required**
- `version` Specifies the version you need to load. **Optional** - `version` Specifies the version you need to load. **Optional**
- `minimum` Minimum versionId required. **Maximum, minimum or both** - `minimum` Minimum versionId required. **Maximum, minimum, both or none**
- `maximum` Maximum versionId required. **Maximum, minimum or both** - `maximum` Maximum versionId required. **Maximum, minimum, both or none**
## Mod loading (aka Main class) ## Mod loading (aka Main class)
Your main class must extend the following class: `de.bixilon.minosoft.MinosoftMod`. Your main class must extend the following class: `de.bixilon.minosoft.MinosoftMod`.

View File

@ -33,19 +33,19 @@ public class Configuration {
private final Object lock = new Object(); private final Object lock = new Object();
public Configuration() throws IOException { 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()) { if (!file.exists()) {
// no configuration file // no configuration file
InputStream input = getClass().getResourceAsStream("/config/" + StaticConfiguration.CONFIG_FILENAME); InputStream input = getClass().getResourceAsStream("/config/" + StaticConfiguration.CONFIG_FILENAME);
if (input == null) { if (input == null) {
throw new FileNotFoundException(String.format("[Config] Missing default config: %s!", StaticConfiguration.CONFIG_FILENAME)); 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()) { if (!folder.exists() && !folder.mkdirs()) {
throw new IOException("[Config] Could not create config folder!"); throw new IOException("[Config] Could not create config folder!");
} }
Files.copy(input, Paths.get(file.getAbsolutePath())); 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()); config = Util.readJsonFromFile(file.getAbsolutePath());
int configVersion = getInt(ConfigurationPaths.CONFIG_VERSION); 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 // 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(); Gson gson = new GsonBuilder().setPrettyPrinting().create();
FileWriter writer; FileWriter writer;
try { try {

View File

@ -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 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 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 { static {
// Sets Config.homeDir to the correct folder per OS // Sets Config.homeDir to the correct folder per OS
String homeDir;
homeDir = System.getProperty("user.home"); homeDir = System.getProperty("user.home");
if (!homeDir.endsWith(File.separator)) { if (!homeDir.endsWith(File.separator)) {
homeDir += "/"; homeDir += "/";
@ -42,5 +43,6 @@ public class StaticConfiguration {
// failed creating folder // failed creating folder
throw new RuntimeException(String.format("Could not create home folder (%s)!", homeDir)); throw new RuntimeException(String.format("Could not create home folder (%s)!", homeDir));
} }
HOME_DIR = folder.getAbsolutePath() + "/";
} }
} }

View File

@ -241,6 +241,6 @@ public class AssetsManager {
} }
private static String getAssetDiskPath(String hash) { 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);
} }
} }

View File

@ -128,7 +128,7 @@ public class Versions {
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
// check if mapping folder exist // 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.exists()) {
if (mappingFolder.mkdirs()) { if (mappingFolder.mkdirs()) {
Log.verbose("Created mappings folder."); 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<String, JsonObject> files; HashMap<String, JsonObject> files;
try { try {
files = Util.readJsonTarGzFile(fileName); files = Util.readJsonTarGzFile(fileName);

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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<ModDependency> serializeDependencyArray(JsonArray json) {
HashSet<ModDependency> 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;
}
}

View File

@ -17,6 +17,7 @@ import com.google.gson.JsonArray;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import de.bixilon.minosoft.util.Util; import de.bixilon.minosoft.util.Util;
import java.util.HashSet;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@ -26,11 +27,14 @@ public class ModInfo {
final String versionName; final String versionName;
final String name; final String name;
final String[] authors; final String[] authors;
final int moddingAPIVersion;
final String identifier; final String identifier;
final String mainClass; final String mainClass;
final HashSet<ModDependency> hardDependencies = new HashSet<>();
final HashSet<ModDependency> softDependencies = new HashSet<>();
LoadingInfo loadingInfo; LoadingInfo loadingInfo;
public ModInfo(JsonObject json) { public ModInfo(JsonObject json) throws ModLoadingException {
this.uuid = Util.getUUIDFromString(json.get("uuid").getAsString()); this.uuid = Util.getUUIDFromString(json.get("uuid").getAsString());
this.versionId = json.get("versionId").getAsInt(); this.versionId = json.get("versionId").getAsInt();
this.versionName = json.get("versionName").getAsString(); this.versionName = json.get("versionName").getAsString();
@ -39,6 +43,10 @@ public class ModInfo {
this.authors = new String[authors.size()]; this.authors = new String[authors.size()];
AtomicInteger i = new AtomicInteger(); AtomicInteger i = new AtomicInteger();
authors.forEach((authorElement) -> this.authors[i.getAndIncrement()] = authorElement.getAsString()); 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.identifier = json.get("identifier").getAsString();
this.mainClass = json.get("mainClass").getAsString(); this.mainClass = json.get("mainClass").getAsString();
if (json.has("loading")) { if (json.has("loading")) {
@ -48,6 +56,15 @@ public class ModInfo {
this.loadingInfo.setLoadingPriority(Priorities.valueOf(loading.get("priority").getAsString())); 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() { public String[] getAuthors() {

View File

@ -24,63 +24,78 @@ import org.xeustechnologies.jcl.JclObjectFactory;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.HashSet;
import java.util.LinkedList; 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; import java.util.zip.ZipFile;
public class ModLoader { public class ModLoader {
static final LinkedList<MinosoftMod> mods = new LinkedList<>(); public static final int CURRENT_MODDING_API_VERSION = 1;
public static final LinkedList<MinosoftMod> mods = new LinkedList<>();
public static void loadMods(CountUpAndDownLatch progress) throws Exception { public static void loadMods(CountUpAndDownLatch progress) throws Exception {
Log.verbose("Start loading mods..."); Log.verbose("Start loading mods...");
ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), Util.getThreadFactory("ModLoader"));
// load all jars, parse the mod.json // load all jars, parse the mod.json
// sort the list and prioritize // sort the list and prioritize
// load all lists and dependencies async // load all lists and dependencies async
HashSet<Callable<MinosoftMod>> callables = new HashSet<>(); File[] files = new File(StaticConfiguration.HOME_DIR + "mods").listFiles();
File[] files = new File(StaticConfiguration.homeDir + "mods").listFiles();
if (files == null) { if (files == null) {
// no mods to load // no mods to load
return; return;
} }
CountDownLatch latch = new CountDownLatch(files.length);
for (File modFile : files) { for (File modFile : files) {
if (modFile.isDirectory()) { if (modFile.isDirectory()) {
continue; continue;
} }
callables.add(() -> { executor.execute(() -> {
MinosoftMod mod = loadMod(progress, modFile); MinosoftMod mod = loadMod(progress, modFile);
if (mod != null) { if (mod != null) {
mods.add(mod); 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 progress.addCount(mods.size() * ModPhases.values().length); // count * mod phases
// sort for priority
mods.sort((a, b) -> { mods.sort((a, b) -> {
if (a == null || b == null) { if (a == null || b == null) {
return 0; return 0;
} }
return -(getLoadingPriorityOrDefault(b.getInfo()).ordinal() - getLoadingPriorityOrDefault(a.getInfo()).ordinal()); return -(getLoadingPriorityOrDefault(b.getInfo()).ordinal() - getLoadingPriorityOrDefault(a.getInfo()).ordinal());
}); });
// ToDo: check dependencies
for (ModPhases phase : ModPhases.values()) { for (ModPhases phase : ModPhases.values()) {
Log.verbose(String.format("Map loading phase changed: %s", phase)); Log.verbose(String.format("Map loading phase changed: %s", phase));
HashSet<Callable<MinosoftMod>> phaseLoaderCallables = new HashSet<>(); CountDownLatch modLatch = new CountDownLatch(mods.size());
mods.forEach((instance) -> phaseLoaderCallables.add(() -> { mods.forEach((instance) -> {
executor.execute(() -> {
if (!instance.isEnabled()) { if (!instance.isEnabled()) {
return instance; modLatch.countDown();
progress.countDown();
return;
} }
if (!instance.start(phase)) { if (!instance.start(phase)) {
Log.warn(String.format("An error occurred while loading %s", instance.getInfo())); Log.warn(String.format("An error occurred while loading %s", instance.getInfo()));
instance.setEnabled(false); instance.setEnabled(false);
} }
modLatch.countDown();
progress.countDown(); progress.countDown();
return instance; });
})); });
Util.executeInThreadPool("ModLoader", phaseLoaderCallables); modLatch.await();
} }
mods.forEach((instance) -> { mods.forEach((instance) -> {
if (instance.isEnabled()) { if (instance.isEnabled()) {
@ -94,6 +109,7 @@ public class ModLoader {
} }
public static MinosoftMod loadMod(CountUpAndDownLatch progress, File file) { public static MinosoftMod loadMod(CountUpAndDownLatch progress, File file) {
MinosoftMod instance;
try { try {
Log.verbose(String.format("[MOD] Loading file %s", file.getAbsolutePath())); Log.verbose(String.format("[MOD] Loading file %s", file.getAbsolutePath()));
progress.countUp(); progress.countUp();
@ -107,18 +123,17 @@ public class ModLoader {
jcl.add(file.getAbsolutePath()); jcl.add(file.getAbsolutePath());
JclObjectFactory factory = JclObjectFactory.getInstance(); JclObjectFactory factory = JclObjectFactory.getInstance();
MinosoftMod instance = (MinosoftMod) factory.create(jcl, modInfo.getMainClass()); instance = (MinosoftMod) factory.create(jcl, modInfo.getMainClass());
instance.setInfo(modInfo); instance.setInfo(modInfo);
Log.verbose(String.format("[MOD] Mod file loaded and added to classpath (%s)", modInfo)); Log.verbose(String.format("[MOD] Mod file loaded and added to classpath (%s)", modInfo));
zipFile.close(); zipFile.close();
progress.countDown(); } catch (IOException | ModLoadingException | NullPointerException e) {
return instance; instance = null;
} catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
Log.warn(String.format("Could not load mod: %s", file.getAbsolutePath())); Log.warn(String.format("Could not load mod: %s", file.getAbsolutePath()));
} }
progress.countDown(); // failed progress.countDown(); // failed
return null; return instance;
} }
private static Priorities getLoadingPriorityOrDefault(ModInfo info) { private static Priorities getLoadingPriorityOrDefault(ModInfo info) {

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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);
}
}

View File

@ -26,10 +26,9 @@ import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.*; import java.util.concurrent.ThreadFactory;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.zip.*; import java.util.zip.*;
@ -246,17 +245,6 @@ public final class Util {
return new BufferedInputStream(new URL(url).openStream()); return new BufferedInputStream(new URL(url).openStream());
} }
public static <T> void executeInThreadPool(String name, Collection<Callable<T>> 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) { public static ThreadFactory getThreadFactory(String threadName) {
return new ThreadFactoryBuilder().setNameFormat(threadName + "#%d").build(); return new ThreadFactoryBuilder().setNameFormat(threadName + "#%d").build();
} }