Compare commits
43 commits
e5d4a343ca
...
acc021ffa0
Author | SHA1 | Date | |
---|---|---|---|
moehreag | acc021ffa0 | ||
moehreag | 8f58e1601c | ||
moehreag | 101dec4e14 | ||
moehreag | 347ad2a58a | ||
owlsys | 4aec13abc2 | ||
moehreag | 75607fc7fe | ||
moehreag | 75b6efdc51 | ||
moehreag | 93e397a19c | ||
moehreag | 932cf3e076 | ||
moehreag | f6f536c182 | ||
moehreag | b82e6bceae | ||
moehreag | 5a312eaba3 | ||
moehreag | 98848bafee | ||
moehreag | e688242ef5 | ||
moehreag | 621816c6d8 | ||
moehreag | 1c3a8bd070 | ||
moehreag | 6ff44aac48 | ||
moehreag | e076c98531 | ||
moehreag | 9675b66276 | ||
moehreag | 7f04454847 | ||
moehreag | 525d16b17d | ||
moehreag | f197ebca4b | ||
moehreag | a62114b201 | ||
moehreag | ef6e0136f1 | ||
owlsys | a163a5ef89 | ||
owlsys | 36260356e2 | ||
moehreag | 6c564df5dd | ||
moehreag | 7f22499f32 | ||
moehreag | ac09413c61 | ||
moehreag | 9e61b40f81 | ||
moehreag | de57a631a2 | ||
moehreag | 0bc8d555d0 | ||
moehreag | dd8b87a1cf | ||
moehreag | 1dcd0c8c05 | ||
moehreag | 1fdb459433 | ||
moehreag | 0e826dac3a | ||
moehreag | f8c6b88d8f | ||
moehreag | dde054f6a9 | ||
Ecorous | 44378d47cc | ||
moehreag | 2e64ad2289 | ||
moehreag | 1b4b1d0d93 | ||
TheKodeToad | 8487df905b | ||
moehreag | 6bd11ad76d |
24
.forgejo/workflows/publish.yml
Normal file
24
.forgejo/workflows/publish.yml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
name: Publish to snapshot maven
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'main'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: docker
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: https://github.com/actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: 21
|
||||||
|
- uses: https://github.com/gradle/actions/setup-gradle@v3
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
chmod +x ./gradlew
|
||||||
|
cat ./settings.gradle.kts | sed "s/^.*:minecraft.*$//g" > settings.gradle.kts
|
||||||
|
./gradlew :publishMavenJavaPublicationToFrogMCSnapshotsMavenRepository \
|
||||||
|
-PFrogMCSnapshotsMavenUsername=${{ secrets.MAVEN_PUSH_USER }} \
|
||||||
|
-PFrogMCSnapshotsMavenPassword=${{ secrets.MAVEN_PUSH_TOKEN }}
|
|
@ -1,21 +1,21 @@
|
||||||
plugins {
|
plugins {
|
||||||
java
|
java
|
||||||
`java-library`
|
`java-library`
|
||||||
id("io.freefair.lombok").version("8.+")
|
id("io.freefair.lombok") version "8.+"
|
||||||
`maven-publish`
|
`maven-publish`
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "org.ecorous.esnesnon"
|
group = "dev.frogmc"
|
||||||
version = "0.0.1-SNAPSHOT"
|
version = "0.0.1-SNAPSHOT"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
maven {
|
maven {
|
||||||
name = "Esnesnon Maven/Snapshots"
|
name = "FrogMC Maven/Snapshots"
|
||||||
url = uri("https://maven-esnesnon.ecorous.org/snapshots")
|
url = uri("https://maven.frogmc.dev/snapshots")
|
||||||
}
|
}
|
||||||
maven {
|
maven {
|
||||||
name = "Esnesnon Maven/Releases"
|
name = "FrogMC Maven/Releases"
|
||||||
url = uri("https://maven-esnesnon.ecorous.org/releases")
|
url = uri("https://maven.frogmc.dev/releases")
|
||||||
}
|
}
|
||||||
maven("https://maven.fabricmc.net/")
|
maven("https://maven.fabricmc.net/")
|
||||||
maven("https://repo.spongepowered.org/maven")
|
maven("https://repo.spongepowered.org/maven")
|
||||||
|
@ -23,19 +23,39 @@ repositories {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.remapper)
|
implementation(libs.thyroxine){
|
||||||
|
isTransitive = false
|
||||||
|
}
|
||||||
compileOnly("org.apache.logging.log4j:log4j-slf4j2-impl:3.0.0-beta2")
|
compileOnly("org.apache.logging.log4j:log4j-slf4j2-impl:3.0.0-beta2")
|
||||||
compileOnly("org.apache.logging.log4j:log4j-api:3.0.0-beta2")
|
compileOnly("org.apache.logging.log4j:log4j-api:3.0.0-beta2")
|
||||||
compileOnly("org.apache.logging.log4j:log4j-core:3.0.0-beta2")
|
compileOnly("org.apache.logging.log4j:log4j-core:3.0.0-beta2")
|
||||||
|
|
||||||
api(libs.mixin)
|
api(libs.mixin)
|
||||||
api(libs.nightconfig)
|
api(libs.mixinextras)
|
||||||
|
implementation(libs.nightconfig)
|
||||||
api(libs.annotations)
|
api(libs.annotations)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
java {
|
java {
|
||||||
sourceCompatibility = JavaVersion.VERSION_21
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
targetCompatibility = JavaVersion.VERSION_21
|
targetCompatibility = JavaVersion.VERSION_21
|
||||||
|
|
||||||
|
withJavadocJar()
|
||||||
|
withSourcesJar()
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.javadoc {
|
||||||
|
include("**/api/**")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.processResources {
|
||||||
|
inputs.property("version", version)
|
||||||
|
|
||||||
|
filesMatching("frog.mod.toml") {
|
||||||
|
expand("version" to version)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
publishing {
|
publishing {
|
||||||
|
@ -47,8 +67,8 @@ publishing {
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
maven {
|
maven {
|
||||||
name = "EsnesnonSnapshotsMaven"
|
name = "FrogMCSnapshotsMaven"
|
||||||
url = uri("https://maven-esnesnon.ecorous.org/snapshots")
|
url = uri("https://maven.frogmc.dev/snapshots")
|
||||||
credentials(PasswordCredentials::class)
|
credentials(PasswordCredentials::class)
|
||||||
authentication {
|
authentication {
|
||||||
create<BasicAuthentication>("basic")
|
create<BasicAuthentication>("basic")
|
||||||
|
@ -56,8 +76,8 @@ publishing {
|
||||||
}
|
}
|
||||||
|
|
||||||
maven {
|
maven {
|
||||||
name = "EsnesnonReleasesMaven"
|
name = "FrogMCReleasesMaven"
|
||||||
url = uri("https://maven-esnesnon.ecorous.org/releases")
|
url = uri("https://maven.frogmc.dev/releases")
|
||||||
credentials(PasswordCredentials::class)
|
credentials(PasswordCredentials::class)
|
||||||
authentication {
|
authentication {
|
||||||
create<BasicAuthentication>("basic")
|
create<BasicAuthentication>("basic")
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
[versions]
|
[versions]
|
||||||
|
|
||||||
remapper = "1.0.0-SNAPSHOT"
|
thyroxine = "1.0.0-SNAPSHOT"
|
||||||
nightconfig = "3.7.1"
|
nightconfig = "3.7.2"
|
||||||
mixin = "0.13.4+mixin.0.8.5"
|
mixin = "0.14.0+mixin.0.8.6"
|
||||||
annotations = "24.1.0"
|
annotations = "24.1.0"
|
||||||
|
mixinextras = "0.3.6"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
|
|
||||||
remapper = { module = "org.ecorous.esnesnon:nonsense-remapper", version.ref = "remapper" }
|
thyroxine = { module = "dev.frogmc:thyroxine", version.ref = "thyroxine" }
|
||||||
nightconfig = { module = "com.electronwill.night-config:toml", version.ref = "nightconfig" }
|
nightconfig = { module = "com.electronwill.night-config:toml", version.ref = "nightconfig" }
|
||||||
mixin = { module = "net.fabricmc:sponge-mixin", version.ref = "mixin" }
|
mixin = { module = "net.fabricmc:sponge-mixin", version.ref = "mixin" }
|
||||||
annotations = { module = "org.jetbrains:annotations", version.ref = "annotations" }
|
annotations = { module = "org.jetbrains:annotations", version.ref = "annotations" }
|
||||||
|
mixinextras = { module = "io.github.llamalad7:mixinextras-common", version.ref = "mixinextras" }
|
||||||
|
|
||||||
[bundles]
|
[bundles]
|
||||||
|
|
||||||
|
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,5 +1,5 @@
|
||||||
#Sat May 11 16:50:23 CEST 2024
|
#Sat May 11 16:50:23 CEST 2024
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import org.ecorous.esnesnon.gradle.ext.minecraft
|
import dev.frogmc.phytotelma.ext.minecraft
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
java
|
java
|
||||||
id("org.ecorous.esnesnon.nonsense-gradle").version("0.0.1-SNAPSHOT")
|
id("dev.frogmc.phytotelma") version "0.0.1-SNAPSHOT"
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
maven {
|
maven {
|
||||||
name = "Esnesnon Maven/Snapshots"
|
name = "FrogMC Maven/Snapshots"
|
||||||
url = uri("https://maven-esnesnon.ecorous.org/snapshots")
|
url = uri("https://maven-frogmc.ecorous.org/snapshots")
|
||||||
}
|
}
|
||||||
maven {
|
maven {
|
||||||
name = "Esnesnon Maven/Releases"
|
name = "FrogMC Maven/Releases"
|
||||||
url = uri("https://maven-esnesnon.ecorous.org/releases")
|
url = uri("https://maven-frogmc.ecorous.org/releases")
|
||||||
}
|
}
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
@ -21,5 +21,9 @@ minecraft("1.20.6")
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":"))
|
implementation(project(":"))
|
||||||
annotationProcessor(libs.mixin)
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
|
targetCompatibility = JavaVersion.VERSION_21
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.example;
|
package dev.frogmc.frogloader.example;
|
||||||
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.api.extensions.PreLaunchExtension;
|
import dev.frogmc.frogloader.api.extensions.PreLaunchExtension;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.example.mixin;
|
package dev.frogmc.frogloader.example.mixin;
|
||||||
|
|
||||||
import net.minecraft.client.gui.components.FocusableTextWidget;
|
import net.minecraft.client.gui.components.FocusableTextWidget;
|
||||||
import net.minecraft.client.gui.screens.Screen;
|
import net.minecraft.client.gui.screens.Screen;
|
||||||
|
@ -16,7 +16,7 @@ public abstract class TitleScreenMixin extends Screen {
|
||||||
super(title);
|
super(title);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject(method = "createNormalMenuOptions", at = @At("TAIL"), remap = false)
|
@Inject(method = "createNormalMenuOptions", at = @At("TAIL"))
|
||||||
private void showExample(int y, int rowHeight, CallbackInfo ci) {
|
private void showExample(int y, int rowHeight, CallbackInfo ci) {
|
||||||
var widget = new FocusableTextWidget(200, Component.literal("<insert frog here!>"), this.font);
|
var widget = new FocusableTextWidget(200, Component.literal("<insert frog here!>"), this.font);
|
||||||
widget.setPosition(width / 2 - widget.getWidth(), 20);
|
widget.setPosition(width / 2 - widget.getWidth(), 20);
|
5
minecraft/src/main/resources/example_mod.accesswidener
Normal file
5
minecraft/src/main/resources/example_mod.accesswidener
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
accessWidener v2 named
|
||||||
|
|
||||||
|
accessible method net/minecraft/world/level/block/TransparentBlock codec ()Lcom/mojang/serialization/MapCodec;
|
||||||
|
mutable field net/minecraft/client/gui/GuiSpriteManager METADATA_SECTIONS Ljava/util/Set;
|
||||||
|
mutable field net/minecraft/client/gui/GuiGraphics$ScissorStack stack Ljava/util/Deque;
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"required": true,
|
"required": true,
|
||||||
"minVersion": "0.8",
|
"minVersion": "0.8",
|
||||||
"package": "org.ecorous.esnesnon.nonsense.loader.example.mixin",
|
"package": "dev.frogmc.frogloader.example.mixin",
|
||||||
"compatibilityLevel": "JAVA_21",
|
"compatibilityLevel": "JAVA_21",
|
||||||
"mixins": [],
|
"mixins": [],
|
||||||
"client": [
|
"client": [
|
||||||
|
|
17
minecraft/src/main/resources/frog.mod.toml
Normal file
17
minecraft/src/main/resources/frog.mod.toml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
[frog]
|
||||||
|
format_version = "1.0.0"
|
||||||
|
|
||||||
|
[frog.mod]
|
||||||
|
id = "example_mod"
|
||||||
|
name = "Example Mod"
|
||||||
|
version = "1.0.0"
|
||||||
|
license = "CC0-1.0"
|
||||||
|
credits = [
|
||||||
|
{ name = "You", roles = ["author", "other_role"] }
|
||||||
|
]
|
||||||
|
|
||||||
|
[frog.extensions]
|
||||||
|
pre_launch = "dev.frogmc.frogloader.example.ExamplePreLaunchExtension"
|
||||||
|
mixin_config = "example_mod.mixins.json"
|
||||||
|
frog_aw = "example_mod.accesswidener"
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
[nonsense]
|
|
||||||
id = "example_mod"
|
|
||||||
name = "Example Mod"
|
|
||||||
version = "1.0.0"
|
|
||||||
license = "CC0-1.0"
|
|
||||||
credits = [
|
|
||||||
{ name = "You", roles = ["author", "other_role"] }
|
|
||||||
]
|
|
||||||
|
|
||||||
[nonsense.dependencies]
|
|
||||||
depends = [
|
|
||||||
{ id = "other_mod", versions = ">=0.2.0" }
|
|
||||||
]
|
|
||||||
breaks = [
|
|
||||||
{ id = "old_mod", versions = "*" }
|
|
||||||
]
|
|
||||||
|
|
||||||
[nonsense.extensions]
|
|
||||||
pre_launch = "org.ecorous.esnesnon.nonsense.loader.example.ExamplePreLaunchExtension"
|
|
||||||
mixin_config = "example_mod.mixins.json"
|
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
pluginManagement {
|
pluginManagement {
|
||||||
repositories {
|
repositories {
|
||||||
maven {
|
maven {
|
||||||
name = "Esnesnos Maven Releases"
|
name = "FrogMC Maven Releases"
|
||||||
url = uri("https://maven-esnesnon.ecorous.org/releases")
|
url = uri("https://maven.frogmc.dev/releases")
|
||||||
}
|
}
|
||||||
maven {
|
maven {
|
||||||
name = "Esnesnos Maven Snapshots"
|
name = "FrogMC Maven Snapshots"
|
||||||
url = uri("https://maven-esnesnon.ecorous.org/snapshots")
|
url = uri("https://maven.frogmc.dev/snapshots")
|
||||||
}
|
}
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.name = "nonsense-loader"
|
rootProject.name = "frogloader"
|
||||||
|
|
||||||
include(":minecraft")
|
include(":minecraft")
|
||||||
|
|
||||||
|
|
100
src/main/java/dev/frogmc/frogloader/api/FrogLoader.java
Normal file
100
src/main/java/dev/frogmc/frogloader/api/FrogLoader.java
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
package dev.frogmc.frogloader.api;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import dev.frogmc.frogloader.api.env.Env;
|
||||||
|
import dev.frogmc.frogloader.api.mod.ModProperties;
|
||||||
|
import dev.frogmc.frogloader.api.plugin.FrogPlugin;
|
||||||
|
import dev.frogmc.frogloader.impl.FrogLoaderImpl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* General API to interact with this loader.
|
||||||
|
*
|
||||||
|
* @see ModProperties
|
||||||
|
* @see FrogPlugin
|
||||||
|
* @see Env
|
||||||
|
*/
|
||||||
|
public interface FrogLoader {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an instance of this loader.
|
||||||
|
*
|
||||||
|
* @return An instance of this loader
|
||||||
|
*/
|
||||||
|
static FrogLoader getInstance() {
|
||||||
|
return FrogLoaderImpl.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all loaded plugins.
|
||||||
|
*
|
||||||
|
* @return A collection of all loaded plugins
|
||||||
|
*/
|
||||||
|
Collection<FrogPlugin> getPlugins();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current (physical) environment.
|
||||||
|
*
|
||||||
|
* @return The current environment
|
||||||
|
* @see Env
|
||||||
|
*/
|
||||||
|
Env getEnv();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current game directory.
|
||||||
|
*
|
||||||
|
* @return The current game directory
|
||||||
|
* <p>Note: Should always be the current working directory, but this method should</p>
|
||||||
|
* be preferred over using the working directory directly.
|
||||||
|
*/
|
||||||
|
Path getGameDir();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current config directory.
|
||||||
|
*
|
||||||
|
* @return The current config directory
|
||||||
|
*/
|
||||||
|
Path getConfigDir();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current mods directory.
|
||||||
|
*
|
||||||
|
* @return The current mods directory
|
||||||
|
*/
|
||||||
|
Path getModsDir();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query whether this loader is currently running in a development environment.
|
||||||
|
*
|
||||||
|
* @return Whether this loader is currently running in a development environment
|
||||||
|
*/
|
||||||
|
boolean isDevelopment();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query whether a specific mod is loaded.
|
||||||
|
*
|
||||||
|
* @param id The mod id to query for
|
||||||
|
* @return Whether a mod with this specific id is loaded
|
||||||
|
*/
|
||||||
|
boolean isModLoaded(String id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a mod's properties.
|
||||||
|
*
|
||||||
|
* @param id The mod id to query for
|
||||||
|
* @return An Optional containing the mod's properties, if it is present
|
||||||
|
* @see ModProperties
|
||||||
|
*/
|
||||||
|
Optional<ModProperties> getModProperties(String id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all loaded mods
|
||||||
|
*
|
||||||
|
* @return A collection of all loaded mods
|
||||||
|
* @see ModProperties
|
||||||
|
* @see FrogPlugin
|
||||||
|
*/
|
||||||
|
Collection<ModProperties> getMods();
|
||||||
|
}
|
40
src/main/java/dev/frogmc/frogloader/api/env/Env.java
vendored
Normal file
40
src/main/java/dev/frogmc/frogloader/api/env/Env.java
vendored
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package dev.frogmc.frogloader.api.env;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Physical environment constants
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
public enum Env {
|
||||||
|
/**
|
||||||
|
* The physical client environment
|
||||||
|
*/
|
||||||
|
CLIENT("CLIENT", "client"),
|
||||||
|
/**
|
||||||
|
* The physical (dedicated) server environment
|
||||||
|
*/
|
||||||
|
SERVER("SERVER", "server"),
|
||||||
|
;
|
||||||
|
|
||||||
|
|
||||||
|
private final String mixinName, identifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get this environment's name, in the format Mixin understands.
|
||||||
|
*
|
||||||
|
* @return This environment's mixin name
|
||||||
|
*/
|
||||||
|
public String getMixinName() {
|
||||||
|
return this.mixinName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get this environment's identifier
|
||||||
|
*
|
||||||
|
* @return This environment's identifier
|
||||||
|
*/
|
||||||
|
public String getIdentifier() {
|
||||||
|
return this.identifier;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package dev.frogmc.frogloader.api.extensions;
|
||||||
|
|
||||||
|
import dev.frogmc.frogloader.api.mod.ModExtensions;
|
||||||
|
import dev.frogmc.frogloader.impl.mod.BuiltinExtensions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Pre-Launch Extension.
|
||||||
|
* <p>This Extension is run right before the game is launched. (provided the used plugin supports it :) )</p>
|
||||||
|
*
|
||||||
|
* @see ModExtensions
|
||||||
|
*/
|
||||||
|
public interface PreLaunchExtension {
|
||||||
|
/**
|
||||||
|
* This extension's id. This is the key to use in your frog.mod.toml.
|
||||||
|
*/
|
||||||
|
String ID = BuiltinExtensions.PRE_LAUNCH;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The initializer. This method will be invoked when this extension is run.
|
||||||
|
*/
|
||||||
|
void onPreLaunch();
|
||||||
|
}
|
158
src/main/java/dev/frogmc/frogloader/api/mod/ModDependencies.java
Normal file
158
src/main/java/dev/frogmc/frogloader/api/mod/ModDependencies.java
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
package dev.frogmc.frogloader.api.mod;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mod's dependencies.
|
||||||
|
*
|
||||||
|
* <p>Mod dependencies are declared in four types:</p>
|
||||||
|
* <code>Type.DEPEND</code>, <code>Type.BREAK</code>, <code>Type.SUGGEST</code> and <code>Type.PROVIDE</code>
|
||||||
|
*
|
||||||
|
* <p>Note: The <code>Type.PROVIDE</code> is a bit of a special case in the way that it only supports a single SemVer version instead of a full version range.</p>
|
||||||
|
*
|
||||||
|
* @see ModDependencies.Type
|
||||||
|
* @see ModDependencies.Entry
|
||||||
|
* @see ModProperties
|
||||||
|
* @see SemVer
|
||||||
|
*/
|
||||||
|
public final class ModDependencies {
|
||||||
|
|
||||||
|
private final Map<Type, Collection<Entry>> entries = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct new mod dependencies for a mod.
|
||||||
|
* <p><strong>Internal use only.</strong></p>
|
||||||
|
*
|
||||||
|
* @param depends <code>Type.DEPEND</code> entries
|
||||||
|
* @param breaks <code>Type.BREAK</code> entries
|
||||||
|
* @param suggests <code>Type.SUGGEST</code> entries
|
||||||
|
* @param provides <code>Type.PROVIDE</code> entries
|
||||||
|
*/
|
||||||
|
@ApiStatus.Internal
|
||||||
|
public ModDependencies(Collection<Entry> depends, Collection<Entry> breaks, Collection<Entry> suggests, Collection<Entry> provides) {
|
||||||
|
entries.put(Type.DEPEND, depends);
|
||||||
|
entries.put(Type.BREAK, breaks);
|
||||||
|
entries.put(Type.SUGGEST, suggests);
|
||||||
|
entries.put(Type.PROVIDE, provides);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get dependencies of a specific mod for a specific type
|
||||||
|
*
|
||||||
|
* @param type the dependency type to query for
|
||||||
|
* @return a collection of dependency entries
|
||||||
|
*/
|
||||||
|
public Collection<Entry> getForType(Type type) {
|
||||||
|
return entries.get(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get entries that depend on a specific mod's id
|
||||||
|
*
|
||||||
|
* @param id the mod id to find dependency entries for
|
||||||
|
* @return a collection of entries that depend on the given mod id
|
||||||
|
*/
|
||||||
|
public Collection<ModEntry> getForModId(String id) {
|
||||||
|
List<ModEntry> entries = new ArrayList<>();
|
||||||
|
for (Type type : Type.values()) {
|
||||||
|
for (Entry entry : getForType(type)) {
|
||||||
|
if (entry.id.equals(id)) {
|
||||||
|
entries.add(new ModEntry(type, entry.range, entry.link, entry.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dependency types to distinguish their variants.
|
||||||
|
*/
|
||||||
|
public enum Type {
|
||||||
|
/**
|
||||||
|
* Depend on another mod
|
||||||
|
*/
|
||||||
|
DEPEND,
|
||||||
|
/**
|
||||||
|
* Declare another mod as breaking with your own
|
||||||
|
*/
|
||||||
|
BREAK,
|
||||||
|
/**
|
||||||
|
* Suggest a user to install another mod
|
||||||
|
*/
|
||||||
|
SUGGEST,
|
||||||
|
/**
|
||||||
|
* Declare your mod to provide another mod
|
||||||
|
*/
|
||||||
|
PROVIDE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A data class to handle entries depending on a specific mod id
|
||||||
|
*/
|
||||||
|
public static class ModEntry {
|
||||||
|
private final Type type;
|
||||||
|
private final String range;
|
||||||
|
private final String link;
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
private ModEntry(Type type, String range, String link, String name) {
|
||||||
|
this.type = type;
|
||||||
|
this.range = range;
|
||||||
|
this.link = link;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get this dependency's type
|
||||||
|
*
|
||||||
|
* @return This dependency's type
|
||||||
|
*/
|
||||||
|
public Type type() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get this dependency's version range
|
||||||
|
*
|
||||||
|
* @return This dependency's version range
|
||||||
|
*/
|
||||||
|
public String range() {
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get this dependency's link
|
||||||
|
* <p>May be null.</p>
|
||||||
|
*
|
||||||
|
* @return This dependency's link
|
||||||
|
*/
|
||||||
|
public @Nullable String link() {
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get this dependency's (friendly) name
|
||||||
|
* <p>May be null.</p>
|
||||||
|
*
|
||||||
|
* @return This dependency's name
|
||||||
|
*/
|
||||||
|
public @Nullable String name() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* General storage for dependencies
|
||||||
|
*
|
||||||
|
* @param id the mod's id
|
||||||
|
* @param range the dependency's version range
|
||||||
|
* @param link an optional link for information about this dependency
|
||||||
|
* @param name an optional (friendly) name for this dependency
|
||||||
|
*/
|
||||||
|
public record Entry(String id, String range, String link, String name) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
127
src/main/java/dev/frogmc/frogloader/api/mod/ModExtensions.java
Normal file
127
src/main/java/dev/frogmc/frogloader/api/mod/ModExtensions.java
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
package dev.frogmc.frogloader.api.mod;
|
||||||
|
|
||||||
|
import java.lang.invoke.MethodHandle;
|
||||||
|
import java.lang.invoke.MethodHandles;
|
||||||
|
import java.lang.invoke.MethodType;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class stores a mod's extensions.
|
||||||
|
* <p>An extension is a simple key-value mapping of a string name to any value.</p>
|
||||||
|
* <p>This class further provides utility methods to easily work with extension values of various types,
|
||||||
|
* especially for extensions providing some form of class or method reference.</p>
|
||||||
|
*
|
||||||
|
* @see ModProperties
|
||||||
|
*/
|
||||||
|
public final class ModExtensions {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(ModExtensions.class);
|
||||||
|
private final Map<String, Object> extensions;
|
||||||
|
|
||||||
|
private ModExtensions(Map<String, Object> entries) {
|
||||||
|
extensions = entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a new instance of this class.
|
||||||
|
* <p><strong>Internal use only.</strong></p>
|
||||||
|
*
|
||||||
|
* @param entries the entries read from the mod's properties file
|
||||||
|
* @return an instance of this class
|
||||||
|
*/
|
||||||
|
public static ModExtensions of(Map<String, Object> entries) {
|
||||||
|
return new ModExtensions(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the value of the provided extension name.
|
||||||
|
*
|
||||||
|
* @param key The extension name to query
|
||||||
|
* @param <T> The type of the value of this extension
|
||||||
|
* @return The value of the extension, or null
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> T get(String key) {
|
||||||
|
return (T) extensions.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the value of the provided extension name.
|
||||||
|
*
|
||||||
|
* @param key The extension name to query
|
||||||
|
* @param defaultValue a default value
|
||||||
|
* @param <T> The type of the value of this extension
|
||||||
|
* @return The value of the extension, or the default value if it isn't present
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> T getOrDefault(String key, T defaultValue) {
|
||||||
|
return (T) extensions.getOrDefault(key, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the given action on this extension if it is present.
|
||||||
|
*
|
||||||
|
* @param key the extension name to query
|
||||||
|
* @param action the action to run on the value of the extension if it is present
|
||||||
|
* @param <T> The type of the value of this extension
|
||||||
|
*/
|
||||||
|
public <T> void runIfPresent(String key, Consumer<T> action) {
|
||||||
|
T value = get(key);
|
||||||
|
if (value != null) {
|
||||||
|
action.accept(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the given action on this extension if it is present.
|
||||||
|
* <p>This method simplifies handling references to classes or methods.</p>
|
||||||
|
* It will first query the string value of the extension and, if it is found, create a new instance of the referenced class
|
||||||
|
* or invoke the referenced method to retrieve an instance of the provided class. Then the provided action is run on this
|
||||||
|
* object.
|
||||||
|
*
|
||||||
|
* @param key The name of the extension
|
||||||
|
* @param type The class type of the extension (The class the extension class is extending/implementing)
|
||||||
|
* @param action The action to run on the newly retrieved instance of the provided class
|
||||||
|
* @param <T> The type of the class
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||||
|
public <T> void runIfPresent(String key, Class<T> type, Consumer<T> action) {
|
||||||
|
|
||||||
|
Object value = get(key);
|
||||||
|
|
||||||
|
if (value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Consumer<String> c = s -> {
|
||||||
|
try {
|
||||||
|
MethodHandle handle;
|
||||||
|
if (s.contains("::")) {
|
||||||
|
String[] parts = s.split("::");
|
||||||
|
handle = MethodHandles.lookup().findVirtual(Class.forName(parts[0]), parts[1], MethodType.methodType(type));
|
||||||
|
} else {
|
||||||
|
handle = MethodHandles.lookup().findConstructor(Class.forName(s), MethodType.methodType(void.class));
|
||||||
|
}
|
||||||
|
T object = (T) handle.invoke();
|
||||||
|
if (object != null) {
|
||||||
|
action.accept(object);
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
LOGGER.warn("Failed to instantiate Extension: ", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (value instanceof String s){
|
||||||
|
c.accept(s);
|
||||||
|
} else if (value instanceof Collection l){
|
||||||
|
((Collection<String>) l).forEach(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
package dev.frogmc.frogloader.api.mod;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mod's properties. This class represents a mod at runtime.
|
||||||
|
* It is read from each mod's <code>frog.mod.toml</code> file.
|
||||||
|
*
|
||||||
|
* @see ModDependencies
|
||||||
|
* @see ModExtensions
|
||||||
|
* @see SemVer
|
||||||
|
*/
|
||||||
|
public interface ModProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get this mod's id.
|
||||||
|
*
|
||||||
|
* @return The mod's id
|
||||||
|
*/
|
||||||
|
String id();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get this mod's name.
|
||||||
|
*
|
||||||
|
* @return The mod's name
|
||||||
|
*/
|
||||||
|
String name();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get this mod's icon.
|
||||||
|
* <p>May be null if the mod doesn't specify an icon.</p>
|
||||||
|
*
|
||||||
|
* @return The mod's icon
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
String icon();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get this mod's version.
|
||||||
|
*
|
||||||
|
* @return The mod's version
|
||||||
|
*/
|
||||||
|
SemVer version();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get this mod's license.
|
||||||
|
*
|
||||||
|
* @return The mod's license
|
||||||
|
*/
|
||||||
|
String license();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get credits for this mod.
|
||||||
|
* <p>This is a map containing people's names as keys and their roles in this mod as values.</p>
|
||||||
|
*
|
||||||
|
* @return The mod's credits.
|
||||||
|
*/
|
||||||
|
Map<String, Collection<String>> credits();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get this mod's dependencies.
|
||||||
|
*
|
||||||
|
* @return The mod's dependencies
|
||||||
|
* @see ModDependencies
|
||||||
|
*/
|
||||||
|
ModDependencies dependencies();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get this mod's declared extensions.
|
||||||
|
*
|
||||||
|
* @return The mod's extensions
|
||||||
|
* @see ModExtensions
|
||||||
|
*/
|
||||||
|
ModExtensions extensions();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get this mod's paths
|
||||||
|
* @return Where this mod is loaded from
|
||||||
|
*/
|
||||||
|
Collection<Path> paths();
|
||||||
|
|
||||||
|
}
|
46
src/main/java/dev/frogmc/frogloader/api/mod/SemVer.java
Normal file
46
src/main/java/dev/frogmc/frogloader/api/mod/SemVer.java
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package dev.frogmc.frogloader.api.mod;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple access to SemVer-style versions. This is used for versioning mods.
|
||||||
|
*
|
||||||
|
* @see ModProperties
|
||||||
|
*/
|
||||||
|
public interface SemVer extends Comparable<SemVer> {
|
||||||
|
/**
|
||||||
|
* Get this version's major version component.
|
||||||
|
*
|
||||||
|
* @return This version's major version component
|
||||||
|
*/
|
||||||
|
int major();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get this version's minor version component.
|
||||||
|
*
|
||||||
|
* @return This version's minor version component
|
||||||
|
*/
|
||||||
|
int minor();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get this version's patch version component.
|
||||||
|
*
|
||||||
|
* @return This version's patch version component
|
||||||
|
*/
|
||||||
|
int patch();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get this version's pre-release version component.
|
||||||
|
*
|
||||||
|
* @return This version's pre-release version component
|
||||||
|
*/
|
||||||
|
String prerelease();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get this version's build version component.
|
||||||
|
*
|
||||||
|
* @return This version's build version component
|
||||||
|
*/
|
||||||
|
String build();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
boolean equals(Object other);
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
package dev.frogmc.frogloader.api.plugin;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import dev.frogmc.frogloader.api.FrogLoader;
|
||||||
|
import dev.frogmc.frogloader.api.mod.ModProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Plugin that may load mods for a specific game and environment
|
||||||
|
*
|
||||||
|
* @see FrogLoader
|
||||||
|
*/
|
||||||
|
public interface FrogPlugin {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* General run method of this plugin. This method is run after all plugins have been initialized.
|
||||||
|
*
|
||||||
|
* @see FrogPlugin#init(FrogLoader)
|
||||||
|
* @see FrogPlugin#getMods()
|
||||||
|
*/
|
||||||
|
default void run() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether this plugin is applicable for the current environment
|
||||||
|
*
|
||||||
|
* @return Whether this plugin is applicable to be loaded in the current environment
|
||||||
|
*/
|
||||||
|
default boolean isApplicable() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialization method for this plugin. This method will be called after <code>isApplicable()</code>
|
||||||
|
* if it returns true to initialize is plugin.
|
||||||
|
*
|
||||||
|
* @param loader the loader loading this plugin
|
||||||
|
* @throws Exception This method may throw any exception, it will be handled by the loader.
|
||||||
|
* @see FrogPlugin#isApplicable()
|
||||||
|
* @see FrogPlugin#getMods()
|
||||||
|
*/
|
||||||
|
default void init(FrogLoader loader) throws Exception {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method should return all mods loaded by this plugin. It will be queried after <code>init(FrogLoader)</code>
|
||||||
|
* and before <code>run()</code>.
|
||||||
|
*
|
||||||
|
* @return A collection of mods loaded by this plugin.
|
||||||
|
* @see FrogPlugin#init(FrogLoader)
|
||||||
|
* @see FrogPlugin#run()
|
||||||
|
*/
|
||||||
|
default Collection<ModProperties> getMods() {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
}
|
37
src/main/java/dev/frogmc/frogloader/impl/Discovery.java
Normal file
37
src/main/java/dev/frogmc/frogloader/impl/Discovery.java
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package dev.frogmc.frogloader.impl;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.FileVisitResult;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.SimpleFileVisitor;
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
public class Discovery {
|
||||||
|
public static Collection<Path> find(Path start, Predicate<Path> directoryFilter, Predicate<Path> fileFilter) throws IOException {
|
||||||
|
List<Path> paths = new ArrayList<>();
|
||||||
|
Files.walkFileTree(start, new SimpleFileVisitor<>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
||||||
|
if (directoryFilter.test(dir)) {
|
||||||
|
return super.preVisitDirectory(dir, attrs);
|
||||||
|
}
|
||||||
|
return FileVisitResult.SKIP_SUBTREE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||||
|
if (fileFilter.test(file)) {
|
||||||
|
paths.add(file);
|
||||||
|
}
|
||||||
|
return super.visitFile(file, attrs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
}
|
171
src/main/java/dev/frogmc/frogloader/impl/FrogLoaderImpl.java
Normal file
171
src/main/java/dev/frogmc/frogloader/impl/FrogLoaderImpl.java
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
package dev.frogmc.frogloader.impl;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.invoke.MethodHandle;
|
||||||
|
import java.lang.invoke.MethodHandles;
|
||||||
|
import java.lang.invoke.MethodType;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import dev.frogmc.frogloader.api.FrogLoader;
|
||||||
|
import dev.frogmc.frogloader.api.env.Env;
|
||||||
|
import dev.frogmc.frogloader.api.mod.ModProperties;
|
||||||
|
import dev.frogmc.frogloader.api.plugin.FrogPlugin;
|
||||||
|
import dev.frogmc.frogloader.impl.gui.LoaderGui;
|
||||||
|
import dev.frogmc.frogloader.impl.launch.MixinClassLoader;
|
||||||
|
import dev.frogmc.frogloader.impl.mod.ModUtil;
|
||||||
|
import dev.frogmc.frogloader.impl.util.CrashReportGenerator;
|
||||||
|
import dev.frogmc.frogloader.impl.util.SystemProperties;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.spongepowered.asm.mixin.MixinEnvironment;
|
||||||
|
|
||||||
|
public class FrogLoaderImpl implements FrogLoader {
|
||||||
|
public static final String MOD_FILE_EXTENSION = ".frogmod";
|
||||||
|
private static final boolean DEV_ENV = Boolean.getBoolean(SystemProperties.DEVELOPMENT);
|
||||||
|
@Getter
|
||||||
|
private static FrogLoaderImpl instance;
|
||||||
|
@Getter
|
||||||
|
private final String[] args;
|
||||||
|
@Getter
|
||||||
|
private final Env env;
|
||||||
|
private final Logger LOGGER = LoggerFactory.getLogger("FrogLoader");
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final List<FrogPlugin> plugins = new ArrayList<>();
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final Path gameDir, configDir, modsDir;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final MixinClassLoader classloader;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final Gson gson = new Gson();
|
||||||
|
|
||||||
|
private Map<String, ModProperties> mods;
|
||||||
|
private Collection<String> modIds;
|
||||||
|
|
||||||
|
|
||||||
|
private FrogLoaderImpl(String[] args, Env env) {
|
||||||
|
instance = this;
|
||||||
|
this.classloader = (MixinClassLoader) this.getClass().getClassLoader();
|
||||||
|
this.args = args;
|
||||||
|
this.env = env;
|
||||||
|
|
||||||
|
gameDir = Paths.get(getArgumentOrElse("gameDir", "."));
|
||||||
|
configDir = gameDir.resolve("config");
|
||||||
|
modsDir = gameDir.resolve("mods");
|
||||||
|
|
||||||
|
try {
|
||||||
|
Files.createDirectories(gameDir);
|
||||||
|
Files.createDirectories(configDir);
|
||||||
|
Files.createDirectories(modsDir);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.warn("Failed to create essential directories ", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
discoverPlugins();
|
||||||
|
advanceMixinState();
|
||||||
|
mods = collectMods();
|
||||||
|
modIds = collectModIds();
|
||||||
|
LOGGER.info(ModUtil.getModList(mods.values()));
|
||||||
|
LOGGER.info("Launching...");
|
||||||
|
plugins.forEach(FrogPlugin::run);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
LoaderGui.builder().setContent(LoaderGui.ContentType.GENERIC_ERROR, t).addReport(CrashReportGenerator.writeReport(t, collectMods().values())).build().show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public static void run(String[] args, Env env) {
|
||||||
|
if (instance != null) {
|
||||||
|
throw new IllegalStateException("Loader was started multiple times!");
|
||||||
|
}
|
||||||
|
|
||||||
|
new FrogLoaderImpl(args, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void advanceMixinState() {
|
||||||
|
try {
|
||||||
|
MethodHandle m = MethodHandles.privateLookupIn(MixinEnvironment.class, MethodHandles.lookup()).findStatic(MixinEnvironment.class, "gotoPhase", MethodType.methodType(void.class, MixinEnvironment.Phase.class));
|
||||||
|
m.invoke(MixinEnvironment.Phase.INIT);
|
||||||
|
m.invoke(MixinEnvironment.Phase.DEFAULT);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void discoverPlugins() {
|
||||||
|
ServiceLoader.load(FrogPlugin.class).forEach(plugin -> {
|
||||||
|
try {
|
||||||
|
if (plugin.isApplicable()) {
|
||||||
|
plugin.init(this);
|
||||||
|
plugins.add(plugin);
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
LOGGER.error("Error during plugin initialisation: ", e);
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (plugins.isEmpty()) {
|
||||||
|
throw new IllegalStateException("No plugin applicable to the current state was found!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getArgument(String name) {
|
||||||
|
for (int i = 0; i < args.length - 1; i += 2) {
|
||||||
|
if (args[i].equals("--" + name)) {
|
||||||
|
return args[i + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getArgumentOrElse(String name, String other) {
|
||||||
|
String res = getArgument(name);
|
||||||
|
if (res.isEmpty()) {
|
||||||
|
return other;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDevelopment() {
|
||||||
|
return DEV_ENV;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isModLoaded(String id) {
|
||||||
|
return modIds.contains(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ModProperties> getModProperties(String id) {
|
||||||
|
return Optional.ofNullable(mods.get(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, ModProperties> collectMods() {
|
||||||
|
Collection<ModProperties> properties = plugins.stream().map(FrogPlugin::getMods).reduce(new HashSet<>(), (s1, s2) -> {
|
||||||
|
s1.addAll(s2);
|
||||||
|
return s1;
|
||||||
|
});
|
||||||
|
return properties.stream().collect(Collectors.toMap(ModProperties::id, m -> m));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Collection<String> collectModIds() {
|
||||||
|
return mods.keySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<ModProperties> getMods() {
|
||||||
|
return mods.values();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.impl;
|
package dev.frogmc.frogloader.impl;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
281
src/main/java/dev/frogmc/frogloader/impl/gui/LoaderGui.java
Normal file
281
src/main/java/dev/frogmc/frogloader/impl/gui/LoaderGui.java
Normal file
|
@ -0,0 +1,281 @@
|
||||||
|
package dev.frogmc.frogloader.impl.gui;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import javax.swing.plaf.basic.BasicBorders;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.event.ActionEvent;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import dev.frogmc.frogloader.impl.FrogLoaderImpl;
|
||||||
|
import dev.frogmc.frogloader.impl.mod.ModDependencyResolver;
|
||||||
|
import dev.frogmc.frogloader.impl.mod.ModUtil;
|
||||||
|
import dev.frogmc.frogloader.impl.util.URLUtil;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public class LoaderGui {
|
||||||
|
|
||||||
|
private final JFrame frame;
|
||||||
|
private final boolean keepRunning;
|
||||||
|
|
||||||
|
public static Builder builder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JFrame getFrame(Path report) {
|
||||||
|
var frame = new JFrame();
|
||||||
|
frame.setTitle("FrogLoader " + LoaderGui.class.getPackage().getImplementationVersion());
|
||||||
|
frame.setSize(952, 560);
|
||||||
|
frame.setLocationRelativeTo(null);
|
||||||
|
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
|
||||||
|
|
||||||
|
JPanel actions = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||||
|
if (report != null) {
|
||||||
|
actions.add(new JButton(new AbstractAction("Open Report") {
|
||||||
|
@Override
|
||||||
|
public void actionPerformed(ActionEvent e) {
|
||||||
|
URLUtil.open(report.toUri());
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
actions.add(new JButton(new AbstractAction("Copy Report") {
|
||||||
|
@Override
|
||||||
|
public void actionPerformed(ActionEvent e) {
|
||||||
|
URLUtil.copyStringContent(report.toUri());
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.add(new JButton(new AbstractAction("Join Discord") {
|
||||||
|
@Override
|
||||||
|
public void actionPerformed(ActionEvent e) {
|
||||||
|
URLUtil.open(URI.create("https://discord.frogmc.dev"));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
actions.add(new JButton(new AbstractAction("Exit") {
|
||||||
|
@Override
|
||||||
|
public void actionPerformed(ActionEvent e) {
|
||||||
|
frame.dispose();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
frame.add(actions, BorderLayout.SOUTH);
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String printVersionRange(ModDependencyResolver.VersionRange range) {
|
||||||
|
return "range: \n" + range.toString().replace("||", "or");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JPanel getEntry(String description, ModDependencyResolver.VersionRange range, Color background, @Nullable String icon, List<JButton> actions) {
|
||||||
|
JPanel entry = new JPanel(new BorderLayout());
|
||||||
|
entry.setBorder(BasicBorders.getInternalFrameBorder());
|
||||||
|
Box text = Box.createVerticalBox();
|
||||||
|
text.setBorder(BorderFactory.createEmptyBorder());
|
||||||
|
JTextPane desc = new JTextPane();
|
||||||
|
desc.setContentType("text/html");
|
||||||
|
desc.setEditable(false);
|
||||||
|
desc.setBackground(background);
|
||||||
|
desc.setText("<html>" + description.replace("<", "<").replace("\n", "<br>") + "</html>");
|
||||||
|
JPanel actionPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||||
|
actions.forEach(actionPanel::add);
|
||||||
|
if (icon != null) {
|
||||||
|
URL location = LoaderGui.class.getResource(icon);
|
||||||
|
if (location != null) {
|
||||||
|
entry.add(new JLabel(new ImageIcon(location)), BorderLayout.WEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
text.add(desc);
|
||||||
|
JCheckBox showAdvanced = new JCheckBox("Show raw version range");
|
||||||
|
JTextPane advanced = new JTextPane();
|
||||||
|
advanced.setEditable(false);
|
||||||
|
advanced.setBackground(background);
|
||||||
|
advanced.setText(range.toString());
|
||||||
|
advanced.setVisible(false);
|
||||||
|
text.add(advanced);
|
||||||
|
showAdvanced.addActionListener(new AbstractAction() {
|
||||||
|
@Override
|
||||||
|
public void actionPerformed(ActionEvent e) {
|
||||||
|
advanced.setVisible(showAdvanced.isSelected());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
entry.add(text);
|
||||||
|
Box bottom = Box.createHorizontalBox();
|
||||||
|
bottom.add(showAdvanced, actionPanel);
|
||||||
|
entry.add(bottom, BorderLayout.SOUTH);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("BusyWait")
|
||||||
|
public void show() {
|
||||||
|
frame.setVisible(true);
|
||||||
|
|
||||||
|
while (frame.isVisible()) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(100);
|
||||||
|
} catch (InterruptedException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!keepRunning) {
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
|
||||||
|
private Path report;
|
||||||
|
private Consumer<JFrame> contentFunc;
|
||||||
|
private boolean keepRunning = false;
|
||||||
|
|
||||||
|
public <T> Builder setContent(ContentType<T> type, T argument) {
|
||||||
|
contentFunc = f -> type.contentSetter.accept(f, argument);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder addReport(Path report) {
|
||||||
|
this.report = report;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder keepRunning() {
|
||||||
|
keepRunning = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoaderGui build() {
|
||||||
|
JFrame frame = getFrame(report);
|
||||||
|
if (contentFunc != null) {
|
||||||
|
contentFunc.accept(frame);
|
||||||
|
}
|
||||||
|
return new LoaderGui(frame, keepRunning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class ContentType<T> {
|
||||||
|
public static final ContentType<ModDependencyResolver.UnfulfilledDependencyException> INFO_UNFULFILLED_DEP = new ContentType<>((frame, ex) -> {
|
||||||
|
JPanel pane = new JPanel(new BorderLayout());
|
||||||
|
JTextPane title = new JTextPane();
|
||||||
|
title.setBackground(pane.getBackground());
|
||||||
|
title.setEditable(false);
|
||||||
|
int size = ex.getDependencies().size();
|
||||||
|
title.setText("Found " + size + " Error" + (size > 1 ? "s:" : ":"));
|
||||||
|
title.setFont(title.getFont().deriveFont(Font.BOLD, 16f));
|
||||||
|
title.setBorder(BorderFactory.createEmptyBorder(8, 0, 8, 0));
|
||||||
|
pane.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
|
||||||
|
pane.add(title, BorderLayout.NORTH);
|
||||||
|
|
||||||
|
Box list = Box.createVerticalBox();
|
||||||
|
ex.getDependencies().forEach(e -> {
|
||||||
|
StringBuilder description = new StringBuilder();
|
||||||
|
if (e.presentVersion() != null) {
|
||||||
|
description.append("Mod ").append(e.source().id()).append(" (").append(e.source().name()).append(") depends on ");
|
||||||
|
if (e.dependencyName() != null) {
|
||||||
|
description.append(e.dependency()).append(" (").append(e.dependencyName()).append(") ");
|
||||||
|
} else {
|
||||||
|
description.append("a Mod with id ").append(e.dependency());
|
||||||
|
}
|
||||||
|
description.append(" with a version matching ").append(printVersionRange(e.range())).append(", but a different version is present or provided: ").append(e.presentVersion());
|
||||||
|
} else {
|
||||||
|
description.append("Mod ").append(e.source().id()).append(" (").append(e.source().name()).append(") depends on ");
|
||||||
|
if (e.dependencyName() != null) {
|
||||||
|
description.append(e.dependency()).append(" (").append(e.dependencyName()).append(") ");
|
||||||
|
} else {
|
||||||
|
description.append("a Mod with id ").append(e.dependency());
|
||||||
|
}
|
||||||
|
description.append(" with a version matching ").append(printVersionRange(e.range())).append(". \nNo version is currently available.");
|
||||||
|
}
|
||||||
|
description.append("\nSuggested Solution: Install ")
|
||||||
|
.append(e.range().maxCompatible().or(e.range()::minCompatible).map(Objects::toString)
|
||||||
|
.map(s -> "0.0.0".equals(s) ? "any version" : "version " + s).orElse("<unknown>"))
|
||||||
|
.append(" of ");
|
||||||
|
if (e.dependencyName() != null) {
|
||||||
|
description.append(e.dependency()).append(" (").append(e.dependencyName()).append(") ");
|
||||||
|
} else {
|
||||||
|
description.append("Mod with id ").append(e.dependency());
|
||||||
|
}
|
||||||
|
List<JButton> actions = new ArrayList<>();
|
||||||
|
if (e.link() != null) {
|
||||||
|
boolean install = e.link().endsWith(FrogLoaderImpl.MOD_FILE_EXTENSION);
|
||||||
|
String name = install ? "Install" : "Open mod page";
|
||||||
|
JButton urlButton = new JButton(new AbstractAction(name) {
|
||||||
|
@Override
|
||||||
|
public void actionPerformed(ActionEvent event) {
|
||||||
|
if (install) {
|
||||||
|
ModUtil.installMod(e.link());
|
||||||
|
} else {
|
||||||
|
URLUtil.open(URI.create(e.link()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
actions.add(urlButton);
|
||||||
|
}
|
||||||
|
JPanel entry = getEntry(description.toString(), e.range(), list.getBackground(), e.source().icon(), actions);
|
||||||
|
list.add(entry);
|
||||||
|
});
|
||||||
|
pane.add(new JScrollPane(list));
|
||||||
|
frame.add(pane);
|
||||||
|
});
|
||||||
|
public static final ContentType<ModDependencyResolver.BreakingModException> INFO_BREAKING_DEP = new ContentType<>((frame, ex) -> {
|
||||||
|
JPanel pane = new JPanel(new BorderLayout());
|
||||||
|
JTextPane title = new JTextPane();
|
||||||
|
title.setBackground(pane.getBackground());
|
||||||
|
title.setEditable(false);
|
||||||
|
int size = ex.getBreaks().size();
|
||||||
|
title.setText("Found " + size + " Error" + (size > 1 ? "s:" : ":"));
|
||||||
|
title.setFont(title.getFont().deriveFont(Font.BOLD, 16f));
|
||||||
|
pane.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
|
||||||
|
pane.add(title, BorderLayout.NORTH);
|
||||||
|
|
||||||
|
Box list = Box.createVerticalBox();
|
||||||
|
ex.getBreaks().forEach(e -> {
|
||||||
|
String description = "Mod %s (%s) breaks with mod %s (%s) for versions matching %s \n(present: %s)".formatted(e.source().id(), e.source().name(), e.broken().id(), e.broken().name(), printVersionRange(e.range()), e.broken().version()) +
|
||||||
|
"\nSuggested Solution: Install " +
|
||||||
|
e.range().maxCompatible().or(e.range()::minCompatible).map(Objects::toString)
|
||||||
|
.map(s -> "0.0.0".equals(s) ? "any version" : "version " + s).orElse("<unknown>") +
|
||||||
|
" of Mod %s (%s) ".formatted(e.broken().id(), e.broken().name());
|
||||||
|
List<JButton> actions = new ArrayList<>();
|
||||||
|
|
||||||
|
JPanel entry = getEntry(description, e.range(), list.getBackground(), e.source().icon(), actions);
|
||||||
|
entry.setSize(list.getWidth(), entry.getHeight());
|
||||||
|
list.add(entry);
|
||||||
|
});
|
||||||
|
pane.add(new JScrollPane(list));
|
||||||
|
frame.add(pane);
|
||||||
|
});
|
||||||
|
public static final ContentType<Throwable> GENERIC_ERROR = new ContentType<>((frame, throwable) -> {
|
||||||
|
JPanel pane = new JPanel(new BorderLayout());
|
||||||
|
|
||||||
|
JTextPane title = new JTextPane();
|
||||||
|
title.setBackground(pane.getBackground());
|
||||||
|
title.setEditable(false);
|
||||||
|
title.setText("Caught Fatal Error during game startup:");
|
||||||
|
title.setFont(title.getFont().deriveFont(Font.BOLD, 16f));
|
||||||
|
pane.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
|
||||||
|
pane.add(title, BorderLayout.NORTH);
|
||||||
|
|
||||||
|
JTextPane error = new JTextPane();
|
||||||
|
title.setEditable(false);
|
||||||
|
StringWriter writer = new StringWriter();
|
||||||
|
throwable.printStackTrace(new PrintWriter(writer));
|
||||||
|
error.setText(writer.toString());
|
||||||
|
|
||||||
|
pane.add(error);
|
||||||
|
|
||||||
|
frame.add(pane);
|
||||||
|
});
|
||||||
|
|
||||||
|
private final BiConsumer<JFrame, T> contentSetter;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,150 @@
|
||||||
|
package dev.frogmc.frogloader.impl.launch;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ConcurrentSkipListSet;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.objectweb.asm.*;
|
||||||
|
|
||||||
|
public class AccessWidener {
|
||||||
|
private static final AccessWidener INSTANCE = new AccessWidener();
|
||||||
|
|
||||||
|
Map<String, Entry> classMap = new ConcurrentHashMap<>();
|
||||||
|
Map<String, Map<String, Entry>> methods = new ConcurrentHashMap<>();
|
||||||
|
Map<String, Map<String, Entry>> fields = new ConcurrentHashMap<>();
|
||||||
|
Map<String, Map<String, Entry>> mutations = new ConcurrentHashMap<>();
|
||||||
|
Set<String> classNames = new ConcurrentSkipListSet<>();
|
||||||
|
|
||||||
|
private static AccessWidener get() {
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void load(Data data) {
|
||||||
|
get().loadFromData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] processClass(byte[] classBytes, String className) {
|
||||||
|
return get().process(classBytes, className);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadFromData(Data data) {
|
||||||
|
classMap.putAll(data.classMap);
|
||||||
|
methods.putAll(data.methods);
|
||||||
|
fields.putAll(data.fields);
|
||||||
|
mutations.putAll(data.mutations);
|
||||||
|
classNames.addAll(data.classNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] process(byte[] classBytes, String className) {
|
||||||
|
if (!classNames.contains(className)) {
|
||||||
|
return classBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClassReader reader = new ClassReader(classBytes);
|
||||||
|
ClassWriter writer = new ClassWriter(0);
|
||||||
|
ClassVisitor mapper = new ClassVisitor(Opcodes.ASM9, writer) {
|
||||||
|
@Override
|
||||||
|
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
|
||||||
|
Entry e = classMap.get(className);
|
||||||
|
if (e != null) {
|
||||||
|
access &= ~(Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED | Opcodes.ACC_PUBLIC);
|
||||||
|
access |= e.type.access;
|
||||||
|
}
|
||||||
|
if (fields.containsKey(className) || methods.containsKey(className) || mutations.containsKey(className)) { // make all classes with modifications public as well
|
||||||
|
access &= ~(Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED | Opcodes.ACC_PUBLIC);
|
||||||
|
access |= Opcodes.ACC_PUBLIC;
|
||||||
|
}
|
||||||
|
super.visit(version, access, name, signature, superName, interfaces);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
|
||||||
|
Map<String, Entry> map = fields.get(className);
|
||||||
|
if (map != null) {
|
||||||
|
Entry e = map.get(name + descriptor);
|
||||||
|
if (e != null) {
|
||||||
|
access &= ~(Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED | Opcodes.ACC_PUBLIC); // remove all access modifiers
|
||||||
|
access |= e.type.access; // re-add the new one
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((map = mutations.get(className)) != null) {
|
||||||
|
var e = map.get(name + descriptor);
|
||||||
|
if (e != null) {
|
||||||
|
access &= ~Opcodes.ACC_FINAL; // always AccessType.MUTABLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visitField(access, name, descriptor, signature, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
|
||||||
|
Map<String, Entry> map = methods.get(className);
|
||||||
|
if (map != null) {
|
||||||
|
Entry e = map.get(name + descriptor);
|
||||||
|
if (e != null) {
|
||||||
|
access &= ~(Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED | Opcodes.ACC_PUBLIC);
|
||||||
|
access |= e.type.access;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visitMethod(access, name, descriptor, signature, exceptions);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.accept(mapper, 0);
|
||||||
|
return writer.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
public enum AccessType {
|
||||||
|
ACCESSIBLE("accessible", Opcodes.ACC_PUBLIC),
|
||||||
|
EXTENDABLE("extendable", Opcodes.ACC_PROTECTED),
|
||||||
|
MUTABLE("mutable", ~Opcodes.ACC_FINAL);
|
||||||
|
private final String id;
|
||||||
|
private final int access;
|
||||||
|
|
||||||
|
public static AccessType of(String name) {
|
||||||
|
return Arrays.stream(values()).filter(a -> a.id.equals(name)).findFirst().orElseThrow(() -> new IllegalStateException("Unknown access type: " + name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
private enum Access {
|
||||||
|
PUBLIC(1), PROTECTED(2), PACKAGE_PRIVATE(3), PRIVATE(4);
|
||||||
|
private final int index;
|
||||||
|
|
||||||
|
public static Access of(int access) {
|
||||||
|
if ((access & Opcodes.ACC_PUBLIC) != 0) {
|
||||||
|
return PUBLIC;
|
||||||
|
}
|
||||||
|
if ((access & Opcodes.ACC_PROTECTED) != 0) {
|
||||||
|
return PROTECTED;
|
||||||
|
}
|
||||||
|
if ((access & Opcodes.ACC_PRIVATE) != 0) {
|
||||||
|
return PRIVATE;
|
||||||
|
}
|
||||||
|
return PACKAGE_PRIVATE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Data(Map<String, Entry> classMap,
|
||||||
|
Map<String, Map<String, Entry>> methods,
|
||||||
|
Map<String, Map<String, Entry>> fields,
|
||||||
|
Map<String, Map<String, Entry>> mutations,
|
||||||
|
Set<String> classNames) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Entry(AccessType type, String targetType, String className, String name, String descriptor) {
|
||||||
|
|
||||||
|
public Entry(String[] line) {
|
||||||
|
this(AccessType.of(line[0]), line[1], line[2], line[3], line[4]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAccessGreaterThan(Entry other) {
|
||||||
|
return Access.of(type().access).index < Access.of(other.type.access).index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
package dev.frogmc.frogloader.impl.launch;
|
||||||
|
|
||||||
|
import java.lang.invoke.MethodHandle;
|
||||||
|
import java.lang.invoke.MethodHandles;
|
||||||
|
import java.lang.invoke.MethodType;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.llamalad7.mixinextras.MixinExtrasBootstrap;
|
||||||
|
import dev.frogmc.frogloader.api.env.Env;
|
||||||
|
import dev.frogmc.frogloader.impl.mixin.FrogMixinService;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.spongepowered.asm.launch.MixinBootstrap;
|
||||||
|
import org.spongepowered.asm.service.IPropertyKey;
|
||||||
|
|
||||||
|
public class FrogLauncher {
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private static FrogLauncher instance;
|
||||||
|
@Getter
|
||||||
|
private final MixinClassLoader targetClassLoader;
|
||||||
|
|
||||||
|
private static final Map<IPropertyKey, Object> globalProperties = new HashMap<>();
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final Env env;
|
||||||
|
|
||||||
|
public FrogLauncher(String[] args, Env env) {
|
||||||
|
if (instance != null) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
instance = this;
|
||||||
|
this.env = env;
|
||||||
|
targetClassLoader = new MixinClassLoader();
|
||||||
|
targetClassLoader.excludePackage("org.slf4j");
|
||||||
|
targetClassLoader.excludePackage("org.spongepowered");
|
||||||
|
targetClassLoader.excludePackage("org.apache.logging");
|
||||||
|
targetClassLoader.excludePackage("dev.frogmc.frogloader.impl.launch");
|
||||||
|
targetClassLoader.excludePackage("dev.frogmc.frogloader.api.env");
|
||||||
|
|
||||||
|
Thread.currentThread().setContextClassLoader(targetClassLoader);
|
||||||
|
|
||||||
|
System.setProperty("mixin.service", FrogMixinService.class.getName());
|
||||||
|
MixinBootstrap.init();
|
||||||
|
MixinExtrasBootstrap.init();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Class<?> clazz = targetClassLoader.findClass("dev.frogmc.frogloader.impl.FrogLoaderImpl");
|
||||||
|
MethodHandle ctor = MethodHandles.publicLookup().findStatic(clazz, "run", MethodType.methodType(void.class, String[].class, Env.class));
|
||||||
|
ctor.invoke(args, env);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
// TODO
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void run(String[] args, Env env) {
|
||||||
|
new FrogLauncher(args, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void putProperty(IPropertyKey key, Object value){
|
||||||
|
globalProperties.put(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object getProperty(IPropertyKey key){
|
||||||
|
return globalProperties.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object getProperty(IPropertyKey key, Object defaultValue){
|
||||||
|
return globalProperties.getOrDefault(key, defaultValue);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
package dev.frogmc.frogloader.impl.launch;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLClassLoader;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import dev.frogmc.frogloader.impl.mixin.FrogMixinService;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import org.spongepowered.asm.mixin.MixinEnvironment;
|
||||||
|
|
||||||
|
public class MixinClassLoader extends URLClassLoader {
|
||||||
|
|
||||||
|
private static final ClassLoader SYSTEM = ClassLoader.getSystemClassLoader();
|
||||||
|
|
||||||
|
static {
|
||||||
|
registerAsParallelCapable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final List<String> exclusions = new ArrayList<>();
|
||||||
|
|
||||||
|
public MixinClassLoader() {
|
||||||
|
super(new URL[0], null);
|
||||||
|
excludePackage("java");
|
||||||
|
excludePackage("com.sun");
|
||||||
|
excludePackage("sun");
|
||||||
|
excludePackage("jdk");
|
||||||
|
excludePackage("javax");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isClassLoaded(String name) {
|
||||||
|
return findLoadedClass(name) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getClassBytes(String name) throws IOException {
|
||||||
|
String binName = name.replace('.', '/');
|
||||||
|
String path = binName.concat(".class");
|
||||||
|
|
||||||
|
try (InputStream in = getResourceAsStream(path)) {
|
||||||
|
if (in == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return FrogMixinService.getTransformer().transformClass(MixinEnvironment.getCurrentEnvironment(), name, AccessWidener.processClass(in.readAllBytes(), binName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void excludePackage(String name) {
|
||||||
|
exclusions.add(name + '.');
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addURL(URL url) {
|
||||||
|
super.addURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||||
|
synchronized (getClassLoadingLock(name)) {
|
||||||
|
for (String prefix : exclusions) {
|
||||||
|
if (name.startsWith(prefix)) {
|
||||||
|
return SYSTEM.loadClass(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Class<?> loaded = findLoadedClass(name);
|
||||||
|
if (loaded != null)
|
||||||
|
return loaded;
|
||||||
|
|
||||||
|
Class<?> result = findClass(name);
|
||||||
|
|
||||||
|
if (resolve)
|
||||||
|
resolveClass(result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized Class<?> findClass(String name) throws ClassNotFoundException {
|
||||||
|
try {
|
||||||
|
byte[] bytes = getClassBytes(name);
|
||||||
|
if (bytes == null)
|
||||||
|
throw new ClassNotFoundException(name);
|
||||||
|
|
||||||
|
return defineClass(name, bytes, 0, bytes.length);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ClassNotFoundException(name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public URL getResource(String name) {
|
||||||
|
URL parentUrl = super.getResource(name);
|
||||||
|
|
||||||
|
if (parentUrl != null)
|
||||||
|
return parentUrl;
|
||||||
|
|
||||||
|
return SYSTEM.getResource(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Enumeration<URL> getResources(String name) throws IOException {
|
||||||
|
Enumeration<URL> parentResources = super.getResources(name);
|
||||||
|
Enumeration<URL> systemResources = SYSTEM.getResources(name);
|
||||||
|
|
||||||
|
if (parentResources.hasMoreElements() && systemResources.hasMoreElements()) {
|
||||||
|
List<URL> list = new ArrayList<>();
|
||||||
|
|
||||||
|
while (parentResources.hasMoreElements())
|
||||||
|
list.add(parentResources.nextElement());
|
||||||
|
|
||||||
|
while (systemResources.hasMoreElements())
|
||||||
|
list.add(systemResources.nextElement());
|
||||||
|
|
||||||
|
return Collections.enumeration(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parentResources.hasMoreElements())
|
||||||
|
return parentResources;
|
||||||
|
|
||||||
|
if (systemResources.hasMoreElements())
|
||||||
|
return systemResources;
|
||||||
|
|
||||||
|
return Collections.enumeration(Collections.emptyList());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package dev.frogmc.frogloader.impl.launch.client;
|
||||||
|
|
||||||
|
import dev.frogmc.frogloader.api.env.Env;
|
||||||
|
import dev.frogmc.frogloader.impl.launch.FrogLauncher;
|
||||||
|
|
||||||
|
public class FrogClient {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
FrogLauncher.run(args, Env.CLIENT);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package dev.frogmc.frogloader.impl.launch.server;
|
||||||
|
|
||||||
|
import dev.frogmc.frogloader.api.env.Env;
|
||||||
|
import dev.frogmc.frogloader.impl.launch.FrogLauncher;
|
||||||
|
|
||||||
|
public class FrogServer {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
FrogLauncher.run(args, Env.SERVER);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
package dev.frogmc.frogloader.impl.mixin;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ConcurrentSkipListSet;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import dev.frogmc.frogloader.api.mod.ModProperties;
|
||||||
|
import dev.frogmc.frogloader.impl.launch.AccessWidener;
|
||||||
|
import dev.frogmc.frogloader.impl.mod.BuiltinExtensions;
|
||||||
|
|
||||||
|
public class AWProcessor {
|
||||||
|
|
||||||
|
private static final String AW_EXTENSION_NAME = BuiltinExtensions.ACCESSWIDENER;
|
||||||
|
private static final Predicate<String> HEADER = Pattern.compile("accessWidener\\s+v[12]\\s+.*").asMatchPredicate();
|
||||||
|
private static final String SEPARATOR = "[\\t ]+";
|
||||||
|
|
||||||
|
public static void load(Collection<ModProperties> mods) {
|
||||||
|
Map<String, AccessWidener.Entry> classMap = new ConcurrentHashMap<>();
|
||||||
|
Map<String, Map<String, AccessWidener.Entry>> methods = new ConcurrentHashMap<>();
|
||||||
|
Map<String, Map<String, AccessWidener.Entry>> fields = new ConcurrentHashMap<>();
|
||||||
|
Map<String, Map<String, AccessWidener.Entry>> mutations = new ConcurrentHashMap<>();
|
||||||
|
Set<String> classNames = new ConcurrentSkipListSet<>();
|
||||||
|
|
||||||
|
mods.stream().map(ModProperties::extensions).map(e -> (String) e.get(AW_EXTENSION_NAME))
|
||||||
|
.filter(Objects::nonNull).map(s -> "/" + s).map(AWProcessor.class::getResourceAsStream).filter(Objects::nonNull)
|
||||||
|
.map(InputStreamReader::new).map(BufferedReader::new).flatMap(BufferedReader::lines)
|
||||||
|
.map(l -> l.contains("#") ? l.split("#")[0] : l).filter(l -> !l.isBlank())
|
||||||
|
.filter(l -> !HEADER.test(l)).distinct()
|
||||||
|
.map(l -> l.replace("transitive-", "")) // ignore all transitive declarations (just make them normal) as they're only relevant for dev envs
|
||||||
|
.map(l -> l.split(SEPARATOR)).filter(l -> l.length > 0).map(AccessWidener.Entry::new).forEach(e -> {
|
||||||
|
classNames.add(e.className());
|
||||||
|
if ("class".equals(e.targetType())) {
|
||||||
|
if (e.type() == AccessWidener.AccessType.MUTABLE) {
|
||||||
|
throw new IllegalArgumentException("aw format error: classes can not have a 'mutable' modifier (at: " + e + ")");
|
||||||
|
}
|
||||||
|
if (!classMap.containsKey(e.className())) {
|
||||||
|
classMap.put(e.className(), e);
|
||||||
|
} else {
|
||||||
|
var other = classMap.get(e.className());
|
||||||
|
if (e.isAccessGreaterThan(other)) {
|
||||||
|
classMap.put(e.className(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ("method".equals(e.targetType())) {
|
||||||
|
if (e.type() == AccessWidener.AccessType.MUTABLE) {
|
||||||
|
throw new IllegalArgumentException("aw format error: methods can not have a 'mutable' modifier (at: " + e + ")");
|
||||||
|
}
|
||||||
|
var map = methods.computeIfAbsent(e.className(), s -> new ConcurrentHashMap<>());
|
||||||
|
var id = e.name() + e.descriptor();
|
||||||
|
if (!map.containsKey(id)) {
|
||||||
|
map.put(id, e);
|
||||||
|
} else {
|
||||||
|
var other = map.get(id);
|
||||||
|
if (e.isAccessGreaterThan(other)) {
|
||||||
|
classMap.put(id, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ("field".equals(e.targetType())) {
|
||||||
|
if (e.type() == AccessWidener.AccessType.EXTENDABLE) {
|
||||||
|
throw new IllegalArgumentException("aw format error: fields can not have a 'extendable' modifier (at: " + e + ")");
|
||||||
|
}
|
||||||
|
var map = fields.computeIfAbsent(e.className(), s -> new ConcurrentHashMap<>());
|
||||||
|
var id = e.name() + e.descriptor();
|
||||||
|
if (e.type() == AccessWidener.AccessType.MUTABLE) {
|
||||||
|
mutations.computeIfAbsent(e.className(), s -> new ConcurrentHashMap<>()).putIfAbsent(id, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!map.containsKey(id)) {
|
||||||
|
map.put(id, e);
|
||||||
|
} else {
|
||||||
|
var other = map.get(id);
|
||||||
|
if (e.isAccessGreaterThan(other)) {
|
||||||
|
classMap.put(id, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
AccessWidener.load(new AccessWidener.Data(classMap, methods, fields, mutations, classNames));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
package dev.frogmc.frogloader.impl.mixin;
|
||||||
|
|
||||||
|
import dev.frogmc.frogloader.impl.launch.FrogLauncher;
|
||||||
|
import org.spongepowered.asm.service.IGlobalPropertyService;
|
||||||
|
import org.spongepowered.asm.service.IPropertyKey;
|
||||||
|
|
||||||
|
public class FrogGlobalPropertyService implements IGlobalPropertyService {
|
||||||
|
@Override
|
||||||
|
public IPropertyKey resolveKey(String name) {
|
||||||
|
return new IPropertyKey() {
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other){
|
||||||
|
if (other instanceof IPropertyKey k){
|
||||||
|
return name.equals(k.toString());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode(){
|
||||||
|
return name.hashCode();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public <T> T getProperty(IPropertyKey key) {
|
||||||
|
return (T) FrogLauncher.getInstance().getProperty(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setProperty(IPropertyKey key, Object value) {
|
||||||
|
FrogLauncher.getInstance().putProperty(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public <T> T getProperty(IPropertyKey key, T defaultValue) {
|
||||||
|
return (T) FrogLauncher.getInstance().getProperty(key, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPropertyString(IPropertyKey key, String defaultValue) {
|
||||||
|
return FrogLauncher.getInstance().getProperty(key, defaultValue).toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.impl.mixin;
|
package dev.frogmc.frogloader.impl.mixin;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -10,29 +10,29 @@ import org.spongepowered.asm.logging.ILogger;
|
||||||
import org.spongepowered.asm.logging.Level;
|
import org.spongepowered.asm.logging.Level;
|
||||||
import org.spongepowered.asm.logging.LoggerAdapterAbstract;
|
import org.spongepowered.asm.logging.LoggerAdapterAbstract;
|
||||||
|
|
||||||
public class NonsenseMixinLogger extends LoggerAdapterAbstract {
|
public class FrogMixinLogger extends LoggerAdapterAbstract {
|
||||||
private static final Map<String, NonsenseMixinLogger> LOGGERS = new ConcurrentHashMap<>();
|
private static final Map<String, FrogMixinLogger> LOGGERS = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public static ILogger get(String name){
|
|
||||||
return LOGGERS.computeIfAbsent(name, NonsenseMixinLogger::new);
|
|
||||||
}
|
|
||||||
|
|
||||||
private final Logger log;
|
private final Logger log;
|
||||||
|
|
||||||
public NonsenseMixinLogger(String name){
|
public FrogMixinLogger(String name) {
|
||||||
super(name);
|
super(name);
|
||||||
log = LoggerFactory.getLogger("Nonsense Loader/"+name.substring(0, 1).toUpperCase(Locale.ROOT)+name.substring(1));
|
log = LoggerFactory.getLogger("FrogLoader/" + name.substring(0, 1).toUpperCase(Locale.ROOT) + name.substring(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ILogger get(String name) {
|
||||||
|
return LOGGERS.computeIfAbsent(name, FrogMixinLogger::new);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getType() {
|
public String getType() {
|
||||||
return "Nonsense Loader/Mixin Logger";
|
return "FrogLoader/Mixin Logger";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void catching(Level level, Throwable t) {
|
public void catching(Level level, Throwable t) {
|
||||||
log.atLevel(org.slf4j.event.Level.valueOf(level.name())).setCause(t).log();
|
log.atLevel(org.slf4j.event.Level.valueOf(level.name())).setCause(t).log();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void log(Level level, String message, Object... params) {
|
public void log(Level level, String message, Object... params) {
|
||||||
log.atLevel(org.slf4j.event.Level.valueOf(level.name())).log(message, params);
|
log.atLevel(org.slf4j.event.Level.valueOf(level.name())).log(message, params);
|
|
@ -1,4 +1,4 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.impl.mixin;
|
package dev.frogmc.frogloader.impl.mixin;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
@ -7,8 +7,8 @@ import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import dev.frogmc.frogloader.impl.launch.FrogLauncher;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import org.ecorous.esnesnon.nonsense.loader.impl.launch.Launcher;
|
|
||||||
import org.objectweb.asm.ClassReader;
|
import org.objectweb.asm.ClassReader;
|
||||||
import org.objectweb.asm.tree.ClassNode;
|
import org.objectweb.asm.tree.ClassNode;
|
||||||
import org.spongepowered.asm.launch.platform.container.ContainerHandleVirtual;
|
import org.spongepowered.asm.launch.platform.container.ContainerHandleVirtual;
|
||||||
|
@ -21,7 +21,7 @@ import org.spongepowered.asm.service.*;
|
||||||
import org.spongepowered.asm.transformers.MixinClassReader;
|
import org.spongepowered.asm.transformers.MixinClassReader;
|
||||||
import org.spongepowered.asm.util.ReEntranceLock;
|
import org.spongepowered.asm.util.ReEntranceLock;
|
||||||
|
|
||||||
public class NonsenseMixinService implements IMixinService, IClassProvider, IClassBytecodeProvider, ITransformerProvider, IClassTracker {
|
public class FrogMixinService implements IMixinService, IClassProvider, IClassBytecodeProvider, ITransformerProvider, IClassTracker {
|
||||||
@Getter
|
@Getter
|
||||||
static IMixinTransformer transformer;
|
static IMixinTransformer transformer;
|
||||||
private final ReEntranceLock lock = new ReEntranceLock(1);
|
private final ReEntranceLock lock = new ReEntranceLock(1);
|
||||||
|
@ -29,7 +29,7 @@ public class NonsenseMixinService implements IMixinService, IClassProvider, ICla
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return "NonsenseMixinService";
|
return "FrogMixinService";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -56,7 +56,6 @@ public class NonsenseMixinService implements IMixinService, IClassProvider, ICla
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init() {
|
public void init() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -116,12 +115,12 @@ public class NonsenseMixinService implements IMixinService, IClassProvider, ICla
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InputStream getResourceAsStream(String name) {
|
public InputStream getResourceAsStream(String name) {
|
||||||
return Launcher.getInstance().getTargetClassLoader().getResourceAsStream(name);
|
return FrogLauncher.getInstance().getTargetClassLoader().getResourceAsStream(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getSideName() {
|
public String getSideName() {
|
||||||
return Launcher.getInstance().getEnv().getMixinName();
|
return FrogLauncher.getInstance().getEnv().getMixinName();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -136,7 +135,7 @@ public class NonsenseMixinService implements IMixinService, IClassProvider, ICla
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ILogger getLogger(String name) {
|
public ILogger getLogger(String name) {
|
||||||
return NonsenseMixinLogger.get(name);
|
return FrogMixinLogger.get(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -150,9 +149,9 @@ public class NonsenseMixinService implements IMixinService, IClassProvider, ICla
|
||||||
byte[] bytes;
|
byte[] bytes;
|
||||||
if (runTransformers && transformer != null) {
|
if (runTransformers && transformer != null) {
|
||||||
bytes = transformer.transformClass(MixinEnvironment.getCurrentEnvironment(), name,
|
bytes = transformer.transformClass(MixinEnvironment.getCurrentEnvironment(), name,
|
||||||
Launcher.getInstance().getTargetClassLoader().getClassBytes(name));
|
FrogLauncher.getInstance().getTargetClassLoader().getClassBytes(name));
|
||||||
} else {
|
} else {
|
||||||
bytes = Launcher.getInstance().getTargetClassLoader().getClassBytes(name);
|
bytes = FrogLauncher.getInstance().getTargetClassLoader().getClassBytes(name);
|
||||||
}
|
}
|
||||||
ClassReader reader = new MixinClassReader(bytes, name);
|
ClassReader reader = new MixinClassReader(bytes, name);
|
||||||
ClassNode node = new ClassNode();
|
ClassNode node = new ClassNode();
|
||||||
|
@ -168,17 +167,17 @@ public class NonsenseMixinService implements IMixinService, IClassProvider, ICla
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Class<?> findClass(String name) throws ClassNotFoundException {
|
public Class<?> findClass(String name) throws ClassNotFoundException {
|
||||||
return Class.forName(name, false, Launcher.getInstance().getTargetClassLoader());
|
return Class.forName(name, false, FrogLauncher.getInstance().getTargetClassLoader());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Class<?> findClass(String name, boolean initialize) throws ClassNotFoundException {
|
public Class<?> findClass(String name, boolean initialize) throws ClassNotFoundException {
|
||||||
return Class.forName(name, initialize, Launcher.getInstance().getTargetClassLoader());
|
return Class.forName(name, initialize, FrogLauncher.getInstance().getTargetClassLoader());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Class<?> findAgentClass(String name, boolean initialize) throws ClassNotFoundException {
|
public Class<?> findAgentClass(String name, boolean initialize) throws ClassNotFoundException {
|
||||||
return Class.forName(name, initialize, Launcher.getInstance().getTargetClassLoader());
|
return Class.forName(name, initialize, FrogLauncher.getInstance().getTargetClassLoader());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -188,7 +187,7 @@ public class NonsenseMixinService implements IMixinService, IClassProvider, ICla
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isClassLoaded(String className) {
|
public boolean isClassLoaded(String className) {
|
||||||
return Launcher.getInstance().getTargetClassLoader().isClassLoaded(className);
|
return FrogLauncher.getInstance().getTargetClassLoader().isClassLoaded(className);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
|
@ -0,0 +1,12 @@
|
||||||
|
package dev.frogmc.frogloader.impl.mod;
|
||||||
|
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
|
@UtilityClass
|
||||||
|
public class BuiltinExtensions {
|
||||||
|
public final String MIXIN_CONFIG = "mixin_config";
|
||||||
|
public final String INCLUDED_JARS = "included_jars";
|
||||||
|
public final String PRE_LAUNCH = "pre_launch";
|
||||||
|
public final String ACCESSWIDENER = "frog_aw";
|
||||||
|
public final String LOADING_TYPE = "loading_type";
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package dev.frogmc.frogloader.impl.mod;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import dev.frogmc.frogloader.api.mod.ModDependencies;
|
||||||
|
import dev.frogmc.frogloader.api.mod.ModExtensions;
|
||||||
|
import dev.frogmc.frogloader.api.mod.ModProperties;
|
||||||
|
import dev.frogmc.frogloader.impl.SemVerParseException;
|
||||||
|
|
||||||
|
public class JavaModProperties {
|
||||||
|
|
||||||
|
private static ModProperties INSTANCE;
|
||||||
|
|
||||||
|
private JavaModProperties() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ModProperties get() throws SemVerParseException {
|
||||||
|
if (INSTANCE == null) {
|
||||||
|
INSTANCE = new ModPropertiesImpl("java",
|
||||||
|
System.getProperty("java.vm.name"),
|
||||||
|
"/duke.png",
|
||||||
|
SemVerImpl.parse(System.getProperty("java.runtime.version")),
|
||||||
|
"",
|
||||||
|
Map.of(System.getProperty("java.vm.vendor"), Collections.singleton("Vendor")),
|
||||||
|
new ModDependencies(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet()),
|
||||||
|
ModExtensions.of(Collections.emptyMap()), Collections.emptySet());
|
||||||
|
}
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,563 @@
|
||||||
|
package dev.frogmc.frogloader.impl.mod;
|
||||||
|
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import dev.frogmc.frogloader.api.mod.ModDependencies;
|
||||||
|
import dev.frogmc.frogloader.api.mod.ModProperties;
|
||||||
|
import dev.frogmc.frogloader.api.mod.SemVer;
|
||||||
|
import dev.frogmc.frogloader.impl.SemVerParseException;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class ModDependencyResolver {
|
||||||
|
|
||||||
|
private final static Logger LOGGER = LoggerFactory.getLogger(ModDependencyResolver.class);
|
||||||
|
private static final Pattern COMPARATOR = Pattern.compile("(=|>=|<=|>|<)?(\\d.*)");
|
||||||
|
private final Collection<ModProperties> original;
|
||||||
|
private final Map<String, DependencyEntry> dependencies = new HashMap<>();
|
||||||
|
private final Map<String, DependencyEntry> breakings = new HashMap<>();
|
||||||
|
private final Map<String, DependencyEntry> suggests = new HashMap<>();
|
||||||
|
private final Map<String, ProvidedMod> provides = new HashMap<>();
|
||||||
|
private final Map<String, ModProperties> presentMods = new HashMap<>();
|
||||||
|
|
||||||
|
public ModDependencyResolver(Collection<ModProperties> mods) throws ResolverException {
|
||||||
|
this.original = Collections.unmodifiableCollection(mods);
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void load() throws ResolverException {
|
||||||
|
for (ModProperties props : original) {
|
||||||
|
for (ModDependencies.Entry entry : props.dependencies().getForType(ModDependencies.Type.DEPEND)) {
|
||||||
|
dependencies.put(entry.id(), new DependencyEntry(entry.range(), props, entry.link(), entry.name()));
|
||||||
|
}
|
||||||
|
for (ModDependencies.Entry entry : props.dependencies().getForType(ModDependencies.Type.BREAK)) {
|
||||||
|
breakings.put(entry.id(), new DependencyEntry(entry.range(), props, entry.link(), entry.name()));
|
||||||
|
}
|
||||||
|
for (ModDependencies.Entry entry : props.dependencies().getForType(ModDependencies.Type.SUGGEST)) {
|
||||||
|
suggests.put(entry.id(), new DependencyEntry(entry.range(), props, entry.link(), entry.name()));
|
||||||
|
}
|
||||||
|
props.dependencies().getForType(ModDependencies.Type.PROVIDE).forEach(e -> {
|
||||||
|
try {
|
||||||
|
provides.put(e.id(), new ProvidedMod(e.id(), SemVerImpl.parse(e.range()), props));
|
||||||
|
} catch (SemVerParseException ex) {
|
||||||
|
LOGGER.warn("Version for {} ({}), provided by mod '{}' ({}) does not meet SemVer specifications. Mod will not be provided.",
|
||||||
|
e.id(), e.range(), props.id(), props.name());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
presentMods.put(props.id(), props);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<ModProperties> solve() throws UnfulfilledDependencyException, BreakingModException {
|
||||||
|
// Step 1: Combine present and provided mods, always use the latest version available
|
||||||
|
Map<String, SemVer> presentOrProvided = new HashMap<>();
|
||||||
|
presentMods.forEach((s, modProperties) -> presentOrProvided.put(s, modProperties.version()));
|
||||||
|
provides.forEach((s, ver) -> {
|
||||||
|
if (presentMods.containsKey(s)) {
|
||||||
|
if (presentMods.get(s).version().compareTo(ver.version()) < 0) {
|
||||||
|
presentOrProvided.replace(s, ver.version());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
presentOrProvided.put(s, ver.version());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 2: look for breakage declarations
|
||||||
|
Collection<BreakingModException.Entry> breaks = new HashSet<>();
|
||||||
|
for (Map.Entry<String, DependencyEntry> e : breakings.entrySet()) {
|
||||||
|
String key = e.getKey();
|
||||||
|
DependencyEntry dependencyEntry = e.getValue();
|
||||||
|
if (presentOrProvided.containsKey(key) && dependencyEntry.range.versionMatches(presentOrProvided.get(key))) {
|
||||||
|
breaks.add(new BreakingModException.Entry(dependencyEntry.origin(), Optional.ofNullable(presentMods.get(key)).orElseGet(() -> provides.get(key).source()), dependencyEntry.range));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!breaks.isEmpty()) {
|
||||||
|
breaks.forEach(e ->
|
||||||
|
LOGGER.error("Mod {} ({}) breaks with mod {} ({}) for versions matching {}", e.source().id(), e.source().name(), e.broken().id(), e.broken().name(), e.range()));
|
||||||
|
throw new BreakingModException(breaks);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Set<ModProperties> result = new HashSet<>();
|
||||||
|
|
||||||
|
|
||||||
|
// Step 3: print out information about suggested mods
|
||||||
|
for (Map.Entry<String, DependencyEntry> e : suggests.entrySet()) {
|
||||||
|
String key = e.getKey();
|
||||||
|
DependencyEntry v = e.getValue();
|
||||||
|
if (!presentOrProvided.containsKey(key) || !v.range.versionMatches(presentOrProvided.get(key))) {
|
||||||
|
LOGGER.info("Mod '{}' ({}) suggests range {} of {}, you should install a matching version for the optimal experience!",
|
||||||
|
v.origin().id(), v.origin().name(), v.range(), key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4.1: Add all mods to the result that do not depend on any other mods
|
||||||
|
presentMods.forEach((s, modProperties) -> {
|
||||||
|
if (modProperties.dependencies().getForType(ModDependencies.Type.DEPEND).isEmpty()) {
|
||||||
|
result.add(modProperties);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Collection<UnfulfilledDependencyException.Entry> unfulfilled = new HashSet<>();
|
||||||
|
// Step 4.2: Check that all dependencies are satisfied by present or provided mods.
|
||||||
|
for (Map.Entry<String, DependencyEntry> entry : dependencies.entrySet()) {
|
||||||
|
String s = entry.getKey();
|
||||||
|
DependencyEntry value = entry.getValue();
|
||||||
|
if (!(presentMods.containsKey(s) && value.range.versionMatches(presentOrProvided.get(s)))) { // The dependency isn't fulfilled by any present mods
|
||||||
|
if (!(provides.containsKey(s) && value.range.versionMatches(provides.get(s).version()))) { // The dependency also isn't fulfilled by any provided mods
|
||||||
|
if (value.origin.extensions().getOrDefault(BuiltinExtensions.LOADING_TYPE, "required").equals("required")) {
|
||||||
|
unfulfilled.add(new UnfulfilledDependencyException.Entry(value.origin, s, value.range, presentOrProvided.get(s), value.link(), value.name()));
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// as there hasn't been thrown an exception the dependency must have been fulfilled, add the mod that set the dependency
|
||||||
|
result.add(value.origin());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!unfulfilled.isEmpty()) {
|
||||||
|
unfulfilled.forEach(e -> {
|
||||||
|
if (e.presentVersion() == null) {
|
||||||
|
LOGGER.error("Mod {} ({}) depends on mod {} with a version matching {} (No version available)", e.source().id(), e.source().name(), e.dependency(), e.range());
|
||||||
|
} else {
|
||||||
|
LOGGER.error("Mod {} ({}) depends on mod {} with a version matching {}, but a different version is present or provided: {}", e.source().id(), e.source().name(), e.dependency(), e.range(), e.presentVersion());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
throw new UnfulfilledDependencyException(unfulfilled);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
private enum DependencyType {
|
||||||
|
EQ("=", Object::equals),
|
||||||
|
GE(">=", (a, b) -> a.compareTo(b) >= 0),
|
||||||
|
LE("<=", (a, b) -> a.compareTo(b) <= 0),
|
||||||
|
GT(">", (a, b) -> a.compareTo(b) > 0),
|
||||||
|
LT("<", (a, b) -> a.compareTo(b) < 0),
|
||||||
|
|
||||||
|
;
|
||||||
|
private final String prefix;
|
||||||
|
private final TypeComparator comparator;
|
||||||
|
|
||||||
|
private static DependencyType of(String comparator) {
|
||||||
|
return Arrays.stream(values()).filter(t -> t.prefix.equals(comparator)).findFirst().orElse(DependencyType.EQ);
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface TypeComparator {
|
||||||
|
boolean compare(SemVer a, SemVer b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ProvidedMod(String modId, SemVer version, ModProperties source) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public static class BreakingModException extends Exception {
|
||||||
|
private final Collection<Entry> breaks;
|
||||||
|
|
||||||
|
public BreakingModException(Collection<Entry> breaks) {
|
||||||
|
super(breaks.stream()
|
||||||
|
.map(e -> "Mod %s (%s) breaks with mod %s (%s) for versions matching %s".formatted(e.source().id(),
|
||||||
|
e.source().name(), e.broken().id(), e.broken().name(), e.range()))
|
||||||
|
.collect(Collectors.joining("\n")));
|
||||||
|
this.breaks = breaks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Entry(ModProperties source, ModProperties broken, VersionRange range) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public static class UnfulfilledDependencyException extends Exception {
|
||||||
|
private final Collection<Entry> dependencies;
|
||||||
|
|
||||||
|
public UnfulfilledDependencyException(Collection<Entry> dependencies) {
|
||||||
|
super(dependencies.stream().map(e -> {
|
||||||
|
if (e.presentVersion() == null) {
|
||||||
|
return "Mod %s (%s) depends on mod %s with a version matching %s (No version available)".formatted(e.source().id(), e.source().name(), e.dependency(), e.range());
|
||||||
|
} else {
|
||||||
|
return "Mod %s (%s) depends on mod %s with a version matching %s, but a different version is present or provided: %s".formatted(e.source().id(), e.source().name(), e.dependency(), e.range(), e.presentVersion());
|
||||||
|
}
|
||||||
|
}).collect(Collectors.joining("\n")));
|
||||||
|
this.dependencies = dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Entry(ModProperties source, String dependency, VersionRange range, SemVer presentVersion,
|
||||||
|
@Nullable String link, String dependencyName) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ResolverException extends Exception {
|
||||||
|
public ResolverException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record DependencyEntry(VersionRange range, ModProperties origin, String link, String name) {
|
||||||
|
|
||||||
|
public DependencyEntry(String range, ModProperties origin, @Nullable String link, @Nullable String name) throws ResolverException {
|
||||||
|
this(VersionRange.parse(range), origin, link, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Comparator(DependencyType type, SemVer version) {
|
||||||
|
|
||||||
|
public static Comparator parse(String comparator) {
|
||||||
|
Matcher matcher = COMPARATOR.matcher(comparator);
|
||||||
|
if (!matcher.find()) {
|
||||||
|
throw new IllegalArgumentException(comparator);
|
||||||
|
}
|
||||||
|
var type = DependencyType.of(matcher.group(1));
|
||||||
|
StringBuilder version = new StringBuilder(matcher.group(2));
|
||||||
|
|
||||||
|
while (version.length() < 5) {
|
||||||
|
if (version.charAt(version.length() - 1) == '.') {
|
||||||
|
version.append('0');
|
||||||
|
} else {
|
||||||
|
version.append('.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new Comparator(type, SemVerImpl.parse(version.toString()));
|
||||||
|
} catch (SemVerParseException e) {
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean versionMatches(SemVer version) {
|
||||||
|
return type.comparator.compare(version, this.version);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return type.prefix + version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SemVer maxCompatible() {
|
||||||
|
return switch (type) {
|
||||||
|
case GT -> null;
|
||||||
|
case LT -> {
|
||||||
|
if (version.patch() > 0) {
|
||||||
|
yield new SemVerImpl(version.major(), version().minor(), version().patch() - 1, version.prerelease(), version.build());
|
||||||
|
}
|
||||||
|
if (version.minor() > 0) {
|
||||||
|
yield new SemVerImpl(version.major(), version().minor() - 1, version().patch(), version.prerelease(), version.build());
|
||||||
|
}
|
||||||
|
yield new SemVerImpl(version.major() - 1, version().minor(), version().patch(), version.prerelease(), version.build());
|
||||||
|
}
|
||||||
|
default -> version;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public SemVer minCompatible() {
|
||||||
|
return switch (type) {
|
||||||
|
case LT -> {
|
||||||
|
if (version.patch() > 0) {
|
||||||
|
yield new SemVerImpl(version.major(), version().minor(), version().patch() - 1, version.prerelease(), version.build());
|
||||||
|
}
|
||||||
|
if (version.minor() > 0) {
|
||||||
|
yield new SemVerImpl(version.major(), version().minor() - 1, version().patch(), version.prerelease(), version.build());
|
||||||
|
}
|
||||||
|
yield new SemVerImpl(version.major() - 1, version().minor(), version().patch(), version.prerelease(), version.build());
|
||||||
|
}
|
||||||
|
case GT -> {
|
||||||
|
if (version.patch() > 0) {
|
||||||
|
yield new SemVerImpl(version.major(), version().minor(), version().patch() + 1, version.prerelease(), version.build());
|
||||||
|
}
|
||||||
|
if (version.minor() > 0) {
|
||||||
|
yield new SemVerImpl(version.major(), version().minor() + 1, version().patch(), version.prerelease(), version.build());
|
||||||
|
}
|
||||||
|
yield new SemVerImpl(version.major() + 1, version().minor(), version().patch(), version.prerelease(), version.build());
|
||||||
|
}
|
||||||
|
default -> version;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ComparatorSet(Collection<Comparator> comparators) {
|
||||||
|
|
||||||
|
public static ComparatorSet parse(String set) {
|
||||||
|
String[] comparators = set.split(" ");
|
||||||
|
return new ComparatorSet(Arrays.stream(comparators).map(Comparator::parse).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean versionMatches(SemVer version) {
|
||||||
|
return comparators.stream().allMatch(c -> c.versionMatches(version));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return comparators.stream().map(Objects::toString).collect(Collectors.joining(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
public SemVer maxCompatible() {
|
||||||
|
return comparators.stream().map(Comparator::maxCompatible).filter(Objects::nonNull).max(Comparable::compareTo).orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SemVer minCompatible() {
|
||||||
|
return comparators.stream().map(Comparator::minCompatible).filter(Objects::nonNull).min(Comparable::compareTo).orElse(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record VersionRange(Collection<ComparatorSet> sets) {
|
||||||
|
|
||||||
|
private static final Pattern NUMBER_EXTRACTOR = Pattern.compile("[^~]?(\\d+)(?:\\.(?:([\\dxX*]+)(?:\\.([\\dxX*]+)?)?)?)?(.*)");
|
||||||
|
|
||||||
|
public static VersionRange parse(String range) throws ResolverException {
|
||||||
|
|
||||||
|
String[] sets = resolveAdvanced(range).split("\\|\\|");
|
||||||
|
|
||||||
|
return new VersionRange(Arrays.stream(sets).map(ComparatorSet::parse).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String resolveAdvanced(String range) throws ResolverException {
|
||||||
|
if (range.isEmpty() || range.equals("*")) {
|
||||||
|
return ">=0.0.0";
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> list = extractRanges(range);
|
||||||
|
|
||||||
|
List<String> ranges = new LinkedList<>();
|
||||||
|
for (int i = 0, listSize = list.size(); i < listSize; i++) {
|
||||||
|
String s = list.get(i);
|
||||||
|
|
||||||
|
if (i < listSize - 1 && list.get(i + 1).equals("-")) {
|
||||||
|
handleHyphenRange(s, ranges, list.get(i + 2));
|
||||||
|
i += 2;
|
||||||
|
} else if (s.startsWith("~")) {
|
||||||
|
handleTilde(s, ranges);
|
||||||
|
} else if (s.startsWith("^")) {
|
||||||
|
handleCaret(s, ranges);
|
||||||
|
} else if (s.contains("x") || s.contains("X") || s.contains("*")) {
|
||||||
|
handleXRanges(s, ranges);
|
||||||
|
} else {
|
||||||
|
ranges.add(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
range = String.join("", ranges);
|
||||||
|
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleTilde(String s, List<String> ranges) throws ResolverException {
|
||||||
|
{
|
||||||
|
StringBuilder builder = new StringBuilder(">=" + s.substring(1));
|
||||||
|
while (builder.length() < 7) {
|
||||||
|
if (builder.charAt(builder.length() - 1) == '.') {
|
||||||
|
builder.append('0');
|
||||||
|
} else {
|
||||||
|
builder.append('.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ranges.add(builder.toString());
|
||||||
|
}
|
||||||
|
ranges.add(" ");
|
||||||
|
|
||||||
|
Matcher matcher = NUMBER_EXTRACTOR.matcher(s);
|
||||||
|
if (!matcher.find()) {
|
||||||
|
throw new ResolverException("Version " + s + " did not match the required pattern to find the numbers within!");
|
||||||
|
}
|
||||||
|
int major = Integer.parseInt(matcher.group(1));
|
||||||
|
int minor = Optional.ofNullable(matcher.group(2)).map(Integer::parseInt).orElse(0);
|
||||||
|
|
||||||
|
if (minor > 0) {
|
||||||
|
ranges.add("<" + major + "." + (minor + 1) + ".0");
|
||||||
|
} else {
|
||||||
|
ranges.add("<" + (major + 1) + ".0.0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleXRanges(String s, List<String> ranges) throws ResolverException {
|
||||||
|
{
|
||||||
|
StringBuilder builder = new StringBuilder(">=" + s.replaceAll("[xX*]", "0"));
|
||||||
|
while (builder.length() < 7) {
|
||||||
|
if (builder.charAt(builder.length() - 1) == '.') {
|
||||||
|
builder.append('0');
|
||||||
|
} else {
|
||||||
|
builder.append('.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ranges.add(builder.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
ranges.add(" ");
|
||||||
|
|
||||||
|
if (s.length() < 5) {
|
||||||
|
Matcher matcher = NUMBER_EXTRACTOR.matcher(s);
|
||||||
|
if (!matcher.find()) {
|
||||||
|
throw new ResolverException("Version " + s + " did not match the required pattern to find the numbers within!");
|
||||||
|
}
|
||||||
|
int major = Integer.parseInt(matcher.group(1));
|
||||||
|
int minor = Optional.ofNullable(matcher.group(2)).map(n -> n.equalsIgnoreCase("x") || n.equals("*") ? null : n).map(Integer::parseInt).orElse(0);
|
||||||
|
int patch = Optional.ofNullable(matcher.group(3)).map(n -> n.equalsIgnoreCase("x") || n.equals("*") ? null : n).map(Integer::parseInt).orElse(0);
|
||||||
|
StringBuilder builder = new StringBuilder("<");
|
||||||
|
int[] ints = new int[]{major, minor, patch};
|
||||||
|
for (int j = 0, intsLength = ints.length; j < intsLength; j++) {
|
||||||
|
int x = ints[j];
|
||||||
|
if (x < intsLength - 1 && ints[j + 1] > 0) {
|
||||||
|
builder.append(x);
|
||||||
|
} else {
|
||||||
|
builder.append(x + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (builder.length() < 6) {
|
||||||
|
if (builder.charAt(builder.length() - 1) == '.') {
|
||||||
|
builder.append('0');
|
||||||
|
} else {
|
||||||
|
builder.append('.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ranges.add(builder.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleCaret(String s, List<String> ranges) throws ResolverException {
|
||||||
|
Matcher matcher = NUMBER_EXTRACTOR.matcher(s);
|
||||||
|
if (!matcher.find()) {
|
||||||
|
throw new ResolverException("Version " + s + " did not match the required pattern to find the numbers within!");
|
||||||
|
}
|
||||||
|
int major = Integer.parseInt(matcher.group(1));
|
||||||
|
int minor = Optional.ofNullable(matcher.group(2)).map(n -> n.equalsIgnoreCase("x") || n.equals("*") ? null : n).map(Integer::parseInt).orElse(0);
|
||||||
|
int patch = Optional.ofNullable(matcher.group(3)).map(n -> n.equalsIgnoreCase("x") || n.equals("*") ? null : n).map(Integer::parseInt).orElse(0);
|
||||||
|
String rest = matcher.group(4);
|
||||||
|
|
||||||
|
ranges.add(">=" + major + "." + minor + "." + patch + rest);
|
||||||
|
ranges.add(" ");
|
||||||
|
StringBuilder builder = new StringBuilder("<");
|
||||||
|
for (int x : new int[]{major, minor, patch}) {
|
||||||
|
if (x <= 0) {
|
||||||
|
builder.append(x);
|
||||||
|
} else {
|
||||||
|
builder.append(x + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (builder.length() < 6) {
|
||||||
|
if (builder.charAt(builder.length() - 1) == '.') {
|
||||||
|
builder.append('0');
|
||||||
|
} else {
|
||||||
|
builder.append('.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ranges.add(builder.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleHyphenRange(String s, List<String> ranges, String end) throws ResolverException {
|
||||||
|
{
|
||||||
|
StringBuilder builder = new StringBuilder(">=" + s);
|
||||||
|
while (builder.length() < 7) {
|
||||||
|
if (builder.charAt(builder.length() - 1) == '.') {
|
||||||
|
builder.append('0');
|
||||||
|
} else {
|
||||||
|
builder.append('.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ranges.add(builder.toString());
|
||||||
|
ranges.add(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end.length() < 5) {
|
||||||
|
Matcher matcher = NUMBER_EXTRACTOR.matcher(end);
|
||||||
|
if (!matcher.find()) {
|
||||||
|
throw new ResolverException("Version " + s + " did not match the required pattern to find the numbers within!");
|
||||||
|
}
|
||||||
|
int major = Integer.parseInt(matcher.group(1));
|
||||||
|
int minor = Optional.ofNullable(matcher.group(2)).map(Integer::parseInt).orElse(0);
|
||||||
|
int patch = Optional.ofNullable(matcher.group(3)).map(Integer::parseInt).orElse(0);
|
||||||
|
StringBuilder builder = new StringBuilder("<");
|
||||||
|
int[] ints = new int[]{major, minor, patch};
|
||||||
|
for (int j = 0, intsLength = ints.length; j < intsLength; j++) {
|
||||||
|
int x = ints[j];
|
||||||
|
if (x < intsLength - 1 && ints[j + 1] > 0) {
|
||||||
|
builder.append(x);
|
||||||
|
} else {
|
||||||
|
builder.append(x + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (builder.length() < 6) {
|
||||||
|
if (builder.charAt(builder.length() - 1) == '.') {
|
||||||
|
builder.append('0');
|
||||||
|
} else {
|
||||||
|
builder.append('.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ranges.add(builder.toString());
|
||||||
|
} else {
|
||||||
|
ranges.add("<=" + end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @NotNull List<String> extractRanges(String range) {
|
||||||
|
if (!range.contains(" ") && !range.contains(" - ") && !range.contains("||")) {
|
||||||
|
return List.of(range);
|
||||||
|
}
|
||||||
|
List<String> parts = new ArrayList<>();
|
||||||
|
for (String p : range.split(" - ")) {
|
||||||
|
if (!parts.isEmpty()) {
|
||||||
|
parts.add("-");
|
||||||
|
}
|
||||||
|
parts.add(p.trim());
|
||||||
|
}
|
||||||
|
List<String> moreParts = new ArrayList<>();
|
||||||
|
parts.forEach(s -> {
|
||||||
|
if (!s.contains("||")) {
|
||||||
|
moreParts.add(s);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (String p : s.split("\\|\\|")) {
|
||||||
|
if (!moreParts.isEmpty()) {
|
||||||
|
moreParts.add("||");
|
||||||
|
}
|
||||||
|
moreParts.add(p.trim());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
List<String> list = new ArrayList<>();
|
||||||
|
moreParts.forEach(s -> {
|
||||||
|
if (!s.contains(" ")) {
|
||||||
|
list.add(s);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (String p : s.split(" ")) {
|
||||||
|
if (!list.isEmpty()) {
|
||||||
|
list.add(" ");
|
||||||
|
}
|
||||||
|
list.add(p.trim());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean versionMatches(SemVer version) {
|
||||||
|
return sets.stream().anyMatch(c -> c.versionMatches(version));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return sets.stream().map(Objects::toString).collect(Collectors.joining(" || "));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<SemVer> maxCompatible() {
|
||||||
|
return sets.stream().map(ComparatorSet::maxCompatible).filter(Objects::nonNull).max(Comparable::compareTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<SemVer> minCompatible() {
|
||||||
|
return sets.stream().map(ComparatorSet::minCompatible).filter(Objects::nonNull).min(Comparable::compareTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package dev.frogmc.frogloader.impl.mod;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import dev.frogmc.frogloader.api.mod.ModDependencies;
|
||||||
|
import dev.frogmc.frogloader.api.mod.ModExtensions;
|
||||||
|
import dev.frogmc.frogloader.api.mod.ModProperties;
|
||||||
|
import dev.frogmc.frogloader.api.mod.SemVer;
|
||||||
|
|
||||||
|
public record ModPropertiesImpl(String id, String name, String icon, SemVer version, String license,
|
||||||
|
Map<String, Collection<String>> credits, ModDependencies dependencies,
|
||||||
|
ModExtensions extensions, Collection<Path> paths) implements ModProperties {
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
package dev.frogmc.frogloader.impl.mod;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.FileSystem;
|
||||||
|
import java.nio.file.FileSystems;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import com.electronwill.nightconfig.core.CommentedConfig;
|
||||||
|
import com.electronwill.nightconfig.core.UnmodifiableConfig;
|
||||||
|
import com.electronwill.nightconfig.core.file.FileNotFoundAction;
|
||||||
|
import com.electronwill.nightconfig.toml.TomlParser;
|
||||||
|
import dev.frogmc.frogloader.api.mod.ModDependencies;
|
||||||
|
import dev.frogmc.frogloader.api.mod.ModExtensions;
|
||||||
|
import dev.frogmc.frogloader.api.mod.ModProperties;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class ModPropertiesReader {
|
||||||
|
public static final String PROPERTIES_FILE_NAME = "frog.mod.toml";
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(ModPropertiesReader.class);
|
||||||
|
private static final TomlParser PARSER = new TomlParser();
|
||||||
|
|
||||||
|
public static Optional<ModProperties> read(Path mod) {
|
||||||
|
|
||||||
|
try (FileSystem fs = FileSystems.newFileSystem(mod)) {
|
||||||
|
CommentedConfig props = PARSER.parse(fs.getPath(PROPERTIES_FILE_NAME), FileNotFoundAction.THROW_ERROR);
|
||||||
|
|
||||||
|
return Optional.of(readProperties(props, Collections.singleton(mod)));
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.warn("Failed to read mod properties: ", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<ModProperties> readFile(URL in) {
|
||||||
|
CommentedConfig props = PARSER.parse(in);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String url = in.toString();
|
||||||
|
Path source = Path.of(url.substring(url.lastIndexOf(":")+1).split("!")[0]).toAbsolutePath();
|
||||||
|
if (!source.getFileName().toString().endsWith(".jar")){
|
||||||
|
source = source.getParent();
|
||||||
|
} else {
|
||||||
|
// TODO will this result in a memory leak?
|
||||||
|
FileSystem fs = FileSystems.newFileSystem(source);
|
||||||
|
source = fs.getPath("/");
|
||||||
|
}
|
||||||
|
return Optional.of(readProperties(props, Collections.singleton(source)));
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("Failed to read mod properties from "+in+": ", e);
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ModProperties readProperties(UnmodifiableConfig config, Collection<Path> sources) {
|
||||||
|
String manifestVer = config.get("frog.format_version");
|
||||||
|
|
||||||
|
try {
|
||||||
|
return FormatVersion.get(manifestVer).parse(config, sources);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
private enum FormatVersion {
|
||||||
|
V1_0_0("1.0.0", (config, sources) -> {
|
||||||
|
String id = config.get("frog.mod.id");
|
||||||
|
String name = config.get("frog.mod.name");
|
||||||
|
String icon = config.get("frog.mod.icon");
|
||||||
|
String version = config.get("frog.mod.version");
|
||||||
|
String license = config.get("frog.mod.license");
|
||||||
|
|
||||||
|
if (license == null) {
|
||||||
|
license = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
List<UnmodifiableConfig> creditsList = config.get("frog.mod.credits");
|
||||||
|
Map<String, Collection<String>> credits = new HashMap<>();
|
||||||
|
if (creditsList != null) {
|
||||||
|
creditsList.forEach(c -> credits.put(c.get("name"), c.get("roles")));
|
||||||
|
}
|
||||||
|
|
||||||
|
Collection<ModDependencies.Entry> depends = new HashSet<>();
|
||||||
|
List<UnmodifiableConfig> dependsConfig = config.get("frog.dependencies.depends");
|
||||||
|
if (dependsConfig != null) {
|
||||||
|
dependsConfig.forEach(entry -> depends.add(new ModDependencies.Entry(entry.get("id"), entry.get("versions"), entry.get("link"), entry.get("name"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
Collection<ModDependencies.Entry> breaks = new HashSet<>();
|
||||||
|
List<UnmodifiableConfig> breaksConfig = config.get("frog.dependencies.breaks");
|
||||||
|
if (breaksConfig != null) {
|
||||||
|
breaksConfig.forEach(entry -> breaks.add(new ModDependencies.Entry(entry.get("id"), entry.get("versions"), entry.get("link"), entry.get("name"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
Collection<ModDependencies.Entry> suggests = new HashSet<>();
|
||||||
|
List<UnmodifiableConfig> suggestsConfig = config.get("frog.dependencies.suggests");
|
||||||
|
if (suggestsConfig != null) {
|
||||||
|
suggestsConfig.forEach(entry -> suggests.add(new ModDependencies.Entry(entry.get("id"), entry.get("versions"), entry.get("link"), entry.get("name"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
Collection<ModDependencies.Entry> provides = new HashSet<>();
|
||||||
|
List<UnmodifiableConfig> providesConfig = config.get("frog.dependencies.provides");
|
||||||
|
if (providesConfig != null) {
|
||||||
|
providesConfig.forEach(entry -> provides.add(new ModDependencies.Entry(entry.get("id"), entry.get("version"), entry.get("link"), entry.get("name"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
UnmodifiableConfig extensionsConfig = config.get("frog.extensions");
|
||||||
|
Map<String, Object> extensions = new HashMap<>();
|
||||||
|
if (extensionsConfig != null) {
|
||||||
|
extensionsConfig.entrySet().forEach(entry -> extensions.put(entry.getKey(), entry.getValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ModPropertiesImpl(id, name, icon, SemVerImpl.parse(version), license, Collections.unmodifiableMap(credits),
|
||||||
|
new ModDependencies(depends, breaks, suggests, provides), ModExtensions.of(extensions), sources);
|
||||||
|
});
|
||||||
|
|
||||||
|
private static final Map<String, Parser> versions = Arrays.stream(values()).collect(Collectors.toMap(v -> v.version, v -> v.parser));
|
||||||
|
private final String version;
|
||||||
|
private final Parser parser;
|
||||||
|
|
||||||
|
public static Parser get(String version) {
|
||||||
|
return versions.get(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface Parser {
|
||||||
|
ModProperties parse(UnmodifiableConfig config, Collection<Path> sources) throws IOException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
50
src/main/java/dev/frogmc/frogloader/impl/mod/ModUtil.java
Normal file
50
src/main/java/dev/frogmc/frogloader/impl/mod/ModUtil.java
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package dev.frogmc.frogloader.impl.mod;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import dev.frogmc.frogloader.api.FrogLoader;
|
||||||
|
import dev.frogmc.frogloader.api.mod.ModProperties;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class ModUtil {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(ModUtil.class);
|
||||||
|
|
||||||
|
public static String getModList(Collection<ModProperties> mods) {
|
||||||
|
int size = mods.size();
|
||||||
|
if (size == 0) {
|
||||||
|
return "No mods loaded.";
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
builder.append("Loaded ").append(size).append(" mod");
|
||||||
|
if (size > 1) {
|
||||||
|
builder.append("s");
|
||||||
|
}
|
||||||
|
builder.append(":");
|
||||||
|
int i = 0;
|
||||||
|
for (ModProperties p : mods) {
|
||||||
|
builder.append("\n\t");
|
||||||
|
if (i < size - 1) {
|
||||||
|
builder.append("|- ");
|
||||||
|
} else {
|
||||||
|
builder.append("\\- ");
|
||||||
|
}
|
||||||
|
builder.append(p.id()).append(" (").append(p.name()).append(") ").append(" ").append(p.version());
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void installMod(String url) {
|
||||||
|
try {
|
||||||
|
Files.copy(URI.create(url).toURL().openStream(), FrogLoader.getInstance().getModsDir().resolve(url.substring(url.lastIndexOf("/" + 1))));
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.error("Failed to install mod:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.api.mod;
|
package dev.frogmc.frogloader.impl.mod;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
@ -6,10 +6,11 @@ import java.util.function.IntSupplier;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import dev.frogmc.frogloader.api.mod.SemVer;
|
||||||
|
import dev.frogmc.frogloader.impl.SemVerParseException;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import org.ecorous.esnesnon.nonsense.loader.impl.SemVerParseException;
|
|
||||||
|
|
||||||
public record SemVer(int major, int minor, int patch, String prerelease, String build) implements Comparable<SemVer> {
|
public record SemVerImpl(int major, int minor, int patch, String prerelease, String build) implements SemVer {
|
||||||
// Adapted from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
|
// Adapted from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
|
||||||
private static final Pattern SEMVER_PATTERN = Pattern.compile("^(?<major>0|[1-9]\\d*)\\." +
|
private static final Pattern SEMVER_PATTERN = Pattern.compile("^(?<major>0|[1-9]\\d*)\\." +
|
||||||
"(?<minor>0|[1-9]\\d*)\\." +
|
"(?<minor>0|[1-9]\\d*)\\." +
|
||||||
|
@ -29,13 +30,13 @@ public record SemVer(int major, int minor, int patch, String prerelease, String
|
||||||
int patch = Integer.parseInt(matcher.group("patch"));
|
int patch = Integer.parseInt(matcher.group("patch"));
|
||||||
String prerelease = matcher.group("prerelease");
|
String prerelease = matcher.group("prerelease");
|
||||||
String buildmetadata = matcher.group("buildmetadata");
|
String buildmetadata = matcher.group("buildmetadata");
|
||||||
return new SemVer(major, minor, patch, prerelease, buildmetadata);
|
return new SemVerImpl(major, minor, patch, prerelease, buildmetadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
StringBuilder b = new StringBuilder();
|
StringBuilder b = new StringBuilder();
|
||||||
b.append(major).append(minor).append(patch);
|
b.append(major).append(".").append(minor).append(".").append(patch);
|
||||||
if (prerelease != null) {
|
if (prerelease != null) {
|
||||||
b.append("-").append(prerelease);
|
b.append("-").append(prerelease);
|
||||||
}
|
}
|
||||||
|
@ -47,10 +48,10 @@ public record SemVer(int major, int minor, int patch, String prerelease, String
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object obj) {
|
public boolean equals(Object obj) {
|
||||||
if (obj instanceof SemVer s) {
|
if (obj instanceof SemVerImpl s) {
|
||||||
return compareTo(s) == 0;
|
return compareTo(s) == 0;
|
||||||
}
|
}
|
||||||
return false;
|
return toString().equals(obj.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -62,10 +63,10 @@ public record SemVer(int major, int minor, int patch, String prerelease, String
|
||||||
public int compareTo(@NonNull SemVer o) {
|
public int compareTo(@NonNull SemVer o) {
|
||||||
int i;
|
int i;
|
||||||
List<IntSupplier> suppliers = List.of(
|
List<IntSupplier> suppliers = List.of(
|
||||||
() -> Integer.compare(major, o.major),
|
() -> Integer.compare(major, o.major()),
|
||||||
() -> Integer.compare(minor, o.minor),
|
() -> Integer.compare(minor, o.minor()),
|
||||||
() -> Integer.compare(patch, o.patch),
|
() -> Integer.compare(patch, o.patch()),
|
||||||
() -> prerelease != null ? o.prerelease != null ? 0 : -1 : o.prerelease != null ? 1 : 0
|
() -> prerelease != null ? o.prerelease() != null ? 0 : -1 : o.prerelease() != null ? 1 : 0
|
||||||
);
|
);
|
||||||
for (IntSupplier comparison : suppliers) {
|
for (IntSupplier comparison : suppliers) {
|
||||||
if ((i = comparison.getAsInt()) != 0) {
|
if ((i = comparison.getAsInt()) != 0) {
|
||||||
|
@ -73,8 +74,12 @@ public record SemVer(int major, int minor, int patch, String prerelease, String
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prerelease == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
String[] self = prerelease.split("\\.");
|
String[] self = prerelease.split("\\.");
|
||||||
String[] other = o.prerelease.split("\\.");
|
String[] other = o.prerelease().split("\\.");
|
||||||
|
|
||||||
for (int index = 0; index < Math.min(self.length, other.length); index++) {
|
for (int index = 0; index < Math.min(self.length, other.length); index++) {
|
||||||
boolean selfNumeric = self[index].matches("\\d+");
|
boolean selfNumeric = self[index].matches("\\d+");
|
|
@ -0,0 +1,206 @@
|
||||||
|
package dev.frogmc.frogloader.impl.plugin.game.minecraft;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.invoke.MethodHandle;
|
||||||
|
import java.lang.invoke.MethodHandles;
|
||||||
|
import java.lang.invoke.MethodType;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import dev.frogmc.frogloader.api.FrogLoader;
|
||||||
|
import dev.frogmc.frogloader.api.extensions.PreLaunchExtension;
|
||||||
|
import dev.frogmc.frogloader.api.mod.ModDependencies;
|
||||||
|
import dev.frogmc.frogloader.api.mod.ModExtensions;
|
||||||
|
import dev.frogmc.frogloader.api.mod.ModProperties;
|
||||||
|
import dev.frogmc.frogloader.api.plugin.FrogPlugin;
|
||||||
|
import dev.frogmc.frogloader.impl.Discovery;
|
||||||
|
import dev.frogmc.frogloader.impl.FrogLoaderImpl;
|
||||||
|
import dev.frogmc.frogloader.impl.gui.LoaderGui;
|
||||||
|
import dev.frogmc.frogloader.impl.mixin.AWProcessor;
|
||||||
|
import dev.frogmc.frogloader.impl.mod.*;
|
||||||
|
import dev.frogmc.frogloader.impl.util.CrashReportGenerator;
|
||||||
|
import dev.frogmc.frogloader.impl.util.SystemProperties;
|
||||||
|
import dev.frogmc.thyroxine.Thyroxine;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.spongepowered.asm.mixin.Mixins;
|
||||||
|
|
||||||
|
public class Minecraft implements FrogPlugin {
|
||||||
|
|
||||||
|
protected static final String[] MINECRAFT_CLASSES = new String[]{
|
||||||
|
"net/minecraft/client/main/Main.class",
|
||||||
|
"net/minecraft/client/MinecraftApplet.class",
|
||||||
|
"net/minecraft/server/Main.class"
|
||||||
|
};
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger("Plugin/Minecraft");
|
||||||
|
protected final Collection<ModProperties> modProperties = new ArrayList<>();
|
||||||
|
protected Path gamePath;
|
||||||
|
protected String foundMainClass;
|
||||||
|
private String version;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isApplicable() {
|
||||||
|
gamePath = findGame();
|
||||||
|
return gamePath != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||||
|
@Override
|
||||||
|
public void init(FrogLoader loader) throws Exception {
|
||||||
|
if (gamePath == null) {
|
||||||
|
throw new IllegalStateException("Game not found yet!");
|
||||||
|
}
|
||||||
|
Path remappedGamePath = loader.getGameDir().resolve(".frogmc/remappedJars").resolve(version).resolve("game-" + version + "-remapped.jar");
|
||||||
|
|
||||||
|
if (!Files.exists(remappedGamePath.getParent())) {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(remappedGamePath.getParent());
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.error("Failed to create directory", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modProperties.add(JavaModProperties.get());
|
||||||
|
modProperties.add(new ModPropertiesImpl("minecraft", "Minecraft", "/assets/minecraft/textures/block/grass_block_side.png",
|
||||||
|
MinecraftSemVerImpl.get(version), "MC-EULA",
|
||||||
|
Map.of("Mojang AB", Collections.singleton("Author")),
|
||||||
|
new ModDependencies(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet()),
|
||||||
|
ModExtensions.of(Collections.emptyMap()), Collections.emptySet()));
|
||||||
|
|
||||||
|
Collection<Path> mods = Discovery.find(loader.getModsDir(), path ->
|
||||||
|
version.equals(path.getFileName().toString()), path ->
|
||||||
|
path.getFileName().toString().endsWith(FrogLoaderImpl.MOD_FILE_EXTENSION));
|
||||||
|
Collection<URL> classpathMods = this.getClass().getClassLoader().resources(ModPropertiesReader.PROPERTIES_FILE_NAME).distinct().toList();
|
||||||
|
|
||||||
|
classpathMods.stream().map(ModPropertiesReader::readFile).map(o -> o.orElse(null)).filter(Objects::nonNull).forEach(modProperties::add);
|
||||||
|
Map<Path, ModProperties> modPaths = new HashMap<>();
|
||||||
|
for (Path mod : new HashSet<>(mods)) {
|
||||||
|
findJiJMods(mod, mods, modPaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
modProperties.retainAll(new ModDependencyResolver(modProperties).solve());
|
||||||
|
} catch (ModDependencyResolver.BreakingModException e) {
|
||||||
|
LoaderGui.builder().setContent(LoaderGui.ContentType.INFO_BREAKING_DEP, e).addReport(CrashReportGenerator.writeReport(e, modProperties)).build().show();
|
||||||
|
} catch (ModDependencyResolver.UnfulfilledDependencyException e) {
|
||||||
|
LoaderGui.builder().setContent(LoaderGui.ContentType.INFO_UNFULFILLED_DEP, e).addReport(CrashReportGenerator.writeReport(e, modProperties)).build().show();
|
||||||
|
}
|
||||||
|
|
||||||
|
mods.stream().filter(p -> modProperties.contains(modPaths.get(p))).map(Path::toUri).map(uri -> {
|
||||||
|
try {
|
||||||
|
return uri.toURL();
|
||||||
|
} catch (MalformedURLException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}).forEach(FrogLoaderImpl.getInstance().getClassloader()::addURL);
|
||||||
|
|
||||||
|
modProperties.forEach(props -> {
|
||||||
|
Object o = props.extensions().get(BuiltinExtensions.MIXIN_CONFIG);
|
||||||
|
if (o instanceof String name) {
|
||||||
|
Mixins.addConfiguration(name);
|
||||||
|
} else if (o instanceof Collection l) {
|
||||||
|
((Collection<String>) l).forEach(Mixins::addConfiguration);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!loader.isDevelopment()) {
|
||||||
|
if (!Files.exists(remappedGamePath)) {
|
||||||
|
Thyroxine.run(version, gamePath, remappedGamePath, true, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AWProcessor.load(modProperties);
|
||||||
|
|
||||||
|
var runtimePath = loader.isDevelopment() ? gamePath : remappedGamePath;
|
||||||
|
FrogLoaderImpl.getInstance().getClassloader().addURL(runtimePath.toUri().toURL());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void findJiJMods(Path mod, Collection<Path> mods, Map<Path, ModProperties> modPaths) throws IOException {
|
||||||
|
Optional<ModProperties> opt = ModPropertiesReader.read(mod);
|
||||||
|
if (opt.isPresent()) {
|
||||||
|
ModProperties p = opt.get();
|
||||||
|
modProperties.add(p);
|
||||||
|
modPaths.put(mod, p);
|
||||||
|
List<List<Map<String, String>>> entries = p.extensions().getOrDefault(BuiltinExtensions.INCLUDED_JARS, Collections.emptyList());
|
||||||
|
if (entries.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (FileSystem fs = FileSystems.newFileSystem(mod)) {
|
||||||
|
for (var jars : entries) {
|
||||||
|
for (Map<String, String> jar : jars) {
|
||||||
|
Path path = fs.getPath(jar.get("path")).toAbsolutePath();
|
||||||
|
mods.add(path);
|
||||||
|
findJiJMods(path, mods, modPaths);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<ModProperties> getMods() {
|
||||||
|
return modProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Path findGame() {
|
||||||
|
LOGGER.info("Locating game..");
|
||||||
|
String jar = System.getProperty(SystemProperties.MINECRAFT_GAME_JAR);
|
||||||
|
if (jar != null) {
|
||||||
|
Path p = Paths.get(jar);
|
||||||
|
if (checkLocation(p)) {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String s : System.getProperty("java.class.path", "").split(File.pathSeparator)) {
|
||||||
|
Path p = Paths.get(s);
|
||||||
|
if (checkLocation(p)) {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LOGGER.warn("Could not locate game!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean checkLocation(Path jar) {
|
||||||
|
if (!Files.exists(jar) || Files.isDirectory(jar)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try (FileSystem fs = FileSystems.newFileSystem(jar)) {
|
||||||
|
for (String n : MINECRAFT_CLASSES) {
|
||||||
|
if (Files.exists(fs.getPath(n)) && n.contains(FrogLoaderImpl.getInstance().getEnv().getIdentifier())) {
|
||||||
|
LOGGER.info("Found game: {}", jar);
|
||||||
|
foundMainClass = n.substring(0, n.length() - 6).replace("/", ".");
|
||||||
|
version = FrogLoaderImpl.getInstance().getGson().fromJson(Files.readString(fs.getPath("version.json")), JsonObject.class).get("id").getAsString();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
if (foundMainClass != null) {
|
||||||
|
modProperties.forEach(props ->
|
||||||
|
props.extensions().runIfPresent(PreLaunchExtension.ID,
|
||||||
|
PreLaunchExtension.class, PreLaunchExtension::onPreLaunch));
|
||||||
|
LOGGER.info("Launching main class: {}", foundMainClass);
|
||||||
|
Class<?> mainClass = Class.forName(foundMainClass);
|
||||||
|
MethodHandle main = MethodHandles.publicLookup().findStatic(mainClass, "main", MethodType.methodType(void.class, String[].class));
|
||||||
|
main.invoke((Object) FrogLoaderImpl.getInstance().getArgs());
|
||||||
|
} else {
|
||||||
|
LOGGER.warn("Failed to locate main class!");
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
package dev.frogmc.frogloader.impl.plugin.game.minecraft;
|
||||||
|
|
||||||
|
import dev.frogmc.frogloader.api.mod.SemVer;
|
||||||
|
import dev.frogmc.frogloader.impl.SemVerParseException;
|
||||||
|
import dev.frogmc.frogloader.impl.mod.SemVerImpl;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
public class MinecraftSemVerImpl implements SemVer {
|
||||||
|
|
||||||
|
private final String version;
|
||||||
|
|
||||||
|
private MinecraftSemVerImpl(String version) {
|
||||||
|
this.version = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
static SemVer get(String version) {
|
||||||
|
try {
|
||||||
|
return SemVerImpl.parse(version);
|
||||||
|
} catch (SemVerParseException e) {
|
||||||
|
return new MinecraftSemVerImpl(version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int major() {
|
||||||
|
throw new UnsupportedOperationException("Minecraft version " + version + " does not represent a semver-compatible version");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int minor() {
|
||||||
|
throw new UnsupportedOperationException("Minecraft version " + version + " does not represent a semver-compatible version");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int patch() {
|
||||||
|
throw new UnsupportedOperationException("Minecraft version " + version + " does not represent a semver-compatible version");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String prerelease() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String build() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(@NotNull SemVer o) {
|
||||||
|
// Best-effort comparison
|
||||||
|
return version.compareTo(o.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (obj instanceof SemVer) {
|
||||||
|
return obj.toString().equals(version);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package dev.frogmc.frogloader.impl.util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import dev.frogmc.frogloader.api.mod.ModProperties;
|
||||||
|
import dev.frogmc.frogloader.impl.FrogLoaderImpl;
|
||||||
|
import dev.frogmc.frogloader.impl.mod.ModUtil;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class CrashReportGenerator {
|
||||||
|
private static final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss_SSSS");
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(CrashReportGenerator.class);
|
||||||
|
|
||||||
|
public static Path writeReport(Throwable t) {
|
||||||
|
return writeReport(t, FrogLoaderImpl.getInstance().getMods());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Path writeReport(Throwable t, Collection<ModProperties> mods) {
|
||||||
|
String fileName = "crash-" + dateFormat.format(ZonedDateTime.now()) + "_frogloader.log";
|
||||||
|
Path out = FrogLoaderImpl.getInstance().getGameDir().resolve("crash-reports").resolve(fileName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String report = getForException(t, mods);
|
||||||
|
Files.createDirectories(out.getParent());
|
||||||
|
Files.writeString(out, report);
|
||||||
|
LOGGER.info("Saved crash report to {}!", out.toAbsolutePath().toUri());
|
||||||
|
System.out.println(report);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.error("Failed to save crash report!", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getForException(Throwable t) {
|
||||||
|
return getForException(t, FrogLoaderImpl.getInstance().getMods());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getForException(Throwable t, Collection<ModProperties> mods) {
|
||||||
|
StringBuilder builder = new StringBuilder("--- FrogLoader Crash Report ---");
|
||||||
|
|
||||||
|
builder.append("\n\n").append("Time: ").append(ZonedDateTime.now()).append(" (").append(ZonedDateTime.now(ZoneOffset.UTC)).append(" UTC)");
|
||||||
|
builder.append("\n");
|
||||||
|
builder.append("\n").append("Exception:");
|
||||||
|
builder.append("\n");
|
||||||
|
StringWriter writer = new StringWriter();
|
||||||
|
t.printStackTrace(new PrintWriter(writer));
|
||||||
|
builder.append(writer);
|
||||||
|
builder.append("\n").append(ModUtil.getModList(mods));
|
||||||
|
builder.append("\n\n").append("--- Report End ---");
|
||||||
|
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package dev.frogmc.frogloader.impl.util;
|
||||||
|
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
|
@UtilityClass
|
||||||
|
public class SystemProperties {
|
||||||
|
|
||||||
|
public final String MINECRAFT_GAME_JAR = "frogmc.plugin.minecraft.gameJar";
|
||||||
|
public final String DEVELOPMENT = "frogmc.development";
|
||||||
|
}
|
57
src/main/java/dev/frogmc/frogloader/impl/util/URLUtil.java
Normal file
57
src/main/java/dev/frogmc/frogloader/impl/util/URLUtil.java
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package dev.frogmc.frogloader.impl.util;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.datatransfer.StringSelection;
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class URLUtil {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(URLUtil.class);
|
||||||
|
|
||||||
|
public static void open(URI url) {
|
||||||
|
|
||||||
|
LOGGER.info("Opening: {}", url);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
||||||
|
Desktop.getDesktop().browse(url);
|
||||||
|
} else {
|
||||||
|
ProcessBuilder builder = new ProcessBuilder("xdg-open", url.toString());
|
||||||
|
builder.start();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.error("Failed to open url: ", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void copyStringContent(URI url) {
|
||||||
|
if (System.getenv().getOrDefault("DESKTOP_SESSION", "").toLowerCase(Locale.ROOT)
|
||||||
|
.contains("wayland")) {
|
||||||
|
try {
|
||||||
|
ProcessBuilder builder = new ProcessBuilder("bash", "-c", "wl-copy < " + url);
|
||||||
|
builder.start();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.error("Failed to copy contents of {}:", url, e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try (InputStream in = url.toURL().openStream();
|
||||||
|
InputStreamReader inReader = new InputStreamReader(in);
|
||||||
|
BufferedReader reader = new BufferedReader(inReader)) {
|
||||||
|
String data = reader.lines().collect(Collectors.joining("\n"));
|
||||||
|
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(data), null);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.error("Failed to copy contents of {}:", url, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,25 +0,0 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.api;
|
|
||||||
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.api.env.Env;
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.impl.LoaderImpl;
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.impl.plugin.NonsensePlugin;
|
|
||||||
|
|
||||||
public interface Loader {
|
|
||||||
|
|
||||||
static Loader getInstance(){
|
|
||||||
return LoaderImpl.getInstance();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<NonsensePlugin> getPlugins();
|
|
||||||
|
|
||||||
Env getEnv();
|
|
||||||
|
|
||||||
Path getGameDir();
|
|
||||||
Path getConfigDir();
|
|
||||||
Path getModsDir();
|
|
||||||
boolean isDevelopment();
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.api.env;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@AllArgsConstructor
|
|
||||||
public enum Env {
|
|
||||||
CLIENT("CLIENT", "client"),
|
|
||||||
SERVER("SERVER", "server"),
|
|
||||||
;
|
|
||||||
|
|
||||||
final String mixinName, identifier;
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.api.extensions;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Pre-Launch Extension.
|
|
||||||
* <p>This Extension is run right before the game is launched. (provided the used plugin supports it :) )</p>
|
|
||||||
*/
|
|
||||||
public interface PreLaunchExtension {
|
|
||||||
String ID = "pre_launch";
|
|
||||||
|
|
||||||
void onPreLaunch();
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.api.mod;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.impl.LoaderImpl;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
public final class License {
|
|
||||||
|
|
||||||
private static final Map<String, String> idToName = new HashMap<>();
|
|
||||||
private static final Map<String, License> idToLicense = new HashMap<>();
|
|
||||||
|
|
||||||
static {
|
|
||||||
loadList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void loadList(){
|
|
||||||
try (InputStream in = License.class.getResourceAsStream("/assets/nonsense-loader/licenses.json")){
|
|
||||||
if (in == null){
|
|
||||||
throw new IllegalStateException("in == null");
|
|
||||||
}
|
|
||||||
JsonObject object = LoaderImpl.getInstance().getGson().fromJson(new InputStreamReader(in), JsonObject.class);
|
|
||||||
object.getAsJsonArray("licenses").forEach(element -> {
|
|
||||||
JsonObject entry = element.getAsJsonObject();
|
|
||||||
idToName.put(entry.get("licenseId").getAsString(), entry.get("name").getAsString());
|
|
||||||
});
|
|
||||||
} catch (Exception e){
|
|
||||||
LoggerFactory.getLogger(License.class).warn("Failed to load license list!", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static License fromId(String id){
|
|
||||||
return idToLicense.computeIfAbsent(id, ignored -> new License(idToName.getOrDefault(id, id), id));
|
|
||||||
}
|
|
||||||
|
|
||||||
private final String name, id;
|
|
||||||
private License(String name, String id) {
|
|
||||||
|
|
||||||
this.name = name;
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String name() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String id() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,85 +0,0 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.api.mod;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
public final class ModCredits implements Map<String, Collection<String>> {
|
|
||||||
|
|
||||||
public static ModCredits of(Map<String, Collection<String>> credits){
|
|
||||||
return new ModCredits(credits);
|
|
||||||
}
|
|
||||||
|
|
||||||
private final Map<String, Collection<String>> credits;
|
|
||||||
private ModCredits(Map<String, Collection<String>> credits){
|
|
||||||
this.credits = credits;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Collection<String> getEntries(){
|
|
||||||
return credits.keySet();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Collection<String> getRoles(String name){
|
|
||||||
return credits.getOrDefault(name, Collections.emptySet());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int size() {
|
|
||||||
return credits.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isEmpty() {
|
|
||||||
return credits.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean containsKey(Object key) {
|
|
||||||
return credits.containsKey(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean containsValue(Object value) {
|
|
||||||
return credits.containsValue(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Collection<String> get(Object key) {
|
|
||||||
return credits.get(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Collection<String> put(String key, Collection<String> value) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Collection<String> remove(Object key) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void putAll(@NotNull Map<? extends String, ? extends Collection<String>> m) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clear() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull Set<String> keySet() {
|
|
||||||
return credits.keySet();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull Collection<Collection<String>> values() {
|
|
||||||
return credits.values();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull Set<Entry<String, Collection<String>>> entrySet() {
|
|
||||||
return credits.entrySet();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.api.mod;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
public final class ModDependencies {
|
|
||||||
|
|
||||||
private final Map<Type, Collection<Entry>> entries = new HashMap<>();
|
|
||||||
|
|
||||||
public ModDependencies(Collection<Entry> depends, Collection<Entry> breaks, Collection<Entry> suggests) {
|
|
||||||
entries.put(Type.DEPEND, depends);
|
|
||||||
entries.put(Type.BREAK, breaks);
|
|
||||||
entries.put(Type.SUGGEST, suggests);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Collection<Entry> getForType(Type type) {
|
|
||||||
return entries.get(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<ModEntry> getForModId(String id) {
|
|
||||||
List<ModEntry> entries = new ArrayList<>();
|
|
||||||
for (Type type : Type.values()) {
|
|
||||||
for (Entry entry : getForType(type)) {
|
|
||||||
if (entry.id.equals(id)) {
|
|
||||||
entries.add(new ModEntry(type, entry.range));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class ModEntry {
|
|
||||||
private final Type type;
|
|
||||||
private final String range;
|
|
||||||
|
|
||||||
private ModEntry(Type type, String range) {
|
|
||||||
this.type = type;
|
|
||||||
this.range = range;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Type type() {
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String range() {
|
|
||||||
return range;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public record Entry(String id, String range) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum Type {
|
|
||||||
DEPEND, BREAK, SUGGEST
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.api.mod;
|
|
||||||
|
|
||||||
import java.lang.invoke.MethodHandle;
|
|
||||||
import java.lang.invoke.MethodHandles;
|
|
||||||
import java.lang.invoke.MethodType;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
public final class ModExtensions {
|
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(ModExtensions.class);
|
|
||||||
|
|
||||||
public static ModExtensions of(Map<String, Object> entries){
|
|
||||||
return new ModExtensions(entries);
|
|
||||||
}
|
|
||||||
|
|
||||||
private final Map<String, Object> extensions;
|
|
||||||
|
|
||||||
private ModExtensions(Map<String, Object> entries){
|
|
||||||
extensions = entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public <T> T get(String key) {
|
|
||||||
return (T) extensions.get(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public <T> T getOrDefault(String key, T defaultValue) {
|
|
||||||
return (T) extensions.getOrDefault(key, defaultValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> void runIfPresent(String key, Consumer<T> action) {
|
|
||||||
T value = get(key);
|
|
||||||
if (value != null){
|
|
||||||
action.accept(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public <T> void runIfPresent(String key, Class<T> type, Consumer<T> action) {
|
|
||||||
String value = get(key);
|
|
||||||
|
|
||||||
if (value == null){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
MethodHandle handle;
|
|
||||||
if (value.contains("::")) {
|
|
||||||
String[] parts = value.split("::");
|
|
||||||
handle = MethodHandles.lookup().findVirtual(Class.forName(parts[0]), parts[1], MethodType.methodType(type));
|
|
||||||
} else {
|
|
||||||
handle = MethodHandles.lookup().findConstructor(Class.forName(value), MethodType.methodType(void.class));
|
|
||||||
}
|
|
||||||
T object = (T) handle.invoke();
|
|
||||||
if (object != null) {
|
|
||||||
action.accept(object);
|
|
||||||
}
|
|
||||||
} catch (Throwable e) {
|
|
||||||
LOGGER.warn("Failed to instantiate Extension: ", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.api.mod;
|
|
||||||
|
|
||||||
public interface ModProperties {
|
|
||||||
|
|
||||||
String id();
|
|
||||||
|
|
||||||
String name();
|
|
||||||
|
|
||||||
SemVer version();
|
|
||||||
|
|
||||||
License license();
|
|
||||||
|
|
||||||
ModCredits credits();
|
|
||||||
|
|
||||||
ModDependencies dependencies();
|
|
||||||
|
|
||||||
ModExtensions extensions();
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.api.mod;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public interface ModProvider {
|
|
||||||
|
|
||||||
ModProperties properties();
|
|
||||||
|
|
||||||
Map<String, String> entrypoints();
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.impl;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.FileVisitResult;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.SimpleFileVisitor;
|
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.function.Predicate;
|
|
||||||
|
|
||||||
public class Discovery {
|
|
||||||
public static Collection<Path> find(Path start, Predicate<Path> directoryFilter, Predicate<Path> fileFilter){
|
|
||||||
List<Path> paths = new ArrayList<>();
|
|
||||||
try {
|
|
||||||
Files.walkFileTree(start, new SimpleFileVisitor<>() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
|
||||||
if (directoryFilter.test(dir)) {
|
|
||||||
return super.preVisitDirectory(dir, attrs);
|
|
||||||
}
|
|
||||||
return FileVisitResult.SKIP_SUBTREE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
|
||||||
if (fileFilter.test(file)) {
|
|
||||||
paths.add(file);
|
|
||||||
}
|
|
||||||
return super.visitFile(file, attrs);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (IOException e) {
|
|
||||||
// TODO error handling.
|
|
||||||
}
|
|
||||||
return paths;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,145 +0,0 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.impl;
|
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
import java.lang.invoke.MethodHandle;
|
|
||||||
import java.lang.invoke.MethodHandles;
|
|
||||||
import java.lang.invoke.MethodType;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.api.Loader;
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.api.env.Env;
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.impl.launch.MixinClassLoader;
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.impl.plugin.NonsensePlugin;
|
|
||||||
import lombok.Getter;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.spongepowered.asm.mixin.MixinEnvironment;
|
|
||||||
|
|
||||||
public class LoaderImpl implements Loader {
|
|
||||||
// TODO decide this
|
|
||||||
public static final String MOD_FILE_EXTENSION = ".nonsense";
|
|
||||||
private final boolean DEV_ENV = Boolean.getBoolean("nonsense.development");
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
private final String[] args;
|
|
||||||
@Getter
|
|
||||||
private final Env env;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
private static LoaderImpl instance;
|
|
||||||
|
|
||||||
private final Logger LOGGER = LoggerFactory.getLogger("Nonsense Loader");
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
private final List<NonsensePlugin> plugins = new ArrayList<>();
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
private final Path gameDir, configDir, modsDir;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
private final MixinClassLoader classloader;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
private final Gson gson = new Gson();
|
|
||||||
|
|
||||||
private LoaderImpl(String[] args, Env env) {
|
|
||||||
instance = this;
|
|
||||||
this.classloader = (MixinClassLoader) this.getClass().getClassLoader();
|
|
||||||
this.args = args;
|
|
||||||
this.env = env;
|
|
||||||
|
|
||||||
gameDir = Paths.get(getArgumentOrElse("gameDir", "."));
|
|
||||||
configDir = gameDir.resolve("config");
|
|
||||||
modsDir = gameDir.resolve("mods");
|
|
||||||
|
|
||||||
try {
|
|
||||||
Files.createDirectories(gameDir);
|
|
||||||
Files.createDirectories(configDir);
|
|
||||||
Files.createDirectories(modsDir);
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOGGER.warn("Failed to create essential directories ", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
discoverPlugins();
|
|
||||||
advanceMixinState();
|
|
||||||
LOGGER.info("Launching...");
|
|
||||||
plugins.forEach(NonsensePlugin::run);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void advanceMixinState(){
|
|
||||||
try {
|
|
||||||
MethodHandle m = MethodHandles.privateLookupIn(MixinEnvironment.class, MethodHandles.lookup()).findStatic(MixinEnvironment.class, "gotoPhase", MethodType.methodType(void.class, MixinEnvironment.Phase.class));
|
|
||||||
m.invoke(MixinEnvironment.Phase.INIT);
|
|
||||||
m.invoke(MixinEnvironment.Phase.DEFAULT);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public static void run(String[] args, Env env) {
|
|
||||||
if (instance != null) {
|
|
||||||
throw new IllegalStateException("Loader was started multiple times!");
|
|
||||||
}
|
|
||||||
|
|
||||||
new LoaderImpl(args, env);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void discoverPlugins() {
|
|
||||||
List<String> classes = new ArrayList<>();
|
|
||||||
this.getClass().getClassLoader().resources("META-INF/services/"+NonsensePlugin.class.getName()).distinct().forEach(url -> {
|
|
||||||
try (InputStream inputStream = url.openStream()) {
|
|
||||||
new BufferedReader(new InputStreamReader(inputStream)).lines().forEach(classes::add);
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOGGER.error("Failed to load plugin: ", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
LOGGER.info("Found plugins: \n{}", String.join("\t\n", classes));
|
|
||||||
|
|
||||||
for (Class<?> c : classes.stream().map((String className) -> {
|
|
||||||
try {
|
|
||||||
return classloader.findClass(className);
|
|
||||||
} catch (ClassNotFoundException e) {
|
|
||||||
LOGGER.error("Failed to load plugin: ", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}).filter(Objects::nonNull).filter(NonsensePlugin.class::isAssignableFrom).toList()) {
|
|
||||||
try {
|
|
||||||
MethodHandle ctor = MethodHandles.publicLookup().findConstructor(c, MethodType.methodType(void.class));
|
|
||||||
NonsensePlugin plugin = (NonsensePlugin) ctor.invoke();
|
|
||||||
plugins.add(plugin);
|
|
||||||
if (plugin.init(this)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (Throwable e) {
|
|
||||||
LOGGER.error("Error during plugin initialisation: ", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getArgument(String name){
|
|
||||||
for (int i=0;i<args.length-1;i+=2){
|
|
||||||
if (args[i].equals("--"+name)){
|
|
||||||
return args[i+1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getArgumentOrElse(String name, String other){
|
|
||||||
String res = getArgument(name);
|
|
||||||
if (res.isEmpty()){
|
|
||||||
return other;
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isDevelopment() {
|
|
||||||
return DEV_ENV;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.impl.launch;
|
|
||||||
|
|
||||||
import java.lang.invoke.MethodHandle;
|
|
||||||
import java.lang.invoke.MethodHandles;
|
|
||||||
import java.lang.invoke.MethodType;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.api.env.Env;
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.impl.mixin.NonsenseMixinService;
|
|
||||||
import lombok.Getter;
|
|
||||||
import org.spongepowered.asm.launch.MixinBootstrap;
|
|
||||||
import org.spongepowered.asm.service.IPropertyKey;
|
|
||||||
|
|
||||||
public class Launcher {
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
private final MixinClassLoader targetClassLoader;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
private static Launcher instance;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
private final Map<IPropertyKey, Object> globalProperties = new HashMap<>();
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
private final Env env;
|
|
||||||
|
|
||||||
public static void run(String[] args, Env env) {
|
|
||||||
new Launcher(args, env);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Launcher(String[] args, Env env){
|
|
||||||
if (instance != null){
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
|
||||||
instance = this;
|
|
||||||
this.env = env;
|
|
||||||
targetClassLoader = new MixinClassLoader();
|
|
||||||
targetClassLoader.excludePackage("org.slf4j");
|
|
||||||
targetClassLoader.excludePackage("org.spongepowered");
|
|
||||||
targetClassLoader.excludePackage("org.apache.logging");
|
|
||||||
targetClassLoader.excludePackage("org.ecorous.esnesnon.nonsense.loader.impl.launch");
|
|
||||||
targetClassLoader.excludePackage("org.ecorous.esnesnon.nonsense.loader.api.env");
|
|
||||||
|
|
||||||
Thread.currentThread().setContextClassLoader(targetClassLoader);
|
|
||||||
|
|
||||||
System.setProperty("mixin.service", NonsenseMixinService.class.getName());
|
|
||||||
MixinBootstrap.init();
|
|
||||||
|
|
||||||
try {
|
|
||||||
Class<?> clazz = targetClassLoader.findClass("org.ecorous.esnesnon.nonsense.loader.impl.LoaderImpl");
|
|
||||||
MethodHandle ctor = MethodHandles.publicLookup().findStatic(clazz, "run", MethodType.methodType(void.class, String[].class, Env.class));
|
|
||||||
ctor.invoke(args, env);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
// TODO
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,129 +0,0 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.impl.launch;
|
|
||||||
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.impl.mixin.NonsenseMixinService;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
import org.spongepowered.asm.mixin.MixinEnvironment;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.net.URLClassLoader;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Enumeration;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class MixinClassLoader extends URLClassLoader {
|
|
||||||
|
|
||||||
private static final ClassLoader SYSTEM = ClassLoader.getSystemClassLoader();
|
|
||||||
private final List<String> exclusions = new ArrayList<>();
|
|
||||||
|
|
||||||
static {
|
|
||||||
registerAsParallelCapable();
|
|
||||||
}
|
|
||||||
|
|
||||||
public MixinClassLoader() {
|
|
||||||
super(new URL[0], null);
|
|
||||||
excludePackage("java");
|
|
||||||
excludePackage("com.sun");
|
|
||||||
excludePackage("sun");
|
|
||||||
excludePackage("jdk");
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isClassLoaded(String name) {
|
|
||||||
return findLoadedClass(name) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getClassBytes(String name) throws IOException {
|
|
||||||
String binName = name.replace('.', '/');
|
|
||||||
String path = binName.concat(".class");
|
|
||||||
|
|
||||||
try (InputStream in = getResourceAsStream(path)) {
|
|
||||||
if (in == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return NonsenseMixinService.getTransformer().transformClass(MixinEnvironment.getCurrentEnvironment(), name, in.readAllBytes());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void excludePackage(String name) {
|
|
||||||
exclusions.add(name + '.');
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addURL(URL url) {
|
|
||||||
super.addURL(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
|
||||||
synchronized (getClassLoadingLock(name)) {
|
|
||||||
for (String prefix : exclusions) {
|
|
||||||
if (name.startsWith(prefix)) {
|
|
||||||
return SYSTEM.loadClass(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Class<?> loaded = findLoadedClass(name);
|
|
||||||
if (loaded != null)
|
|
||||||
return loaded;
|
|
||||||
|
|
||||||
Class<?> result = findClass(name);
|
|
||||||
|
|
||||||
if (resolve)
|
|
||||||
resolveClass(result);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized Class<?> findClass(String name) throws ClassNotFoundException {
|
|
||||||
try {
|
|
||||||
byte[] bytes = getClassBytes(name);
|
|
||||||
if (bytes == null)
|
|
||||||
throw new ClassNotFoundException(name);
|
|
||||||
|
|
||||||
return defineClass(name, bytes, 0, bytes.length);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new ClassNotFoundException(name, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public URL getResource(String name) {
|
|
||||||
URL parentUrl = super.getResource(name);
|
|
||||||
|
|
||||||
if (parentUrl != null)
|
|
||||||
return parentUrl;
|
|
||||||
|
|
||||||
return SYSTEM.getResource(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Enumeration<URL> getResources(String name) throws IOException {
|
|
||||||
Enumeration<URL> parentResources = super.getResources(name);
|
|
||||||
Enumeration<URL> systemResources = SYSTEM.getResources(name);
|
|
||||||
|
|
||||||
if (parentResources.hasMoreElements() && systemResources.hasMoreElements()) {
|
|
||||||
List<URL> list = new ArrayList<>();
|
|
||||||
|
|
||||||
while (parentResources.hasMoreElements())
|
|
||||||
list.add(parentResources.nextElement());
|
|
||||||
|
|
||||||
while (systemResources.hasMoreElements())
|
|
||||||
list.add(systemResources.nextElement());
|
|
||||||
|
|
||||||
return Collections.enumeration(list);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parentResources.hasMoreElements())
|
|
||||||
return parentResources;
|
|
||||||
|
|
||||||
if (systemResources.hasMoreElements())
|
|
||||||
return systemResources;
|
|
||||||
|
|
||||||
return Collections.enumeration(Collections.emptyList());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.impl.launch.client;
|
|
||||||
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.impl.launch.Launcher;
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.api.env.Env;
|
|
||||||
|
|
||||||
public class NonsenseClient {
|
|
||||||
|
|
||||||
public static void main(String[] args){
|
|
||||||
Launcher.run(args, Env.CLIENT);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.impl.launch.server;
|
|
||||||
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.impl.launch.Launcher;
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.api.env.Env;
|
|
||||||
|
|
||||||
public class NonsenseServer {
|
|
||||||
|
|
||||||
public static void main(String[] args){
|
|
||||||
Launcher.run(args, Env.SERVER);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.impl.mixin;
|
|
||||||
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.impl.launch.Launcher;
|
|
||||||
import org.spongepowered.asm.service.IGlobalPropertyService;
|
|
||||||
import org.spongepowered.asm.service.IPropertyKey;
|
|
||||||
|
|
||||||
public class NonsenseGlobalPropertyService implements IGlobalPropertyService {
|
|
||||||
@Override
|
|
||||||
public IPropertyKey resolveKey(String name) {
|
|
||||||
return new IPropertyKey() {
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
@Override
|
|
||||||
public <T> T getProperty(IPropertyKey key) {
|
|
||||||
return (T) Launcher.getInstance().getGlobalProperties().get(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setProperty(IPropertyKey key, Object value) {
|
|
||||||
Launcher.getInstance().getGlobalProperties().put(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
@Override
|
|
||||||
public <T> T getProperty(IPropertyKey key, T defaultValue) {
|
|
||||||
return (T) Launcher.getInstance().getGlobalProperties().getOrDefault(key, defaultValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getPropertyString(IPropertyKey key, String defaultValue) {
|
|
||||||
return Launcher.getInstance().getGlobalProperties().getOrDefault(key, defaultValue).toString();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.impl.mod;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.api.mod.ModProperties;
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.api.mod.ModProvider;
|
|
||||||
|
|
||||||
public record BuiltinModProvider(ModProperties properties, Map<String, String> entrypoints) implements ModProvider {
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.impl.mod;
|
|
||||||
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.api.mod.*;
|
|
||||||
|
|
||||||
public record ModPropertiesImpl(String id, String name, SemVer version, License license,
|
|
||||||
ModCredits credits, ModDependencies dependencies,
|
|
||||||
ModExtensions extensions) implements ModProperties {
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.impl.mod;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.UncheckedIOException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.nio.file.FileSystem;
|
|
||||||
import java.nio.file.FileSystems;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import com.electronwill.nightconfig.core.CommentedConfig;
|
|
||||||
import com.electronwill.nightconfig.core.UnmodifiableConfig;
|
|
||||||
import com.electronwill.nightconfig.core.file.FileNotFoundAction;
|
|
||||||
import com.electronwill.nightconfig.toml.TomlParser;
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.api.mod.*;
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.impl.SemVerParseException;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
public class ModPropertiesReader {
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(ModPropertiesReader.class);
|
|
||||||
public static final String PROPERTIES_FILE_NAME = "nonsense.mod.toml";
|
|
||||||
|
|
||||||
private static final TomlParser PARSER = new TomlParser();
|
|
||||||
|
|
||||||
public static Optional<ModProperties> read(Path mod) {
|
|
||||||
|
|
||||||
try (FileSystem fs = FileSystems.newFileSystem(mod)) {
|
|
||||||
CommentedConfig props = PARSER.parse(fs.getPath(PROPERTIES_FILE_NAME), FileNotFoundAction.THROW_ERROR);
|
|
||||||
|
|
||||||
return Optional.of(readProperties(props));
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOGGER.warn("Failed to read mod properties: ", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ModProperties readFile(URL in) {
|
|
||||||
CommentedConfig props = PARSER.parse(in);
|
|
||||||
|
|
||||||
return readProperties(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ModProperties readProperties(UnmodifiableConfig config) {
|
|
||||||
String id = config.get("nonsense.id");
|
|
||||||
String name = config.get("nonsense.name");
|
|
||||||
String version = config.get("nonsense.version");
|
|
||||||
String license = config.get("nonsense.license");
|
|
||||||
|
|
||||||
List<UnmodifiableConfig> creditsList = config.get("nonsense.credits");
|
|
||||||
Map<String, Collection<String>> credits = new HashMap<>();
|
|
||||||
creditsList.forEach(c -> credits.put(c.get("name"), c.get("roles")));
|
|
||||||
|
|
||||||
Collection<ModDependencies.Entry> depends = config.get("nonsense.dependencies.depends");
|
|
||||||
Collection<ModDependencies.Entry> breaks = config.get("nonsense.dependencies.breaks");
|
|
||||||
Collection<ModDependencies.Entry> suggests = config.get("nonsense.dependencies.suggests");
|
|
||||||
|
|
||||||
UnmodifiableConfig extensionsConfig = config.get("nonsense.extensions");
|
|
||||||
Map<String, Object> extensions = new HashMap<>();
|
|
||||||
extensionsConfig.entrySet().forEach(entry -> extensions.put(entry.getKey(), entry.getValue()));
|
|
||||||
try {
|
|
||||||
return new ModPropertiesImpl(id, name, SemVer.parse(version), License.fromId(license), ModCredits.of(credits), new ModDependencies(depends, breaks, suggests), ModExtensions.of(extensions));
|
|
||||||
} catch (SemVerParseException e) {
|
|
||||||
throw new UncheckedIOException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.impl.plugin;
|
|
||||||
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.impl.LoaderImpl;
|
|
||||||
|
|
||||||
public interface NonsensePlugin extends Runnable {
|
|
||||||
|
|
||||||
default void run() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
default boolean init(LoaderImpl loader) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,156 +0,0 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.impl.plugin.game;
|
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.api.extensions.PreLaunchExtension;
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.api.mod.ModProperties;
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.impl.Discovery;
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.impl.LoaderImpl;
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.impl.mod.ModPropertiesReader;
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.impl.plugin.NonsensePlugin;
|
|
||||||
import org.ecorous.esnesnon.nonsense_remapper.NonsenseRemapper;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.spongepowered.asm.mixin.Mixins;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.lang.invoke.MethodHandle;
|
|
||||||
import java.lang.invoke.MethodHandles;
|
|
||||||
import java.lang.invoke.MethodType;
|
|
||||||
import java.net.MalformedURLException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.nio.file.*;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class Minecraft implements NonsensePlugin {
|
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger("Plugin/Minecraft");
|
|
||||||
private static final String[] MINECRAFT_CLASSES = new String[]{
|
|
||||||
"net/minecraft/client/main/Main.class",
|
|
||||||
"net/minecraft/client/MinecraftApplet.class",
|
|
||||||
"net/minecraft/server/Main.class"
|
|
||||||
};
|
|
||||||
|
|
||||||
private final List<ModProperties> modProperties = new ArrayList<>();
|
|
||||||
private String version;
|
|
||||||
private Path remappedGamePath;
|
|
||||||
private String foundMainClass;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean init(LoaderImpl loader) {
|
|
||||||
Path gameJar = findGame();
|
|
||||||
if (gameJar == null){
|
|
||||||
LOGGER.error("Could not find game jar on classpath! ({})", System.getProperty("java.class.path", ""));
|
|
||||||
return false;
|
|
||||||
//throw new IllegalStateException("Could not find game jar!");
|
|
||||||
}
|
|
||||||
|
|
||||||
remappedGamePath = loader.getGameDir().resolve(".nonsense/remappedJars").resolve(version).resolve("game-"+version+"-remapped.jar");
|
|
||||||
|
|
||||||
if (!Files.exists(remappedGamePath)){
|
|
||||||
try {
|
|
||||||
Files.createDirectories(remappedGamePath.getParent());
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOGGER.error("Failed to create directory", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Collection<Path> mods = Discovery.find(loader.getModsDir(), path ->
|
|
||||||
version.equals(path.getFileName().toString()), path ->
|
|
||||||
path.getFileName().toString().endsWith(LoaderImpl.MOD_FILE_EXTENSION));
|
|
||||||
// TODO add mods found on the classpath
|
|
||||||
Collection<URL> classpathMods = this.getClass().getClassLoader().resources(ModPropertiesReader.PROPERTIES_FILE_NAME).distinct().toList();
|
|
||||||
mods.parallelStream().map(Path::toUri).map(uri -> {
|
|
||||||
try {
|
|
||||||
return uri.toURL();
|
|
||||||
} catch (MalformedURLException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}).forEachOrdered(LoaderImpl.getInstance().getClassloader()::addURL);
|
|
||||||
LOGGER.info("Found {} mod(s)", mods.size()+classpathMods.size());
|
|
||||||
|
|
||||||
classpathMods.parallelStream().map(ModPropertiesReader::readFile).forEachOrdered(modProperties::add);
|
|
||||||
mods.parallelStream().map(ModPropertiesReader::read).forEachOrdered(opt -> opt.ifPresent(modProperties::add));
|
|
||||||
|
|
||||||
modProperties.parallelStream().forEach(props -> {
|
|
||||||
String name = props.extensions().get("mixin_config");
|
|
||||||
Mixins.addConfiguration(name);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!Files.exists(remappedGamePath) && !loader.isDevelopment()){
|
|
||||||
try {
|
|
||||||
NonsenseRemapper.run(version, gameJar, remappedGamePath, true, false);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
LoaderImpl.getInstance().getClassloader().addURL(remappedGamePath.toUri().toURL());
|
|
||||||
} catch (Throwable e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Path findGame() {
|
|
||||||
LOGGER.info("Locating game..");
|
|
||||||
String jar = System.getProperty("nonsense.plugin.minecraft.gameJar");
|
|
||||||
if (jar != null){
|
|
||||||
Path p = Paths.get(jar);
|
|
||||||
if (checkLocation(p)){
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String s : System.getProperty("java.class.path", "").split(File.pathSeparator)) {
|
|
||||||
Path p = Paths.get(s);
|
|
||||||
if (checkLocation(p)){
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LOGGER.warn("Could not locate game!");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean checkLocation(Path jar){
|
|
||||||
if (!Files.exists(jar) || Files.isDirectory(jar)){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try (FileSystem fs = FileSystems.newFileSystem(jar)) {
|
|
||||||
for (String n : MINECRAFT_CLASSES) {
|
|
||||||
if (Files.exists(fs.getPath(n)) && n.contains(LoaderImpl.getInstance().getEnv().getIdentifier())){
|
|
||||||
LOGGER.info("Found game: {}", jar);
|
|
||||||
foundMainClass = n.substring(0, n.length()-6).replace("/", ".");
|
|
||||||
version = LoaderImpl.getInstance().getGson().fromJson(Files.readString(fs.getPath("version.json")), JsonObject.class).get("id").getAsString();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
try {
|
|
||||||
if (foundMainClass != null) {
|
|
||||||
modProperties.parallelStream().forEach(props ->
|
|
||||||
props.extensions().runIfPresent(PreLaunchExtension.ID,
|
|
||||||
PreLaunchExtension.class, PreLaunchExtension::onPreLaunch));
|
|
||||||
LOGGER.info("Launching main class: {}", foundMainClass);
|
|
||||||
Class<?> mainClass = Class.forName(foundMainClass);
|
|
||||||
MethodHandle main = MethodHandles.publicLookup().findStatic(mainClass, "main", MethodType.methodType(void.class, String[].class));
|
|
||||||
main.invoke((Object) LoaderImpl.getInstance().getArgs());
|
|
||||||
} else {
|
|
||||||
LOGGER.warn("Failed to locate main class!");
|
|
||||||
}
|
|
||||||
} catch (Throwable e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1 @@
|
||||||
|
dev.frogmc.frogloader.impl.plugin.game.minecraft.Minecraft
|
|
@ -1 +0,0 @@
|
||||||
org.ecorous.esnesnon.nonsense.loader.impl.plugin.game.Minecraft
|
|
|
@ -1 +1 @@
|
||||||
org.ecorous.esnesnon.nonsense.loader.impl.mixin.NonsenseGlobalPropertyService
|
dev.frogmc.frogloader.impl.mixin.FrogGlobalPropertyService
|
File diff suppressed because it is too large
Load diff
BIN
src/main/resources/duke.png
Normal file
BIN
src/main/resources/duke.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.1 KiB |
12
src/main/resources/frog.mod.toml
Normal file
12
src/main/resources/frog.mod.toml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
[frog]
|
||||||
|
format_version = "1.0.0"
|
||||||
|
|
||||||
|
[frog.mod]
|
||||||
|
id = "frogloader"
|
||||||
|
name = "FrogLoader"
|
||||||
|
version = "${version}"
|
||||||
|
license = "Apache-2.0"
|
||||||
|
credits = [
|
||||||
|
{ name = "FrogMC Team", roles = ["author"] }
|
||||||
|
]
|
||||||
|
|
Loading…
Reference in a new issue