wip mod loading

This commit is contained in:
Bixilon 2022-10-25 20:18:54 +02:00
parent 6ba4928b1f
commit 0eb773f7fa
No known key found for this signature in database
GPG Key ID: 5CAD791931B09AC4
15 changed files with 416 additions and 5 deletions

View File

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

View File

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

View File

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

View File

@ -27,5 +27,6 @@ enum class BootTasks {
STARTUP_PROGRESS,
ASSETS_OVERRIDE,
CLI,
MODS,
;
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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<String, JclJarEntry>
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
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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<LoadingPhases> {
override val VALUES: Array<LoadingPhases> = values()
override val NAME_MAP: Map<String, LoadingPhases> = EnumUtil.getEnumValues(VALUES)
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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<MinosoftMod> = 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<LoadingPhases> = 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<MinosoftMod> = 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!")
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,6 +22,8 @@ import de.bixilon.minosoft.util.KUtil.toResourceLocation
enum class PlayConnectionStates : Translatable {
WAITING,
WAITING_MODS,
LOADING_ASSETS,
LOADING,

View File

@ -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<String>) {
@ -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")
}
}

View File

@ -66,4 +66,6 @@ object RunConfiguration {
var IGNORE_YGGDRASIL = false
var IGNORE_MODS = false
}