From f42e0ac28d6ca971c1708ce62cc7f809b8089615 Mon Sep 17 00:00:00 2001 From: LucHeart Date: Wed, 26 Aug 2026 21:02:12 +0200 Subject: [PATCH 1/6] first working version --- .github/workflows/build.yml | 38 +++--- .gitignore | 7 ++ README.md | 66 ++++++++++ build.gradle | 108 ----------------- build.gradle.kts | 99 +++++++++++++++ gradle.properties | 25 +--- gradle/wrapper/gradle-wrapper.jar | Bin 45633 -> 47505 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 6 +- gradlew.bat | 35 ++---- settings.gradle | 10 -- settings.gradle.kts | 43 +++++++ .../{ConfigGuiFactory.kt => ConfigScreen.kt} | 113 +++++++++--------- .../minecraft/ModMenuEntryPoint.kt | 10 -- .../integrations/minecraft/ShockCraft.kt | 63 +++++----- .../minecraft/api/OpenShockApi.kt | 69 ++++++----- .../minecraft/config/ShockCraftConfig.kt | 8 +- .../minecraft/platform/Entrypoint.kt | 64 ++++++++++ .../minecraft/platform/McCompat.kt | 43 +++++++ .../minecraft/platform/Platform.kt | 19 +++ .../integrations/minecraft/utils/CallAwait.kt | 56 --------- .../minecraft/utils/CallStackRecorder.kt | 23 ---- .../resources/META-INF/neoforge.mods.toml | 43 +++++++ src/main/resources/fabric.mod.json | 28 ++--- src/main/resources/shockcraft.mixins.json | 11 -- stonecutter.gradle.kts | 11 ++ versions/dependencies/1.21.1.properties | 14 +++ versions/dependencies/1.21.11.properties | 14 +++ versions/dependencies/1.21.4.properties | 14 +++ versions/dependencies/1.21.8.properties | 15 +++ versions/dependencies/26.1.properties | 16 +++ versions/dependencies/26.2.properties | 14 +++ 32 files changed, 669 insertions(+), 420 deletions(-) delete mode 100644 build.gradle create mode 100644 build.gradle.kts delete mode 100644 settings.gradle create mode 100644 settings.gradle.kts rename src/main/kotlin/openshock/integrations/minecraft/{ConfigGuiFactory.kt => ConfigScreen.kt} (72%) delete mode 100644 src/main/kotlin/openshock/integrations/minecraft/ModMenuEntryPoint.kt create mode 100644 src/main/kotlin/openshock/integrations/minecraft/platform/Entrypoint.kt create mode 100644 src/main/kotlin/openshock/integrations/minecraft/platform/McCompat.kt create mode 100644 src/main/kotlin/openshock/integrations/minecraft/platform/Platform.kt delete mode 100644 src/main/kotlin/openshock/integrations/minecraft/utils/CallAwait.kt delete mode 100644 src/main/kotlin/openshock/integrations/minecraft/utils/CallStackRecorder.kt create mode 100644 src/main/resources/META-INF/neoforge.mods.toml delete mode 100644 src/main/resources/shockcraft.mixins.json create mode 100644 stonecutter.gradle.kts create mode 100644 versions/dependencies/1.21.1.properties create mode 100644 versions/dependencies/1.21.11.properties create mode 100644 versions/dependencies/1.21.4.properties create mode 100644 versions/dependencies/1.21.8.properties create mode 100644 versions/dependencies/26.1.properties create mode 100644 versions/dependencies/26.2.properties diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2b100f2..e265862 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,37 +1,41 @@ -# Automatically build the project and run any configured tests for every push -# and submitted pull request. This can help catch issues that only occur on -# certain platforms or Java versions, and provides a first line of defence -# against bad commits. +# Builds every Minecraft version / loader combination declared in settings.gradle.kts. +# Stonecutter fans the build out across all targets; chiseledBuildAndCollect gathers the +# resulting jars into the root build/libs directory. name: build on: [pull_request, push] jobs: build: - strategy: - matrix: - # Use these Java versions - java: [ - 21, # Current Java LTS - ] runs-on: ubuntu-latest steps: - name: checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: validate gradle wrapper uses: gradle/actions/wrapper-validation@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 - - name: setup jdk ${{ matrix.java }} + + # 1.21.x targets compile against Java 21, 26.x targets against Java 25. + # Both toolchains have to be present for the whole matrix to build in one pass. + - name: setup jdks uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: - java-version: ${{ matrix.java }} - distribution: 'microsoft' + java-version: | + 21 + 25 + distribution: 'temurin' + + - name: setup gradle + uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 + - name: make gradle wrapper executable run: chmod +x ./gradlew - - name: build - run: ./gradlew build + + - name: build all versions + run: ./gradlew chiseledBuildAndCollect --stacktrace + - name: capture build artifacts - if: ${{ matrix.java == '21' }} # Only upload artifacts built from latest java on one OS uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: Mod Jars - path: build/libs/ \ No newline at end of file + path: build/libs/ diff --git a/.gitignore b/.gitignore index c476faf..925bb68 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,13 @@ build/ out/ classes/ +# stonecutter + +versions/*/build/ +versions/*/src/ +versions/*/.gradle/ +.stonecutter/ + # eclipse *.launch diff --git a/README.md b/README.md index c923bc8..bddbd56 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,69 @@ ## Support You can support the openshock dev team here: [Sponsor OpenShock](https://github.com/sponsors/OpenShock) + +## Development + +The mod is built from a single source tree for every supported Minecraft version and both mod +loaders, using [Stonecutter](https://github.com/stonecutter-versioning/stonecutter) for the version +matrix and [Stonecraft](https://github.com/meza/Stonecraft) (which wraps Architectury Loom) for the +loader wiring. + +| | | +|---|---| +| Minecraft | 1.21.1, 1.21.4, 1.21.8, 1.21.11, 26.1, 26.2 | +| Loaders | Fabric, NeoForge | +| Targets | 12 (every version × every loader) | + +### Building + +```bash +./gradlew chiseledBuildAndCollect # build every target; jars land in build/libs/ +./gradlew build # build only the currently active target +``` + +The active target is the one your IDE and `runClient` use. Switch it with: + +```bash +./gradlew "Set active project to 1.21.1-neoforge" +``` + +Stonecutter rewrites the files under `src/` in place when you switch, commenting out the branches +that do not apply to the new target. That is expected — do not revert it. + +### Running the game + +Architectury Loom generates one IntelliJ run configuration per target, named +`Minecraft Client (:-)`. They are created during Gradle sync, so after cloning (or +after adding a version) use **Gradle tool window -> Reload All Gradle Projects** before looking for +them. `.idea/` is gitignored, so these are local-only and always regenerated. + +From the command line: + +```bash +./gradlew :26.2-fabric:runClient +./gradlew :1.21.1-neoforge:runClient +``` + +You do not need to switch the active target to run a different one - every target is a real Gradle +subproject and can be launched directly. Switching only changes which one your IDE indexes and +edits. + +Each target runs in its own directory, `run/-/`, because Minecraft 1.21.1 and 26.2 +cannot share a world or an options file and the two loaders cannot share a mods folder. + +### Layout + +- `settings.gradle.kts` — the list of targets. Adding a Minecraft version is one line here plus a + matching `versions/dependencies/.properties` file. +- `versions/dependencies/.properties` — every dependency version for that Minecraft + version, shared by its Fabric and NeoForge targets. This is the only file to touch when bumping + Fabric API, NeoForge, YACL, Mod Menu or Kotlin for Forge. +- `build.gradle.kts` — applied to every target. +- `src/main/kotlin/.../platform/` — the only loader- and version-specific code: + - `Entrypoint.kt` — the Fabric and NeoForge entrypoints. + - `Platform.kt` — the handful of loader APIs with no common equivalent. + - `McCompat.kt` — the few vanilla APIs that changed across the version range. + +Everything outside `platform/` compiles unchanged on all twelve targets. Sources use **Mojang +mappings**, which both loaders share. diff --git a/build.gradle b/build.gradle deleted file mode 100644 index c198f5c..0000000 --- a/build.gradle +++ /dev/null @@ -1,108 +0,0 @@ -plugins { - id 'fabric-loom' version "${loom_version}" - id 'maven-publish' - id "org.jetbrains.kotlin.jvm" version "2.2.21" - id("com.gradleup.shadow") version "9.3.0" -} - -version = project.mod_version -group = project.maven_group - -base { - archivesName = project.archives_base_name -} - - -repositories { - // Add repositories to retrieve artifacts from in here. - // You should only use this when depending on other mods because - // Loom adds the essential maven repositories to download Minecraft and libraries from automatically. - // See https://docs.gradle.org/current/userguide/declaring_repositories.html - // for more information about repositories. - maven { - name = 'Xander Maven' - url = 'https://maven.isxander.dev/releases' - } - - maven { - name = 'TerraformersMC' - url = 'https://maven.terraformersmc.com/releases' - } -} - -dependencies { - // To change the versions see the gradle.properties file - minecraft "com.mojang:minecraft:${project.minecraft_version}" - mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" - - shadow(implementation("com.squareup.okhttp3:okhttp:4.12.0")) - - // Fabric API. - modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" - modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" - modImplementation "net.fabricmc:fabric-language-kotlin:${project.fabric_kotlin_version}" - - modImplementation "dev.isxander:yet-another-config-lib:${project.yet_another_config_lib_version}" - modImplementation "com.terraformersmc:modmenu:${project.mod_menu_version}" -} - -processResources { - inputs.property "version", project.version - - filesMatching("fabric.mod.json") { - expand "version": project.version - } -} - -tasks.withType(JavaCompile).configureEach { - it.options.release = 21 -} - -tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { - kotlinOptions { - jvmTarget = 21 - } -} - -java { - // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task - // if it is present. - // If you remove this line, sources will not be generated. - withSourcesJar() - - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 -} - -jar { - from("LICENSE") { - rename { "${it}_${project.base.archivesName.get()}" } - } -} - -shadowJar { - exclude 'kotlin*' - configurations = [project.configurations.shadow] -} - -remapJar { - dependsOn(shadowJar) - inputFile = tasks.shadowJar.archiveFile -} - -// configure the maven publication -publishing { - publications { - mavenJava(MavenPublication) { - from components.java - } - } - - // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. - repositories { - // Add repositories to publish to here. - // Notice: This block does NOT have the same function as the block in the top level. - // The repositories here will be used for publishing your artifact, not for - // retrieving dependencies. - } -} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..6f0d6e1 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,99 @@ +import gg.meza.stonecraft.mod +import org.gradle.api.artifacts.dsl.DependencyHandler +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + // Kotlin first: Architectury Loom snapshots the source set output directories when it is + // applied, and if Kotlin has not registered build/classes/kotlin/main by then, that directory + // is missing from MOD_CLASSES and the dev-time NeoForge run cannot find the mod. + kotlin("jvm") + + // Stonecraft must be applied before Architectury Loom, which it applies itself. + id("gg.meza.stonecraft") + id("dev.kikugie.stonecutter") +} + +// Minecraft 26.1 and newer ship deobfuscated, so those targets skip Loom's remapping entirely +// and consume mod dependencies straight off the compile classpath. +val deobfuscated = stonecutter.eval(mod.minecraftVersion, ">=26.1") + +// Stonecraft picks the Java version from the Minecraft version (21 for 1.21.x, 25 for 26.x). +// Kotlin has to follow it rather than pick its own. +val javaVersion = java.toolchain.languageVersion.get().asInt() + +// Bundled inside YACL's release jar via JarJar; needed explicitly only for NeoForge dev runs. +val yaclBundledLibraries = listOf( + "org.quiltmc.parsers:json:0.2.1", + "org.quiltmc.parsers:gson:0.2.1", + "com.twelvemonkeys.imageio:imageio-core:3.12.0", + "com.twelvemonkeys.imageio:imageio-webp:3.12.0", + "com.twelvemonkeys.imageio:imageio-metadata:3.12.0", + "com.twelvemonkeys.common:common-lang:3.12.0", + "com.twelvemonkeys.common:common-io:3.12.0", + "com.twelvemonkeys.common:common-image:3.12.0", +) + +repositories { + maven("https://maven.isxander.dev/releases") + maven("https://maven.terraformersmc.com/releases") + exclusiveContent { + forRepository { maven("https://thedarkcolour.github.io/KotlinForForge/") } + filter { includeGroup("thedarkcolour") } + } +} + +kotlin { + jvmToolchain(javaVersion) +} + +/** Loom-remapped on obfuscated versions, plain on deobfuscated ones. */ +fun DependencyHandler.modDependency(notation: String) = + add(if (deobfuscated) "implementation" else "modImplementation", notation) + +dependencies { + modDependency("dev.isxander:yet-another-config-lib:${mod.prop("yacl_version")}-${mod.loader}") + + if (mod.isFabric) { + modDependency("net.fabricmc:fabric-language-kotlin:${mod.prop("fabric_kotlin_version")}") + modDependency("com.terraformersmc:modmenu:${mod.prop("modmenu_version")}") + } + + if (mod.isNeoforge) { + // Kotlin for Forge supplies the Kotlin stdlib and coroutines at runtime, the same way + // fabric-language-kotlin does on Fabric. + implementation("thedarkcolour:kotlinforforge-neoforge:${mod.prop("kff_version")}") + + // YACL ships these as jar-in-jar in its release jar, so they are only missing in the dev + // environment: Loom strips nested jars, and NeoForge's module classloader does not pick up + // the Gradle-resolved replacements sitting on the runtime classpath. Without them YACL + // cannot build its config serializer and both YACL and this mod fail to construct. + // The list mirrors the dependencies in YACL's own POM (minus kotlin-stdlib, which Kotlin + // for Forge already provides); if YACL changes them, the dev run fails with a + // ClassNotFoundException naming the one that is missing. + for (library in yaclBundledLibraries) { + add("forgeRuntimeLibrary", library) + } + } +} + +modSettings { + // Each target gets its own run directory. Minecraft 1.21.1 and 26.2 cannot share a world or an + // options file, and a shared directory also mixes Fabric and NeoForge mod jars together. + // Stonecraft applies this in an afterEvaluate, so overriding loom.runConfigs directly here + // would be silently ignored. + runDirectory = rootProject.layout.projectDirectory.dir("run/${stonecutter.current.project}") + + // Exposed to fabric.mod.json / neoforge.mods.toml as ${...} + variableReplacements.put("yaclVersion", mod.prop("yacl_version")) + variableReplacements.put("fabricKotlinVersion", mod.prop("fabric_kotlin_version")) + variableReplacements.put("kffVersion", mod.prop("kff_version")) + variableReplacements.put("javaVersion", javaVersion.toString()) + variableReplacements.put("mcDepFabric", mod.prop("mc_dep_fabric")) + variableReplacements.put("mcDepNeoforge", mod.prop("mc_dep_neoforge")) +} + +tasks.withType().configureEach { + compilerOptions.jvmTarget = JvmTarget.fromTarget(javaVersion.toString()) + dependsOn("stonecutterGenerate") +} diff --git a/gradle.properties b/gradle.properties index cc8c03e..7d85525 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,23 +1,10 @@ # Done to increase the memory available to gradle. -org.gradle.jvmargs=-Xmx1G +org.gradle.jvmargs=-Xmx3G org.gradle.parallel=true -# Fabric Properties -# check these on https://fabricmc.net/develop -minecraft_version=1.21.11 -yarn_mappings=1.21.11+build.4 -loader_version=0.18.4 -fabric_kotlin_version=1.13.7+kotlin.2.2.21 -loom_version=1.15-SNAPSHOT - -# Dependencies -yet_another_config_lib_version=3.8.2+1.21.11-fabric -mod_menu_version=17.0.0-beta.2 - # Mod Properties -mod_version=1.2.0+mc1.21.11 -maven_group=openshock.integrations.minecraft -archives_base_name=shockcraft - -# Dependencies -fabric_version=0.141.3+1.21.11 \ No newline at end of file +mod.id=shockcraft +mod.name=ShockCraft +mod.version=1.3.0 +mod.group=openshock.integrations.minecraft +mod.description=OpenShock integration for Minecraft diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index f8e1ee3125fe0768e9a76ee977ac089eb657005e..eddabd2eef8d94a5437d6168ff9c87a78ff725b3 100644 GIT binary patch delta 39258 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%0D}_YK{Q?32MR^ESETLLJ>zQ@qcaOVwT<` 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 delta 37448 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 O00CYA5YPTOBmD&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,29 +65,18 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 75c4d72..0000000 --- a/settings.gradle +++ /dev/null @@ -1,10 +0,0 @@ -pluginManagement { - repositories { - maven { - name = 'Fabric' - url = 'https://maven.fabricmc.net/' - } - mavenCentral() - gradlePluginPortal() - } -} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..78b2216 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,43 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + maven("https://maven.kikugie.dev/releases") + maven("https://maven.fabricmc.net/") + maven("https://maven.architectury.dev") + maven("https://maven.minecraftforge.net") + maven("https://maven.neoforged.net/releases/") + } +} + +plugins { + id("gg.meza.stonecraft") version "1.12.6" + id("dev.kikugie.stonecutter") version "0.9.7" +} + +stonecutter { + centralScript = "build.gradle.kts" + kotlinController = true + + shared { + // Every entry here becomes one build target. Adding a Minecraft version is a one-line + // change; the matching versions/dependencies/.properties file supplies the + // dependency versions for it. + fun mc(version: String, vararg loaders: String) { + for (loader in loaders) version("$version-$loader", version) + } + + mc("1.21.1", "fabric", "neoforge") + mc("1.21.4", "fabric", "neoforge") + mc("1.21.8", "fabric", "neoforge") + mc("1.21.11", "fabric", "neoforge") + mc("26.1", "fabric", "neoforge") + mc("26.2", "fabric", "neoforge") + + vcsVersion = "26.2-fabric" + } + + create(rootProject) +} + +rootProject.name = "Integrations.Minecraft" diff --git a/src/main/kotlin/openshock/integrations/minecraft/ConfigGuiFactory.kt b/src/main/kotlin/openshock/integrations/minecraft/ConfigScreen.kt similarity index 72% rename from src/main/kotlin/openshock/integrations/minecraft/ConfigGuiFactory.kt rename to src/main/kotlin/openshock/integrations/minecraft/ConfigScreen.kt index 712e597..e63dac1 100644 --- a/src/main/kotlin/openshock/integrations/minecraft/ConfigGuiFactory.kt +++ b/src/main/kotlin/openshock/integrations/minecraft/ConfigScreen.kt @@ -1,19 +1,22 @@ package openshock.integrations.minecraft -import com.terraformersmc.modmenu.api.ConfigScreenFactory import dev.isxander.yacl3.api.* import dev.isxander.yacl3.api.controller.EnumControllerBuilder import dev.isxander.yacl3.api.controller.IntegerSliderControllerBuilder import dev.isxander.yacl3.api.controller.StringControllerBuilder import dev.isxander.yacl3.api.controller.TickBoxControllerBuilder -import net.minecraft.client.gui.screen.Screen -import net.minecraft.text.Text +import net.minecraft.client.gui.screens.Screen +import net.minecraft.network.chat.Component import openshock.integrations.minecraft.config.DamageShockMode import openshock.integrations.minecraft.config.ShockCraftConfig -object ConfigGuiFactory : ConfigScreenFactory { +/** + * Builds the YACL settings screen. Loader-agnostic: Fabric reaches it through Mod Menu and + * NeoForge through IConfigScreenFactory, both in [openshock.integrations.minecraft.platform]. + */ +object ConfigScreen { - override fun create(parent: Screen): Screen { + fun create(parent: Screen?): Screen { val yacl = YetAnotherConfigLib.create(ShockCraftConfig.HANDLER) { defaults: ShockCraftConfig, config: ShockCraftConfig, builder: YetAnotherConfigLib.Builder -> createBuilder( @@ -32,19 +35,19 @@ object ConfigGuiFactory : ConfigScreenFactory { builder: YetAnotherConfigLib.Builder ): YetAnotherConfigLib.Builder { return builder - .title(Text.literal("ShockCraft - OpenShock Minecraft Integration")) + .title(Component.literal("ShockCraft - OpenShock Minecraft Integration")) .category( ConfigCategory.createBuilder() - .name(Text.literal("Behaviour / Shock Settings")) + .name(Component.literal("Behaviour / Shock Settings")) .group(OptionGroup.createBuilder() - .name(Text.literal("General")) - .description(OptionDescription.of(Text.literal("General settings for the mod"))) + .name(Component.literal("General")) + .description(OptionDescription.of(Component.literal("General settings for the mod"))) .option(Option.createBuilder() - .name(Text.literal("Display Shocks in Action Bar")) - .description(OptionDescription.of(Text.literal("Displays Shocks or all kinds of commands in the action bar on your screen"))) + .name(Component.literal("Display Shocks in Action Bar")) + .description(OptionDescription.of(Component.literal("Displays Shocks or all kinds of commands in the action bar on your screen"))) .controller { TickBoxControllerBuilder.create(it) } .binding(defaults.displayShocksInActionBar, { config.displayShocksInActionBar }, { config.displayShocksInActionBar = it }) .build() @@ -52,21 +55,21 @@ object ConfigGuiFactory : ConfigScreenFactory { ) .group(OptionGroup.createBuilder() - .name(Text.literal("On Damage")) - .description(OptionDescription.of(Text.literal("Settings for shocking on damage"))) + .name(Component.literal("On Damage")) + .description(OptionDescription.of(Component.literal("Settings for shocking on damage"))) .option(Option.createBuilder() - .name(Text.literal("Enabled")) - .description(OptionDescription.of(Text.literal("Enable shocking on damage"))) + .name(Component.literal("Enabled")) + .description(OptionDescription.of(Component.literal("Enable shocking on damage"))) .controller { TickBoxControllerBuilder.create(it) } .binding(defaults.onDamage, { config.onDamage }, { config.onDamage = it }) .build() ) .option(Option.createBuilder() - .name(Text.literal("On Damage Action")) + .name(Component.literal("On Damage Action")) .description( OptionDescription.of( - Text.literal( + Component.literal( "Defines what happens when you receive damage.\n" + "Low Hp = You get shocked at higher intensity the less HP you have\n" + "Damage Amount = You get shocked the amount of damage you have received" @@ -80,7 +83,7 @@ object ConfigGuiFactory : ConfigScreenFactory { .build() ) .option(Option.createBuilder() - .name(Text.literal("Minimum Intensity")) + .name(Component.literal("Minimum Intensity")) .controller { option: Option -> IntegerSliderControllerBuilder.create(option) .range(1, 100) @@ -93,7 +96,7 @@ object ConfigGuiFactory : ConfigScreenFactory { .build() ) .option(Option.createBuilder() - .name(Text.literal("Maximum Intensity")) + .name(Component.literal("Maximum Intensity")) .controller { option: Option -> IntegerSliderControllerBuilder.create(option) .range(1, 100) @@ -107,8 +110,8 @@ object ConfigGuiFactory : ConfigScreenFactory { ) .option(Option.createBuilder() - .name(Text.literal("Damage Threshold")) - .description(OptionDescription.of(Text.literal("How much damage you need to take, or have until a shock is sent"))) + .name(Component.literal("Damage Threshold")) + .description(OptionDescription.of(Component.literal("How much damage you need to take, or have until a shock is sent"))) .controller { option: Option -> IntegerSliderControllerBuilder.create(option) .range(1, 20) @@ -122,12 +125,12 @@ object ConfigGuiFactory : ConfigScreenFactory { ) .option(Option.createBuilder() - .name(Text.literal("Cooldown")) - .description(OptionDescription.of(Text.literal("Cooldown between on damage shocks"))) + .name(Component.literal("Cooldown")) + .description(OptionDescription.of(Component.literal("Cooldown between on damage shocks"))) .controller { option -> IntegerSliderControllerBuilder.create(option) .range(300, 60_000) - .step(100).formatValue { Text.literal((it / 1000f).toString() + " seconds") } + .step(100).formatValue { Component.literal((it / 1000f).toString() + " seconds") } } .binding( defaults.cooldown.toInt(), @@ -140,18 +143,18 @@ object ConfigGuiFactory : ConfigScreenFactory { ) .group(OptionGroup.createBuilder() - .name(Text.literal("On Death")) - .description(OptionDescription.of(Text.literal("Defines what happens when you die"))) + .name(Component.literal("On Death")) + .description(OptionDescription.of(Component.literal("Defines what happens when you die"))) .option(Option.createBuilder() - .name(Text.literal("Enabled")) - .description(OptionDescription.of(Text.literal("Enable shocking on death"))) + .name(Component.literal("Enabled")) + .description(OptionDescription.of(Component.literal("Enable shocking on death"))) .controller { TickBoxControllerBuilder.create(it) } .binding(defaults.onDeath, { config.onDeath }, { config.onDeath = it }) .build() ) .option(Option.createBuilder() - .name(Text.literal("Intensity")) + .name(Component.literal("Intensity")) .controller { option -> IntegerSliderControllerBuilder.create(option) .range(1, 100) @@ -164,11 +167,11 @@ object ConfigGuiFactory : ConfigScreenFactory { .build() ) .option(Option.createBuilder() - .name(Text.literal("Duration")) + .name(Component.literal("Duration")) .controller { option -> IntegerSliderControllerBuilder.create(option) .range(300, 30_000) - .step(100).formatValue { Text.literal((it / 1000f).toString() + " seconds") } + .step(100).formatValue { Component.literal((it / 1000f).toString() + " seconds") } } .binding( defaults.onDeathDuration.toInt(), @@ -181,18 +184,18 @@ object ConfigGuiFactory : ConfigScreenFactory { ) .group(OptionGroup.createBuilder() - .name(Text.literal("On Level Up")) - .description(OptionDescription.of(Text.literal("Defines what happens when you gain an XP level"))) + .name(Component.literal("On Level Up")) + .description(OptionDescription.of(Component.literal("Defines what happens when you gain an XP level"))) .option(Option.createBuilder() - .name(Text.literal("Enabled")) - .description(OptionDescription.of(Text.literal("Enable shocking on level up"))) + .name(Component.literal("Enabled")) + .description(OptionDescription.of(Component.literal("Enable shocking on level up"))) .controller { TickBoxControllerBuilder.create(it) } .binding(defaults.onLevelUp, { config.onLevelUp }, { config.onLevelUp = it }) .build() ) .option(Option.createBuilder() - .name(Text.literal("Intensity")) + .name(Component.literal("Intensity")) .controller { option -> IntegerSliderControllerBuilder.create(option) .range(1, 100) @@ -205,11 +208,11 @@ object ConfigGuiFactory : ConfigScreenFactory { .build() ) .option(Option.createBuilder() - .name(Text.literal("Duration")) + .name(Component.literal("Duration")) .controller { option -> IntegerSliderControllerBuilder.create(option) .range(300, 30_000) - .step(100).formatValue { Text.literal((it / 1000f).toString() + " seconds") } + .step(100).formatValue { Component.literal((it / 1000f).toString() + " seconds") } } .binding( defaults.onLevelUpDuration.toInt(), @@ -222,25 +225,25 @@ object ConfigGuiFactory : ConfigScreenFactory { ) .group(OptionGroup.createBuilder() - .name(Text.literal("On Chat Message")) - .description(OptionDescription.of(Text.literal("Defines what happens when a specific chat phrase is sent or received"))) + .name(Component.literal("On Chat Message")) + .description(OptionDescription.of(Component.literal("Defines what happens when a specific chat phrase is sent or received"))) .option(Option.createBuilder() - .name(Text.literal("Enable for Chat Messages")) - .description(OptionDescription.of(Text.literal("Enable shocking when a message with the key phrase is sent/received"))) + .name(Component.literal("Enable for Chat Messages")) + .description(OptionDescription.of(Component.literal("Enable shocking when a message with the key phrase is sent/received"))) .controller { TickBoxControllerBuilder.create(it) } .binding(defaults.onChatEvent, { config.onChatEvent }, { config.onChatEvent = it }) .build() ) .option(Option.createBuilder() - .name(Text.literal("Key Phrase")) - .description(OptionDescription.of(Text.literal("The phrase to trigger the shock"))) + .name(Component.literal("Key Phrase")) + .description(OptionDescription.of(Component.literal("The phrase to trigger the shock"))) .controller { StringControllerBuilder.create(it) } .binding(defaults.chatMessagePhrase, { config.chatMessagePhrase }, { config.chatMessagePhrase = it }) .build() ) .option(Option.createBuilder() - .name(Text.literal("Intensity")) + .name(Component.literal("Intensity")) .controller { option -> IntegerSliderControllerBuilder.create(option) .range(1, 100) @@ -253,11 +256,11 @@ object ConfigGuiFactory : ConfigScreenFactory { .build() ) .option(Option.createBuilder() - .name(Text.literal("Duration")) + .name(Component.literal("Duration")) .controller { option -> IntegerSliderControllerBuilder.create(option) .range(300, 30_000) - .step(100).formatValue { Text.literal((it / 1000f).toString() + " seconds") } + .step(100).formatValue { Component.literal((it / 1000f).toString() + " seconds") } } .binding( defaults.onChatMessageDuration.toInt(), @@ -274,16 +277,16 @@ object ConfigGuiFactory : ConfigScreenFactory { .category( ConfigCategory.createBuilder() - .name(Text.literal("Setup")) + .name(Component.literal("Setup")) // Server group .group(OptionGroup.createBuilder() - .name(Text.literal("Server")) - .description(OptionDescription.of(Text.literal("Server / OpenShock Backend Settings and Shocker Setup"))) + .name(Component.literal("Server")) + .description(OptionDescription.of(Component.literal("Server / OpenShock Backend Settings and Shocker Setup"))) .option( Option.createBuilder() - .name(Text.literal("API URL")) - .description(OptionDescription.of(Text.literal("The API base URL of the OpenShock Backend. For the official instance this is https://api.openshock.app"))) + .name(Component.literal("API URL")) + .description(OptionDescription.of(Component.literal("The API base URL of the OpenShock Backend. For the official instance this is https://api.openshock.app"))) .controller { option: Option? -> StringControllerBuilder.create(option) } .binding( defaults.apiBaseUrl, @@ -293,8 +296,8 @@ object ConfigGuiFactory : ConfigScreenFactory { ) .option( Option.createBuilder() - .name(Text.literal("API Token")) - .description(OptionDescription.of(Text.literal("API Token generated on the web, needs shocker use permission"))) + .name(Component.literal("API Token")) + .description(OptionDescription.of(Component.literal("API Token generated on the web, needs shocker use permission"))) .controller { option: Option -> StringControllerBuilder.create(option) } .binding( defaults.apiToken, @@ -307,7 +310,7 @@ object ConfigGuiFactory : ConfigScreenFactory { // Shocker group .group(ListOption.createBuilder() - .name(Text.literal("Shockers")) + .name(Component.literal("Shockers")) .controller { option: Option -> StringControllerBuilder.create(option) } .binding( defaults.shockers, diff --git a/src/main/kotlin/openshock/integrations/minecraft/ModMenuEntryPoint.kt b/src/main/kotlin/openshock/integrations/minecraft/ModMenuEntryPoint.kt deleted file mode 100644 index ae44ece..0000000 --- a/src/main/kotlin/openshock/integrations/minecraft/ModMenuEntryPoint.kt +++ /dev/null @@ -1,10 +0,0 @@ -package openshock.integrations.minecraft - -import com.terraformersmc.modmenu.api.ConfigScreenFactory -import com.terraformersmc.modmenu.api.ModMenuApi - -class ModMenuEntryPoint : ModMenuApi { - override fun getModConfigScreenFactory(): ConfigScreenFactory<*> { - return ConfigGuiFactory - } -} \ No newline at end of file diff --git a/src/main/kotlin/openshock/integrations/minecraft/ShockCraft.kt b/src/main/kotlin/openshock/integrations/minecraft/ShockCraft.kt index 399b5cf..e56f5b7 100644 --- a/src/main/kotlin/openshock/integrations/minecraft/ShockCraft.kt +++ b/src/main/kotlin/openshock/integrations/minecraft/ShockCraft.kt @@ -3,39 +3,33 @@ package openshock.integrations.minecraft import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import net.fabricmc.api.ClientModInitializer -import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents -import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents.EndTick -import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents -import net.minecraft.client.MinecraftClient -import net.minecraft.client.gui.screen.GameMenuScreen -import net.minecraft.client.network.ClientPlayerEntity -import net.minecraft.entity.damage.DamageSource +import net.minecraft.client.Minecraft +import net.minecraft.client.gui.screens.PauseScreen +import net.minecraft.client.player.LocalPlayer +import net.minecraft.world.damagesource.DamageSource import openshock.integrations.minecraft.api.ControlType import openshock.integrations.minecraft.api.OpenShockApi import openshock.integrations.minecraft.config.DamageShockMode import openshock.integrations.minecraft.config.ShockCraftConfig +import openshock.integrations.minecraft.platform.McCompat import openshock.integrations.minecraft.utils.MathUtils import org.slf4j.Logger import org.slf4j.LoggerFactory import java.util.* -object ShockCraft : ClientModInitializer { - val logger: Logger = LoggerFactory.getLogger("shockcraft") +/** + * Loader-agnostic core of the mod. Everything here compiles against plain Minecraft classes only, + * so it is shared verbatim by every Minecraft version and both loaders. The Fabric and NeoForge + * entrypoints in [openshock.integrations.minecraft.platform] are the only things that differ. + */ +object ShockCraft { + const val MOD_ID: String = "shockcraft" - override fun onInitializeClient() { - logger.info("Hello Fabric world!") + val logger: Logger = LoggerFactory.getLogger(MOD_ID) + fun init() { + logger.info("ShockCraft starting up") ShockCraftConfig.HANDLER.load() - - ClientTickEvents.END_CLIENT_TICK.register(EndTick { clientTickLoopFun() }) - - ClientReceiveMessageEvents.CHAT.register { message, _, _, _, _ -> - val config = ShockCraftConfig.HANDLER.instance() - if (config.onChatEvent) { - onChatMessage(message.string) - } - } } var lastTickHealth: Float = 20f @@ -45,11 +39,11 @@ object ShockCraft : ClientModInitializer { var lastTickXpLevel: Int = 0 val DamageSource?.attackerName: String - get() = this?.attacker?.stringifiedName ?: "Unknown" + get() = this?.entity?.name?.string ?: "Unknown" private fun reset() { lastTickReset = true - val player = MinecraftClient.getInstance().player + val player = Minecraft.getInstance().player if (player == null) { lastTickHealth = 20f @@ -62,12 +56,12 @@ object ShockCraft : ClientModInitializer { } @OptIn(DelicateCoroutinesApi::class) - private fun clientTickLoopFun() { - val currentScreen = MinecraftClient.getInstance().currentScreen + fun onClientTick() { + val currentScreen = McCompat.currentScreen // Cursed if logic to see if pause menu was opened, might not work with all mods if (currentScreen != null) { - if (!pauseMenuOpen && currentScreen is GameMenuScreen) { + if (!pauseMenuOpen && currentScreen is PauseScreen) { pauseMenuOpen = true logger.debug("Game menu opened") } @@ -82,7 +76,7 @@ object ShockCraft : ClientModInitializer { return } - val player = MinecraftClient.getInstance().player + val player = Minecraft.getInstance().player // Player does not exist, reset and return if (player == null) { @@ -114,9 +108,9 @@ object ShockCraft : ClientModInitializer { // Did we take damage? if (damageSinceLastTick > 0) { - logger.debug(player.recentDamageSource?.name + " - " + damageSinceLastTick.toString()) + logger.debug(player.lastDamageSource?.msgId + " - " + damageSinceLastTick.toString()) - if (player.isDead) { + if (player.isDeadOrDying) { logger.debug("Player died") GlobalScope.launch { onDeath(player) @@ -148,8 +142,9 @@ object ShockCraft : ClientModInitializer { } @OptIn(DelicateCoroutinesApi::class) - private fun onChatMessage(message: String) { + fun onChatMessage(message: String) { val config = ShockCraftConfig.HANDLER.instance() + if (!config.onChatEvent) return if (config.chatMessagePhrase.isBlank()) return if (message.contains(config.chatMessagePhrase, ignoreCase = true)) { @@ -165,7 +160,7 @@ object ShockCraft : ClientModInitializer { } } - private suspend fun onDeath(player: ClientPlayerEntity) { + private suspend fun onDeath(player: LocalPlayer) { val config = ShockCraftConfig.HANDLER.instance() if (!config.onDeath) return @@ -173,13 +168,13 @@ object ShockCraft : ClientModInitializer { ControlType.Shock, config.onDeathIntensity, config.onDeathDuration, - player.recentDamageSource.attackerName, + player.lastDamageSource.attackerName, ) } private var lastShock: Long = -1 - private suspend fun onDamage(player: ClientPlayerEntity, damage: Float) { + private suspend fun onDamage(player: LocalPlayer, damage: Float) { val config = ShockCraftConfig.HANDLER.instance() if (!config.onDamage) return @@ -224,7 +219,7 @@ object ShockCraft : ClientModInitializer { ControlType.Shock, intensity, duration, - player.recentDamageSource.attackerName, + player.lastDamageSource.attackerName, ) } } diff --git a/src/main/kotlin/openshock/integrations/minecraft/api/OpenShockApi.kt b/src/main/kotlin/openshock/integrations/minecraft/api/OpenShockApi.kt index 88664d4..a3e2587 100644 --- a/src/main/kotlin/openshock/integrations/minecraft/api/OpenShockApi.kt +++ b/src/main/kotlin/openshock/integrations/minecraft/api/OpenShockApi.kt @@ -1,59 +1,64 @@ package openshock.integrations.minecraft.api import com.google.gson.Gson -import net.minecraft.client.MinecraftClient -import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.MediaType -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody -import okhttp3.RequestBody.Companion.toRequestBody +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import net.minecraft.client.Minecraft +import net.minecraft.network.chat.Component import openshock.integrations.minecraft.config.ShockCraftConfig -import openshock.integrations.minecraft.utils.await +import openshock.integrations.minecraft.platform.McCompat import org.slf4j.LoggerFactory - +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse +import java.time.Duration object OpenShockApi { private val logger = LoggerFactory.getLogger("OpenShockApi") - + private const val SUFFIX: String = " (Integrations.Minecraft)" - private val JSON: MediaType = "application/json".toMediaType() - private val client: OkHttpClient = OkHttpClient() + // The JDK client keeps us free of a shaded HTTP library, which would otherwise have to be + // bundled differently for each loader. + private val client: HttpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build() suspend fun control(type: ControlType, intensity: Byte, duration: UShort, name: String) { logger.info("Sending $type with $intensity intensity for $duration ms [$name]") - val shocks = ArrayList() + val config = ShockCraftConfig.HANDLER.instance() - ShockCraftConfig.HANDLER.instance().shockers.forEach { - shocks.add(ControlItem(it, type, intensity, duration)) - } + val shocks = config.shockers.map { ControlItem(it, type, intensity, duration) } val requestObject = ControlRequest(shocks, name + SUFFIX) val json = Gson().toJson(requestObject) - val url = ShockCraftConfig.HANDLER.instance().apiBaseUrl.toHttpUrl() - val concatUrl = url.resolve("/2/shockers/control") + val url = URI.create(config.apiBaseUrl.trimEnd('/') + "/2/shockers/control") - val body: RequestBody = json.toRequestBody(JSON) - val request: Request = Request.Builder() - .url(concatUrl!!) - .header("OpenShockToken", ShockCraftConfig.HANDLER.instance().apiToken) - .header("User-Agent", "Integrations.Minecraft/1.0.0 (Minecraft ${MinecraftClient.getInstance().gameVersion}; Java ${System.getProperty("java.version")})") - .post(body) + val request = HttpRequest.newBuilder(url) + .header("Content-Type", "application/json") + .header("OpenShockToken", config.apiToken) + .header( + "User-Agent", + "Integrations.Minecraft/1.0.0 (Minecraft ${Minecraft.getInstance().launchedVersion}; Java ${System.getProperty("java.version")})" + ) + .POST(HttpRequest.BodyPublishers.ofString(json)) .build() - val response = client.newCall(request).await() + val response = withContext(Dispatchers.IO) { + client.send(request, HttpResponse.BodyHandlers.ofString()) + } - logger.debug(response.body!!.string()) + logger.debug(response.body()) val inSeconds = (duration.toFloat() / 1000f) - val config = ShockCraftConfig.HANDLER.instance() - if(!config.displayShocksInActionBar) return - - MinecraftClient.getInstance().player?.sendMessage(net.minecraft.text.Text.literal("$type at $intensity% for ${String.format("%.1f", inSeconds)}s [$name]"), true) + if (!config.displayShocksInActionBar) return + + McCompat.sendActionBar( + Component.literal("$type at $intensity% for ${String.format("%.1f", inSeconds)}s [$name]") + ) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/openshock/integrations/minecraft/config/ShockCraftConfig.kt b/src/main/kotlin/openshock/integrations/minecraft/config/ShockCraftConfig.kt index 60f0e94..72e4fbf 100644 --- a/src/main/kotlin/openshock/integrations/minecraft/config/ShockCraftConfig.kt +++ b/src/main/kotlin/openshock/integrations/minecraft/config/ShockCraftConfig.kt @@ -4,8 +4,8 @@ import com.google.gson.GsonBuilder import dev.isxander.yacl3.config.v2.api.ConfigClassHandler import dev.isxander.yacl3.config.v2.api.SerialEntry import dev.isxander.yacl3.config.v2.api.serializer.GsonConfigSerializerBuilder -import net.fabricmc.loader.api.FabricLoader -import net.minecraft.util.Identifier +import openshock.integrations.minecraft.platform.McCompat +import openshock.integrations.minecraft.platform.Platform class ShockCraftConfig { @@ -95,10 +95,10 @@ class ShockCraftConfig { companion object { var HANDLER: ConfigClassHandler = ConfigClassHandler.createBuilder(ShockCraftConfig::class.java) - .id(Identifier.of("shockcraft", "config")) + .id(McCompat.identifier("shockcraft", "config")) .serializer { config: ConfigClassHandler? -> GsonConfigSerializerBuilder.create(config) - .setPath(FabricLoader.getInstance().configDir.resolve("ShockCraft.json5")) + .setPath(Platform.configDir.resolve("ShockCraft.json5")) .appendGsonBuilder(GsonBuilder::setPrettyPrinting) // not needed, pretty print by default .setJson5(true) .build() diff --git a/src/main/kotlin/openshock/integrations/minecraft/platform/Entrypoint.kt b/src/main/kotlin/openshock/integrations/minecraft/platform/Entrypoint.kt new file mode 100644 index 0000000..9a992a2 --- /dev/null +++ b/src/main/kotlin/openshock/integrations/minecraft/platform/Entrypoint.kt @@ -0,0 +1,64 @@ +package openshock.integrations.minecraft.platform + +//? if fabric { +import com.terraformersmc.modmenu.api.ConfigScreenFactory +import com.terraformersmc.modmenu.api.ModMenuApi +import net.fabricmc.api.ClientModInitializer +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents +import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents +import net.minecraft.client.gui.screens.Screen +import openshock.integrations.minecraft.ConfigScreen +import openshock.integrations.minecraft.ShockCraft + +class FabricEntrypoint : ClientModInitializer { + override fun onInitializeClient() { + ShockCraft.init() + + ClientTickEvents.END_CLIENT_TICK.register( + ClientTickEvents.EndTick { ShockCraft.onClientTick() } + ) + + ClientReceiveMessageEvents.CHAT.register( + ClientReceiveMessageEvents.Chat { message, _, _, _, _ -> + ShockCraft.onChatMessage(message.string) + } + ) + } +} + +/** Adds the config button to Mod Menu's mod list. NeoForge does this through IConfigScreenFactory. */ +class ModMenuEntrypoint : ModMenuApi { + override fun getModConfigScreenFactory(): ConfigScreenFactory<*> = + ConfigScreenFactory { parent -> ConfigScreen.create(parent) } +} +//?} elif neoforge { +/*import net.neoforged.api.distmarker.Dist +import net.neoforged.fml.ModLoadingContext +import net.neoforged.fml.common.Mod +import net.neoforged.neoforge.client.event.ClientChatReceivedEvent +import net.neoforged.neoforge.client.event.ClientTickEvent +import net.neoforged.neoforge.client.gui.IConfigScreenFactory +import net.neoforged.neoforge.common.NeoForge +import openshock.integrations.minecraft.ConfigScreen +import openshock.integrations.minecraft.ShockCraft + +// Kotlin for Forge ("kotlinforforge" modLoader in neoforge.mods.toml) expects an object declaration. +@Mod(value = ShockCraft.MOD_ID, dist = [Dist.CLIENT]) +object NeoForgeEntrypoint { + init { + ShockCraft.init() + + ModLoadingContext.get().registerExtensionPoint(IConfigScreenFactory::class.java) { + IConfigScreenFactory { _, parent -> ConfigScreen.create(parent) } + } + + NeoForge.EVENT_BUS.addListener(ClientTickEvent.Post::class.java) { + ShockCraft.onClientTick() + } + + NeoForge.EVENT_BUS.addListener(ClientChatReceivedEvent::class.java) { event -> + ShockCraft.onChatMessage(event.message.string) + } + } +} +*///?} diff --git a/src/main/kotlin/openshock/integrations/minecraft/platform/McCompat.kt b/src/main/kotlin/openshock/integrations/minecraft/platform/McCompat.kt new file mode 100644 index 0000000..a2827fd --- /dev/null +++ b/src/main/kotlin/openshock/integrations/minecraft/platform/McCompat.kt @@ -0,0 +1,43 @@ +package openshock.integrations.minecraft.platform + +import net.minecraft.client.Minecraft +import net.minecraft.client.gui.screens.Screen +import net.minecraft.network.chat.Component + +//? if >=1.21.11 { +import net.minecraft.resources.Identifier as ModIdentifier +//?} else { +/*import net.minecraft.resources.ResourceLocation as ModIdentifier +*///?} + +/** + * The only place where vanilla's own API drifts across the supported Minecraft versions. + * Everything else in the mod compiles unchanged from 1.21.1 all the way to 26.2, so when a new + * Minecraft version breaks something, this file is the first (and usually only) thing to touch. + */ +object McCompat { + + /** `Minecraft.screen` moved onto `Gui` in 26.2. */ + val currentScreen: Screen? + get() { + //? if >=26.2 { + return Minecraft.getInstance().gui.screen() + //?} else { + /*return Minecraft.getInstance().screen + *///?} + } + + /** `displayClientMessage(text, true)` was split out into `sendOverlayMessage(text)` in 26.1. */ + fun sendActionBar(text: Component) { + val player = Minecraft.getInstance().player ?: return + //? if >=26.1 { + player.sendOverlayMessage(text) + //?} else { + /*player.displayClientMessage(text, true) + *///?} + } + + /** `ResourceLocation` was renamed to `Identifier` in 1.21.11. */ + fun identifier(namespace: String, path: String): ModIdentifier = + ModIdentifier.fromNamespaceAndPath(namespace, path) +} diff --git a/src/main/kotlin/openshock/integrations/minecraft/platform/Platform.kt b/src/main/kotlin/openshock/integrations/minecraft/platform/Platform.kt new file mode 100644 index 0000000..d8ec2fd --- /dev/null +++ b/src/main/kotlin/openshock/integrations/minecraft/platform/Platform.kt @@ -0,0 +1,19 @@ +package openshock.integrations.minecraft.platform + +import java.nio.file.Path + +//? if fabric { +import net.fabricmc.loader.api.FabricLoader + +/** The handful of things the two loaders disagree on outside of the entrypoints. */ +object Platform { + val configDir: Path get() = FabricLoader.getInstance().configDir +} +//?} elif neoforge { +/*import net.neoforged.fml.loading.FMLPaths + +/** The handful of things the two loaders disagree on outside of the entrypoints. */ +object Platform { + val configDir: Path get() = FMLPaths.CONFIGDIR.get() +} +*///?} diff --git a/src/main/kotlin/openshock/integrations/minecraft/utils/CallAwait.kt b/src/main/kotlin/openshock/integrations/minecraft/utils/CallAwait.kt deleted file mode 100644 index 72e396e..0000000 --- a/src/main/kotlin/openshock/integrations/minecraft/utils/CallAwait.kt +++ /dev/null @@ -1,56 +0,0 @@ -package openshock.integrations.minecraft.utils - -import kotlinx.coroutines.suspendCancellableCoroutine -import okhttp3.Call -import okhttp3.Callback -import okhttp3.Response -import java.io.IOException -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException - -/** - * Suspend extension that allows to suspend [Call] inside coroutine. - * - * [recordStack] enables track recording, so in case of exception stacktrace will contain call stacktrace, may be useful for debugging - * Not free! Creates exception on each request so disabled by default, but may be enabled using system properties: - * - * ``` - * System.setProperty(OKHTTP_STACK_RECORDER_PROPERTY, OKHTTP_STACK_RECORDER_ON) - * ``` - * see [README.md](https://github.com/gildor/kotlin-coroutines-okhttp/blob/master/README.md#Debugging) with details about debugging using this feature - * - * @return Result of request or throw exception - */ -public suspend fun Call.await(recordStack: Boolean = isRecordStack): Response { - val callStack = if (recordStack) { - IOException().apply { - // Remove unnecessary lines from stacktrace - // This doesn't remove await$default, but better than nothing - stackTrace = stackTrace.copyOfRange(1, stackTrace.size) - } - } else { - null - } - return suspendCancellableCoroutine { continuation -> - enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { - continuation.resume(response) - } - - override fun onFailure(call: Call, e: IOException) { - // Don't bother with resuming the continuation if it is already cancelled. - if (continuation.isCancelled) return - callStack?.initCause(e) - continuation.resumeWithException(callStack ?: e) - } - }) - - continuation.invokeOnCancellation { - try { - cancel() - } catch (ex: Throwable) { - //Ignore cancel exception - } - } - } -} diff --git a/src/main/kotlin/openshock/integrations/minecraft/utils/CallStackRecorder.kt b/src/main/kotlin/openshock/integrations/minecraft/utils/CallStackRecorder.kt deleted file mode 100644 index efde628..0000000 --- a/src/main/kotlin/openshock/integrations/minecraft/utils/CallStackRecorder.kt +++ /dev/null @@ -1,23 +0,0 @@ -package openshock.integrations.minecraft.utils - - -public const val OKHTTP_STACK_RECORDER_PROPERTY = "ru.gildor.coroutines.okhttp.stackrecorder" - -/** - * Debug turned on value for [DEBUG_PROPERTY_NAME]. See [newCoroutineContext][CoroutineScope.newCoroutineContext]. - */ -public const val OKHTTP_STACK_RECORDER_ON = "on" - -/** - * Debug turned on value for [DEBUG_PROPERTY_NAME]. See [newCoroutineContext][CoroutineScope.newCoroutineContext]. - */ -public const val OKHTTP_STACK_RECORDER_OFF = "off" - -@JvmField -val isRecordStack = when (System.getProperty(OKHTTP_STACK_RECORDER_PROPERTY)) { - OKHTTP_STACK_RECORDER_ON -> true - OKHTTP_STACK_RECORDER_OFF, null, "" -> false - else -> error("System property '$OKHTTP_STACK_RECORDER_PROPERTY' has unrecognized value '${System.getProperty( - OKHTTP_STACK_RECORDER_PROPERTY - )}'") -} diff --git a/src/main/resources/META-INF/neoforge.mods.toml b/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..f431066 --- /dev/null +++ b/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,43 @@ +# Kotlin for Forge provides the Kotlin runtime and the object-declaration @Mod entrypoint. +modLoader = "kotlinforforge" +loaderVersion = "[5.0,)" +license = "GPL-3.0" +issueTrackerURL = "https://github.com/OpenShock/Integrations.Minecraft/issues" + +[[mods]] +modId = "${id}" +version = "${version}" +displayName = "${name}" +description = '''${description}''' +authors = "Luc, OpenShock Team" +logoFile = "assets/${id}/icon.png" +logoBlur = false +displayURL = "https://openshock.org/" + +[[dependencies.${id}]] +modId = "minecraft" +type = "required" +versionRange = "${mcDepNeoforge}" +ordering = "NONE" +side = "CLIENT" + +[[dependencies.${id}]] +modId = "neoforge" +type = "required" +versionRange = "[${neoforgeVersion},)" +ordering = "NONE" +side = "CLIENT" + +[[dependencies.${id}]] +modId = "kotlinforforge" +type = "required" +versionRange = "[${kffVersion},)" +ordering = "NONE" +side = "CLIENT" + +[[dependencies.${id}]] +modId = "yet_another_config_lib_v3" +type = "required" +versionRange = "[0,)" +ordering = "NONE" +side = "CLIENT" diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index f3965ef..af827b0 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -1,9 +1,9 @@ { "schemaVersion": 1, - "id": "shockcraft", + "id": "${id}", "version": "${version}", - "name": "ShockCraft", - "description": "OpenShock integration for Minecraft", + "name": "${name}", + "description": "${description}", "authors": [ "Luc", "OpenShock Team" @@ -14,28 +14,28 @@ "sources": "https://github.com/OpenShock/Integrations.Minecraft" }, "license": "GPL-3.0", - "icon": "assets/shockcraft/icon.png", + "icon": "assets/${id}/icon.png", "environment": "client", "entrypoints": { "client": [ { - "value": "openshock.integrations.minecraft.ShockCraft", + "value": "${group}.platform.FabricEntrypoint", "adapter": "kotlin" } ], "modmenu": [ - "openshock.integrations.minecraft.ModMenuEntryPoint" + { + "value": "${group}.platform.ModMenuEntrypoint", + "adapter": "kotlin" + } ] }, - "mixins": [ - "shockcraft.mixins.json" - ], "depends": { - "fabricloader": ">=0.18.0", - "minecraft": "~1.21.10", - "java": ">=17", + "minecraft": "${mcDepFabric}", + "java": ">=${javaVersion}", + "fabricloader": "*", "fabric-api": "*", - "fabric-language-kotlin": ">=1.13.7+kotlin.2.2.21", + "fabric-language-kotlin": ">=${fabricKotlinVersion}", "yet_another_config_lib_v3": "*", "modmenu": "*" }, @@ -46,4 +46,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/main/resources/shockcraft.mixins.json b/src/main/resources/shockcraft.mixins.json deleted file mode 100644 index 9971696..0000000 --- a/src/main/resources/shockcraft.mixins.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "required": true, - "package": "openshock.integrations.minecraft.mixin", - "compatibilityLevel": "JAVA_17", - "mixins": [ - - ], - "injectors": { - "defaultRequire": 1 - } -} \ No newline at end of file diff --git a/stonecutter.gradle.kts b/stonecutter.gradle.kts new file mode 100644 index 0000000..7eec5f1 --- /dev/null +++ b/stonecutter.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("dev.kikugie.stonecutter") + id("gg.meza.stonecraft") + + // Declared here so the central build.gradle.kts can apply it without repeating the version. + // 2.4.0 is the lowest Kotlin bundled by our runtime providers (Kotlin for Forge 5.12/6.3 ship + // 2.4.0, fabric-language-kotlin 1.13.13 ships 2.4.10), so compiling against it is safe on both. + kotlin("jvm") version "2.4.0" apply false +} + +stonecutter active "26.2-fabric" /* [SC] DO NOT EDIT */ diff --git a/versions/dependencies/1.21.1.properties b/versions/dependencies/1.21.1.properties new file mode 100644 index 0000000..568e163 --- /dev/null +++ b/versions/dependencies/1.21.1.properties @@ -0,0 +1,14 @@ +# Dependency versions for Minecraft 1.21.1 (shared by its Fabric and NeoForge targets). +loader_version=0.19.3 +fabric_version=0.116.15+1.21.1 +neoforge_version=21.1.248 + +fabric_kotlin_version=1.13.13+kotlin.2.4.10 +kff_version=5.12.0 + +yacl_version=3.8.2+1.21.1 +modmenu_version=11.0.4 + +# Minecraft version ranges published in the mod metadata +mc_dep_fabric=>=1.21 <1.21.2 +mc_dep_neoforge=[1.21,1.21.2) diff --git a/versions/dependencies/1.21.11.properties b/versions/dependencies/1.21.11.properties new file mode 100644 index 0000000..a1fb49b --- /dev/null +++ b/versions/dependencies/1.21.11.properties @@ -0,0 +1,14 @@ +# Dependency versions for Minecraft 1.21.11 (shared by its Fabric and NeoForge targets). +loader_version=0.19.3 +fabric_version=0.141.6+1.21.11 +neoforge_version=21.11.45 + +fabric_kotlin_version=1.13.13+kotlin.2.4.10 +kff_version=6.3.0 + +yacl_version=3.8.2+1.21.11 +modmenu_version=17.0.1-beta.1 + +# Minecraft version ranges published in the mod metadata +mc_dep_fabric=>=1.21.9 <26 +mc_dep_neoforge=[1.21.9,26) diff --git a/versions/dependencies/1.21.4.properties b/versions/dependencies/1.21.4.properties new file mode 100644 index 0000000..d2e6371 --- /dev/null +++ b/versions/dependencies/1.21.4.properties @@ -0,0 +1,14 @@ +# Dependency versions for Minecraft 1.21.4 (shared by its Fabric and NeoForge targets). +loader_version=0.19.3 +fabric_version=0.119.4+1.21.4 +neoforge_version=21.4.157 + +fabric_kotlin_version=1.13.13+kotlin.2.4.10 +kff_version=5.12.0 + +yacl_version=3.8.2+1.21.4 +modmenu_version=13.0.4 + +# Minecraft version ranges published in the mod metadata +mc_dep_fabric=>=1.21.4 <1.21.5 +mc_dep_neoforge=[1.21.4,1.21.5) diff --git a/versions/dependencies/1.21.8.properties b/versions/dependencies/1.21.8.properties new file mode 100644 index 0000000..1a9fede --- /dev/null +++ b/versions/dependencies/1.21.8.properties @@ -0,0 +1,15 @@ +# Dependency versions for Minecraft 1.21.8 (shared by its Fabric and NeoForge targets). +loader_version=0.19.3 +fabric_version=0.136.1+1.21.8 +neoforge_version=21.8.54 + +fabric_kotlin_version=1.13.13+kotlin.2.4.10 +kff_version=5.12.0 + +# YACL has no dedicated 1.21.7/1.21.8 build; the 1.21.6 one covers the whole family. +yacl_version=3.8.2+1.21.6 +modmenu_version=15.0.2 + +# Minecraft version ranges published in the mod metadata +mc_dep_fabric=>=1.21.6 <1.21.9 +mc_dep_neoforge=[1.21.6,1.21.9) diff --git a/versions/dependencies/26.1.properties b/versions/dependencies/26.1.properties new file mode 100644 index 0000000..f8d73d7 --- /dev/null +++ b/versions/dependencies/26.1.properties @@ -0,0 +1,16 @@ +# Dependency versions for Minecraft 26.1 (shared by its Fabric and NeoForge targets). +minecraft_version=26.1.2 + +loader_version=0.19.3 +fabric_version=0.155.2+26.1.2 +neoforge_version=26.1.2.98 + +fabric_kotlin_version=1.13.13+kotlin.2.4.10 +kff_version=6.3.0 + +yacl_version=3.9.6+26.1 +modmenu_version=18.0.0 + +# Minecraft version ranges published in the mod metadata +mc_dep_fabric=>=26.1 <26.2 +mc_dep_neoforge=[26.1,26.2) diff --git a/versions/dependencies/26.2.properties b/versions/dependencies/26.2.properties new file mode 100644 index 0000000..da2ebd3 --- /dev/null +++ b/versions/dependencies/26.2.properties @@ -0,0 +1,14 @@ +# Dependency versions for Minecraft 26.2 (shared by its Fabric and NeoForge targets). +loader_version=0.19.3 +fabric_version=0.158.0+26.2 +neoforge_version=26.2.0.69 + +fabric_kotlin_version=1.13.13+kotlin.2.4.10 +kff_version=6.3.0 + +yacl_version=3.9.6+26.2 +modmenu_version=20.0.1 + +# Minecraft version ranges published in the mod metadata +mc_dep_fabric=>=26.2 <26.3 +mc_dep_neoforge=[26.2,26.3) From b8d3b056fd591317cd8d9000db93826ee0de9cf3 Mon Sep 17 00:00:00 2001 From: LucHeart Date: Wed, 26 Aug 2026 22:48:39 +0200 Subject: [PATCH 2/6] fix 26.2 loader warning --- .../openshock/integrations/minecraft/ShockCraft.kt | 14 +++++++++++++- src/main/resources/META-INF/neoforge.mods.toml | 9 +++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/openshock/integrations/minecraft/ShockCraft.kt b/src/main/kotlin/openshock/integrations/minecraft/ShockCraft.kt index e56f5b7..4619d3d 100644 --- a/src/main/kotlin/openshock/integrations/minecraft/ShockCraft.kt +++ b/src/main/kotlin/openshock/integrations/minecraft/ShockCraft.kt @@ -38,8 +38,20 @@ object ShockCraft { var lastTickXpLevel: Int = 0 + /** + * A human-readable label for what hurt us, used as the OpenShock control name. + * + * The server only sends a causing/direct entity when something actually attacked you, so + * environmental damage (fall, lava, drowning, fire, cactus, ...) has neither. In that case fall + * back to the damage type id, which is what vanilla names the death message after. + */ val DamageSource?.attackerName: String - get() = this?.entity?.name?.string ?: "Unknown" + get() { + if (this == null) return "Unknown" + // getEntity() is the mob/player behind it, getDirectEntity() the projectile it used. + val attacker = this.entity ?: this.directEntity + return attacker?.name?.string ?: this.msgId + } private fun reset() { lastTickReset = true diff --git a/src/main/resources/META-INF/neoforge.mods.toml b/src/main/resources/META-INF/neoforge.mods.toml index f431066..f490886 100644 --- a/src/main/resources/META-INF/neoforge.mods.toml +++ b/src/main/resources/META-INF/neoforge.mods.toml @@ -10,8 +10,13 @@ version = "${version}" displayName = "${name}" description = '''${description}''' authors = "Luc, OpenShock Team" -logoFile = "assets/${id}/icon.png" -logoBlur = false +# NeoForge 26.2 deprecated logoFile in favour of iconFile (square) and bannerFile (wide). +#? if >=26.2 { +iconFile = "assets/${id}/icon.png" +#?} else { +#logoFile = "assets/${id}/icon.png" +#logoBlur = false +#?} displayURL = "https://openshock.org/" [[dependencies.${id}]] From 53bbc319b0b1d26a46bed9e4c41090e7af1017b2 Mon Sep 17 00:00:00 2001 From: LucHeart Date: Wed, 26 Aug 2026 23:11:45 +0200 Subject: [PATCH 3/6] specify java version to use for each --- gradle.properties | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gradle.properties b/gradle.properties index 7d85525..1488f35 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,6 +2,12 @@ org.gradle.jvmargs=-Xmx3G org.gradle.parallel=true +# 1.21.x targets need a Java 21 toolchain, 26.x targets need Java 25. Locally Gradle finds both by +# auto-detection, but on CI actions/setup-java installs into a tool cache Gradle does not scan, so +# point it at the JAVA_HOME_* variables that action exports. Missing entries are ignored, which +# makes this a no-op on a normal dev machine. +org.gradle.java.installations.fromEnv=JAVA_HOME_21_X64,JAVA_HOME_25_X64,JAVA_HOME_21_ARM64,JAVA_HOME_25_ARM64 + # Mod Properties mod.id=shockcraft mod.name=ShockCraft From 26147f5520f6aedc8d2d95ce8bb5759fbef5adbd Mon Sep 17 00:00:00 2001 From: LucHeart Date: Thu, 27 Aug 2026 00:15:18 +0200 Subject: [PATCH 4/6] add 1.20.4, 1.21, 1.21.5 --- .github/workflows/build.yml | 1 + README.md | 28 +++++++--- build.gradle.kts | 5 ++ gradle.properties | 2 +- settings.gradle.kts | 8 ++- .../minecraft/platform/Entrypoint.kt | 43 +++++++++++---- .../minecraft/platform/McCompat.kt | 38 +++++++------ .../minecraft/platform/Platform.kt | 8 +-- .../resources/META-INF/neoforge.mods.toml | 6 +-- stonecutter.gradle.kts | 54 ++++++++++++++++++- versions/dependencies/1.20.4.properties | 15 ++++++ versions/dependencies/1.21.5.properties | 14 +++++ versions/dependencies/1.21.8.properties | 15 ------ versions/dependencies/1.21.properties | 15 ++++++ 14 files changed, 196 insertions(+), 56 deletions(-) create mode 100644 versions/dependencies/1.20.4.properties create mode 100644 versions/dependencies/1.21.5.properties delete mode 100644 versions/dependencies/1.21.8.properties create mode 100644 versions/dependencies/1.21.properties diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e265862..1f2da25 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,6 +21,7 @@ jobs: uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: | + 17 21 25 distribution: 'temurin' diff --git a/README.md b/README.md index bddbd56..49565d2 100644 --- a/README.md +++ b/README.md @@ -37,9 +37,10 @@ loader wiring. | | | |---|---| -| Minecraft | 1.21.1, 1.21.4, 1.21.8, 1.21.11, 26.1, 26.2 | +| Minecraft | 1.20.4, 1.21, 1.21.1, 1.21.4, 1.21.5, 1.21.11, 26.1, 26.2 | | Loaders | Fabric, NeoForge | -| Targets | 12 (every version × every loader) | +| Targets | 16 (every version × every loader) | +| Java | 17 for 1.20.x, 21 for 1.21.x, 25 for 26.x (Gradle downloads any it is missing) | ### Building @@ -59,10 +60,16 @@ that do not apply to the new target. That is expected — do not revert it. ### Running the game -Architectury Loom generates one IntelliJ run configuration per target, named -`Minecraft Client (:-)`. They are created during Gradle sync, so after cloning (or -after adding a version) use **Gradle tool window -> Reload All Gradle Projects** before looking for -them. `.idea/` is gitignored, so these are local-only and always regenerated. +There are two sets of IntelliJ run configurations: + +- `Client -` - generated by `./gradlew generateRunConfigs`. These invoke + `::runClient` through Gradle and work without a successful Gradle sync. **Re-run that + task after adding or removing a Minecraft version**, and it will rewrite the whole set. +- `Minecraft Client (:-)` - generated by Architectury Loom during Gradle sync. + These launch the game directly (faster, easier to debug) but need the IDE to have created the + per-target modules first. + +`.idea/` is gitignored, so both sets are local to your machine. From the command line: @@ -91,5 +98,12 @@ cannot share a world or an options file and the two loaders cannot share a mods - `Platform.kt` — the handful of loader APIs with no common equivalent. - `McCompat.kt` — the few vanilla APIs that changed across the version range. -Everything outside `platform/` compiles unchanged on all twelve targets. Sources use **Mojang +Everything outside `platform/` compiles unchanged on all sixteen targets. Sources use **Mojang mappings**, which both loaders share. + +The version-conditional code is deliberately confined to those files. `McCompat.kt` covers the +vanilla renames (`ResourceLocation`/`Identifier`, the `fromNamespaceAndPath` factory, +`displayClientMessage`/`sendOverlayMessage`, `Minecraft.screen` moving onto `Gui`), and the +NeoForge half of `Entrypoint.kt` covers the 1.21 API break (`TickEvent.ClientTickEvent` became +`ClientTickEvent.Post`, `ConfigScreenHandler.ConfigScreenFactory` became `IConfigScreenFactory`, +and `@Mod` gained its `dist` element). diff --git a/build.gradle.kts b/build.gradle.kts index 6f0d6e1..7da70c7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -41,6 +41,11 @@ repositories { forRepository { maven("https://thedarkcolour.github.io/KotlinForForge/") } filter { includeGroup("thedarkcolour") } } + // Mod Menu 9.x (Minecraft 1.20.x) pulls in Patbox's placeholder-api, published only here. + exclusiveContent { + forRepository { maven("https://maven.nucleoid.xyz/") } + filter { includeGroup("eu.pb4") } + } } kotlin { diff --git a/gradle.properties b/gradle.properties index 1488f35..cd25e06 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ org.gradle.parallel=true # auto-detection, but on CI actions/setup-java installs into a tool cache Gradle does not scan, so # point it at the JAVA_HOME_* variables that action exports. Missing entries are ignored, which # makes this a no-op on a normal dev machine. -org.gradle.java.installations.fromEnv=JAVA_HOME_21_X64,JAVA_HOME_25_X64,JAVA_HOME_21_ARM64,JAVA_HOME_25_ARM64 +org.gradle.java.installations.fromEnv=JAVA_HOME_17_X64,JAVA_HOME_21_X64,JAVA_HOME_25_X64,JAVA_HOME_17_ARM64,JAVA_HOME_21_ARM64,JAVA_HOME_25_ARM64 # Mod Properties mod.id=shockcraft diff --git a/settings.gradle.kts b/settings.gradle.kts index 78b2216..eef8765 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,6 +13,10 @@ pluginManagement { plugins { id("gg.meza.stonecraft") version "1.12.6" id("dev.kikugie.stonecutter") version "0.9.7" + + // Targets span three Java versions (17 for 1.20.x, 21 for 1.21.x, 25 for 26.x). This lets + // Gradle download any toolchain the machine is missing instead of failing the build. + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } stonecutter { @@ -27,9 +31,11 @@ stonecutter { for (loader in loaders) version("$version-$loader", version) } + mc("1.20.4", "fabric", "neoforge") + mc("1.21", "fabric", "neoforge") mc("1.21.1", "fabric", "neoforge") mc("1.21.4", "fabric", "neoforge") - mc("1.21.8", "fabric", "neoforge") + mc("1.21.5", "fabric", "neoforge") mc("1.21.11", "fabric", "neoforge") mc("26.1", "fabric", "neoforge") mc("26.2", "fabric", "neoforge") diff --git a/src/main/kotlin/openshock/integrations/minecraft/platform/Entrypoint.kt b/src/main/kotlin/openshock/integrations/minecraft/platform/Entrypoint.kt index 9a992a2..8f695ff 100644 --- a/src/main/kotlin/openshock/integrations/minecraft/platform/Entrypoint.kt +++ b/src/main/kotlin/openshock/integrations/minecraft/platform/Entrypoint.kt @@ -1,7 +1,7 @@ package openshock.integrations.minecraft.platform //? if fabric { -import com.terraformersmc.modmenu.api.ConfigScreenFactory +/*import com.terraformersmc.modmenu.api.ConfigScreenFactory import com.terraformersmc.modmenu.api.ModMenuApi import net.fabricmc.api.ClientModInitializer import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents @@ -31,34 +31,59 @@ class ModMenuEntrypoint : ModMenuApi { override fun getModConfigScreenFactory(): ConfigScreenFactory<*> = ConfigScreenFactory { parent -> ConfigScreen.create(parent) } } -//?} elif neoforge { -/*import net.neoforged.api.distmarker.Dist +*///?} elif neoforge { import net.neoforged.fml.ModLoadingContext import net.neoforged.fml.common.Mod import net.neoforged.neoforge.client.event.ClientChatReceivedEvent +import net.neoforged.neoforge.common.NeoForge +//? if >=1.21 { +/*import net.neoforged.api.distmarker.Dist import net.neoforged.neoforge.client.event.ClientTickEvent import net.neoforged.neoforge.client.gui.IConfigScreenFactory -import net.neoforged.neoforge.common.NeoForge +*///?} else { +import net.neoforged.neoforge.client.ConfigScreenHandler +import net.neoforged.neoforge.event.TickEvent +//?} import openshock.integrations.minecraft.ConfigScreen import openshock.integrations.minecraft.ShockCraft -// Kotlin for Forge ("kotlinforforge" modLoader in neoforge.mods.toml) expects an object declaration. -@Mod(value = ShockCraft.MOD_ID, dist = [Dist.CLIENT]) +// Kotlin for Forge ("kotlinforforge" modLoader in neoforge.mods.toml) expects an object +// declaration. @Mod only gained its `dist` element after 1.20.4, so that target registers for both +// sides; the mod stays client-only there via the `side = "CLIENT"` entries in neoforge.mods.toml. +//? if >=1.21 { +/*@Mod(value = ShockCraft.MOD_ID, dist = [Dist.CLIENT]) +*///?} else { +@Mod(ShockCraft.MOD_ID) +//?} object NeoForgeEntrypoint { init { ShockCraft.init() - ModLoadingContext.get().registerExtensionPoint(IConfigScreenFactory::class.java) { + // 1.21 replaced ConfigScreenHandler.ConfigScreenFactory with IConfigScreenFactory. + //? if >=1.21 { + /*ModLoadingContext.get().registerExtensionPoint(IConfigScreenFactory::class.java) { IConfigScreenFactory { _, parent -> ConfigScreen.create(parent) } } + *///?} else { + ModLoadingContext.get().registerExtensionPoint(ConfigScreenHandler.ConfigScreenFactory::class.java) { + ConfigScreenHandler.ConfigScreenFactory { _, parent -> ConfigScreen.create(parent) } + } + //?} - NeoForge.EVENT_BUS.addListener(ClientTickEvent.Post::class.java) { + // 1.21 split the phase-based TickEvent.ClientTickEvent into ClientTickEvent.Pre/Post. + //? if >=1.21 { + /*NeoForge.EVENT_BUS.addListener(ClientTickEvent.Post::class.java) { ShockCraft.onClientTick() } + *///?} else { + NeoForge.EVENT_BUS.addListener(TickEvent.ClientTickEvent::class.java) { event -> + if (event.phase == TickEvent.Phase.END) ShockCraft.onClientTick() + } + //?} NeoForge.EVENT_BUS.addListener(ClientChatReceivedEvent::class.java) { event -> ShockCraft.onChatMessage(event.message.string) } } } -*///?} +//?} diff --git a/src/main/kotlin/openshock/integrations/minecraft/platform/McCompat.kt b/src/main/kotlin/openshock/integrations/minecraft/platform/McCompat.kt index a2827fd..e8ccb35 100644 --- a/src/main/kotlin/openshock/integrations/minecraft/platform/McCompat.kt +++ b/src/main/kotlin/openshock/integrations/minecraft/platform/McCompat.kt @@ -5,10 +5,10 @@ import net.minecraft.client.gui.screens.Screen import net.minecraft.network.chat.Component //? if >=1.21.11 { -import net.minecraft.resources.Identifier as ModIdentifier -//?} else { -/*import net.minecraft.resources.ResourceLocation as ModIdentifier -*///?} +/*import net.minecraft.resources.Identifier as ModIdentifier +*///?} else { +import net.minecraft.resources.ResourceLocation as ModIdentifier +//?} /** * The only place where vanilla's own API drifts across the supported Minecraft versions. @@ -21,23 +21,31 @@ object McCompat { val currentScreen: Screen? get() { //? if >=26.2 { - return Minecraft.getInstance().gui.screen() - //?} else { - /*return Minecraft.getInstance().screen - *///?} + /*return Minecraft.getInstance().gui.screen() + *///?} else { + return Minecraft.getInstance().screen + //?} } /** `displayClientMessage(text, true)` was split out into `sendOverlayMessage(text)` in 26.1. */ fun sendActionBar(text: Component) { val player = Minecraft.getInstance().player ?: return //? if >=26.1 { - player.sendOverlayMessage(text) - //?} else { - /*player.displayClientMessage(text, true) - *///?} + /*player.sendOverlayMessage(text) + *///?} else { + player.displayClientMessage(text, true) + //?} } - /** `ResourceLocation` was renamed to `Identifier` in 1.21.11. */ - fun identifier(namespace: String, path: String): ModIdentifier = - ModIdentifier.fromNamespaceAndPath(namespace, path) + /** + * `ResourceLocation` was renamed to `Identifier` in 1.21.11 (handled by the import alias), and + * 1.21 replaced its public constructor with the `fromNamespaceAndPath` factory. + */ + fun identifier(namespace: String, path: String): ModIdentifier { + //? if >=1.21 { + /*return ModIdentifier.fromNamespaceAndPath(namespace, path) + *///?} else { + return ModIdentifier(namespace, path) + //?} + } } diff --git a/src/main/kotlin/openshock/integrations/minecraft/platform/Platform.kt b/src/main/kotlin/openshock/integrations/minecraft/platform/Platform.kt index d8ec2fd..a0672c4 100644 --- a/src/main/kotlin/openshock/integrations/minecraft/platform/Platform.kt +++ b/src/main/kotlin/openshock/integrations/minecraft/platform/Platform.kt @@ -3,17 +3,17 @@ package openshock.integrations.minecraft.platform import java.nio.file.Path //? if fabric { -import net.fabricmc.loader.api.FabricLoader +/*import net.fabricmc.loader.api.FabricLoader /** The handful of things the two loaders disagree on outside of the entrypoints. */ object Platform { val configDir: Path get() = FabricLoader.getInstance().configDir } -//?} elif neoforge { -/*import net.neoforged.fml.loading.FMLPaths +*///?} elif neoforge { +import net.neoforged.fml.loading.FMLPaths /** The handful of things the two loaders disagree on outside of the entrypoints. */ object Platform { val configDir: Path get() = FMLPaths.CONFIGDIR.get() } -*///?} +//?} diff --git a/src/main/resources/META-INF/neoforge.mods.toml b/src/main/resources/META-INF/neoforge.mods.toml index f490886..fdab7c4 100644 --- a/src/main/resources/META-INF/neoforge.mods.toml +++ b/src/main/resources/META-INF/neoforge.mods.toml @@ -12,10 +12,10 @@ description = '''${description}''' authors = "Luc, OpenShock Team" # NeoForge 26.2 deprecated logoFile in favour of iconFile (square) and bannerFile (wide). #? if >=26.2 { -iconFile = "assets/${id}/icon.png" +#iconFile = "assets/${id}/icon.png" #?} else { -#logoFile = "assets/${id}/icon.png" -#logoBlur = false +logoFile = "assets/${id}/icon.png" +logoBlur = false #?} displayURL = "https://openshock.org/" diff --git a/stonecutter.gradle.kts b/stonecutter.gradle.kts index 7eec5f1..c5e00bf 100644 --- a/stonecutter.gradle.kts +++ b/stonecutter.gradle.kts @@ -8,4 +8,56 @@ plugins { kotlin("jvm") version "2.4.0" apply false } -stonecutter active "26.2-fabric" /* [SC] DO NOT EDIT */ +stonecutter active "1.20.4-neoforge" /* [SC] DO NOT EDIT */ + +// Architectury Loom generates its own "Minecraft Client (:target)" run configurations, but those +// are Application configs that need the IDE's per-target modules to exist, so they only work after +// a successful Gradle sync. These Gradle-type ones just invoke ::runClient and work +// regardless. Regenerated from the live target list so they cannot drift when versions are added +// or removed - run ./gradlew generateRunConfigs after changing settings.gradle.kts. +tasks.register("generateRunConfigs") { + group = "ide" + description = "Writes an IntelliJ run configuration for every build target" + + val targets = subprojects.map { it.name } + val outputDir = rootProject.file(".idea/runConfigurations") + + doLast { + outputDir.mkdirs() + // Clear ours first so targets that were removed do not linger. + outputDir.listFiles { file -> file.name.startsWith("Gradle_Client_") }?.forEach { it.delete() } + + targets.forEach { target -> + val fileName = "Gradle_Client_" + target.replace('.', '_').replace('-', '_') + ".xml" + File(outputDir, fileName).writeText( + """ + + + + + + + true + true + false + false + + + + """.trimIndent() + "\n" + ) + } + logger.lifecycle("Wrote ${targets.size} run configurations to .idea/runConfigurations") + } +} diff --git a/versions/dependencies/1.20.4.properties b/versions/dependencies/1.20.4.properties new file mode 100644 index 0000000..81b27c8 --- /dev/null +++ b/versions/dependencies/1.20.4.properties @@ -0,0 +1,15 @@ +# Dependency versions for Minecraft 1.20.4 (shared by its Fabric and NeoForge targets). +# Note: 1.20.4 predates 1.20.6, so Stonecraft compiles this target against Java 17. +loader_version=0.19.3 +fabric_version=0.97.3+1.20.4 +neoforge_version=20.4.251 + +fabric_kotlin_version=1.13.13+kotlin.2.4.10 +kff_version=4.12.0 + +yacl_version=3.6.6+1.20.4 +modmenu_version=9.2.0 + +# Minecraft version ranges published in the mod metadata +mc_dep_fabric=>=1.20.3 <1.20.5 +mc_dep_neoforge=[1.20.3,1.20.5) diff --git a/versions/dependencies/1.21.5.properties b/versions/dependencies/1.21.5.properties new file mode 100644 index 0000000..9abe466 --- /dev/null +++ b/versions/dependencies/1.21.5.properties @@ -0,0 +1,14 @@ +# Dependency versions for Minecraft 1.21.5 (shared by its Fabric and NeoForge targets). +loader_version=0.19.3 +fabric_version=0.128.2+1.21.5 +neoforge_version=21.5.98 + +fabric_kotlin_version=1.13.13+kotlin.2.4.10 +kff_version=5.12.0 + +yacl_version=3.8.2+1.21.5 +modmenu_version=14.0.2 + +# Minecraft version ranges published in the mod metadata +mc_dep_fabric=>=1.21.5 <1.21.6 +mc_dep_neoforge=[1.21.5,1.21.6) diff --git a/versions/dependencies/1.21.8.properties b/versions/dependencies/1.21.8.properties deleted file mode 100644 index 1a9fede..0000000 --- a/versions/dependencies/1.21.8.properties +++ /dev/null @@ -1,15 +0,0 @@ -# Dependency versions for Minecraft 1.21.8 (shared by its Fabric and NeoForge targets). -loader_version=0.19.3 -fabric_version=0.136.1+1.21.8 -neoforge_version=21.8.54 - -fabric_kotlin_version=1.13.13+kotlin.2.4.10 -kff_version=5.12.0 - -# YACL has no dedicated 1.21.7/1.21.8 build; the 1.21.6 one covers the whole family. -yacl_version=3.8.2+1.21.6 -modmenu_version=15.0.2 - -# Minecraft version ranges published in the mod metadata -mc_dep_fabric=>=1.21.6 <1.21.9 -mc_dep_neoforge=[1.21.6,1.21.9) diff --git a/versions/dependencies/1.21.properties b/versions/dependencies/1.21.properties new file mode 100644 index 0000000..f2c7dca --- /dev/null +++ b/versions/dependencies/1.21.properties @@ -0,0 +1,15 @@ +# Dependency versions for Minecraft 1.21 (shared by its Fabric and NeoForge targets). +loader_version=0.19.3 +fabric_version=0.102.0+1.21 +neoforge_version=21.0.167 + +fabric_kotlin_version=1.13.13+kotlin.2.4.10 +kff_version=5.12.0 + +# YACL publishes one build for the 1.21/1.21.1 family. +yacl_version=3.8.2+1.21.1 +modmenu_version=11.0.4 + +# Minecraft version ranges published in the mod metadata +mc_dep_fabric=>=1.21 <1.21.1 +mc_dep_neoforge=[1.21,1.21.1) From 1fd0c869e01a3d2d3b0383771179f38fb86b290d Mon Sep 17 00:00:00 2001 From: LucHeart Date: Thu, 27 Aug 2026 01:05:32 +0200 Subject: [PATCH 5/6] add release workflow --- .github/workflows/release.yml | 95 +++++++++++++++++++++++++++++++++++ README.md | 37 ++++++++++++++ build.gradle.kts | 44 ++++++++++++++++ 3 files changed, 176 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..bb4b947 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,95 @@ +# Publishes every Minecraft version / loader combination to Modrinth and CurseForge, and attaches +# the jars to the GitHub release, when a version tag is pushed (e.g. 1.4.0, 1.4.0-beta.1). +# +# Required repository secrets: +# MODRINTH_TOKEN - Modrinth PAT with "Create versions" scope +# CURSEFORGE_TOKEN - CurseForge API token +# +# Required repository variables (Settings -> Secrets and variables -> Actions -> Variables): +# MODRINTH_ID - Modrinth project id (e.g. DwMSqx5B) +# CURSEFORGE_ID - CurseForge numeric project id (e.g. 980833) +# CURSEFORGE_SLUG - CurseForge project slug (e.g. openshock-shockcraft) +# +# Nothing is uploaded unless PUBLISH_RELEASE is true, so this workflow is the only thing that can +# publish; running publishMods anywhere else is a dry run. + +name: release +on: + push: + # Version tags carry no prefix. This pattern uses only [] and *, which GitHub's tag filters + # definitely support, so a release can never silently fail to trigger. + tags: + - '[0-9]*' + +permissions: + contents: write + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: validate gradle wrapper + uses: gradle/actions/wrapper-validation@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 + + - name: setup jdks + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + java-version: | + 17 + 21 + 25 + distribution: 'temurin' + + - name: setup gradle + uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 + + # The tag is the source of truth for the published version: tag 1.4.0 publishes 1.4.0. + - name: derive version from tag + id: version + run: echo "version=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" + + # An unset variable expands to an empty string, which still counts as "present" to Gradle, so + # check before spending ten minutes building only to publish with a blank project id. + - name: check publishing credentials + env: + MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} + MODRINTH_ID: ${{ vars.MODRINTH_ID }} + CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }} + CURSEFORGE_ID: ${{ vars.CURSEFORGE_ID }} + CURSEFORGE_SLUG: ${{ vars.CURSEFORGE_SLUG }} + run: | + missing="" + for name in MODRINTH_TOKEN MODRINTH_ID CURSEFORGE_TOKEN CURSEFORGE_ID CURSEFORGE_SLUG; do + [ -n "${!name}" ] || missing="$missing $name" + done + if [ -n "$missing" ]; then + echo "::error::Missing required secrets/variables:$missing" + exit 1 + fi + + - name: build all versions + run: ./gradlew chiseledBuildAndCollect -Pmod.version=${{ steps.version.outputs.version }} --stacktrace + + - name: create github release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "$GITHUB_REF_NAME" \ + --title "$GITHUB_REF_NAME" \ + --generate-notes \ + build/libs/*.jar + + - name: publish to modrinth and curseforge + env: + PUBLISH_RELEASE: 'true' + # Point at the GitHub release rather than duplicating its notes on each platform. + CHANGELOG: "Full changelog: https://github.com/${{ github.repository }}/releases/tag/${{ github.ref_name }}" + MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} + MODRINTH_ID: ${{ vars.MODRINTH_ID }} + CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }} + CURSEFORGE_ID: ${{ vars.CURSEFORGE_ID }} + CURSEFORGE_SLUG: ${{ vars.CURSEFORGE_SLUG }} + run: ./gradlew chiseledPublishMods -Pmod.version=${{ steps.version.outputs.version }} --stacktrace diff --git a/README.md b/README.md index 49565d2..a8af705 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,43 @@ edits. Each target runs in its own directory, `run/-/`, because Minecraft 1.21.1 and 26.2 cannot share a world or an options file and the two loaders cannot share a mods folder. +### Releasing + +Pushing a version tag runs `.github/workflows/release.yml`, which builds all targets, creates a +GitHub release with the jars attached, and publishes every one of them to Modrinth and CurseForge. + +```bash +git tag 1.4.0 && git push origin 1.4.0 +``` + +The tag is the source of truth for the version (tag `1.4.0` publishes `1.4.0`), so `mod.version` in +`gradle.properties` does not need bumping first. The changelog on Modrinth and CurseForge is a link +back to the GitHub release, so the notes only ever live in one place. A tag containing `beta` or `alpha` is published as a pre-release. + +Configure these under **Settings -> Secrets and variables -> Actions**: + +| Secret | | +|---|---| +| `MODRINTH_TOKEN` | Modrinth PAT with the "Create versions" scope | +| `CURSEFORGE_TOKEN` | CurseForge API token | + +| Variable | Current value | +|---|---| +| `MODRINTH_ID` | `DwMSqx5B` | +| `CURSEFORGE_ID` | `980833` | +| `CURSEFORGE_SLUG` | `openshock-shockcraft` | + +The workflow checks all five are set before building, so a missing one fails in seconds with a +clear message rather than part-way through publishing. + +Nothing is ever uploaded unless `PUBLISH_RELEASE=true`, which only the release workflow sets, so +running `./gradlew chiseledPublishMods` locally is always a dry run. Use it to preview exactly what +would be sent: + +```bash +./gradlew chiseledPublishMods +``` + ### Layout - `settings.gradle.kts` — the list of targets. Adding a Minecraft version is one line here plus a diff --git a/build.gradle.kts b/build.gradle.kts index 7da70c7..61da15b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -98,6 +98,50 @@ modSettings { variableReplacements.put("mcDepNeoforge", mod.prop("mc_dep_neoforge")) } +// Stonecraft already wires the Modrinth/CurseForge credentials, jar, version and display name +// from environment variables; only the things unique to this mod belong here. +// Stonecraft only configures a platform when its credentials are present, so these blocks have to +// be guarded the same way - declaring them unconditionally leaves projectId unset and fails the +// task on any machine without the secrets. +fun hasEnv(vararg names: String) = names.all { providers.environmentVariable(it).isPresent } +val publishToModrinth = hasEnv("MODRINTH_TOKEN", "MODRINTH_ID") +val publishToCurseforge = hasEnv("CURSEFORGE_TOKEN", "CURSEFORGE_ID", "CURSEFORGE_SLUG") + +publishMods { + // Stonecraft derives dryRun from DO_PUBLISH, but its docs and its code disagree about which + // way round that is. Publishing is not reversible, so decide it here instead: nothing is + // uploaded unless PUBLISH_RELEASE is explicitly true. + dryRun = !providers.environmentVariable("PUBLISH_RELEASE").getOrElse("false").toBoolean() + + // Stonecraft defaults the changelog to the contents of CHANGELOG.md. The release workflow + // passes the GitHub release body instead, so a tag's notes reach both platforms. + providers.environmentVariable("CHANGELOG").orNull?.let { changelog = it } + + if (publishToModrinth) modrinth { + requires("yacl") + if (mod.isFabric) { + requires("fabric-api") + requires("fabric-language-kotlin") + requires("modmenu") + } else { + requires("kotlin-for-forge") + } + } + + if (publishToCurseforge) curseforge { + client = true + server = false + requires("yacl") + if (mod.isFabric) { + requires("fabric-api") + requires("fabric-language-kotlin") + requires("modmenu") + } else { + requires("kotlin-for-forge") + } + } +} + tasks.withType().configureEach { compilerOptions.jvmTarget = JvmTarget.fromTarget(javaVersion.toString()) dependsOn("stonecutterGenerate") From 0384c47f78af0c0c4863af8d868d89945d5a8b80 Mon Sep 17 00:00:00 2001 From: LucHeart Date: Thu, 27 Aug 2026 01:42:46 +0200 Subject: [PATCH 6/6] fix review comments --- .github/workflows/release.yml | 30 ++++++++-- README.md | 13 ++++- .../minecraft/api/OpenShockApi.kt | 35 ++++++++++-- .../minecraft/platform/Entrypoint.kt | 55 ++++++++++++------- .../minecraft/platform/McCompat.kt | 32 +++++------ .../minecraft/platform/Platform.kt | 8 +-- .../resources/META-INF/neoforge.mods.toml | 10 ++-- stonecutter.gradle.kts | 2 +- versions/dependencies/1.20.4.properties | 4 +- versions/dependencies/1.21.1.properties | 4 +- versions/dependencies/1.21.11.properties | 4 +- 11 files changed, 138 insertions(+), 59 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bb4b947..4183754 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,9 +47,25 @@ jobs: uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 # The tag is the source of truth for the published version: tag 1.4.0 publishes 1.4.0. - - name: derive version from tag + # + # It also decides the release type in one place so GitHub and the mod platforms cannot + # disagree. Stonecraft infers a type from the version string but only recognises alpha, + # beta and next - an "rc" tag would otherwise reach Modrinth and CurseForge as stable. + # Setting RELEASE_TYPE explicitly overrides that inference. + - name: derive version and release type from tag id: version - run: echo "version=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" + run: | + case "$GITHUB_REF_NAME" in + *alpha*) release_type=alpha ;; + *beta*|*next*|*rc*) release_type=beta ;; + *) release_type=stable ;; + esac + if [ "$release_type" = stable ]; then prerelease=""; else prerelease="--prerelease"; fi + { + echo "version=$GITHUB_REF_NAME" + echo "release_type=$release_type" + echo "prerelease=$prerelease" + } >> "$GITHUB_OUTPUT" # An unset variable expands to an empty string, which still counts as "present" to Gradle, so # check before spending ten minutes building only to publish with a blank project id. @@ -71,20 +87,25 @@ jobs: fi - name: build all versions - run: ./gradlew chiseledBuildAndCollect -Pmod.version=${{ steps.version.outputs.version }} --stacktrace + env: + MOD_VERSION: ${{ steps.version.outputs.version }} + run: ./gradlew chiseledBuildAndCollect -Pmod.version="$MOD_VERSION" --stacktrace - name: create github release env: GH_TOKEN: ${{ github.token }} + PRERELEASE: ${{ steps.version.outputs.prerelease }} run: | gh release create "$GITHUB_REF_NAME" \ --title "$GITHUB_REF_NAME" \ --generate-notes \ + $PRERELEASE \ build/libs/*.jar - name: publish to modrinth and curseforge env: PUBLISH_RELEASE: 'true' + RELEASE_TYPE: ${{ steps.version.outputs.release_type }} # Point at the GitHub release rather than duplicating its notes on each platform. CHANGELOG: "Full changelog: https://github.com/${{ github.repository }}/releases/tag/${{ github.ref_name }}" MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} @@ -92,4 +113,5 @@ jobs: CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }} CURSEFORGE_ID: ${{ vars.CURSEFORGE_ID }} CURSEFORGE_SLUG: ${{ vars.CURSEFORGE_SLUG }} - run: ./gradlew chiseledPublishMods -Pmod.version=${{ steps.version.outputs.version }} --stacktrace + MOD_VERSION: ${{ steps.version.outputs.version }} + run: ./gradlew chiseledPublishMods -Pmod.version="$MOD_VERSION" --stacktrace diff --git a/README.md b/README.md index a8af705..9fda995 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,18 @@ git tag 1.4.0 && git push origin 1.4.0 The tag is the source of truth for the version (tag `1.4.0` publishes `1.4.0`), so `mod.version` in `gradle.properties` does not need bumping first. The changelog on Modrinth and CurseForge is a link -back to the GitHub release, so the notes only ever live in one place. A tag containing `beta` or `alpha` is published as a pre-release. +back to the GitHub release, so the notes only ever live in one place. + +The tag also decides the release type, consistently across all three destinations: + +| Tag contains | Modrinth / CurseForge | GitHub release | +|---|---|---| +| `alpha` | Alpha | pre-release | +| `beta`, `next`, `rc` | Beta | pre-release | +| anything else | Stable | normal | + +`rc` needs an explicit mapping because Stonecraft's own inference recognises only `alpha`, `beta` +and `next`; left to it, an `rc` tag would reach both platforms marked stable. Configure these under **Settings -> Secrets and variables -> Actions**: diff --git a/src/main/kotlin/openshock/integrations/minecraft/api/OpenShockApi.kt b/src/main/kotlin/openshock/integrations/minecraft/api/OpenShockApi.kt index a3e2587..66b14cf 100644 --- a/src/main/kotlin/openshock/integrations/minecraft/api/OpenShockApi.kt +++ b/src/main/kotlin/openshock/integrations/minecraft/api/OpenShockApi.kt @@ -8,10 +8,12 @@ import net.minecraft.network.chat.Component import openshock.integrations.minecraft.config.ShockCraftConfig import openshock.integrations.minecraft.platform.McCompat import org.slf4j.LoggerFactory +import java.io.IOException import java.net.URI import java.net.http.HttpClient import java.net.http.HttpRequest import java.net.http.HttpResponse +import java.net.http.HttpTimeoutException import java.time.Duration object OpenShockApi { @@ -20,10 +22,18 @@ object OpenShockApi { private const val SUFFIX: String = " (Integrations.Minecraft)" + private val CONNECT_TIMEOUT: Duration = Duration.ofSeconds(10) + + // connectTimeout only bounds connection setup, so without this a stalled server would leave + // the request hanging on a Dispatchers.IO thread forever. + private val REQUEST_TIMEOUT: Duration = Duration.ofSeconds(15) + // The JDK client keeps us free of a shaded HTTP library, which would otherwise have to be - // bundled differently for each loader. + // bundled differently for each loader. OkHttp followed redirects by default; the JDK client + // does not, so it has to be asked for explicitly or a 3xx would silently drop the POST. private val client: HttpClient = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(10)) + .connectTimeout(CONNECT_TIMEOUT) + .followRedirects(HttpClient.Redirect.NORMAL) .build() suspend fun control(type: ControlType, intensity: Byte, duration: UShort, name: String) { @@ -45,10 +55,27 @@ object OpenShockApi { "Integrations.Minecraft/1.0.0 (Minecraft ${Minecraft.getInstance().launchedVersion}; Java ${System.getProperty("java.version")})" ) .POST(HttpRequest.BodyPublishers.ofString(json)) + .timeout(REQUEST_TIMEOUT) .build() - val response = withContext(Dispatchers.IO) { - client.send(request, HttpResponse.BodyHandlers.ofString()) + // A network failure must not escape into the coroutine's uncaught handler - the shock is + // fire-and-forget, so log it and give up rather than take the game down with us. + val response = try { + withContext(Dispatchers.IO) { + client.send(request, HttpResponse.BodyHandlers.ofString()) + } + } catch (e: HttpTimeoutException) { + // Must precede IOException: HttpTimeoutException is a subclass of it. + logger.error("Timed out sending $type to the OpenShock API after $REQUEST_TIMEOUT", e) + return + } catch (e: IOException) { + logger.error("Failed to send $type to the OpenShock API", e) + return + } + + if (response.statusCode() !in 200..299) { + logger.error("OpenShock API returned ${response.statusCode()}: ${response.body()}") + return } logger.debug(response.body()) diff --git a/src/main/kotlin/openshock/integrations/minecraft/platform/Entrypoint.kt b/src/main/kotlin/openshock/integrations/minecraft/platform/Entrypoint.kt index 8f695ff..13c7bd8 100644 --- a/src/main/kotlin/openshock/integrations/minecraft/platform/Entrypoint.kt +++ b/src/main/kotlin/openshock/integrations/minecraft/platform/Entrypoint.kt @@ -1,7 +1,7 @@ package openshock.integrations.minecraft.platform //? if fabric { -/*import com.terraformersmc.modmenu.api.ConfigScreenFactory +import com.terraformersmc.modmenu.api.ConfigScreenFactory import com.terraformersmc.modmenu.api.ModMenuApi import net.fabricmc.api.ClientModInitializer import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents @@ -31,19 +31,21 @@ class ModMenuEntrypoint : ModMenuApi { override fun getModConfigScreenFactory(): ConfigScreenFactory<*> = ConfigScreenFactory { parent -> ConfigScreen.create(parent) } } -*///?} elif neoforge { -import net.neoforged.fml.ModLoadingContext +//?} elif neoforge { +/*import net.neoforged.fml.ModLoadingContext import net.neoforged.fml.common.Mod import net.neoforged.neoforge.client.event.ClientChatReceivedEvent import net.neoforged.neoforge.common.NeoForge //? if >=1.21 { -/*import net.neoforged.api.distmarker.Dist +import net.neoforged.api.distmarker.Dist import net.neoforged.neoforge.client.event.ClientTickEvent import net.neoforged.neoforge.client.gui.IConfigScreenFactory -*///?} else { +//?} else { +/*import net.neoforged.api.distmarker.Dist +import net.neoforged.fml.loading.FMLEnvironment import net.neoforged.neoforge.client.ConfigScreenHandler import net.neoforged.neoforge.event.TickEvent -//?} +*///?} import openshock.integrations.minecraft.ConfigScreen import openshock.integrations.minecraft.ShockCraft @@ -51,39 +53,54 @@ import openshock.integrations.minecraft.ShockCraft // declaration. @Mod only gained its `dist` element after 1.20.4, so that target registers for both // sides; the mod stays client-only there via the `side = "CLIENT"` entries in neoforge.mods.toml. //? if >=1.21 { -/*@Mod(value = ShockCraft.MOD_ID, dist = [Dist.CLIENT]) -*///?} else { -@Mod(ShockCraft.MOD_ID) -//?} +@Mod(value = ShockCraft.MOD_ID, dist = [Dist.CLIENT]) +//?} else { +/*@Mod(ShockCraft.MOD_ID) +*///?} object NeoForgeEntrypoint { init { + //? if >=1.21 { + ClientBootstrap.run() + //?} else { + /*// @Mod only gained its `dist` element after 1.20.4, so the entrypoint is constructed on + // dedicated servers too. Everything below touches client-only APIs, so keep it in a + // separate class that a server never loads. + if (FMLEnvironment.dist == Dist.CLIENT) { + ClientBootstrap.run() + } + *///?} + } +} + +private object ClientBootstrap { + fun run() { ShockCraft.init() // 1.21 replaced ConfigScreenHandler.ConfigScreenFactory with IConfigScreenFactory. //? if >=1.21 { - /*ModLoadingContext.get().registerExtensionPoint(IConfigScreenFactory::class.java) { + ModLoadingContext.get().registerExtensionPoint(IConfigScreenFactory::class.java) { IConfigScreenFactory { _, parent -> ConfigScreen.create(parent) } } - *///?} else { - ModLoadingContext.get().registerExtensionPoint(ConfigScreenHandler.ConfigScreenFactory::class.java) { + //?} else { + /*ModLoadingContext.get().registerExtensionPoint(ConfigScreenHandler.ConfigScreenFactory::class.java) { ConfigScreenHandler.ConfigScreenFactory { _, parent -> ConfigScreen.create(parent) } } - //?} + *///?} // 1.21 split the phase-based TickEvent.ClientTickEvent into ClientTickEvent.Pre/Post. //? if >=1.21 { - /*NeoForge.EVENT_BUS.addListener(ClientTickEvent.Post::class.java) { + NeoForge.EVENT_BUS.addListener(ClientTickEvent.Post::class.java) { ShockCraft.onClientTick() } - *///?} else { - NeoForge.EVENT_BUS.addListener(TickEvent.ClientTickEvent::class.java) { event -> + //?} else { + /*NeoForge.EVENT_BUS.addListener(TickEvent.ClientTickEvent::class.java) { event -> if (event.phase == TickEvent.Phase.END) ShockCraft.onClientTick() } - //?} + *///?} NeoForge.EVENT_BUS.addListener(ClientChatReceivedEvent::class.java) { event -> ShockCraft.onChatMessage(event.message.string) } } } -//?} +*///?} diff --git a/src/main/kotlin/openshock/integrations/minecraft/platform/McCompat.kt b/src/main/kotlin/openshock/integrations/minecraft/platform/McCompat.kt index e8ccb35..715677c 100644 --- a/src/main/kotlin/openshock/integrations/minecraft/platform/McCompat.kt +++ b/src/main/kotlin/openshock/integrations/minecraft/platform/McCompat.kt @@ -5,10 +5,10 @@ import net.minecraft.client.gui.screens.Screen import net.minecraft.network.chat.Component //? if >=1.21.11 { -/*import net.minecraft.resources.Identifier as ModIdentifier -*///?} else { -import net.minecraft.resources.ResourceLocation as ModIdentifier -//?} +import net.minecraft.resources.Identifier as ModIdentifier +//?} else { +/*import net.minecraft.resources.ResourceLocation as ModIdentifier +*///?} /** * The only place where vanilla's own API drifts across the supported Minecraft versions. @@ -21,20 +21,20 @@ object McCompat { val currentScreen: Screen? get() { //? if >=26.2 { - /*return Minecraft.getInstance().gui.screen() - *///?} else { - return Minecraft.getInstance().screen - //?} + return Minecraft.getInstance().gui.screen() + //?} else { + /*return Minecraft.getInstance().screen + *///?} } /** `displayClientMessage(text, true)` was split out into `sendOverlayMessage(text)` in 26.1. */ fun sendActionBar(text: Component) { val player = Minecraft.getInstance().player ?: return //? if >=26.1 { - /*player.sendOverlayMessage(text) - *///?} else { - player.displayClientMessage(text, true) - //?} + player.sendOverlayMessage(text) + //?} else { + /*player.displayClientMessage(text, true) + *///?} } /** @@ -43,9 +43,9 @@ object McCompat { */ fun identifier(namespace: String, path: String): ModIdentifier { //? if >=1.21 { - /*return ModIdentifier.fromNamespaceAndPath(namespace, path) - *///?} else { - return ModIdentifier(namespace, path) - //?} + return ModIdentifier.fromNamespaceAndPath(namespace, path) + //?} else { + /*return ModIdentifier(namespace, path) + *///?} } } diff --git a/src/main/kotlin/openshock/integrations/minecraft/platform/Platform.kt b/src/main/kotlin/openshock/integrations/minecraft/platform/Platform.kt index a0672c4..d8ec2fd 100644 --- a/src/main/kotlin/openshock/integrations/minecraft/platform/Platform.kt +++ b/src/main/kotlin/openshock/integrations/minecraft/platform/Platform.kt @@ -3,17 +3,17 @@ package openshock.integrations.minecraft.platform import java.nio.file.Path //? if fabric { -/*import net.fabricmc.loader.api.FabricLoader +import net.fabricmc.loader.api.FabricLoader /** The handful of things the two loaders disagree on outside of the entrypoints. */ object Platform { val configDir: Path get() = FabricLoader.getInstance().configDir } -*///?} elif neoforge { -import net.neoforged.fml.loading.FMLPaths +//?} elif neoforge { +/*import net.neoforged.fml.loading.FMLPaths /** The handful of things the two loaders disagree on outside of the entrypoints. */ object Platform { val configDir: Path get() = FMLPaths.CONFIGDIR.get() } -//?} +*///?} diff --git a/src/main/resources/META-INF/neoforge.mods.toml b/src/main/resources/META-INF/neoforge.mods.toml index fdab7c4..292bfc5 100644 --- a/src/main/resources/META-INF/neoforge.mods.toml +++ b/src/main/resources/META-INF/neoforge.mods.toml @@ -1,6 +1,8 @@ # Kotlin for Forge provides the Kotlin runtime and the object-declaration @Mod entrypoint. +# The range has to track the target's KFF version - 1.20.x builds against 4.x, and a hard-coded +# [5.0,) would make FML reject the jar outright. modLoader = "kotlinforforge" -loaderVersion = "[5.0,)" +loaderVersion = "[${kffVersion},)" license = "GPL-3.0" issueTrackerURL = "https://github.com/OpenShock/Integrations.Minecraft/issues" @@ -12,10 +14,10 @@ description = '''${description}''' authors = "Luc, OpenShock Team" # NeoForge 26.2 deprecated logoFile in favour of iconFile (square) and bannerFile (wide). #? if >=26.2 { -#iconFile = "assets/${id}/icon.png" +iconFile = "assets/${id}/icon.png" #?} else { -logoFile = "assets/${id}/icon.png" -logoBlur = false +#logoFile = "assets/${id}/icon.png" +#logoBlur = false #?} displayURL = "https://openshock.org/" diff --git a/stonecutter.gradle.kts b/stonecutter.gradle.kts index c5e00bf..978946b 100644 --- a/stonecutter.gradle.kts +++ b/stonecutter.gradle.kts @@ -8,7 +8,7 @@ plugins { kotlin("jvm") version "2.4.0" apply false } -stonecutter active "1.20.4-neoforge" /* [SC] DO NOT EDIT */ +stonecutter active "26.2-fabric" /* [SC] DO NOT EDIT */ // Architectury Loom generates its own "Minecraft Client (:target)" run configurations, but those // are Application configs that need the IDE's per-target modules to exist, so they only work after diff --git a/versions/dependencies/1.20.4.properties b/versions/dependencies/1.20.4.properties index 81b27c8..d9db931 100644 --- a/versions/dependencies/1.20.4.properties +++ b/versions/dependencies/1.20.4.properties @@ -11,5 +11,5 @@ yacl_version=3.6.6+1.20.4 modmenu_version=9.2.0 # Minecraft version ranges published in the mod metadata -mc_dep_fabric=>=1.20.3 <1.20.5 -mc_dep_neoforge=[1.20.3,1.20.5) +mc_dep_fabric=>=1.20.4 <1.20.5 +mc_dep_neoforge=[1.20.4,1.20.5) diff --git a/versions/dependencies/1.21.1.properties b/versions/dependencies/1.21.1.properties index 568e163..69341dc 100644 --- a/versions/dependencies/1.21.1.properties +++ b/versions/dependencies/1.21.1.properties @@ -10,5 +10,5 @@ yacl_version=3.8.2+1.21.1 modmenu_version=11.0.4 # Minecraft version ranges published in the mod metadata -mc_dep_fabric=>=1.21 <1.21.2 -mc_dep_neoforge=[1.21,1.21.2) +mc_dep_fabric=>=1.21.1 <1.21.2 +mc_dep_neoforge=[1.21.1,1.21.2) diff --git a/versions/dependencies/1.21.11.properties b/versions/dependencies/1.21.11.properties index a1fb49b..55f7b10 100644 --- a/versions/dependencies/1.21.11.properties +++ b/versions/dependencies/1.21.11.properties @@ -10,5 +10,5 @@ yacl_version=3.8.2+1.21.11 modmenu_version=17.0.1-beta.1 # Minecraft version ranges published in the mod metadata -mc_dep_fabric=>=1.21.9 <26 -mc_dep_neoforge=[1.21.9,26) +mc_dep_fabric=>=1.21.11 <26 +mc_dep_neoforge=[1.21.11,26)