mirror of
https://gitlab.bixilon.de/bixilon/minosoft.git
synced 2025-09-09 07:20:04 -04:00
wip mod dependencies, improve mod loading (reuse threads), rename StaticConfiguration.homeDir to HOME_DIR, remove old code
This commit is contained in:
parent
3d169d8e37
commit
4b4ae6903d
@ -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`.
|
||||
|
@ -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 {
|
||||
|
@ -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() + "/";
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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() {
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user