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"
],
"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`.

View File

@ -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 {

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 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() + "/";
}
}

View File

@ -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);
}
}

View File

@ -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<String, JsonObject> files;
try {
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 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<ModDependency> hardDependencies = new HashSet<>();
final HashSet<ModDependency> 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() {

View File

@ -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<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 {
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<Callable<MinosoftMod>> 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<Callable<MinosoftMod>> 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) {

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.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 <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) {
return new ThreadFactoryBuilder().setNameFormat(threadName + "#%d").build();
}