diff --git a/build.gradle.kts b/build.gradle.kts index 8a8676427..4477914ef 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -307,6 +307,7 @@ dependencies { implementation("org.kamranzafar", "jtar", "2.3") implementation("org.reflections", "reflections", "0.10.2") implementation("it.unimi.dsi", "fastutil-core", "8.5.9") + implementation("org.xeustechnologies", "jcl-core", version = "2.8") // ikonli diff --git a/doc/Modding2.md b/doc/Modding2.md index c2f6ab35f..178e87e41 100644 --- a/doc/Modding2.md +++ b/doc/Modding2.md @@ -22,7 +22,6 @@ - load meta -> inject to classpath -> initialize main class - main class template (with logging, assets, ...) - Multiple `mods` folders - - pre load (before loading anything) - - while load (while loading everything else) - - background load (start loading in while, but don't wait for them. Only wait before loading connection) - - connection load (load before connecting to server) + - pre boot (before loading anything) + - boot (while loading everything else) + - post boot (start loading in while, but don't wait for them. Only wait before loading connection) diff --git a/src/main/java/de/bixilon/minosoft/Minosoft.kt b/src/main/java/de/bixilon/minosoft/Minosoft.kt index cf2727533..68e03a279 100644 --- a/src/main/java/de/bixilon/minosoft/Minosoft.kt +++ b/src/main/java/de/bixilon/minosoft/Minosoft.kt @@ -42,6 +42,8 @@ import de.bixilon.minosoft.gui.eros.util.JavaFXInitializer import de.bixilon.minosoft.main.BootTasks import de.bixilon.minosoft.modding.event.events.FinishInitializingEvent import de.bixilon.minosoft.modding.event.master.GlobalEventMaster +import de.bixilon.minosoft.modding.loader.LoadingPhases +import de.bixilon.minosoft.modding.loader.ModLoader import de.bixilon.minosoft.properties.MinosoftPropertiesLoader import de.bixilon.minosoft.protocol.packets.factory.PacketTypeRegistry import de.bixilon.minosoft.protocol.protocol.LANServerListener @@ -70,11 +72,15 @@ object Minosoft { val start = nanos() Log::class.java.forceInit() CommandLineArguments.parse(args) + Log.log(LogMessageType.OTHER, LogLevels.INFO) { "Starting minosoft..." } + KUtil.initUtilClasses() KUtil.init() + ModLoader.initModLoading() + ModLoader.load(LoadingPhases.PRE_BOOT, CountUpAndDownLatch(0)) + ModLoader.await(LoadingPhases.PRE_BOOT) MINOSOFT_ASSETS_MANAGER.load(CountUpAndDownLatch(0)) - Log.log(LogMessageType.OTHER, LogLevels.INFO) { "Starting minosoft..." } warnMacOS() MinosoftPropertiesLoader.load() @@ -105,6 +111,7 @@ object Minosoft { taskWorker += WorkerTask(identifier = BootTasks.YGGDRASIL, executor = { YggdrasilUtil.load() }) taskWorker += WorkerTask(identifier = BootTasks.ASSETS_OVERRIDE, executor = { OVERRIDE_ASSETS_MANAGER.load(it) }) + taskWorker += WorkerTask(identifier = BootTasks.MODS, executor = { ModLoader.load(LoadingPhases.BOOT, it) }) taskWorker.work(BOOT_LATCH) @@ -114,6 +121,7 @@ object Minosoft { val end = nanos() Log.log(LogMessageType.OTHER, LogLevels.INFO) { "Minosoft boot sequence finished in ${(end - start).formatNanos()}!" } GlobalEventMaster.fireEvent(FinishInitializingEvent()) + DefaultThreadPool += { ModLoader.load(LoadingPhases.POST_BOOT, CountUpAndDownLatch(0)) } RunConfiguration.AUTO_CONNECT_TO?.let { AutoConnect.autoConnect(it) } diff --git a/src/main/java/de/bixilon/minosoft/main/BootTasks.kt b/src/main/java/de/bixilon/minosoft/main/BootTasks.kt index e1c86c5e1..65bbba6f1 100644 --- a/src/main/java/de/bixilon/minosoft/main/BootTasks.kt +++ b/src/main/java/de/bixilon/minosoft/main/BootTasks.kt @@ -27,5 +27,6 @@ enum class BootTasks { STARTUP_PROGRESS, ASSETS_OVERRIDE, CLI, + MODS, ; } diff --git a/src/main/java/de/bixilon/minosoft/modding/loader/LoaderUtil.kt b/src/main/java/de/bixilon/minosoft/modding/loader/LoaderUtil.kt new file mode 100644 index 000000000..968144f5a --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/modding/loader/LoaderUtil.kt @@ -0,0 +1,50 @@ +/* + * Minosoft + * Copyright (C) 2020-2022 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.loader + +import de.bixilon.kutil.cast.CastUtil.unsafeCast +import org.xeustechnologies.jcl.JarClassLoader +import org.xeustechnologies.jcl.JarResources +import org.xeustechnologies.jcl.JclJarEntry +import java.util.jar.JarEntry +import java.util.jar.JarInputStream + +object LoaderUtil { + private val contentsField = JarResources::class.java.getDeclaredField("jarEntryContents") + private val classpathResourcesField = JarClassLoader::class.java.getDeclaredField("classpathResources") + + init { + contentsField.isAccessible = true + classpathResourcesField.isAccessible = true + } + + val JarResources.contents: MutableMap + get() = contentsField.get(this).unsafeCast() + + + val JarClassLoader.classpathResources: JarResources + get() = classpathResourcesField.get(this).unsafeCast() + + fun JarClassLoader.load(entry: JarEntry, stream: JarInputStream) { + load(entry.name, stream.readAllBytes()) + } + + fun JarClassLoader.load(name: String, data: ByteArray) { + val content = this.classpathResources.contents + val entry = JclJarEntry() + entry.baseUrl = null + entry.resourceBytes = data + content[name] = entry + } +} diff --git a/src/main/java/de/bixilon/minosoft/modding/loader/LoadingPhases.kt b/src/main/java/de/bixilon/minosoft/modding/loader/LoadingPhases.kt new file mode 100644 index 000000000..7f2a70405 --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/modding/loader/LoadingPhases.kt @@ -0,0 +1,29 @@ +/* + * Minosoft + * Copyright (C) 2020-2022 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.loader + +import de.bixilon.kutil.enums.EnumUtil +import de.bixilon.kutil.enums.ValuesEnum + +enum class LoadingPhases { + PRE_BOOT, + BOOT, + POST_BOOT, + ; + + companion object : ValuesEnum { + override val VALUES: Array = values() + override val NAME_MAP: Map = EnumUtil.getEnumValues(VALUES) + } +} diff --git a/src/main/java/de/bixilon/minosoft/modding/loader/ModLoader.kt b/src/main/java/de/bixilon/minosoft/modding/loader/ModLoader.kt new file mode 100644 index 000000000..7c80133b4 --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/modding/loader/ModLoader.kt @@ -0,0 +1,208 @@ +/* + * Minosoft + * Copyright (C) 2020-2022 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.loader + +import de.bixilon.kutil.concurrent.pool.DefaultThreadPool +import de.bixilon.kutil.concurrent.worker.unconditional.UnconditionalWorker +import de.bixilon.kutil.latch.CountUpAndDownLatch +import de.bixilon.kutil.watcher.DataWatcher.Companion.watched +import de.bixilon.minosoft.assets.util.FileUtil.readJson +import de.bixilon.minosoft.modding.loader.LoaderUtil.load +import de.bixilon.minosoft.modding.loader.mod.MinosoftMod +import de.bixilon.minosoft.modding.loader.mod.ModMain +import de.bixilon.minosoft.terminal.RunConfiguration +import de.bixilon.minosoft.util.logging.Log +import de.bixilon.minosoft.util.logging.LogLevels +import de.bixilon.minosoft.util.logging.LogMessageType +import java.io.File +import java.io.FileInputStream +import java.util.jar.JarEntry +import java.util.jar.JarInputStream + + +object ModLoader { + private val BASE_PATH = RunConfiguration.HOME_DIRECTORY + "mods/" + private const val MANFIEST = "manifest.json" + private val mods: MutableList = mutableListOf() + private var latch: CountUpAndDownLatch? = null + var currentPhase by watched(LoadingPhases.PRE_BOOT) + private set + var state by watched(PhaseStates.WAITING) + private set + + private val LoadingPhases.path: File get() = File(BASE_PATH + name.lowercase()) + + private fun createDirectories() { + val created: MutableList = mutableListOf() + for (phase in LoadingPhases.VALUES) { + val path = phase.path + if (!path.exists()) { + path.mkdirs() + created += phase + } + } + if (created.isNotEmpty()) { + Log.log(LogMessageType.MOD_LOADING, LogLevels.VERBOSE) { "Created mod folders: $created" } + } + } + + fun initModLoading() { + DefaultThreadPool += { createDirectories() } + } + + private fun MinosoftMod.processFile(path: String, data: ByteArray) { + TODO("Directory") + } + + private fun MinosoftMod.processJar(file: File) { + val stream = JarInputStream(FileInputStream(file)) + while (true) { + val entry = stream.nextEntry ?: break + if (entry.isDirectory) { + continue + } + + if (entry.name.endsWith(".class") && entry is JarEntry) { + this.classLoader.load(entry, stream) + } else if (entry.name.startsWith("assets/")) { + TODO("Assets") + } else if (entry.name == MANFIEST) { + manifest = stream.readJson(false) + } + } + stream.close() + } + + private fun MinosoftMod.construct() { + val manifest = manifest ?: throw IllegalStateException("Mod $path has no manifest!") + val mainClass = Class.forName(manifest.main, true, classLoader) + val main = mainClass.kotlin.objectInstance ?: throw IllegalStateException("${manifest.main} is not an kotlin object!") + if (main !is ModMain) { + throw IllegalStateException("${manifest.main} does not inherit ModMain!") + } + this.main = main + main.init() + } + + private fun MinosoftMod.postInit() { + main!!.postInit() + } + + @Synchronized + fun load(phase: LoadingPhases, latch: CountUpAndDownLatch) { + if (RunConfiguration.IGNORE_MODS) { + return + } + Log.log(LogMessageType.MOD_LOADING, LogLevels.VERBOSE) { "Starting mod load: $phase" } + // ToDo: check phase + this.currentPhase = phase + + val inner = CountUpAndDownLatch(1, latch) + this.latch = inner + this.state = PhaseStates.LISTING + + val path = phase.path + val files = path.listFiles() + if (files == null || files.isEmpty()) { + // no mods to load + inner.dec() + state = PhaseStates.COMPLETE + return + } + val mods: MutableList = mutableListOf() + + state = PhaseStates.INJECTING + for (file in files) { + if (!file.isDirectory && !file.name.endsWith(".jar") && !file.name.endsWith(".zip")) { + continue + } + val mod = MinosoftMod(file, phase, CountUpAndDownLatch(3, inner)) + Log.log(LogMessageType.MOD_LOADING, LogLevels.VERBOSE) { "Injecting $file" } + try { + if (file.isDirectory) { + TODO("Scanning directory") + } + mod.processJar(file) + mods += mod + mod.latch.dec() + } catch (exception: Throwable) { + Log.log(LogMessageType.MOD_LOADING, LogLevels.WARN) { "Error injecting mod: $file" } + exception.printStackTrace() + mod.latch.count = 0 + } + } + + + state = PhaseStates.CONSTRUCTING + var worker = UnconditionalWorker() + for (mod in mods) { + worker += { + try { + mod.construct() + mod.latch.dec() + } catch (error: Throwable) { + mod.latch.count = 0 + error.printStackTrace() + mods -= mod + } + } + } + worker.work(inner) + + state = PhaseStates.POST_INIT + worker = UnconditionalWorker() + for (mod in mods) { + worker += { + try { + mod.postInit() + mod.latch.dec() + } catch (error: Throwable) { + mod.latch.count = 0 + error.printStackTrace() + mods -= mod + } + } + } + worker.work(inner) + + + this.mods += mods + state = PhaseStates.COMPLETE + inner.dec() + if (phase == LoadingPhases.POST_BOOT) { + Log.log(LogMessageType.MOD_LOADING, LogLevels.INFO) { "Mod loading completed!" } + } + } + + fun await(phase: LoadingPhases) { + if (RunConfiguration.IGNORE_MODS) { + return + } + val latch = this.latch + val currentPhase = this.currentPhase + val state = this.state + if (currentPhase == phase) { + if (state == PhaseStates.COMPLETE) { + return + } + latch!!.await() // ToDo: What if phase has not started yet? + return + } + if (phase.ordinal < currentPhase.ordinal) { + // already done + return + } + throw IllegalStateException("$phase has not started yet!") + } +} diff --git a/src/main/java/de/bixilon/minosoft/modding/loader/PhaseStates.kt b/src/main/java/de/bixilon/minosoft/modding/loader/PhaseStates.kt new file mode 100644 index 000000000..f7106956a --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/modding/loader/PhaseStates.kt @@ -0,0 +1,24 @@ +/* + * Minosoft + * Copyright (C) 2020-2022 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.loader + +enum class PhaseStates { + WAITING, + LISTING, + INJECTING, + CONSTRUCTING, + POST_INIT, + COMPLETE, + ; +} diff --git a/src/main/java/de/bixilon/minosoft/modding/loader/mod/MinosoftMod.kt b/src/main/java/de/bixilon/minosoft/modding/loader/mod/MinosoftMod.kt new file mode 100644 index 000000000..d3d3eec85 --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/modding/loader/mod/MinosoftMod.kt @@ -0,0 +1,32 @@ +/* + * Minosoft + * Copyright (C) 2020-2022 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.loader.mod + +import de.bixilon.kutil.latch.CountUpAndDownLatch +import de.bixilon.minosoft.assets.AssetsManager +import de.bixilon.minosoft.modding.loader.LoadingPhases +import de.bixilon.minosoft.modding.loader.mod.manifest.ModManifest +import org.xeustechnologies.jcl.JarClassLoader +import java.io.File + +class MinosoftMod( + val path: File, + val phase: LoadingPhases, + val latch: CountUpAndDownLatch, +) { + val classLoader = JarClassLoader() + var manifest: ModManifest? = null + var assetsManager: AssetsManager? = null + var main: ModMain? = null +} diff --git a/src/main/java/de/bixilon/minosoft/modding/loader/mod/ModMain.kt b/src/main/java/de/bixilon/minosoft/modding/loader/mod/ModMain.kt new file mode 100644 index 000000000..16b75fbe2 --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/modding/loader/mod/ModMain.kt @@ -0,0 +1,22 @@ +/* + * Minosoft + * Copyright (C) 2020-2022 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.loader.mod + +import de.bixilon.kutil.cast.CastUtil.unsafeNull +import de.bixilon.minosoft.assets.AssetsManager +import de.bixilon.minosoft.gui.rendering.gui.hud.Initializable + +abstract class ModMain : Initializable { + val assets: AssetsManager = unsafeNull() +} diff --git a/src/main/java/de/bixilon/minosoft/modding/loader/mod/manifest/ModManifest.kt b/src/main/java/de/bixilon/minosoft/modding/loader/mod/manifest/ModManifest.kt new file mode 100644 index 000000000..4f4b95f6a --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/modding/loader/mod/manifest/ModManifest.kt @@ -0,0 +1,23 @@ +/* + * Minosoft + * Copyright (C) 2020-2022 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.loader.mod.manifest + +import java.util.* + +data class ModManifest( + val name: String, + val uuid: UUID, + val version: String, + val main: String, +) diff --git a/src/main/java/de/bixilon/minosoft/protocol/network/connection/play/PlayConnection.kt b/src/main/java/de/bixilon/minosoft/protocol/network/connection/play/PlayConnection.kt index 30a8afd6a..915ad2fd3 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/network/connection/play/PlayConnection.kt +++ b/src/main/java/de/bixilon/minosoft/protocol/network/connection/play/PlayConnection.kt @@ -48,6 +48,8 @@ import de.bixilon.minosoft.gui.rendering.Rendering import de.bixilon.minosoft.modding.event.events.chat.ChatMessageReceiveEvent import de.bixilon.minosoft.modding.event.events.loading.RegistriesLoadEvent import de.bixilon.minosoft.modding.event.invoker.CallbackEventInvoker +import de.bixilon.minosoft.modding.loader.LoadingPhases +import de.bixilon.minosoft.modding.loader.ModLoader import de.bixilon.minosoft.protocol.network.connection.Connection import de.bixilon.minosoft.protocol.network.connection.play.clientsettings.ClientSettingsManager import de.bixilon.minosoft.protocol.network.connection.play.plugin.DefaultPluginHandler @@ -168,6 +170,9 @@ class PlayConnection( val count = latch.count check(!wasConnected) { "Connection was already connected!" } try { + state = PlayConnectionStates.WAITING_MODS + ModLoader.await(LoadingPhases.BOOT) + state = PlayConnectionStates.LOADING_ASSETS var error: Throwable? = null val taskWorker = TaskWorker(errorHandler = { _, exception -> if (error == null) error = exception }, criticalErrorHandler = { _, exception -> if (error == null) error = exception }) diff --git a/src/main/java/de/bixilon/minosoft/protocol/network/connection/play/PlayConnectionStates.kt b/src/main/java/de/bixilon/minosoft/protocol/network/connection/play/PlayConnectionStates.kt index a9e842244..65bc958d1 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/network/connection/play/PlayConnectionStates.kt +++ b/src/main/java/de/bixilon/minosoft/protocol/network/connection/play/PlayConnectionStates.kt @@ -22,6 +22,8 @@ import de.bixilon.minosoft.util.KUtil.toResourceLocation enum class PlayConnectionStates : Translatable { WAITING, + WAITING_MODS, + LOADING_ASSETS, LOADING, diff --git a/src/main/java/de/bixilon/minosoft/terminal/CommandLineArguments.kt b/src/main/java/de/bixilon/minosoft/terminal/CommandLineArguments.kt index bc704b674..742f619be 100644 --- a/src/main/java/de/bixilon/minosoft/terminal/CommandLineArguments.kt +++ b/src/main/java/de/bixilon/minosoft/terminal/CommandLineArguments.kt @@ -82,6 +82,10 @@ object CommandLineArguments { addArgument("--ignore_yggdrasil") .action(Arguments.storeTrue()) .help("Disable all yggdrasil (mojang) signature checking") + + addArgument("--ignore_mods") + .action(Arguments.storeTrue()) + .help("Ignores all mods and disable mod loading") } fun parse(args: Array) { @@ -125,5 +129,6 @@ object CommandLineArguments { RunConfiguration.VERBOSE_LOGGING = namespace.getBoolean("verbose") RunConfiguration.IGNORE_YGGDRASIL = namespace.getBoolean("ignore_yggdrasil") + RunConfiguration.IGNORE_MODS = namespace.getBoolean("ignore_mods") } } diff --git a/src/main/java/de/bixilon/minosoft/terminal/RunConfiguration.kt b/src/main/java/de/bixilon/minosoft/terminal/RunConfiguration.kt index 339aa1b5a..507aea535 100644 --- a/src/main/java/de/bixilon/minosoft/terminal/RunConfiguration.kt +++ b/src/main/java/de/bixilon/minosoft/terminal/RunConfiguration.kt @@ -66,4 +66,6 @@ object RunConfiguration { var IGNORE_YGGDRASIL = false + + var IGNORE_MODS = false }