diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2b100f2..1f2da25 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,37 +1,42 @@ -# 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: | + 17 + 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/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4183754 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,117 @@ +# 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. + # + # 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: | + 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. + - 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 + 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 }} + MODRINTH_ID: ${{ vars.MODRINTH_ID }} + CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }} + CURSEFORGE_ID: ${{ vars.CURSEFORGE_ID }} + CURSEFORGE_SLUG: ${{ vars.CURSEFORGE_SLUG }} + MOD_VERSION: ${{ steps.version.outputs.version }} + run: ./gradlew chiseledPublishMods -Pmod.version="$MOD_VERSION" --stacktrace 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..9fda995 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,131 @@ ## 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.20.4, 1.21, 1.21.1, 1.21.4, 1.21.5, 1.21.11, 26.1, 26.2 | +| Loaders | Fabric, NeoForge | +| 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 + +```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 + +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: + +```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. + +### 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. + +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**: + +| 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 + 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 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 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..61da15b --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,148 @@ +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") } + } + // 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 { + 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")) +} + +// 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") +} diff --git a/gradle.properties b/gradle.properties index cc8c03e..cd25e06 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,23 +1,16 @@ # 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 +# 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_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_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 f8e1ee3..eddabd2 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 23449a2..ad7845b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,9 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index adff685..249efbb 100755 --- a/gradlew +++ b/gradlew @@ -20,7 +20,7 @@ ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -29,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. diff --git a/gradlew.bat b/gradlew.bat index c4bdd3a..a51ec4f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -19,12 +19,12 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -51,7 +51,7 @@ 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 :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..eef8765 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,49 @@ +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" + + // 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 { + 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.20.4", "fabric", "neoforge") + mc("1.21", "fabric", "neoforge") + mc("1.21.1", "fabric", "neoforge") + mc("1.21.4", "fabric", "neoforge") + mc("1.21.5", "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..4619d3d 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 @@ -44,12 +38,24 @@ object ShockCraft : ClientModInitializer { 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?.attacker?.stringifiedName ?: "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 - val player = MinecraftClient.getInstance().player + val player = Minecraft.getInstance().player if (player == null) { lastTickHealth = 20f @@ -62,12 +68,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 +88,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 +120,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 +154,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 +172,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 +180,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 +231,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..66b14cf 100644 --- a/src/main/kotlin/openshock/integrations/minecraft/api/OpenShockApi.kt +++ b/src/main/kotlin/openshock/integrations/minecraft/api/OpenShockApi.kt @@ -1,59 +1,91 @@ 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.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 { 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() + 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. 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(CONNECT_TIMEOUT) + .followRedirects(HttpClient.Redirect.NORMAL) + .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)) + .timeout(REQUEST_TIMEOUT) .build() - val response = client.newCall(request).await() + // 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!!.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..13c7bd8 --- /dev/null +++ b/src/main/kotlin/openshock/integrations/minecraft/platform/Entrypoint.kt @@ -0,0 +1,106 @@ +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.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 +//?} 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 + +// 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 { + //? 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) { + IConfigScreenFactory { _, parent -> ConfigScreen.create(parent) } + } + //?} 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) { + 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 new file mode 100644 index 0000000..715677c --- /dev/null +++ b/src/main/kotlin/openshock/integrations/minecraft/platform/McCompat.kt @@ -0,0 +1,51 @@ +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 (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 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..292bfc5 --- /dev/null +++ b/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,50 @@ +# 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 = "[${kffVersion},)" +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" +# 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}]] +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..978946b --- /dev/null +++ b/stonecutter.gradle.kts @@ -0,0 +1,63 @@ +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 */ + +// 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..d9db931 --- /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.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 new file mode 100644 index 0000000..69341dc --- /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 <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 new file mode 100644 index 0000000..55f7b10 --- /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.11 <26 +mc_dep_neoforge=[1.21.11,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.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.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) 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)