From 945fb47a0ca05da5808f204851a19b7392426252 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 22:57:07 +0800 Subject: [PATCH 001/314] Plan 1.1.0 for 1.20.1, 1.21.1 and 26.3 on Forge, NeoForge and Fabric --- docs/ROADMAP.md | 96 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 65 insertions(+), 31 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index b532c4a..1aa0b3b 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -12,7 +12,7 @@ tagged pre-release that installs and runs on its own. | 3 | `1.0.0-alpha.3` | Weapons and combat | Implemented, in-game test in progress | | 4 | `1.0.0-beta.1` | HUD and polish | Done | | | `1.0.0` | Release: code-only jar | Released | -| 5-10 | `1.1.0-beta.1` to `beta.6` | Content packs, older versions, equipment, workshop ([plan](#road-to-110)) | Planned | +| 5-11 | `1.1.0-beta.1` to `beta.7` | Content packs, NeoForge and Fabric, 1.21.1 and 1.20.1, equipment, workshop ([plan](#road-to-110)) | In progress | | | `1.1.0` | Release on CurseForge | Planned | ## Phase 1: foundation and flyable helicopters @@ -84,7 +84,7 @@ textures (back faces are culled again). See the [test plan](TESTING.md#phase-4-c the recipe book from the first login - [x] Code-only jar: the original content is imported from the player's own copy of the 1.0.3 zip -The rest of the original phase 4 list moved to 1.1.0 (phase 8 below), so 1.0.0 can ship. +The rest of the original phase 4 list moved to 1.1.0 (phase 9 below), so 1.0.0 can ship. ## 1.0.0 release criteria @@ -95,9 +95,9 @@ The rest of the original phase 4 list moved to 1.1.0 (phase 8 below), so 1.0.0 c ## Road to 1.1.0 -1.1.0 adds three big things on top of 1.0.0: content packs, older Minecraft versions, and the -vehicle workshop. It also picks up the phase 4 items that were left out of 1.0.0. The work is -split into six phases, each ending in a beta. +1.1.0 adds three big things on top of 1.0.0: content packs, more Minecraft versions and loaders, +and the vehicle workshop. It also picks up the phase 4 items that were left out of 1.0.0. The work is +split into seven phases, each ending in a beta. ### Release channels @@ -133,17 +133,37 @@ Rules that hold for every beta: |-------|---------|-------|-------| | 5 | `1.1.0-beta.1` | Groundwork: runtime registry, config, multi-version build | Released | | 6 | `1.1.0-beta.2` | Content packs and multiplayer sync | Released | -| 7 | `1.1.0-beta.3` | Older Minecraft versions | Planned | -| 8 | `1.1.0-beta.4` | Equipment and the rest of phase 4 | Planned | -| 9 | `1.1.0-beta.5` | Workshop: model, parts and tuning | Planned | -| 10 | `1.1.0-beta.6` | Workshop: textures, sounds and sharing | Planned | +| 7 | `1.1.0-beta.3` | Loaders (Forge, NeoForge, Fabric) and 1.21.1 | Planned | +| 8 | `1.1.0-beta.4` | 1.20.1 | Planned | +| 9 | `1.1.0-beta.5` | Equipment and the rest of phase 4 | Planned | +| 10 | `1.1.0-beta.6` | Workshop: model, parts and tuning | Planned | +| 11 | `1.1.0-beta.7` | Workshop: textures, sounds and sharing | Planned | | | `1.1.0` | Release on CurseForge | Planned | The order is deliberate. The registry and the source split come first because everything else sits on them. Content packs come before the workshop, since the workshop's output is a pack. The ports come right after packs, while the code is still close to 1.0.0; from then on each -feature is built once in shared code and only its Minecraft glue is written per version, instead -of back-porting four phases of work at the end. +feature is built once in shared code and only its Minecraft glue is written per version and +loader, instead of back-porting several phases of work at the end. + +### Supported versions + +MCHeli stopped at Minecraft 1.7.10. 1.1.0 is the bridge back: it runs on the versions most +players and modpacks are on today, so anyone can pick the mod up again, and then follows the +latest Minecraft. + +| Minecraft | Also runs on | Java | Forge | NeoForge | Fabric | +|-----------|--------------|------|-------|----------|--------| +| 26.3 | | 25 | yes | yes | yes | +| 1.21.1 | 1.21 | 21 | yes | yes | yes | +| 1.20.1 | 1.20 | 17 | yes | the Forge jar (NeoForge 1.20.1 is a Forge fork) | yes | + +Each major version gets its most played minor rather than its last one (1.20.6 and 1.21.11 have +few players). Nothing below 1.20. + +**After 1.1.0, the 1.20.1 and 1.21.1 builds are frozen.** They get no further fixes or features; +development continues on the latest Minecraft version only, and players are expected to move +up with it. ## Phase 5: groundwork (`1.1.0-beta.1`) @@ -184,24 +204,41 @@ The external content packs from phase 4, in the package format the workshop will - [x] Night glow: tracers, rocket motors, flares and explosions glow, faint by day and bright at night; a crash keeps burning with a glow and the wreck burns for 30 seconds. Client config `effects.nightGlow` turns it off. -## Phase 7: older Minecraft versions (`1.1.0-beta.3`) +## Phase 7: loaders and 1.21.1 (`1.1.0-beta.3`) + +1.1.0 runs on three Minecraft versions, each on three loaders (see +[supported versions](#supported-versions)). This phase splits the code so the loaders share +almost everything, adds NeoForge and Fabric on 26.3, then brings all three to 1.21.1. + +- [ ] Three layers: `common` (pure Java, as now), shared code per Minecraft version (rendering, + entities, sounds, HUD, which are the same on every loader), and a thin module per loader + (registration, events, networking, config) +- [ ] NeoForge and Fabric on 26.3, with the same features and checklists as Forge +- [ ] 1.21.1 (Java 21) on Forge, NeoForge and Fabric: older rendering (no render states), older + networking, item models from the item JSON instead of `item_model` +- [ ] Content packs and their sync work across loaders on the same Minecraft version (a Fabric + client on a NeoForge server) +- [ ] Legacy importer checked on 1.21.1 (pack format numbers and the asset layout differ) +- [ ] Jar names carry the loader and the Minecraft version, such as + `mcheli-fabric-1.21.1-1.1.0-beta.3.jar`; the release workflow publishes every jar +- [ ] CI builds and tests every module on every pull request +- [ ] [TESTING.md](TESTING.md) gets a short smoke test per jar: import, fly a helicopter, drive a + tank, fire a missile, join a dedicated server -Ports of the platform module. The shared code should need no changes; if it does, the split -from phase 5 gets fixed rather than worked around. +## Phase 8: 1.20.1 (`1.1.0-beta.4`) -- [ ] Decide the target list and loader for each version (see [open decisions](#open-decisions)) -- [ ] 26.2 and 26.1 on Forge: closest to 26.3, mostly renamed methods -- [ ] 1.21.1 on Java 21: older rendering and networking APIs, item stacks with NBT where 26.x - uses data components -- [ ] Legacy importer checked on each version (pack format numbers and the asset layout differ) -- [ ] CI builds and tests every module on every pull request -- [ ] [TESTING.md](TESTING.md) gets a short per-version smoke test: import, fly a helicopter, - drive a tank, fire a missile, join a dedicated server +The largest modpack version, and the furthest from 26.3. -1.20.1 (Java 17) is the next candidate after 1.21.1. It joins 1.1.0 only if it is ready in this -phase; otherwise it moves to 1.2.0 so it does not hold the release back. +- [ ] `common` compiles for Java 17 (no pattern matching `switch`, no `SequencedCollection` + methods) +- [ ] 1.20.1 on Forge and Fabric. NeoForge's 1.20.1 is a fork of Forge 47, so the Forge jar is + tested there instead of built twice +- [ ] Vehicle items keep their vehicle in NBT, since data components only arrived in 1.20.5 +- [ ] Rendering, networking and registration for 1.20.1 +- [ ] Legacy importer and content packs checked on 1.20.1 +- [ ] Same smoke tests as phase 7, for every 1.20.1 jar -## Phase 8: equipment and the rest of phase 4 (`1.1.0-beta.4`) +## Phase 9: equipment and the rest of phase 4 (`1.1.0-beta.5`) Gameplay carried over from phase 4, built once in `common` with per-version glue. @@ -219,7 +256,7 @@ Gameplay carried over from phase 4, built once in `common` with per-version glue - [ ] The drafting table and its recipe browser (pack vehicles included) - [ ] Multiplayer team manager and spotting -## Phase 9: workshop, model, parts and tuning (`1.1.0-beta.5`) +## Phase 10: workshop, model, parts and tuning (`1.1.0-beta.6`) The first half of the [editor](#the-editor). Its output is a pack from phase 6, so anything built here syncs to other players with no extra work. @@ -232,7 +269,7 @@ built here syncs to other players with no extra work. - [ ] Save, export and load packages; undo and redo - [ ] Workshop guide in `docs/content/workshop.md` -## Phase 10: workshop, textures, sounds and sharing (`1.1.0-beta.6`) +## Phase 11: workshop, textures, sounds and sharing (`1.1.0-beta.7`) - [ ] Texture painting on the model and PNG import per part (cockpit textures included), with live preview @@ -242,7 +279,7 @@ built here syncs to other players with no extra work. ## 1.1.0 release -No new features after beta.6; a `1.1.0-rc.N` follows only if beta.6 testing finds problems. +No new features after beta.7; a `1.1.0-rc.N` follows only if beta.7 testing finds problems. - [ ] Every phase checklist done on every supported Minecraft version - [ ] A 1.0.0 world and the beta worlds load with their vehicles and items intact @@ -252,9 +289,6 @@ No new features after beta.6; a `1.1.0-rc.N` follows only if beta.6 testing find ## Open decisions -- **Older versions and loaders.** Which versions are in 1.1.0, and Forge or NeoForge for each - one below 26.x. NeoForge is where most 1.21.1 mods live; Forge keeps one loader for the whole - project. The current plan is 26.2, 26.1 and 1.21.1, with 1.20.1 next. - **Workshop entry point.** A new workshop block, or a tab in the drafting table. - **Pack hosting.** Whether there is ever a shared place to publish packs, or only hand sharing and server sync. From 3155ae5d7333c7f00c65285ed0059272a8a0dd3f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 22:57:08 +0800 Subject: [PATCH 002/314] Point the architecture at the loader and version phases --- docs/ARCHITECTURE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 23ef79a..84bed2f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -41,8 +41,8 @@ Arrows only point down. The bottom half lives in the `common` Gradle module, whi and tested with nothing from Minecraft or Forge on its classpath (only the JDK, SLF4J, JSpecify and JOML), so a stray import fails the build; `CommonBoundaryTest` also scans the sources. The top half is a platform module, one per Minecraft version, which compiles `common`'s sources together -with its own into one jar. Ports to older versions (phase 7) add platform modules and leave -`common` alone; see [DEVELOPMENT.md](DEVELOPMENT.md#common-and-platform-modules). +with its own into one jar. Other versions and loaders (phases 7 and 8) add platform modules +and leave `common` alone; see [DEVELOPMENT.md](DEVELOPMENT.md#common-and-platform-modules). `client` is only referenced from `MCHeli` behind a `Dist.CLIENT` check, so a dedicated server never loads a client class. From 82b90dbaed977d94210f47f1cce25d3d90495ea5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 22:57:08 +0800 Subject: [PATCH 003/314] Update the workshop phase numbers in the modeling guide --- docs/content/modeling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/modeling.md b/docs/content/modeling.md index 4c655bf..9ff67dd 100644 --- a/docs/content/modeling.md +++ b/docs/content/modeling.md @@ -3,7 +3,7 @@ This guide covers the model side of a new vehicle: the part you build outside Minecraft. The vehicle itself (seats, rotors, wings, engine, weapons, flight values) is described by a definition file, see [aircraft.md](aircraft.md). The vehicle workshop planned for 1.1.0 (see -[ROADMAP.md](../ROADMAP.md), phases 9 and 10) will let you do that second half in game by clicking on the +[ROADMAP.md](../ROADMAP.md), phases 10 and 11) will let you do that second half in game by clicking on the model instead of typing coordinates. The model rules below are the same either way, so a model made today keeps working in the workshop. From 2ed02d748e7a642390083800ffc2f1bcfce0d8e5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:14 +0800 Subject: [PATCH 004/314] Use Gradle 9.8.2, which Fabric Loom 1.19 needs --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 54f208b..d4fc8e0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 08b1decc88fc720329a935671fd96bcc88477422 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:20 +0800 Subject: [PATCH 005/314] Resolve the Fabric and NeoForge Gradle plugins and include one module per loader --- settings.gradle | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/settings.gradle b/settings.gradle index ad88f52..141df28 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,19 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven { url = 'https://maven.fabricmc.net/' } + maven { url = 'https://maven.neoforged.net/releases/' } + } + resolutionStrategy { + eachPlugin { + // Loom publishes every plugin id from one artifact, without plugin markers. + if (requested.id.id.startsWith('net.fabricmc.fabric-loom')) { + useModule("net.fabricmc:fabric-loom:${requested.version}") + } + } + } +} + plugins { id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' } @@ -7,8 +23,9 @@ rootProject.name = 'mcheli-rework' // Version-free code: content, models, HUD scripts, flight maths. No Minecraft on its classpath. include 'common' -// One module per Minecraft version, each compiling common's sources together with its own -// rendering, networking, registration and event code. See docs/DEVELOPMENT.md. +// One module per loader and Minecraft version, each compiling common's sources and its Minecraft +// version's shared sources together with its own registration, events and networking. See +// docs/DEVELOPMENT.md. for (String platform : providers.gradleProperty('platforms').get().split(',')) { include platform.trim() } From d9f32580a139fc5fd87cf2d3b79465f9b4ebc5ed Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:20 +0800 Subject: [PATCH 006/314] Declare the NeoForge and Loom plugins and ship every loader jar from releaseJars --- build.gradle | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index e4aa2c3..8be37ce 100644 --- a/build.gradle +++ b/build.gradle @@ -1,15 +1,24 @@ plugins { id 'base' id 'net.minecraftforge.gradle' version '[7.0.17,8)' apply false + id 'net.neoforged.moddev' version '2.0.148' apply false + // Loom 1.19 needs Gradle 9.8. The plain plugin is for unobfuscated Minecraft (26.x), the remap + // one for versions that still ship obfuscated (1.21.1 and older). + id 'net.fabricmc.fabric-loom' version '1.19.3' apply false + id 'net.fabricmc.fabric-loom-remap' version '1.19.3' apply false } def platforms = providers.gradleProperty('platforms').get().split(',').collect { it.trim() } -// One jar per Minecraft version, gathered in one folder for the release workflow. +// One jar per loader and Minecraft version, gathered in one folder for the release workflow. def releaseJars = tasks.register('releaseJars', Sync) { group = 'build' - description = 'Collects the jar of every platform module into build/release.' - platforms.each { platform -> from(project(":$platform").tasks.named('jar')) } + description = 'Collects the jar of every loader module into build/release.' + platforms.each { platform -> + // On the obfuscated versions (1.x) Fabric's shippable jar is the remapped one. + def jarTask = platform.startsWith('fabric-1.') ? 'remapJar' : 'jar' + from(project(":$platform").tasks.named(jarTask)) + } into layout.buildDirectory.dir('release') } From 3c47e4c8fec6927e9f43a393e4beabfaa9f970c1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:21 +0800 Subject: [PATCH 007/314] Build the Forge, NeoForge and Fabric modules for 26.3 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8e32c12..7c676d0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mod_license=MCHeli Rework Licence 1.0 (see LICENSE) # Platform modules built by this project, one per Minecraft version. Each has its own # gradle.properties with the Minecraft, loader and Java versions. -platforms=forge-26.3 +platforms=forge-26.3,neoforge-26.3,fabric-26.3 # Libraries the common module compiles against. Every supported Minecraft version ships them. slf4j_version=2.0.17 From 516313343fe90d2bc3c7b8f5da19a947e97415f1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:21 +0800 Subject: [PATCH 008/314] Share source folders, resources and jar rules between the loader modules --- gradle/loader.gradle | 79 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 gradle/loader.gradle diff --git a/gradle/loader.gradle b/gradle/loader.gradle new file mode 100644 index 0000000..1e856ee --- /dev/null +++ b/gradle/loader.gradle @@ -0,0 +1,79 @@ +// Shared by every loader module: which sources go in, how resources are expanded and what the jar +// may contain. Applied after the loader plugin, with these project properties set in the +// module's gradle.properties: loader, minecraft_version, java_version. + +version = mod_version +group = mod_group + +base { + // The loader and Minecraft version are part of the name, such as mcheli-fabric-26.3-1.1.0.jar. + archivesName = "mcheli-${loader}-${minecraft_version}" +} + +java.toolchain.languageVersion = JavaLanguageVersion.of(java_version as int) + +// common holds the version-free code, minecraft- the code shared by every loader on that +// Minecraft version. Both are compiled into this module, so the mod stays one jar. common checks +// on its own that it needs nothing from Minecraft; compiling minecraft- under three +// loaders keeps it free of any one loader's API. +def shared = rootProject.file("minecraft-${minecraft_version}") +sourceSets.main.java.srcDir(project(':common').file('src/main/java')) +sourceSets.main.resources.srcDir(project(':common').file('src/main/resources')) +sourceSets.main.java.srcDir(new File(shared, 'src/main/java')) +sourceSets.main.resources.srcDir(new File(shared, 'src/main/resources')) + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + options.release = java_version as int + options.compilerArgs.addAll(['-Xlint:deprecation', '-Xlint:unchecked']) +} + +def expandProps = [ + mod_id : mod_id, + mod_name : mod_name, + mod_version : version, + mod_authors : mod_authors, + mod_license : mod_license, + minecraft_version : minecraft_version, + minecraft_version_range: minecraft_version_range, + loader_version_range : loader_version_range, + java_version : java_version, +] + +tasks.named('processResources', ProcessResources) { + inputs.properties(expandProps) + filesMatching(['META-INF/mods.toml', 'META-INF/neoforge.mods.toml', 'fabric.mod.json', 'pack.mcmeta']) { + expand expandProps + } +} + +tasks.named('jar', Jar) { + // EMB4's content is never redistributed: players import it from their own copy of the original + // zip (LegacyContent). The repository no longer holds it, but the jar is packed from the + // resource output, which can still have copies from older builds and from the IDE. + def shipped = ['assets/mcheli/items/helicopter.json', 'assets/mcheli/items/plane.json', + 'assets/mcheli/items/tank.json', 'assets/mcheli/items/vehicle.json', + // Our own: the night glow sprite (generated, no EMB4 content) and its particle definition. + 'assets/mcheli/particles/glow.json', 'assets/mcheli/textures/particle/glow.png'] + exclude { FileTreeElement element -> + String path = element.relativePath.pathString + !element.directory && path.startsWith('assets/mcheli/') && !path.startsWith('assets/mcheli/lang/') && + !shipped.contains(path) + } + exclude 'data/mcheli/recipe/**' + includeEmptyDirs = false + // The licence travels with every copy of the jar. + from(rootProject.file('LICENSE')) + manifest { + attributes([ + 'Specification-Title' : mod_id, + 'Specification-Vendor' : 'JnH Projects', + 'Specification-Version' : '1', + 'Implementation-Title' : mod_name, + 'Implementation-Version': version, + 'Implementation-Vendor' : 'JnH Projects', + 'Minecraft-Version' : minecraft_version, + 'Mod-Loader' : loader, + ]) + } +} From 87455f4ea79421a6cbd13b10cbc4f026f57ec88c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:21 +0800 Subject: [PATCH 009/314] Add a loader-free TOML config with the layout Forge's config spec wrote --- .../jnhprojects/mcheli/config/ConfigSpec.java | 384 ++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 common/src/main/java/com/jnhprojects/mcheli/config/ConfigSpec.java diff --git a/common/src/main/java/com/jnhprojects/mcheli/config/ConfigSpec.java b/common/src/main/java/com/jnhprojects/mcheli/config/ConfigSpec.java new file mode 100644 index 0000000..8f8b002 --- /dev/null +++ b/common/src/main/java/com/jnhprojects/mcheli/config/ConfigSpec.java @@ -0,0 +1,384 @@ +package com.jnhprojects.mcheli.config; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; + +/** + * A small TOML config with the layout Forge's config specs wrote: {@code [section]} tables, flat + * keys, comments above each key. Files written by MCHeli Rework 1.0.0 and the first betas keep + * their values. Fabric has no config system of its own, so every loader uses this one. + * + *

Reading never fails: a missing file, an unknown key or a bad value gives the default, and + * {@link #load(Path)} writes the file back with every key and comment in place, like Forge did. + * Until the first load, and after {@link #unload()}, every value is its default. + */ +public final class ConfigSpec { + private final List

sections; + private volatile boolean loaded; + + private ConfigSpec(List
sections) { + this.sections = List.copyOf(sections); + } + + public static Builder builder() { + return new Builder(); + } + + public boolean isLoaded() { + return loaded; + } + + /** Reads {@code file}, or the defaults if it is missing or unreadable, then writes it back complete. */ + public void load(Path file) throws IOException { + Map raw = Files.isRegularFile(file) ? parse(Files.readString(file, StandardCharsets.UTF_8)) : Map.of(); + for (Section section : sections) { + for (Value value : section.values) { + value.read(raw.get(section.name + "." + value.key)); + } + } + loaded = true; + Files.createDirectories(file.toAbsolutePath().getParent()); + Files.writeString(file, write(), StandardCharsets.UTF_8); + } + + /** Back to the defaults, for when the world that owns the file closes. */ + public void unload() { + loaded = false; + for (Section section : sections) { + for (Value value : section.values) { + value.reset(); + } + } + } + + /** The file as {@link #load(Path)} writes it. */ + public String write() { + StringBuilder out = new StringBuilder(); + for (Section section : sections) { + if (!out.isEmpty()) { + out.append('\n'); + } + for (String line : section.comment) { + out.append('#').append(line).append('\n'); + } + out.append('[').append(section.name).append("]\n"); + for (Value value : section.values) { + for (String line : value.comment) { + out.append("\t#").append(line).append('\n'); + } + if (value.range != null) { + out.append("\t#Range: ").append(value.range).append('\n'); + } + out.append('\t').append(value.key).append(" = ").append(value.format()).append('\n'); + } + } + return out.toString(); + } + + /** + * Flattens a TOML document into {@code section.key -> raw value}. Only what this class writes + * is understood: tables, bare keys, and single or multi-line arrays. + */ + static Map parse(String text) { + Map values = new LinkedHashMap<>(); + String section = ""; + String[] lines = text.split("\\R"); + for (int i = 0; i < lines.length; i++) { + String line = stripComment(lines[i]).trim(); + if (line.isEmpty()) { + continue; + } + if (line.startsWith("[") && line.endsWith("]")) { + section = line.substring(1, line.length() - 1).trim(); + continue; + } + int eq = line.indexOf('='); + if (eq <= 0) { + continue; + } + String key = line.substring(0, eq).trim(); + StringBuilder value = new StringBuilder(line.substring(eq + 1).trim()); + // An array may run over several lines. + while (value.toString().startsWith("[") && !closes(value) && i + 1 < lines.length) { + value.append(' ').append(stripComment(lines[++i]).trim()); + } + values.put(section.isEmpty() ? key : section + "." + key, value.toString()); + } + return values; + } + + private static boolean closes(CharSequence array) { + return array.toString().stripTrailing().endsWith("]"); + } + + /** Drops a {@code #} comment, unless the {@code #} is inside a quoted string. */ + private static String stripComment(String line) { + boolean quoted = false; + for (int i = 0; i < line.length(); i++) { + char c = line.charAt(i); + if (c == '"' && (i == 0 || line.charAt(i - 1) != '\\')) { + quoted = !quoted; + } else if (c == '#' && !quoted) { + return line.substring(0, i); + } + } + return line; + } + + private static String quote(String s) { + return '"' + s.replace("\\", "\\\\").replace("\"", "\\\"") + '"'; + } + + private static String unquote(String s) { + String t = s.trim(); + if (t.length() >= 2 && (t.startsWith("\"") && t.endsWith("\"") || t.startsWith("'") && t.endsWith("'"))) { + return t.substring(1, t.length() - 1).replace("\\\"", "\"").replace("\\\\", "\\"); + } + throw new IllegalArgumentException("Not a string: " + s); + } + + private record Section(String name, List comment, List> values) { + } + + /** One setting. {@link #get()} is safe from any thread. */ + public abstract static sealed class Value permits BooleanValue, IntValue, DoubleValue, StringListValue { + private final String key; + private final List comment; + private final T defaultValue; + private final String range; + private volatile T value; + + Value(String key, List comment, T defaultValue, String range) { + this.key = key; + this.comment = comment; + this.defaultValue = defaultValue; + this.range = range; + this.value = defaultValue; + } + + public T get() { + return value; + } + + public T getDefault() { + return defaultValue; + } + + void read(String raw) { + if (raw == null) { + value = defaultValue; + return; + } + try { + value = Objects.requireNonNull(parse(raw.trim())); + } catch (RuntimeException e) { + value = defaultValue; + } + } + + void reset() { + value = defaultValue; + } + + abstract T parse(String raw); + + abstract String format(); + } + + public static final class BooleanValue extends Value { + BooleanValue(String key, List comment, boolean defaultValue) { + super(key, comment, defaultValue, null); + } + + @Override + Boolean parse(String raw) { + return switch (raw.toLowerCase(Locale.ROOT)) { + case "true" -> true; + case "false" -> false; + default -> throw new IllegalArgumentException(raw); + }; + } + + @Override + String format() { + return get().toString(); + } + } + + public static final class IntValue extends Value { + private final int min; + private final int max; + + IntValue(String key, List comment, int defaultValue, int min, int max) { + super(key, comment, defaultValue, min + " ~ " + max); + this.min = min; + this.max = max; + } + + @Override + Integer parse(String raw) { + int v = Integer.parseInt(raw); + if (v < min || v > max) { + throw new IllegalArgumentException(raw); + } + return v; + } + + @Override + String format() { + return get().toString(); + } + } + + public static final class DoubleValue extends Value { + private final double min; + private final double max; + + DoubleValue(String key, List comment, double defaultValue, double min, double max) { + super(key, comment, defaultValue, min + " ~ " + max); + this.min = min; + this.max = max; + } + + @Override + Double parse(String raw) { + double v = Double.parseDouble(raw); + if (!(v >= min && v <= max)) { + throw new IllegalArgumentException(raw); + } + return v; + } + + @Override + String format() { + return get().toString(); + } + } + + public static final class StringListValue extends Value> { + StringListValue(String key, List comment, List defaultValue) { + super(key, comment, List.copyOf(defaultValue), null); + } + + @Override + List parse(String raw) { + if (!raw.startsWith("[") || !raw.endsWith("]")) { + throw new IllegalArgumentException(raw); + } + String body = raw.substring(1, raw.length() - 1).trim(); + List items = new ArrayList<>(); + if (body.isEmpty()) { + return List.of(); + } + for (String item : splitArray(body)) { + if (!item.isBlank()) { + items.add(unquote(item)); + } + } + return List.copyOf(items); + } + + /** Splits on commas outside quotes. */ + private static List splitArray(String body) { + List parts = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + boolean quoted = false; + for (int i = 0; i < body.length(); i++) { + char c = body.charAt(i); + if (c == '"' && (i == 0 || body.charAt(i - 1) != '\\')) { + quoted = !quoted; + } + if (c == ',' && !quoted) { + parts.add(current.toString()); + current.setLength(0); + } else { + current.append(c); + } + } + parts.add(current.toString()); + return parts; + } + + @Override + String format() { + List quoted = new ArrayList<>(); + for (String s : get()) { + quoted.add(quote(s)); + } + return "[" + String.join(", ", quoted) + "]"; + } + } + + /** Collects sections and values in file order. */ + public static final class Builder { + private final List
sections = new ArrayList<>(); + private Section current; + private List pendingComment = List.of(); + + private Builder() { + } + + /** The comment for the next section or value. */ + public Builder comment(String... lines) { + List spaced = new ArrayList<>(); + for (String line : lines) { + spaced.add(" " + line); + } + pendingComment = List.copyOf(spaced); + return this; + } + + public Builder push(String section) { + current = new Section(section, takeComment(), new ArrayList<>()); + sections.add(current); + return this; + } + + public Builder pop() { + current = null; + return this; + } + + public BooleanValue define(String key, boolean defaultValue) { + return add(new BooleanValue(key, takeComment(), defaultValue)); + } + + public IntValue defineInRange(String key, int defaultValue, int min, int max) { + return add(new IntValue(key, takeComment(), defaultValue, min, max)); + } + + public DoubleValue defineInRange(String key, double defaultValue, double min, double max) { + return add(new DoubleValue(key, takeComment(), defaultValue, min, max)); + } + + public StringListValue defineList(String key, List defaultValue) { + return add(new StringListValue(key, takeComment(), defaultValue)); + } + + public ConfigSpec build() { + return new ConfigSpec(sections); + } + + private List takeComment() { + List comment = pendingComment; + pendingComment = List.of(); + return comment; + } + + private > V add(V value) { + if (current == null) { + throw new IllegalStateException("Values belong in a section; call push first"); + } + current.values.add(value); + return value; + } + } +} From c645656ca3a653fa6746dc0a2093cea19c98aca0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:22 +0800 Subject: [PATCH 010/314] Test reading Forge-written configs, bad values and the written file --- .../mcheli/config/ConfigSpecTest.java | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 common/src/test/java/com/jnhprojects/mcheli/config/ConfigSpecTest.java diff --git a/common/src/test/java/com/jnhprojects/mcheli/config/ConfigSpecTest.java b/common/src/test/java/com/jnhprojects/mcheli/config/ConfigSpecTest.java new file mode 100644 index 0000000..bb9ae00 --- /dev/null +++ b/common/src/test/java/com/jnhprojects/mcheli/config/ConfigSpecTest.java @@ -0,0 +1,110 @@ +package com.jnhprojects.mcheli.config; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ConfigSpecTest { + private final ConfigSpec.Builder builder = ConfigSpec.builder(); + private final ConfigSpec.BooleanValue breakBlocks; + private final ConfigSpec.DoubleValue damageFactor; + private final ConfigSpec.StringListValue disabled; + private final ConfigSpec.IntValue maxPackSize; + private final ConfigSpec spec; + + ConfigSpecTest() { + builder.push("explosions"); + breakBlocks = builder.comment("Whether weapons break blocks.").define("breakBlocks", true); + builder.pop(); + builder.push("vehicles"); + damageFactor = builder.defineInRange("damageFactor", 1.0, 0.0, 100.0); + disabled = builder.defineList("disabled", List.of()); + builder.pop(); + builder.push("packs"); + maxPackSize = builder.defineInRange("maxPackSize", 8, 1, 512); + builder.pop(); + spec = builder.build(); + } + + @Test + void readsTheFileForgeWrote(@TempDir Path dir) throws IOException { + Path file = dir.resolve("mcheli-server.toml"); + Files.writeString(file, """ + [explosions] + #Whether weapons and crashing vehicles break blocks. + breakBlocks = false + + [vehicles] + #Range: 0.0 ~ 100.0 + damageFactor = 0.5 + disabled = ["ah-64", "f-35b"] + + [packs] + maxPackSize = 16 + """, StandardCharsets.UTF_8); + + spec.load(file); + + assertFalse(breakBlocks.get()); + assertEquals(0.5, damageFactor.get()); + assertEquals(List.of("ah-64", "f-35b"), disabled.get()); + assertEquals(16, maxPackSize.get()); + } + + @Test + void badValuesFallBackToTheDefault(@TempDir Path dir) throws IOException { + Path file = dir.resolve("bad.toml"); + Files.writeString(file, """ + [explosions] + breakBlocks = maybe + [vehicles] + damageFactor = 1000.0 + disabled = [ "a", + "b # not a comment" ] + [packs] + maxPackSize = 0 + """, StandardCharsets.UTF_8); + + spec.load(file); + + assertTrue(breakBlocks.get()); + assertEquals(1.0, damageFactor.get()); + assertEquals(List.of("a", "b # not a comment"), disabled.get()); + assertEquals(8, maxPackSize.get()); + } + + @Test + void writesEveryKeyBackAndReadsItAgain(@TempDir Path dir) throws IOException { + Path file = dir.resolve("sub").resolve("new.toml"); + + spec.load(file); + String written = Files.readString(file, StandardCharsets.UTF_8); + spec.load(file); + + assertTrue(written.contains("\t# Whether weapons break blocks.\n\tbreakBlocks = true\n"), written); + assertTrue(written.contains("\t#Range: 1 ~ 512\n\tmaxPackSize = 8\n"), written); + assertEquals(written, Files.readString(file, StandardCharsets.UTF_8)); + } + + @Test + void unloadingGoesBackToTheDefaults(@TempDir Path dir) throws IOException { + Path file = dir.resolve("c.toml"); + Files.writeString(file, "[packs]\nmaxPackSize = 32\n", StandardCharsets.UTF_8); + spec.load(file); + assertTrue(spec.isLoaded()); + + spec.unload(); + + assertFalse(spec.isLoaded()); + assertEquals(8, maxPackSize.get()); + } +} From 1688f0f086e09a3e921c95554a388f684cdc9cd9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:22 +0800 Subject: [PATCH 011/314] Name the vehicle ingredient type under the Forge, NeoForge and Fabric keys --- .../mcheli/content/recipe/LegacyRecipe.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/com/jnhprojects/mcheli/content/recipe/LegacyRecipe.java b/common/src/main/java/com/jnhprojects/mcheli/content/recipe/LegacyRecipe.java index 79accd8..9e9af5c 100644 --- a/common/src/main/java/com/jnhprojects/mcheli/content/recipe/LegacyRecipe.java +++ b/common/src/main/java/com/jnhprojects/mcheli/content/recipe/LegacyRecipe.java @@ -37,6 +37,12 @@ private LegacyRecipe() { public static final String VEHICLE_COMPONENT = "mcheli:vehicle"; /** Ingredient type matching one vehicle, since every vehicle of a kind shares an item. */ public static final String VEHICLE_INGREDIENT = "mcheli:vehicle"; + /** + * The keys naming a custom ingredient's type on Forge, NeoForge and Fabric. A vehicle + * ingredient carries all three, so the imported recipes work whichever loader reads them; each + * loader ignores the other two. + */ + public static final List INGREDIENT_TYPE_KEYS = List.of("type", "neoforge:ingredient_type", "fabric:type"); /** * @param result the crafted vehicle @@ -179,7 +185,11 @@ private static String ingredient(String name, int damage, Predicate modI if (!modItems.test(lower)) { throw new InvalidRecipeException(lower + " does not exist (yet)"); } - return "{\"type\": \"" + VEHICLE_INGREDIENT + "\", \"vehicle\": \"" + lower.substring("mcheli:".length()) + "\"}"; + StringBuilder json = new StringBuilder("{"); + for (String key : INGREDIENT_TYPE_KEYS) { + json.append('"').append(key).append("\": \"").append(VEHICLE_INGREDIENT).append("\", "); + } + return json.append("\"vehicle\": \"").append(lower.substring("mcheli:".length())).append("\"}").toString(); } String mapped = LegacyItems.ingredient(lower, damage); if (mapped == null) { From 531a978c17d7d2d926c158a12ac5d70f75381c74 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:22 +0800 Subject: [PATCH 012/314] Expect the vehicle ingredient with all three loader keys --- .../java/com/jnhprojects/mcheli/content/RecipeDataTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/test/java/com/jnhprojects/mcheli/content/RecipeDataTest.java b/common/src/test/java/com/jnhprojects/mcheli/content/RecipeDataTest.java index 41dafb3..0d4f911 100644 --- a/common/src/test/java/com/jnhprojects/mcheli/content/RecipeDataTest.java +++ b/common/src/test/java/com/jnhprojects/mcheli/content/RecipeDataTest.java @@ -64,7 +64,7 @@ void aVehicleIngredientMatchesThatVehicleOnly() { "category": "misc", "key": { "R": "#c:dusts/redstone", - "X": {"type": "mcheli:vehicle", "vehicle": "ah-6"} + "X": {"type": "mcheli:vehicle", "neoforge:ingredient_type": "mcheli:vehicle", "fabric:type": "mcheli:vehicle", "vehicle": "ah-6"} }, "pattern": [ "RXR" From 9755257fb506d4fa37a2ddbec9e3c0ff19b3a8bc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:23 +0800 Subject: [PATCH 013/314] Bump the import format so existing installs get the new recipes --- .../java/com/jnhprojects/mcheli/content/LegacyImporter.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/com/jnhprojects/mcheli/content/LegacyImporter.java b/common/src/main/java/com/jnhprojects/mcheli/content/LegacyImporter.java index 1daae4b..c17001f 100644 --- a/common/src/main/java/com/jnhprojects/mcheli/content/LegacyImporter.java +++ b/common/src/main/java/com/jnhprojects/mcheli/content/LegacyImporter.java @@ -44,9 +44,10 @@ public final class LegacyImporter { /** * Bump whenever the output changes, so existing installs import again. 2: recipes craft the - * generic vehicle items of 1.1.0. + * generic vehicle items of 1.1.0. 3: vehicle ingredients name + * their type for every loader. */ - public static final int FORMAT = 2; + public static final int FORMAT = 3; private static final String ASSET_ROOT = "mcheli/assets/mcheli/"; private static final Set SKIPPED_EXTENSIONS = Set.of("xcf"); From 34e50f59c04e4d0f508502ff10cb4dab9e0f9c78 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:23 +0800 Subject: [PATCH 014/314] Add the Platform interface the loader modules implement --- .../jnhprojects/mcheli/platform/Platform.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 minecraft-26.3/src/main/java/com/jnhprojects/mcheli/platform/Platform.java diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/platform/Platform.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/platform/Platform.java new file mode 100644 index 0000000..e951f49 --- /dev/null +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/platform/Platform.java @@ -0,0 +1,66 @@ +package com.jnhprojects.mcheli.platform; + +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; + +import java.nio.file.Path; +import java.util.Objects; + +/** + * What the shared code needs from the mod loader. Each loader module installs its implementation + * first thing during mod construction, before anything else of ours runs. + * + *

Kept small on purpose: registration and events go the other way, with the loader module + * calling into the shared code (see {@code Registration} and the {@code *Hooks} classes). + */ +public interface Platform { + + static Platform get() { + Platform platform = Holder.instance; + if (platform == null) { + throw new IllegalStateException("No mod loader platform installed yet"); + } + return platform; + } + + static void install(Platform platform) { + if (Holder.instance != null) { + throw new IllegalStateException("Platform already installed: " + Holder.instance.loaderName()); + } + Holder.instance = Objects.requireNonNull(platform); + } + + /** {@code forge}, {@code neoforge} or {@code fabric}, for logs and {@code /mcheli status}. */ + String loaderName(); + + /** The game directory, which holds the {@code mcheli} folder. */ + Path gameDir(); + + /** The config directory, which holds the client config. */ + Path configDir(); + + /** Our version as the loader read it from the mod metadata. */ + String modVersion(); + + /** Whether this is a physical client, as opposed to a dedicated server. */ + boolean isClient(); + + void sendToServer(CustomPacketPayload payload); + + void sendToPlayer(ServerPlayer player, CustomPacketPayload payload); + + /** + * A builder for one of our creative tabs, placed after {@code after}. Vanilla's builder is not + * public on every loader and only Forge and NeoForge can order tabs. + */ + CreativeModeTab.Builder creativeTab(ResourceKey after); + + final class Holder { + private static volatile Platform instance; + + private Holder() { + } + } +} From 6ce8747f8c863945dc6c65a23bf2b8063b2c9e29 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:23 +0800 Subject: [PATCH 015/314] Add Registered, a registry entry the loader module fills in --- .../mcheli/platform/Registered.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 minecraft-26.3/src/main/java/com/jnhprojects/mcheli/platform/Registered.java diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/platform/Registered.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/platform/Registered.java new file mode 100644 index 0000000..3d1b908 --- /dev/null +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/platform/Registered.java @@ -0,0 +1,35 @@ +package com.jnhprojects.mcheli.platform; + +import net.minecraft.resources.Identifier; + +import java.util.function.Supplier; + +/** + * A registry entry the shared code declares and the loader module registers, like Forge's + * {@code RegistryObject}. {@link #get()} works once the loader has registered it. + */ +public final class Registered implements Supplier { + private final Identifier id; + private volatile T value; + + Registered(Identifier id) { + this.id = id; + } + + public Identifier id() { + return id; + } + + @Override + public T get() { + T v = value; + if (v == null) { + throw new IllegalStateException("Registry entry " + id + " is not registered yet"); + } + return v; + } + + void bind(T value) { + this.value = value; + } +} From 9136f90068988bba0e65d9e45a535fad6a5a4f54 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:24 +0800 Subject: [PATCH 016/314] Collect registry entries for the loader module to register --- .../mcheli/platform/Registration.java | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 minecraft-26.3/src/main/java/com/jnhprojects/mcheli/platform/Registration.java diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/platform/Registration.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/platform/Registration.java new file mode 100644 index 0000000..030a07d --- /dev/null +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/platform/Registration.java @@ -0,0 +1,80 @@ +package com.jnhprojects.mcheli.platform; + +import com.jnhprojects.mcheli.MCHeli; +import net.minecraft.core.Registry; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * Every vanilla registry entry the mod adds. The shared registry classes declare their entries + * here during mod construction; the loader module then registers them its own way: Forge and + * NeoForge from their register event, Fabric straight away. + */ +public final class Registration { + private static final List> PENDING = new ArrayList<>(); + private static final Set>> REGISTRIES = new LinkedHashSet<>(); + + private Registration() { + } + + /** + * Declares an entry. + * + * @param factory makes the value; it gets the entry's key for {@code Item.Properties#setId} + * and the like + */ + public static synchronized Registered register(ResourceKey> registry, String name, + Function, T> factory) { + Identifier id = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, name); + Registered entry = new Registered<>(id); + PENDING.add(new Pending<>(registry, entry, factory)); + REGISTRIES.add(registry); + return entry; + } + + /** The registries with entries of ours, in the order they were first used. */ + public static synchronized List>> registries() { + return List.copyOf(REGISTRIES); + } + + /** + * A key from {@link #registries()} with its element type filled in, for the loader's generic + * register calls. The type is whatever the caller needs; entries are only ever handed to the + * registry they were declared for. + */ + @SuppressWarnings("unchecked") + public static ResourceKey> typed(ResourceKey> registry) { + return (ResourceKey>) registry; + } + + /** + * Creates our entries for one registry, in declaration order, and hands each to the loader. + * Called once per registry. + */ + public static synchronized void registerAll(ResourceKey> registry, BiConsumer sink) { + for (Pending pending : PENDING) { + if (pending.registry.equals(registry)) { + @SuppressWarnings("unchecked") + Pending ours = (Pending) pending; + ours.create(sink); + } + } + } + + private record Pending(ResourceKey> registry, Registered entry, + Function, T> factory) { + @SuppressWarnings("unchecked") + void create(BiConsumer sink) { + T value = factory.apply(ResourceKey.create((ResourceKey>) registry, entry.id())); + entry.bind(value); + sink.accept(entry.id(), value); + } + } +} From df3e916685eeb6a0029d32c9d789ec1d1df24be0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:42 +0800 Subject: [PATCH 017/314] Move ClientConfig to the shared sources on the shared config format --- .../com/jnhprojects/mcheli/ClientConfig.java | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/ClientConfig.java (66%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/ClientConfig.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/ClientConfig.java similarity index 66% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/ClientConfig.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/ClientConfig.java index 299d482..ff25884 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/ClientConfig.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/ClientConfig.java @@ -1,19 +1,22 @@ package com.jnhprojects.mcheli; -import net.minecraftforge.common.ForgeConfigSpec; +import com.jnhprojects.mcheli.config.ConfigSpec; + +import java.io.IOException; +import java.nio.file.Path; /** * Per player look and feel, in {@code config/mcheli-client.toml}. Nothing here changes how the * game plays, so each player picks for themselves and the server never sees it. */ public final class ClientConfig { - public static final ForgeConfigSpec SPEC; + public static final ConfigSpec SPEC; - private static final ForgeConfigSpec.BooleanValue NIGHT_GLOW; - private static final ForgeConfigSpec.IntValue PACK_CACHE_SIZE; + private static final ConfigSpec.BooleanValue NIGHT_GLOW; + private static final ConfigSpec.IntValue PACK_CACHE_SIZE; static { - ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder(); + ConfigSpec.Builder builder = ConfigSpec.builder(); builder.push("effects"); NIGHT_GLOW = builder @@ -36,6 +39,16 @@ public final class ClientConfig { private ClientConfig() { } + /** Reads {@code config/mcheli-client.toml}, writing it with the defaults if it is new. */ + public static void load(Path configDir) { + Path file = configDir.resolve(MCHeli.MOD_ID + "-client.toml"); + try { + SPEC.load(file); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not read {}; using the defaults", file, e); + } + } + public static boolean nightGlow() { return SPEC.isLoaded() ? NIGHT_GLOW.get() : NIGHT_GLOW.getDefault(); } From 3f1c04437b42ec4265fa2e5b52910721e42d9fd2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:42 +0800 Subject: [PATCH 018/314] Move ContentPacks to the shared 26.3 sources --- .../src/main/java/com/jnhprojects/mcheli/ContentPacks.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/ContentPacks.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/ContentPacks.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/ContentPacks.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/ContentPacks.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/ContentPacks.java From f12480b98067d88492069e1884dea2890a4e5a5e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:43 +0800 Subject: [PATCH 019/314] Move ContentReload to the shared sources and reread the server config --- .../src/main/java/com/jnhprojects/mcheli/ContentReload.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/ContentReload.java (75%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/ContentReload.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/ContentReload.java similarity index 75% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/ContentReload.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/ContentReload.java index 44df1d1..7846b7b 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/ContentReload.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/ContentReload.java @@ -3,11 +3,12 @@ import com.jnhprojects.mcheli.content.ContentRegistry; import com.jnhprojects.mcheli.network.PackSync; import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.storage.LevelResource; /** * Reads the vehicle and weapon definitions from disk again while the game runs, for - * {@code /mcheli reload}: the imported content and the content packs, with the server config - * applied. Vehicles in the world pick up their new definition on their next tick (see + * {@code /mcheli reload}: the server config, then the imported content and the content packs. + * Vehicles in the world pick up their new definition on their next tick (see * {@code AircraftEntity#definition}). Every player gets the new pack list, downloads what changed * and rebuilds their content the same way. */ @@ -17,6 +18,7 @@ private ContentReload() { /** @return the newly loaded content, already in use */ public static ContentRegistry reload(MinecraftServer server) { + ServerConfig.load(server.getWorldPath(LevelResource.ROOT)); ContentRegistry fresh = MCHeli.packs().load(true); MCHeli.content().replaceWith(fresh); MCHeli.LOGGER.info("Content reloaded: {} vehicles, {} weapons", fresh.all().size(), fresh.weapons().size()); From 6d3c53a8e34f8d48b464286569804a632a672ba3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:43 +0800 Subject: [PATCH 020/314] Move LegacyContent to the shared sources and hand out the pack instead of registering it --- .../com/jnhprojects/mcheli/LegacyContent.java | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/LegacyContent.java (88%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/LegacyContent.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/LegacyContent.java similarity index 88% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/LegacyContent.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/LegacyContent.java index b98c15e..db29fdf 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/LegacyContent.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/LegacyContent.java @@ -9,7 +9,6 @@ import net.minecraft.server.packs.repository.PackCompatibility; import net.minecraft.server.packs.repository.PackSource; import net.minecraft.world.flag.FeatureFlagSet; -import net.minecraftforge.event.AddPackFindersEvent; import org.jspecify.annotations.Nullable; import java.io.IOException; @@ -76,22 +75,22 @@ public Optional problem() { return Optional.ofNullable(problem); } - /** Adds the imported folder to both the client resource packs and the server data packs. */ - void registerPacks() { - AddPackFindersEvent.BUS.addListener(event -> { - if (!Files.isDirectory(packRoot)) { - return; - } - PackLocationInfo location = new PackLocationInfo(PACK_ID, - Component.translatable("pack.mcheli.content"), PackSource.BUILT_IN, Optional.empty()); - Pack.Metadata metadata = new Pack.Metadata(Component.translatable("pack.mcheli.content.description"), - PackCompatibility.COMPATIBLE, FeatureFlagSet.of(), List.of()); - // Required, so it is enabled in every world and cannot be switched off. It sits at the - // bottom so that regular resource packs can still replace its textures and models. - Pack pack = new Pack(location, new PathPackResources.PathResourcesSupplier(packRoot), metadata, - new PackSelectionConfig(true, Pack.Position.BOTTOM, false)); - event.addRepositorySource(onLoad -> onLoad.accept(pack)); - }); + /** + * The imported folder as a pack, for both the client resource packs and the server data packs. + * Empty when nothing was imported. + */ + public Optional pack() { + if (!Files.isDirectory(packRoot)) { + return Optional.empty(); + } + PackLocationInfo location = new PackLocationInfo(PACK_ID, + Component.translatable("pack.mcheli.content"), PackSource.BUILT_IN, Optional.empty()); + Pack.Metadata metadata = new Pack.Metadata(Component.translatable("pack.mcheli.content.description"), + PackCompatibility.COMPATIBLE, FeatureFlagSet.of(), List.of()); + // Required, so it is enabled in every world and cannot be switched off. It sits at the + // bottom so that regular resource packs can still replace its textures and models. + return Optional.of(new Pack(location, new PathPackResources.PathResourcesSupplier(packRoot), metadata, + new PackSelectionConfig(true, Pack.Position.BOTTOM, false))); } private void importIfNeeded() throws IOException { From 80c940e8b9d4ac62fc210ab012593369d4d5276f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:43 +0800 Subject: [PATCH 021/314] Move McheliCommand to the shared sources and take the dispatcher from the loader --- .../main/java/com/jnhprojects/mcheli/McheliCommand.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/McheliCommand.java (96%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/McheliCommand.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/McheliCommand.java similarity index 96% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/McheliCommand.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/McheliCommand.java index 0e4c226..78a3612 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/McheliCommand.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/McheliCommand.java @@ -17,7 +17,6 @@ import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerPlayer; import net.minecraft.util.Prediction; -import net.minecraftforge.event.RegisterCommandsEvent; import java.util.Collection; import java.util.List; @@ -43,11 +42,7 @@ public final class McheliCommand { private McheliCommand() { } - public static void register() { - RegisterCommandsEvent.BUS.addListener(event -> register(event.getDispatcher())); - } - - private static void register(CommandDispatcher dispatcher) { + public static void register(CommandDispatcher dispatcher) { dispatcher.register(Commands.literal(MCHeli.MOD_ID) .executes(McheliCommand::status) .then(Commands.literal("list") From 28c861ddf784da67fa95972c042acbbc8ead3f11 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:44 +0800 Subject: [PATCH 022/314] Move ModSounds to the shared 26.3 sources --- .../src/main/java/com/jnhprojects/mcheli/ModSounds.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/ModSounds.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/ModSounds.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/ModSounds.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/ModSounds.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/ModSounds.java From 865aa42a69e1e0ba06d4aa3613601751cc674d36 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:44 +0800 Subject: [PATCH 023/314] Move ServerConfig to the shared sources on the shared config format --- .../com/jnhprojects/mcheli/ServerConfig.java | 48 +++++++++++++------ 1 file changed, 33 insertions(+), 15 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/ServerConfig.java (73%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/ServerConfig.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/ServerConfig.java similarity index 73% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/ServerConfig.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/ServerConfig.java index ab5e7db..eef7dc3 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/ServerConfig.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/ServerConfig.java @@ -1,32 +1,35 @@ package com.jnhprojects.mcheli; -import net.minecraftforge.common.ForgeConfigSpec; +import com.jnhprojects.mcheli.config.ConfigSpec; +import java.io.IOException; +import java.nio.file.Path; import java.util.List; import java.util.Locale; /** * Server rules, the part of the original {@code mcheli.cfg} that decides how the game plays. - * Forge keeps the file per world in {@code serverconfig/mcheli-server.toml}, sends nothing to - * clients (only the server applies these rules) and picks up edits while the server runs. + * The file lives per world in {@code serverconfig/mcheli-server.toml}, where Forge kept it, and is + * never sent to clients (only the server applies these rules). It is read when the world starts + * and again on {@code /mcheli reload}. * *

The values only exist while a world is loaded. Before that, and on clients, the getters return * the defaults, which match 1.0.0. */ public final class ServerConfig { - public static final ForgeConfigSpec SPEC; + public static final ConfigSpec SPEC; - private static final ForgeConfigSpec.BooleanValue EXPLOSIONS_BREAK_BLOCKS; - private static final ForgeConfigSpec.BooleanValue EXPLOSIONS_START_FIRES; - private static final ForgeConfigSpec.DoubleValue VEHICLE_DAMAGE_FACTOR; - private static final ForgeConfigSpec.BooleanValue INFINITE_AMMO; - private static final ForgeConfigSpec.ConfigValue> DISABLED_VEHICLES; - private static final ForgeConfigSpec.ConfigValue> PACK_ALLOW_LIST; - private static final ForgeConfigSpec.IntValue MAX_PACK_SIZE; - private static final ForgeConfigSpec.IntValue MAX_TOTAL_SIZE; + private static final ConfigSpec.BooleanValue EXPLOSIONS_BREAK_BLOCKS; + private static final ConfigSpec.BooleanValue EXPLOSIONS_START_FIRES; + private static final ConfigSpec.DoubleValue VEHICLE_DAMAGE_FACTOR; + private static final ConfigSpec.BooleanValue INFINITE_AMMO; + private static final ConfigSpec.StringListValue DISABLED_VEHICLES; + private static final ConfigSpec.StringListValue PACK_ALLOW_LIST; + private static final ConfigSpec.IntValue MAX_PACK_SIZE; + private static final ConfigSpec.IntValue MAX_TOTAL_SIZE; static { - ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder(); + ConfigSpec.Builder builder = ConfigSpec.builder(); builder.push("explosions"); EXPLOSIONS_BREAK_BLOCKS = builder @@ -45,7 +48,7 @@ public final class ServerConfig { DISABLED_VEHICLES = builder .comment("Vehicles that cannot be placed, by definition name (the file name, such as \"ah-64\").", "Vehicles already in the world stay.") - .defineListAllowEmpty("disabled", List.of(), o -> o instanceof String); + .defineList("disabled", List.of()); builder.pop(); builder.push("weapons"); @@ -59,7 +62,7 @@ public final class ServerConfig { .push("packs"); PACK_ALLOW_LIST = builder .comment("Pack ids the server loads, such as \"rework-example\". Empty loads every pack in the folder.") - .defineListAllowEmpty("allowList", List.of(), o -> o instanceof String); + .defineList("allowList", List.of()); MAX_PACK_SIZE = builder .comment("Largest pack the server loads and sends to players, in megabytes.") .defineInRange("maxPackSize", 8, 1, 512); @@ -75,6 +78,21 @@ public final class ServerConfig { private ServerConfig() { } + /** Reads the world's file, writing it with the defaults if it is new. */ + public static void load(Path worldDir) { + Path file = worldDir.resolve("serverconfig").resolve(MCHeli.MOD_ID + "-server.toml"); + try { + SPEC.load(file); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not read {}; using the defaults", file, e); + } + } + + /** The world closed: back to the defaults. */ + public static void unload() { + SPEC.unload(); + } + public static boolean explosionsBreakBlocks() { return SPEC.isLoaded() ? EXPLOSIONS_BREAK_BLOCKS.get() : EXPLOSIONS_BREAK_BLOCKS.getDefault(); } From bd62d936ccd37f6513da784c44ff47b059d68906 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:45 +0800 Subject: [PATCH 024/314] Move ClientContentReload to the shared 26.3 sources --- .../java/com/jnhprojects/mcheli/client/ClientContentReload.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java From c4e10be3ccba3f357e0ee42da0b07becd6030ab2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:45 +0800 Subject: [PATCH 025/314] Move ClientPacks to the shared sources and get the game folder from the platform --- .../main/java/com/jnhprojects/mcheli/client/ClientPacks.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java (98%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java similarity index 98% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java index 6679322..13f2bad 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java @@ -13,7 +13,7 @@ import com.jnhprojects.mcheli.network.PackRequestPacket; import net.minecraft.client.Minecraft; import net.minecraft.network.chat.Component; -import net.minecraftforge.fml.loading.FMLPaths; +import com.jnhprojects.mcheli.platform.Platform; import org.jspecify.annotations.Nullable; import java.io.IOException; @@ -52,7 +52,7 @@ final class ClientPacks implements ClientAccess.PackReceiver { private static final Pattern CACHED = Pattern.compile("[0-9a-f]{64}\\.zip"); - private final Path cache = FMLPaths.GAMEDIR.get().resolve(MCHeli.MOD_ID).resolve("cache"); + private final Path cache = Platform.get().gameDir().resolve(MCHeli.MOD_ID).resolve("cache"); private List wanted = List.of(); private final Map downloads = new LinkedHashMap<>(); /** Packs this class opened for the current server; closed when the next list replaces them. */ From 800b4ab7e61d1b0662249c452f2952011c444a4b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:45 +0800 Subject: [PATCH 026/314] Move CrewController to the shared sources and take the camera from any loader --- .../mcheli/client/CrewController.java | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/client/CrewController.java (94%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/CrewController.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/CrewController.java similarity index 94% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/client/CrewController.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/CrewController.java index 25c6bfb..f1cce61 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/CrewController.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/CrewController.java @@ -24,15 +24,12 @@ import net.minecraft.util.Mth; import net.minecraft.world.entity.player.Input; import net.minecraft.world.phys.Vec3; -import net.minecraftforge.client.event.RenderHandEvent; -import net.minecraftforge.client.event.ViewportEvent; -import net.minecraftforge.event.TickEvent; -import net.minecraftforge.fml.util.ObfuscationReflectionHelper; import org.joml.Vector3f; import org.jspecify.annotations.Nullable; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; /** * Turns local player input into vehicle control, for whichever seat the player occupies. @@ -68,17 +65,11 @@ final class CrewController { private CrewController() { } - static void init() { - TickEvent.ClientTickEvent.Post.BUS.addListener(event -> onClientTick()); - ViewportEvent.ComputeCameraAngles.BUS.addListener(CrewController::onCameraAngles); - RenderHandEvent.BUS.addListener(CrewController::onRenderHand); - } - /** * Like the original, the first person hand stays hidden in a vehicle: always for the pilot, * and for anyone else who looks through a camera or has a weapon to aim. */ - private static boolean onRenderHand(RenderHandEvent event) { + static boolean hidesHand() { LocalPlayer player = Minecraft.getInstance().player; AircraftEntity vehicle = vehicleOf(player); if (vehicle == null) { @@ -98,7 +89,7 @@ private static boolean onRenderHand(RenderHandEvent event) { return player != null && player.getVehicle() instanceof AircraftEntity vehicle && vehicle.definition() != null ? vehicle : null; } - private static void onClientTick() { + static void onClientTick() { ticks++; Minecraft mc = Minecraft.getInstance(); LocalPlayer player = mc.player; @@ -245,7 +236,7 @@ private static boolean isFreeLook(AircraftEntity vehicle) { return held; } - private static void onCameraAngles(ViewportEvent.ComputeCameraAngles event) { + static void onCameraAngles(CameraAngles event) { Minecraft mc = Minecraft.getInstance(); LocalPlayer player = mc.player; AircraftEntity vehicle = vehicleOf(player); @@ -257,7 +248,7 @@ private static void onCameraAngles(ViewportEvent.ComputeCameraAngles event) { if (seat < 0 || seat >= def.seatCount()) { return; } - float partialTicks = event.getPartialTick(); + float partialTicks = event.partialTick(); boolean firstPerson = mc.options.getCameraType().isFirstPerson(); if (seat == 0 && vehicle.steersWithMouse() && !vehicle.isGunnerMode() && vehicle.getControllingPassenger() == player) { @@ -271,13 +262,13 @@ private static void onCameraAngles(ViewportEvent.ComputeCameraAngles event) { if (!(firstPerson && missile != null)) { // Every seat of every kind tilts with the hull, measured along the way the rider looks. event.setRoll(AircraftRotation.cameraRoll(vehicle.getYRot(partialTicks), vehicle.getXRot(partialTicks), - vehicle.roll(partialTicks), event.getYaw(), event.getPitch())); + vehicle.roll(partialTicks), event.yaw(), event.pitch())); } if (firstPerson && missile != null) { - placeCamera(event.getCamera(), missile.getPosition(partialTicks)); + placeCamera(event.camera(), missile.getPosition(partialTicks)); } else if (firstPerson) { Vector3 eye = eyePosition(vehicle, def, seat); - moveCamera(event.getCamera(), vehicle, def, eye != null ? eye : seatEye(vehicle, def, seat, player), seat, partialTicks); + moveCamera(event.camera(), vehicle, def, eye != null ? eye : seatEye(vehicle, def, seat, player), seat, partialTicks); } } @@ -293,7 +284,7 @@ private static Vector3 seatEye(AircraftEntity vehicle, AircraftDefinition def, i } /** The phase one pilot model: mouse movement becomes aircraft rotation, the view stays on the nose. */ - private static void steerWithMouse(AircraftEntity vehicle, LocalPlayer player, ViewportEvent.ComputeCameraAngles event, + private static void steerWithMouse(AircraftEntity vehicle, LocalPlayer player, CameraAngles event, float partialTicks, boolean firstPerson) { boolean freeLook = KeyBindings.FREE_LOOK.isDown(); boolean ownPitch = vehicle.isHovering(); @@ -432,12 +423,17 @@ private static void placeCamera(Camera camera, Vec3 at) { } } + private static Method accessible(Method method) { + method.setAccessible(true); + return method; + } + /** {@code Camera.setPosition} is protected. Looked up once; on failure the feature turns itself off. */ private static @Nullable MethodHandle cameraSetter() { if (setCameraPosition == null && !cameraLookupFailed) { try { setCameraPosition = MethodHandles.lookup().unreflect( - ObfuscationReflectionHelper.findMethod(Camera.class, "setPosition", Vec3.class)); + accessible(Camera.class.getDeclaredMethod("setPosition", Vec3.class))); } catch (Exception e) { MCHeli.LOGGER.warn("Camera.setPosition is not accessible; the first person eye stays where vanilla puts it", e); cameraLookupFailed = true; From 7dd1fa84e8d56c237f039a5e2704c6dd28480849 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:46 +0800 Subject: [PATCH 027/314] Move KeyBindings to the shared 26.3 sources --- .../src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java From 82a9ffef3461026be0952c4a1208ab842eff8a39 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:46 +0800 Subject: [PATCH 028/314] Move RuntimeAssets to the shared 26.3 sources --- .../main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java From 633b5d5974032d16e8ddec2da5035ced6c9daa88 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:47 +0800 Subject: [PATCH 029/314] Move VehiclePicker to the shared sources as a plain use key hook --- .../com/jnhprojects/mcheli/client/VehiclePicker.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java (87%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java similarity index 87% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java index cbb2119..9507412 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java @@ -11,7 +11,6 @@ import net.minecraft.world.phys.EntityHitResult; import net.minecraft.world.phys.HitResult; import net.minecraft.world.phys.Vec3; -import net.minecraftforge.client.event.InputEvent; import org.jspecify.annotations.Nullable; import java.util.Optional; @@ -28,14 +27,10 @@ final class VehiclePicker { private VehiclePicker() { } - static void init() { - InputEvent.InteractionKeyMappingTriggered.UseItem.BUS.addListener(VehiclePicker::onUseItem); - } - - private static boolean onUseItem(InputEvent.InteractionKeyMappingTriggered.UseItem event) { + static boolean onUseItem(InteractionHand hand) { Minecraft mc = Minecraft.getInstance(); LocalPlayer player = mc.player; - if (player == null || event.getHand() != InteractionHand.MAIN_HAND || player.getVehicle() instanceof AircraftEntity) { + if (player == null || hand != InteractionHand.MAIN_HAND || player.getVehicle() instanceof AircraftEntity) { return false; } // Vanilla already handles a click on the main box. From 62d8500791c601402b3aabdc2240f2d053a70042 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:47 +0800 Subject: [PATCH 030/314] Move WeaponController to the shared sources as plain key hooks --- .../mcheli/client/WeaponController.java | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java (90%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java similarity index 90% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java index 904d871..1bc2ec0 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java @@ -23,8 +23,6 @@ import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.HitResult; import net.minecraft.world.phys.Vec3; -import net.minecraftforge.client.event.InputEvent; -import net.minecraftforge.event.TickEvent; import org.jspecify.annotations.Nullable; /** @@ -38,12 +36,6 @@ final class WeaponController { private WeaponController() { } - static void init() { - TickEvent.ClientTickEvent.Post.BUS.addListener(event -> onClientTick()); - InputEvent.InteractionKeyMappingTriggered.UseItem.BUS.addListener(WeaponController::onUseItem); - InputEvent.InteractionKeyMappingTriggered.PickBlock.BUS.addListener(WeaponController::onPickBlock); - } - private record Seat(AircraftEntity vehicle, VehicleWeapons weapons, int seat) { } @@ -60,16 +52,12 @@ private record Seat(AircraftEntity vehicle, VehicleWeapons weapons, int seat) { } /** The use key is the trigger while the seat has a weapon, so it must not also use items. */ - private static boolean onUseItem(InputEvent.InteractionKeyMappingTriggered.UseItem event) { + static boolean onUseItem() { Seat s = seat(Minecraft.getInstance().player); - if (s != null && s.weapons.selected(s.seat) >= 0) { - event.setSwingHand(false); - return true; - } - return false; + return s != null && s.weapons.selected(s.seat) >= 0; } - private static boolean onPickBlock(InputEvent.InteractionKeyMappingTriggered.PickBlock event) { + static boolean onPickBlock() { Seat s = seat(Minecraft.getInstance().player); if (s == null || s.weapons.sets().isEmpty()) { return false; @@ -80,7 +68,7 @@ private static boolean onPickBlock(InputEvent.InteractionKeyMappingTriggered.Pic return true; } - private static void onClientTick() { + static void onClientTick() { Minecraft mc = Minecraft.getInstance(); LocalPlayer player = mc.player; Seat s = seat(player); From bcd56ba114f93fcc9a99500694d703f8f2b69736 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:48 +0800 Subject: [PATCH 031/314] Move WeaponHud to the shared sources and let the loader add the layer --- .../java/com/jnhprojects/mcheli/client/WeaponHud.java | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java (91%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java similarity index 91% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java index 68db92e..3b98002 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java @@ -1,6 +1,5 @@ package com.jnhprojects.mcheli.client; -import com.jnhprojects.mcheli.MCHeli; import com.jnhprojects.mcheli.client.hud.HudRenderer; import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; import com.jnhprojects.mcheli.entity.AircraftEntity; @@ -13,9 +12,6 @@ import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.player.LocalPlayer; import net.minecraft.network.chat.Component; -import net.minecraft.resources.Identifier; -import net.minecraftforge.client.event.AddGuiOverlayLayersEvent; -import net.minecraftforge.client.gui.overlay.ForgeLayeredDraw; /** * The vehicle HUD layer. In first person it runs the seat's HUD script (the {@code HUD} line of @@ -34,17 +30,12 @@ final class WeaponHud { private WeaponHud() { } - static void init() { - AddGuiOverlayLayersEvent.BUS.addListener(event -> event.getLayeredDraw() - .add(ForgeLayeredDraw.POST_SLEEP_STACK, Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "weapons"), WeaponHud::draw)); - } - /** A pilot in gunner mode gets the gunner seat's HUD, as in the original. */ private static int hudSeat(AircraftEntity vehicle, int seat) { return seat == 0 && vehicle.isGunnerMode() ? 1 : seat; } - private static void draw(GuiGraphicsExtractor g, DeltaTracker delta) { + static void draw(GuiGraphicsExtractor g, DeltaTracker delta) { Minecraft mc = Minecraft.getInstance(); LocalPlayer player = mc.player; if (player == null || !(player.getVehicle() instanceof AircraftEntity vehicle)) { From a5d5c2a714fb722e6e566006b69b9b494b6f1573 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:48 +0800 Subject: [PATCH 032/314] Move HudCanvas to the shared 26.3 sources --- .../main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java From 29b9595bdaac0d29fb978387036daf96b11a03ae Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:49 +0800 Subject: [PATCH 033/314] Move HudRenderer to the shared sources with a plain tick method --- .../com/jnhprojects/mcheli/client/hud/HudRenderer.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java (97%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java similarity index 97% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java index 0de1a15..0115b4c 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java @@ -13,7 +13,6 @@ import net.minecraft.client.player.LocalPlayer; import net.minecraft.resources.Identifier; import net.minecraft.util.Mth; -import net.minecraftforge.event.TickEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,11 +57,9 @@ private HudRenderer(HudCanvas canvas, AircraftEntity vehicle, AircraftDefinition this.centerY = height / 2.0; } - public static void init() { - TickEvent.ClientTickEvent.Post.BUS.addListener(event -> { - LocalPlayer player = Minecraft.getInstance().player; - STATE.tick(player != null && player.getVehicle() instanceof AircraftEntity vehicle ? vehicle : null); - }); + public static void tick() { + LocalPlayer player = Minecraft.getInstance().player; + STATE.tick(player != null && player.getVehicle() instanceof AircraftEntity vehicle ? vehicle : null); } /** Fed by the pilot's mouse steering; moves the control stick indicator. */ From b5449aea0e10f6701cf486c0697ca7a875093984 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:49 +0800 Subject: [PATCH 034/314] Move HudScripts to the shared 26.3 sources --- .../main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java From 8c089e228c93281a869e6a00a693f4b577563e60 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:49 +0800 Subject: [PATCH 035/314] Move HudState to the shared sources and read the mod version from the platform --- .../java/com/jnhprojects/mcheli/client/hud/HudState.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java (98%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java similarity index 98% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java index b701fed..d63fc2a 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java @@ -1,6 +1,5 @@ package com.jnhprojects.mcheli.client.hud; -import com.jnhprojects.mcheli.MCHeli; import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; import com.jnhprojects.mcheli.entity.AircraftEntity; @@ -10,6 +9,7 @@ import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; import com.jnhprojects.mcheli.hud.HudVariable; import com.jnhprojects.mcheli.hud.TextArg; +import com.jnhprojects.mcheli.platform.Platform; import com.jnhprojects.mcheli.weapon.Guidance; import com.jnhprojects.mcheli.weapon.VehicleWeapons; import com.jnhprojects.mcheli.weapon.WeaponSet; @@ -19,7 +19,6 @@ import net.minecraft.world.entity.Mob; import net.minecraft.world.entity.monster.Enemy; import net.minecraft.world.level.Level; -import net.minecraftforge.fml.ModList; import org.jspecify.annotations.Nullable; import java.util.ArrayList; @@ -320,8 +319,7 @@ boolean hasMagazine() { case RELOAD_SEC -> reloadSeconds; case MORTAR_DIST -> -1.0F; case MC_VER -> "26.3"; - case MOD_VER -> ModList.getModContainerById(MCHeli.MOD_ID) - .map(c -> c.getModInfo().getVersion().toString()).orElse(""); + case MOD_VER -> Platform.get().modVersion(); case MOD_NAME -> "MCHeli Rework"; case YAW -> rotate360(vehicle.getYRot() + 180.0); case PITCH -> -vehicle.getXRot(); From fe12f4732cc68b312924a2fbacbefc40c3b119e5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:50 +0800 Subject: [PATCH 036/314] Move GlowParticle to the shared 26.3 sources --- .../java/com/jnhprojects/mcheli/client/particle/GlowParticle.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java From 2af23024617cb6ef99625361578376455785e60d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:50 +0800 Subject: [PATCH 037/314] Move A10Renderer to the shared 26.3 sources --- .../java/com/jnhprojects/mcheli/client/render/A10Renderer.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java From 81d303f3c8bc546370591d770be04d81224052c0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:51 +0800 Subject: [PATCH 038/314] Move AircraftRenderState to the shared 26.3 sources --- .../com/jnhprojects/mcheli/client/render/AircraftRenderState.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java From 3437fb98325047000352c587074ee6cf1e082c17 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:51 +0800 Subject: [PATCH 039/314] Move AircraftRenderer to the shared 26.3 sources --- .../com/jnhprojects/mcheli/client/render/AircraftRenderer.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java From 1114482ae5a8e5125e157162370146ade9115f41 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:52 +0800 Subject: [PATCH 040/314] Move FlareRenderer to the shared 26.3 sources --- .../java/com/jnhprojects/mcheli/client/render/FlareRenderer.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java From 7becd10d3c092bfd65501b9b0a76f239c80ae040 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:52 +0800 Subject: [PATCH 041/314] Move GlassSplit to the shared 26.3 sources --- .../java/com/jnhprojects/mcheli/client/render/GlassSplit.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java From 0243149b97b7e95a8f8f7a6d8f55c12762360d98 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:52 +0800 Subject: [PATCH 042/314] Move Glow to the shared 26.3 sources --- .../src/main/java/com/jnhprojects/mcheli/client/render/Glow.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java From f9b4413d866571a4835bb9fd1905e4f48fa91fac Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:53 +0800 Subject: [PATCH 043/314] Move MeshCache to the shared 26.3 sources --- .../main/java/com/jnhprojects/mcheli/client/render/MeshCache.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java From 2fe52c936d279dd4122fe91fec870005c6ea272e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:53 +0800 Subject: [PATCH 044/314] Move MeshRenderer to the shared 26.3 sources --- .../java/com/jnhprojects/mcheli/client/render/MeshRenderer.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java From a23dd0b7106dc74390eb309028100158e904a839 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:54 +0800 Subject: [PATCH 045/314] Move PartRenderer to the shared 26.3 sources --- .../java/com/jnhprojects/mcheli/client/render/PartRenderer.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java From efd0da461f143741c574d0b00dcd70f5c3533fbe Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:54 +0800 Subject: [PATCH 046/314] Move ProjectileRenderer to the shared 26.3 sources --- .../com/jnhprojects/mcheli/client/render/ProjectileRenderer.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java From f6e1cd7d0d814ea8ae32db07e19a5168b5f48ee7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:55 +0800 Subject: [PATCH 047/314] Move RiderPose to the shared sources as a plain render hook --- .../mcheli/client/render/RiderPose.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java (79%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java similarity index 79% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java index c297d9d..60c1e9c 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java @@ -4,10 +4,11 @@ import com.jnhprojects.mcheli.physics.AircraftRotation; import net.minecraft.client.Minecraft; import net.minecraft.client.multiplayer.ClientLevel; +import com.mojang.blaze3d.vertex.PoseStack; import net.minecraft.client.renderer.entity.state.AvatarRenderState; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; import net.minecraft.world.entity.Entity; import net.minecraft.world.phys.Vec3; -import net.minecraftforge.client.event.RenderLivingEvent; import org.joml.Quaternionf; /** @@ -24,13 +25,10 @@ public final class RiderPose { private RiderPose() { } - public static void init() { - RenderLivingEvent.Pre.BUS.addListener(RiderPose::onRenderLiving); - } - - private static void onRenderLiving(RenderLivingEvent.Pre event) { + /** Called before a living entity is drawn, with the pose the renderer is about to use. */ + public static void beforeRender(LivingEntityRenderState renderState, PoseStack pose, float partialTicks) { // Render states carry no entity reference; players are the ones that keep their id. - if (!(event.getState() instanceof AvatarRenderState state)) { + if (!(renderState instanceof AvatarRenderState state)) { return; } ClientLevel level = Minecraft.getInstance().level; @@ -38,7 +36,6 @@ private static void onRenderLiving(RenderLivingEvent.Pre event) { if (rider == null || !(rider.getVehicle() instanceof AircraftEntity vehicle)) { return; } - float partialTicks = event.getCameraState().cameraEntityPartialTicks; float yaw = vehicle.getYRot(partialTicks); float pitch = vehicle.getXRot(partialTicks); float roll = vehicle.roll(partialTicks); @@ -47,6 +44,6 @@ private static void onRenderLiving(RenderLivingEvent.Pre event) { } Quaternionf tilt = AircraftRotation.orientation(yaw, pitch, roll).rotateY((float) Math.toRadians(yaw)); Vec3 hip = rider.getVehicleAttachmentPoint(vehicle); - event.getPoseStack().rotateAround(tilt, (float) hip.x, (float) hip.y, (float) hip.z); + pose.rotateAround(tilt, (float) hip.x, (float) hip.y, (float) hip.z); } } From 201f1b21ed0dd013c5b2218b59235c94c2737a1e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:55 +0800 Subject: [PATCH 048/314] Move EngineSound to the shared 26.3 sources --- .../java/com/jnhprojects/mcheli/client/sound/EngineSound.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java From e26b10879bf3d6cbec23cb18841a0a68ad68c23b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:55 +0800 Subject: [PATCH 049/314] Move EngineSoundPlayer to the shared sources with a plain tick method --- .../jnhprojects/mcheli/client/sound/EngineSoundPlayer.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java (93%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java similarity index 93% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java index 75c7d5f..b0d46e3 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java @@ -7,7 +7,6 @@ import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.client.player.LocalPlayer; import net.minecraft.world.entity.Entity; -import net.minecraftforge.event.TickEvent; import java.util.HashMap; import java.util.Map; @@ -26,11 +25,7 @@ public final class EngineSoundPlayer { private EngineSoundPlayer() { } - public static void init() { - TickEvent.ClientTickEvent.Post.BUS.addListener(event -> tick()); - } - - private static void tick() { + public static void tick() { Minecraft mc = Minecraft.getInstance(); ClientLevel level = mc.level; LocalPlayer player = mc.player; From 784e2f548816addaf60bfe46a85533c5a22fd8fa Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:56 +0800 Subject: [PATCH 050/314] Move RuntimeSoundInstance to the shared sources and stream sounds on any loader --- .../mcheli/client/sound/RuntimeSoundInstance.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java (80%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java similarity index 80% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java index 64f39e7..6c87314 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java @@ -9,6 +9,7 @@ import net.minecraft.client.sounds.SoundBufferLibrary; import net.minecraft.client.sounds.SoundManager; import net.minecraft.client.sounds.WeighedSoundEvents; +import net.minecraft.resources.Identifier; import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundSource; import net.minecraft.util.RandomSource; @@ -30,7 +31,7 @@ * Content added while the game runs can bring sounds this way without touching * {@code sounds.json} or reloading resources. */ -abstract class RuntimeSoundInstance extends AbstractTickableSoundInstance { +public abstract class RuntimeSoundInstance extends AbstractTickableSoundInstance { private @Nullable Path file; protected RuntimeSoundInstance(SoundEvent event, SoundSource source, RandomSource random) { @@ -53,11 +54,19 @@ protected RuntimeSoundInstance(SoundEvent event, SoundSource source, RandomSourc return soundEvent; } - @Override + /** + * Forge and NeoForge call this through their sound instance extension, which it overrides + * without saying so: Fabric has no such method and calls {@link #openStream} from a mixin. + */ public CompletableFuture getStream(SoundBufferLibrary buffers, Sound sound, boolean looping) { + return openStream(buffers, sound.getPath(), looping); + } + + /** Streams the file found on disk, or the resource {@code path} when there is none. */ + public CompletableFuture openStream(SoundBufferLibrary buffers, Identifier path, boolean looping) { Path source = file; if (source == null) { - return buffers.getStream(sound.getPath(), looping); + return buffers.getStream(path, looping); } return CompletableFuture.supplyAsync(() -> { try { From 993c6a215a76806400acf3dabe91d38b19265d0e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:56 +0800 Subject: [PATCH 051/314] Move AircraftEntity to the shared 26.3 sources --- .../main/java/com/jnhprojects/mcheli/entity/AircraftEntity.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/entity/AircraftEntity.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/AircraftEntity.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/AircraftEntity.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/AircraftEntity.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/AircraftEntity.java From 2dd9751301668f04f86251170b093308eb5f926b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:57 +0800 Subject: [PATCH 052/314] Move AnimationState to the shared 26.3 sources --- .../main/java/com/jnhprojects/mcheli/entity/AnimationState.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java From eb0d27186c0b426cf4ad018ef8c935d38fe8af32 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:57 +0800 Subject: [PATCH 053/314] Move ClientAccess to the shared 26.3 sources --- .../src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java From c26c0f47ae4cc78b93d3024f4965048046d1fb21 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:58 +0800 Subject: [PATCH 054/314] Move EngineSoundSource to the shared 26.3 sources --- .../java/com/jnhprojects/mcheli/entity/EngineSoundSource.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java From ce2ba29748372d25d541179e3229bb195ab1994b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:58 +0800 Subject: [PATCH 055/314] Move GroundVehicleEntity to the shared 26.3 sources --- .../java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java From 447ab768df5dfbd05ae4aa96a0b4eb131704d4d7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:59 +0800 Subject: [PATCH 056/314] Move HelicopterEntity to the shared 26.3 sources --- .../main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java From c1805bcaa48881f3442c15cabcee676b2df39de9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:59 +0800 Subject: [PATCH 057/314] Move PartState to the shared 26.3 sources --- .../src/main/java/com/jnhprojects/mcheli/entity/PartState.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/entity/PartState.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/PartState.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/PartState.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/PartState.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/PartState.java From 964405302e939e5fed1781adc1d68dcbc46fb355 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:43:59 +0800 Subject: [PATCH 058/314] Move PlaneEntity to the shared 26.3 sources --- .../src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java From 441d1b5eed832825e27949614a0888022bcc753d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:00 +0800 Subject: [PATCH 059/314] Move TankEntity to the shared 26.3 sources --- .../src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java From f817f93fe67e25a9d79875b19dead42d2059f730 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:00 +0800 Subject: [PATCH 060/314] Move TerrainFollower to the shared 26.3 sources --- .../main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java From 9b62568bd5fa0cfb8ce7aff60845dca7e6278a64 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:01 +0800 Subject: [PATCH 061/314] Move A10Entity to the shared 26.3 sources --- .../java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java From 59d5cf90ec4e2001c0838eb8d201963212c70205 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:01 +0800 Subject: [PATCH 062/314] Move AAMissileEntity to the shared 26.3 sources --- .../com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java From da65f71c128572070d0d1551dbb49ecff735c2d8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:02 +0800 Subject: [PATCH 063/314] Move ASMissileEntity to the shared 26.3 sources --- .../com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java From dfeba2d485575606735dbd6246cc70544a378036 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:02 +0800 Subject: [PATCH 064/314] Move ATMissileEntity to the shared 26.3 sources --- .../com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java From 40472a0c3cb355b140a8fb12ee949e5bdb488887 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:02 +0800 Subject: [PATCH 065/314] Move BombEntity to the shared 26.3 sources --- .../java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java From cc156bd3a0f46eb32db1c0e22e3a76cd85cbeea2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:03 +0800 Subject: [PATCH 066/314] Move BulletEntity to the shared 26.3 sources --- .../com/jnhprojects/mcheli/entity/projectile/BulletEntity.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java From 9581c98a57020c6be9df321e3a31bdb11a3380d7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:03 +0800 Subject: [PATCH 067/314] Move DispensedItemEntity to the shared 26.3 sources --- .../jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java From 0175c64881724dd3e7c4d4282b5bd8e029996038 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:04 +0800 Subject: [PATCH 068/314] Move FlareEntity to the shared 26.3 sources --- .../com/jnhprojects/mcheli/entity/projectile/FlareEntity.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java From cf8ff8f33adde2bca6d74b230f48bc14a8657eec Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:04 +0800 Subject: [PATCH 069/314] Move MarkerRocketEntity to the shared 26.3 sources --- .../jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java From 5a7792ddf3c9319784f7c0afa626b64944a4c031 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:05 +0800 Subject: [PATCH 070/314] Move ProjectileEntity to the shared 26.3 sources --- .../jnhprojects/mcheli/entity/projectile/ProjectileEntity.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java From 582904e537367a9c6ce93c5df132510be653e331 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:05 +0800 Subject: [PATCH 071/314] Move RocketEntity to the shared 26.3 sources --- .../com/jnhprojects/mcheli/entity/projectile/RocketEntity.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java From 0bd3bfda88fdc2abfb24377b4f45a57b48642815 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:05 +0800 Subject: [PATCH 072/314] Move TorpedoEntity to the shared 26.3 sources --- .../com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java From e92092f0199c0001f2fe360741df0a3c8e3845cc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:06 +0800 Subject: [PATCH 073/314] Move TvMissileEntity to the shared 26.3 sources --- .../com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java From e298cfcd951ee6a034b3d91c2737e54f6f78e39e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:06 +0800 Subject: [PATCH 074/314] Move AircraftItem to the shared 26.3 sources --- .../src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java From b20902f6ab00e1b0a48f6d61882bb8040ed3de7f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:07 +0800 Subject: [PATCH 075/314] Move LegacyAircraftItem to the shared 26.3 sources --- .../main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java From c73a6eaa82db08fce6ba74466d87224ac7b03e7c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:07 +0800 Subject: [PATCH 076/314] Move BoardVehiclePacket to the shared sources as a custom payload --- .../mcheli/network/BoardVehiclePacket.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java (73%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java similarity index 73% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java index 0bb9a08..ba56ca3 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java @@ -2,10 +2,12 @@ import com.jnhprojects.mcheli.entity.AircraftEntity; import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.InteractionHand; import net.minecraft.world.phys.Vec3; -import net.minecraftforge.event.network.CustomPayloadEvent; /** * Client to server: the player right-clicked a vehicle's hull outside its main hit box. The @@ -13,7 +15,10 @@ * * @param location where on the hull the click landed, in world coordinates */ -public record BoardVehiclePacket(int entityId, Vec3 location) { +public record BoardVehiclePacket(int entityId, Vec3 location) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("board_vehicle"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(BoardVehiclePacket::encode, BoardVehiclePacket::decode); + /** The same slack vanilla allows on top of the player's reach for a click on an entity. */ private static final double REACH_SLACK = 3.0; @@ -28,8 +33,7 @@ public static BoardVehiclePacket decode(FriendlyByteBuf buf) { return new BoardVehiclePacket(buf.readVarInt(), new Vec3(buf.readDouble(), buf.readDouble(), buf.readDouble())); } - public static void handle(BoardVehiclePacket msg, CustomPayloadEvent.Context ctx) { - ServerPlayer sender = ctx.getSender(); + public static void handle(BoardVehiclePacket msg, ServerPlayer sender) { if (sender == null || !(sender.level().getEntity(msg.entityId) instanceof AircraftEntity vehicle) || vehicle.isRemoved()) { return; } @@ -46,4 +50,9 @@ public static void handle(BoardVehiclePacket msg, CustomPayloadEvent.Context ctx vehicle.interact(sender, InteractionHand.MAIN_HAND, msg.location.subtract(vehicle.position())); } } + + @Override + public Type type() { + return TYPE; + } } From 8c3eeb5a3942a31dba34b3ca57ddfefa157f3e12 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:08 +0800 Subject: [PATCH 077/314] Move CrewActionPacket to the shared sources as a custom payload --- .../mcheli/network/CrewActionPacket.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java (87%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java similarity index 87% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java index 270ac69..8e7136f 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java @@ -5,8 +5,10 @@ import com.jnhprojects.mcheli.weapon.Resupply; import com.jnhprojects.mcheli.weapon.VehicleWeapons; import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; import net.minecraft.server.level.ServerPlayer; -import net.minecraftforge.event.network.CustomPayloadEvent; /** * A one-off request from a crew member: lower the gear, fold the wings, change seats and so on. @@ -15,7 +17,9 @@ * allowed to do it, and then applies the same conditions the original checked before switching. * The result reaches every client through synched data. */ -public record CrewActionPacket(int entityId, Action action) { +public record CrewActionPacket(int entityId, Action action) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("crew_action"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(CrewActionPacket::encode, CrewActionPacket::decode); public enum Action { /** Raise or lower the landing gear. Pilot only. */ @@ -61,8 +65,7 @@ public static CrewActionPacket decode(FriendlyByteBuf buf) { return new CrewActionPacket(entityId, actions[ordinal]); } - public static void handle(CrewActionPacket msg, CustomPayloadEvent.Context ctx) { - ServerPlayer sender = ctx.getSender(); + public static void handle(CrewActionPacket msg, ServerPlayer sender) { if (sender == null || !(sender.getVehicle() instanceof AircraftEntity vehicle) || vehicle.getId() != msg.entityId) { return; } @@ -131,4 +134,9 @@ public static void handle(CrewActionPacket msg, CustomPayloadEvent.Context ctx) case RESUPPLY -> Resupply.fromInventory(sender, vehicle); } } + + @Override + public Type type() { + return TYPE; + } } From 753877f343bf46326aab7c0064f21541c7551e30 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:08 +0800 Subject: [PATCH 078/314] Move ExplosionEffectPacket to the shared sources as a custom payload --- .../mcheli/network/ExplosionEffectPacket.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java (70%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java similarity index 70% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java index d9ff24a..279b151 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java @@ -3,14 +3,18 @@ import com.jnhprojects.mcheli.entity.ClientAccess; import com.jnhprojects.mcheli.weapon.ExplosionEffect; import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; import net.minecraft.world.entity.Entity; -import net.minecraftforge.event.network.CustomPayloadEvent; /** * Server to nearby players: draw an MCHeli explosion here. The original's * {@code MCH_PacketEffectExplosion}, plus a flag for the bigger fireball of a crashing vehicle. */ -public record ExplosionEffectPacket(double x, double y, double z, float size, boolean inWater, boolean crash) { +public record ExplosionEffectPacket(double x, double y, double z, float size, boolean inWater, boolean crash) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("explosion_effect"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(ExplosionEffectPacket::encode, ExplosionEffectPacket::decode); public ExplosionEffectPacket(double x, double y, double z, float size, boolean inWater) { this(x, y, z, size, inWater, false); @@ -30,7 +34,7 @@ public static ExplosionEffectPacket decode(FriendlyByteBuf buf) { buf.readBoolean()); } - public static void handle(ExplosionEffectPacket msg, CustomPayloadEvent.Context ctx) { + public static void handle(ExplosionEffectPacket msg) { Entity player = ClientAccess.localPlayer(); if (player == null || !Float.isFinite(msg.size)) { return; @@ -42,4 +46,9 @@ public static void handle(ExplosionEffectPacket msg, CustomPayloadEvent.Context ExplosionEffect.play(player.level(), msg.x, msg.y, msg.z, size, msg.inWater); } } + + @Override + public Type type() { + return TYPE; + } } From 576e56b4370b77232aec9d8fb26dad931cf11cf7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:08 +0800 Subject: [PATCH 079/314] Move FireWeaponPacket to the shared sources as a custom payload --- .../mcheli/network/FireWeaponPacket.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java (73%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java similarity index 73% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java index 763a492..0056dcc 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java @@ -5,8 +5,10 @@ import com.jnhprojects.mcheli.weapon.Shot; import com.jnhprojects.mcheli.weapon.VehicleWeapons; import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; import net.minecraft.server.level.ServerPlayer; -import net.minecraftforge.event.network.CustomPayloadEvent; /** * Client to server: the crew member's selected weapon fired on their side, so fire it for real. @@ -15,7 +17,9 @@ * * @param weapon the weapon the client fired, ignored unless it is still the seat's selection */ -public record FireWeaponPacket(int entityId, int weapon, int option1, int option2) { +public record FireWeaponPacket(int entityId, int weapon, int option1, int option2) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("fire_weapon"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(FireWeaponPacket::encode, FireWeaponPacket::decode); public static void encode(FireWeaponPacket msg, FriendlyByteBuf buf) { buf.writeVarInt(msg.entityId); @@ -28,8 +32,7 @@ public static FireWeaponPacket decode(FriendlyByteBuf buf) { return new FireWeaponPacket(buf.readVarInt(), buf.readVarInt(), buf.readVarInt(), buf.readVarInt()); } - public static void handle(FireWeaponPacket msg, CustomPayloadEvent.Context ctx) { - ServerPlayer sender = ctx.getSender(); + public static void handle(FireWeaponPacket msg, ServerPlayer sender) { if (sender == null || !(sender.getVehicle() instanceof AircraftEntity vehicle) || vehicle.getId() != msg.entityId) { return; } @@ -43,4 +46,9 @@ public static void handle(FireWeaponPacket msg, CustomPayloadEvent.Context ctx) shot.option2 = msg.option2; weapons.use(sender, seat, shot, ServerFiring.INSTANCE); } + + @Override + public Type type() { + return TYPE; + } } From 7be62f9e88bd3a5da1be9d87335b7def21fcb995 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:09 +0800 Subject: [PATCH 080/314] Move LockWarningPacket to the shared sources as a custom payload --- .../mcheli/network/LockWarningPacket.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java (63%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java similarity index 63% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java index 30d8be7..97d2cbf 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java @@ -2,15 +2,19 @@ import com.jnhprojects.mcheli.entity.AircraftEntity; import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.Entity; -import net.minecraftforge.event.network.CustomPayloadEvent; /** * Client to server: this player is locking on to an entity. If it is a vehicle with flares, or a * crew member of one, its crew hears the warning. The original's {@code MCH_PacketNotifyLock}. */ -public record LockWarningPacket(int targetId) { +public record LockWarningPacket(int targetId) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("lock_warning"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(LockWarningPacket::encode, LockWarningPacket::decode); public static void encode(LockWarningPacket msg, FriendlyByteBuf buf) { buf.writeVarInt(msg.targetId); @@ -20,8 +24,7 @@ public static LockWarningPacket decode(FriendlyByteBuf buf) { return new LockWarningPacket(buf.readVarInt()); } - public static void handle(LockWarningPacket msg, CustomPayloadEvent.Context ctx) { - ServerPlayer sender = ctx.getSender(); + public static void handle(LockWarningPacket msg, ServerPlayer sender) { if (sender == null || !(sender.getVehicle() instanceof AircraftEntity)) { return; } @@ -33,4 +36,9 @@ public static void handle(LockWarningPacket msg, CustomPayloadEvent.Context ctx) } } } + + @Override + public Type type() { + return TYPE; + } } From 8c5cf83ac26ceed3400e5f760d36288a2d3cc38f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:09 +0800 Subject: [PATCH 081/314] Move PackDataPacket to the shared sources as a custom payload --- .../mcheli/network/PackDataPacket.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java (63%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java similarity index 63% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java index dc11b95..89c1644 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java @@ -2,7 +2,9 @@ import com.jnhprojects.mcheli.entity.ClientAccess; import net.minecraft.network.FriendlyByteBuf; -import net.minecraftforge.event.network.CustomPayloadEvent; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; /** * Server to player: one chunk of a pack's zip. Chunks of a pack arrive in order, so @@ -10,7 +12,10 @@ * * @param total length of the whole zip, as in the {@link PackListPacket} */ -public record PackDataPacket(String hash, int offset, int total, byte[] data) { +public record PackDataPacket(String hash, int offset, int total, byte[] data) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_data"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackDataPacket::encode, PackDataPacket::decode); + /** Well under the 1 MiB limit of a clientbound custom payload. */ public static final int CHUNK_SIZE = 128 * 1024; @@ -25,7 +30,12 @@ public static PackDataPacket decode(FriendlyByteBuf buf) { return new PackDataPacket(buf.readUtf(64), buf.readVarInt(), buf.readVarInt(), buf.readByteArray(CHUNK_SIZE)); } - public static void handle(PackDataPacket msg, CustomPayloadEvent.Context ctx) { + public static void handle(PackDataPacket msg) { ClientAccess.packs().onPackData(msg); } + + @Override + public Type type() { + return TYPE; + } } From d46da4476bf2253b7a9b3d6e79a373469e86f01d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:10 +0800 Subject: [PATCH 082/314] Move PackListPacket to the shared sources as a custom payload --- .../mcheli/network/PackListPacket.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java (73%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java similarity index 73% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java index b475ead..ad7f0c8 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java @@ -3,7 +3,9 @@ import com.jnhprojects.mcheli.entity.ClientAccess; import io.netty.handler.codec.DecoderException; import net.minecraft.network.FriendlyByteBuf; -import net.minecraftforge.event.network.CustomPayloadEvent; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; import java.util.ArrayList; import java.util.List; @@ -14,7 +16,10 @@ * and then rebuilds its content from exactly this list, so both sides fly the same definitions. * The imported 1.0.3 content is never listed: every player imports their own copy. */ -public record PackListPacket(List packs) { +public record PackListPacket(List packs) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_list"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackListPacket::encode, PackListPacket::decode); + public static final int MAX_PACKS = 256; /** @@ -45,7 +50,12 @@ public static PackListPacket decode(FriendlyByteBuf buf) { return new PackListPacket(List.copyOf(packs)); } - public static void handle(PackListPacket msg, CustomPayloadEvent.Context ctx) { + public static void handle(PackListPacket msg) { ClientAccess.packs().onPackList(msg); } + + @Override + public Type type() { + return TYPE; + } } From b43aee95135ca832338309c00503b90e86fd1729 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:10 +0800 Subject: [PATCH 083/314] Move PackRequestPacket to the shared sources as a custom payload --- .../mcheli/network/PackRequestPacket.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java (63%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java similarity index 63% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java index 8ff7d32..50f10f7 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java @@ -2,8 +2,10 @@ import io.netty.handler.codec.DecoderException; import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; import net.minecraft.server.level.ServerPlayer; -import net.minecraftforge.event.network.CustomPayloadEvent; import java.util.ArrayList; import java.util.List; @@ -12,7 +14,9 @@ * Player to server: send me these packs, by content hash. The server only answers for hashes in * the list it sent, so a client cannot ask for anything else. */ -public record PackRequestPacket(List hashes) { +public record PackRequestPacket(List hashes) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_request"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackRequestPacket::encode, PackRequestPacket::decode); public static void encode(PackRequestPacket msg, FriendlyByteBuf buf) { buf.writeVarInt(msg.hashes.size()); @@ -33,10 +37,14 @@ public static PackRequestPacket decode(FriendlyByteBuf buf) { return new PackRequestPacket(List.copyOf(hashes)); } - public static void handle(PackRequestPacket msg, CustomPayloadEvent.Context ctx) { - ServerPlayer sender = ctx.getSender(); + public static void handle(PackRequestPacket msg, ServerPlayer sender) { if (sender != null) { PackSync.request(sender, msg.hashes); } } + + @Override + public Type type() { + return TYPE; + } } From eaa4f03b312276058098dd5cd79efa8f544573d0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:10 +0800 Subject: [PATCH 084/314] Move PackSync to the shared sources with plain login, logout and tick methods --- .../jnhprojects/mcheli/network/PackSync.java | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/network/PackSync.java (88%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/network/PackSync.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/PackSync.java similarity index 88% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/network/PackSync.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/PackSync.java index fc462d7..fe9c668 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/network/PackSync.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/PackSync.java @@ -5,8 +5,6 @@ import net.minecraft.commands.Commands; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; -import net.minecraftforge.event.TickEvent; -import net.minecraftforge.event.entity.player.PlayerEvent; import org.jspecify.annotations.Nullable; import java.io.IOException; @@ -39,17 +37,16 @@ public final class PackSync { private PackSync() { } - public static void init() { - PlayerEvent.PlayerLoggedInEvent.BUS.addListener(event -> { - if (event.getEntity() instanceof ServerPlayer player) { - ModNetwork.sendToPlayer(player, list()); - if (seesProblems(player)) { - MCHeli.packs().reportProblems(player::sendSystemMessage); - } - } - }); - PlayerEvent.PlayerLoggedOutEvent.BUS.addListener(event -> TRANSFERS.remove(event.getEntity().getUUID())); - TickEvent.ServerTickEvent.Post.BUS.addListener(event -> pump(event.server())); + /** Sends a joining player the pack list, and operators any pack problems. */ + public static void onLogin(ServerPlayer player) { + ModNetwork.sendToPlayer(player, list()); + if (seesProblems(player)) { + MCHeli.packs().reportProblems(player::sendSystemMessage); + } + } + + public static void onLogout(ServerPlayer player) { + TRANSFERS.remove(player.getUUID()); } /** The packs the server uses now, as the client should load them. */ @@ -101,7 +98,7 @@ static void request(ServerPlayer player, List hashes) { return null; } - private static void pump(MinecraftServer server) { + public static void pump(MinecraftServer server) { Iterator>> it = TRANSFERS.entrySet().iterator(); while (it.hasNext()) { Map.Entry> entry = it.next(); From c04f06db5cf11b6506965e67e6ab9fe97f104b58 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:11 +0800 Subject: [PATCH 085/314] Move PilotStatePacket to the shared sources as a custom payload --- .../mcheli/network/PilotStatePacket.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java (71%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java similarity index 71% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java index 1d71598..6426f06 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java @@ -2,8 +2,10 @@ import com.jnhprojects.mcheli.entity.AircraftEntity; import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; import net.minecraft.server.level.ServerPlayer; -import net.minecraftforge.event.network.CustomPayloadEvent; /** * Sent by the pilot every tick. Position, yaw and pitch already travel through the vanilla @@ -12,7 +14,9 @@ * @param keys the pilot's key state as packed by {@link AircraftEntity.ControlInput#write()}, so * other players see wheels steer and tracks turn */ -public record PilotStatePacket(int entityId, float throttle, float reverse, float roll, boolean hovering, byte keys) { +public record PilotStatePacket(int entityId, float throttle, float reverse, float roll, boolean hovering, byte keys) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pilot_state"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PilotStatePacket::encode, PilotStatePacket::decode); public static void encode(PilotStatePacket msg, FriendlyByteBuf buf) { buf.writeVarInt(msg.entityId); @@ -27,8 +31,7 @@ public static PilotStatePacket decode(FriendlyByteBuf buf) { return new PilotStatePacket(buf.readVarInt(), buf.readFloat(), buf.readFloat(), buf.readFloat(), buf.readBoolean(), buf.readByte()); } - public static void handle(PilotStatePacket msg, CustomPayloadEvent.Context ctx) { - ServerPlayer sender = ctx.getSender(); + public static void handle(PilotStatePacket msg, ServerPlayer sender) { if (sender == null) { return; } @@ -42,4 +45,9 @@ public static void handle(PilotStatePacket msg, CustomPayloadEvent.Context ctx) aircraft.acceptPilotState(msg.throttle, msg.reverse, msg.roll, msg.hovering, msg.keys); } } + + @Override + public Type type() { + return TYPE; + } } From d40425d93365fabe2d08f1a059c81cd575bb7dc8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:11 +0800 Subject: [PATCH 086/314] Move WeaponStatePacket to the shared sources as a custom payload --- .../mcheli/network/WeaponStatePacket.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java (67%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java similarity index 67% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java index eb1c191..292f643 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java @@ -4,14 +4,18 @@ import com.jnhprojects.mcheli.entity.ClientAccess; import com.jnhprojects.mcheli.weapon.VehicleWeapons; import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; import net.minecraft.world.entity.Entity; -import net.minecraftforge.event.network.CustomPayloadEvent; /** * Server to crew: the magazine and reserve of one weapon after it fired, reloaded or was * resupplied. The original's {@code MCH_PacketNotifyAmmoNum}. */ -public record WeaponStatePacket(int entityId, int weapon, int ammo, int rest) { +public record WeaponStatePacket(int entityId, int weapon, int ammo, int rest) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("weapon_state"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(WeaponStatePacket::encode, WeaponStatePacket::decode); public static void encode(WeaponStatePacket msg, FriendlyByteBuf buf) { buf.writeVarInt(msg.entityId); @@ -24,7 +28,7 @@ public static WeaponStatePacket decode(FriendlyByteBuf buf) { return new WeaponStatePacket(buf.readVarInt(), buf.readVarInt(), buf.readVarInt(), buf.readVarInt()); } - public static void handle(WeaponStatePacket msg, CustomPayloadEvent.Context ctx) { + public static void handle(WeaponStatePacket msg) { Entity player = ClientAccess.localPlayer(); if (player != null && player.level().getEntity(msg.entityId) instanceof AircraftEntity vehicle) { VehicleWeapons weapons = vehicle.weapons(); @@ -33,4 +37,9 @@ public static void handle(WeaponStatePacket msg, CustomPayloadEvent.Context ctx) } } } + + @Override + public Type type() { + return TYPE; + } } From 68710b11e6ef88a134682659cfc9cfa7a17d11fe Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:12 +0800 Subject: [PATCH 087/314] Move GlowParticleOptions to the shared 26.3 sources --- .../java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java From 95b04e2b31ad9f8d0c536ff33183ca6edea217be Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:12 +0800 Subject: [PATCH 088/314] Move ModCreativeTabs to the shared sources and build tabs through the platform --- .../mcheli/registry/ModCreativeTabs.java | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java (60%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java similarity index 60% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java index fe1dc6f..8b24af0 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java @@ -3,15 +3,15 @@ import com.jnhprojects.mcheli.MCHeli; import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; import net.minecraft.core.registries.Registries; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceKey; import net.minecraft.world.item.CreativeModeTab; import net.minecraft.world.item.CreativeModeTabs; import net.minecraft.world.item.ItemStack; -import net.minecraftforge.eventbus.api.bus.BusGroup; -import net.minecraftforge.registries.DeferredRegister; -import net.minecraftforge.registries.RegistryObject; import java.util.List; @@ -20,21 +20,22 @@ * read from the content registry whenever a tab is built, so reloaded content shows up. */ public final class ModCreativeTabs { - private static final DeferredRegister TABS = DeferredRegister.create(Registries.CREATIVE_MODE_TAB, MCHeli.MOD_ID); - - public static final RegistryObject HELICOPTERS = register("helicopters", "helicopter", "ah-64", + public static final Registered HELICOPTERS = register("helicopters", "helicopter", "ah-64", CreativeModeTabs.SPAWN_EGGS); - public static final RegistryObject PLANES = register("planes", "plane", "f-35b", HELICOPTERS.getKey()); - public static final RegistryObject TANKS = register("tanks", "tank", "m1a2", PLANES.getKey()); - public static final RegistryObject VEHICLES = register("vehicles", "vehicle", "mk15", TANKS.getKey()); + public static final Registered PLANES = register("planes", "plane", "f-35b", key(HELICOPTERS)); + public static final Registered TANKS = register("tanks", "tank", "m1a2", key(PLANES)); + public static final Registered VEHICLES = register("vehicles", "vehicle", "mk15", key(TANKS)); private ModCreativeTabs() { } - private static RegistryObject register(String name, String kind, String icon, ResourceKey after) { - return TABS.register(name, () -> CreativeModeTab.builder() + private static ResourceKey key(Registered tab) { + return ResourceKey.create(Registries.CREATIVE_MODE_TAB, tab.id()); + } + + private static Registered register(String name, String kind, String icon, ResourceKey after) { + return Registration.register(Registries.CREATIVE_MODE_TAB, name, key -> Platform.get().creativeTab(after) .title(Component.translatable("itemGroup.mcheli." + name)) - .withTabsBefore(after) .icon(() -> { AircraftDefinition def = MCHeli.content().any(icon); return def != null ? AircraftItem.stackOf(def) : new ItemStack(ModItems.forKind(kind)); @@ -54,7 +55,7 @@ public static List all() { return List.of(HELICOPTERS.get(), PLANES.get(), TANKS.get(), VEHICLES.get()); } - public static void register(BusGroup modBus) { - TABS.register(modBus); + /** Declares the entries; the loader module registers them. */ + public static void init() { } } From 2f4b3de7cc3bb7f68ad06e1ad66f79c28809d9cb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:12 +0800 Subject: [PATCH 089/314] Move ModDataComponents to the shared sources on Registration --- .../mcheli/registry/ModDataComponents.java | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java (51%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java similarity index 51% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java index 2cb9b7f..08b564e 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java @@ -1,24 +1,19 @@ package com.jnhprojects.mcheli.registry; -import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; import com.mojang.serialization.Codec; import net.minecraft.core.component.DataComponentType; import net.minecraft.core.registries.Registries; import net.minecraft.network.codec.ByteBufCodecs; -import net.minecraftforge.eventbus.api.bus.BusGroup; -import net.minecraftforge.registries.DeferredRegister; -import net.minecraftforge.registries.RegistryObject; public final class ModDataComponents { - private static final DeferredRegister> COMPONENTS = - DeferredRegister.create(Registries.DATA_COMPONENT_TYPE, MCHeli.MOD_ID); - /** * The definition a vehicle item places, such as {@code ah-64}. Saved with the stack and sent * to clients; the item itself only says which kind of vehicle it is. */ - public static final RegistryObject> VEHICLE = COMPONENTS.register("vehicle", - () -> DataComponentType.builder() + public static final Registered> VEHICLE = Registration.register(Registries.DATA_COMPONENT_TYPE, "vehicle", + key -> DataComponentType.builder() .persistent(Codec.STRING) .networkSynchronized(ByteBufCodecs.STRING_UTF8) .build()); @@ -26,7 +21,7 @@ public final class ModDataComponents { private ModDataComponents() { } - public static void register(BusGroup modBus) { - COMPONENTS.register(modBus); + /** Declares the entries; the loader module registers them. */ + public static void init() { } } From 75507b457eb14bd2eaa5e81beb5a6e0a3b7c01ec Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:13 +0800 Subject: [PATCH 090/314] Move ModItems to the shared sources on Registration --- .../jnhprojects/mcheli/registry/ModItems.java | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java (55%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java similarity index 55% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java index 73db594..5292745 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java @@ -1,16 +1,14 @@ package com.jnhprojects.mcheli.registry; -import com.jnhprojects.mcheli.MCHeli; import com.jnhprojects.mcheli.content.ContentRegistry; import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; import com.jnhprojects.mcheli.content.pack.ContentSource; import com.jnhprojects.mcheli.item.AircraftItem; import com.jnhprojects.mcheli.item.LegacyAircraftItem; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; import net.minecraft.world.item.Item; -import net.minecraftforge.eventbus.api.bus.BusGroup; -import net.minecraftforge.registries.DeferredRegister; -import net.minecraftforge.registries.ForgeRegistries; -import net.minecraftforge.registries.RegistryObject; import java.util.Map; @@ -19,24 +17,26 @@ * items, one per vehicle, are kept for the content present at startup so old worlds keep them. */ public final class ModItems { - private static final DeferredRegister ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, MCHeli.MOD_ID); + public static final Registered HELICOPTER = kind("helicopter"); + public static final Registered PLANE = kind("plane"); + public static final Registered TANK = kind("tank"); + public static final Registered VEHICLE = kind("vehicle"); - public static final RegistryObject HELICOPTER = kind("helicopter"); - public static final RegistryObject PLANE = kind("plane"); - public static final RegistryObject TANK = kind("tank"); - public static final RegistryObject VEHICLE = kind("vehicle"); - - private static final Map> BY_KIND = + private static final Map> BY_KIND = Map.of("helicopter", HELICOPTER, "plane", PLANE, "tank", TANK, "vehicle", VEHICLE); private ModItems() { } - private static RegistryObject kind(String kind) { - return ITEMS.register(kind, () -> new AircraftItem(kind, new Item.Properties().setId(ITEMS.key(kind)).stacksTo(1))); + private static Registered kind(String kind) { + return Registration.register(Registries.ITEM, kind, key -> new AircraftItem(kind, new Item.Properties().setId(key).stacksTo(1))); } - public static void register(BusGroup modBus, ContentRegistry content) { + /** + * Declares the entries, with a 1.0.0 item for each original vehicle in {@code content}; the + * loader module registers them. + */ + public static void init(ContentRegistry content) { for (AircraftDefinition def : content.all()) { String name = def.name(); // Only the imported content existed in 1.0.0; pack vehicles never had their own item. @@ -45,15 +45,14 @@ public static void register(BusGroup modBus, ContentRegistry content) { continue; } String kind = def.kind(); - ITEMS.register(name, () -> new LegacyAircraftItem(kind, name, - new Item.Properties().setId(ITEMS.key(name)).stacksTo(1))); + Registration.register(Registries.ITEM, name, key -> new LegacyAircraftItem(kind, name, + new Item.Properties().setId(key).stacksTo(1))); } - ITEMS.register(modBus); } /** The generic item of a vehicle kind ({@code helicopter}, {@code plane}, {@code tank} or {@code vehicle}). */ public static Item forKind(String kind) { - RegistryObject item = BY_KIND.get(kind); + Registered item = BY_KIND.get(kind); if (item == null) { throw new IllegalArgumentException("Unknown vehicle kind " + kind); } From 01aca568b7f1013b17b234ce3444854889c57540 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:13 +0800 Subject: [PATCH 091/314] Move ModParticles to the shared sources on Registration --- .../mcheli/registry/ModParticles.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java (60%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java similarity index 60% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java index 956a36b..ce9a1da 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java @@ -1,22 +1,18 @@ package com.jnhprojects.mcheli.registry; -import com.jnhprojects.mcheli.MCHeli; import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; import com.mojang.serialization.MapCodec; import net.minecraft.core.particles.ParticleType; import net.minecraft.core.registries.Registries; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; -import net.minecraftforge.eventbus.api.bus.BusGroup; -import net.minecraftforge.registries.DeferredRegister; -import net.minecraftforge.registries.RegistryObject; public final class ModParticles { - private static final DeferredRegister> PARTICLE_TYPES = DeferredRegister.create(Registries.PARTICLE_TYPE, MCHeli.MOD_ID); - /** Ignores the particle limit: an explosion that loses its flash to the cap looks broken. */ - public static final RegistryObject> GLOW = PARTICLE_TYPES.register("glow", - () -> new ParticleType<>(true) { + public static final Registered> GLOW = Registration.register(Registries.PARTICLE_TYPE, "glow", + key -> new ParticleType<>(true) { @Override public MapCodec codec() { return GlowParticleOptions.CODEC; @@ -31,7 +27,7 @@ public StreamCodec streamC private ModParticles() { } - public static void register(BusGroup modBus) { - PARTICLE_TYPES.register(modBus); + /** Declares the entries; the loader module registers them. */ + public static void init() { } } From 05e7b3490fe2a6eaaa2de0a8a3cdeb37cdfe05d6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:14 +0800 Subject: [PATCH 092/314] Move Combat to the shared sources and use the c:glass_panes tag --- .../java/com/jnhprojects/mcheli/weapon/Combat.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java (86%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java similarity index 86% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java index 16da1c7..ebe0a0a 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java @@ -5,15 +5,18 @@ import com.jnhprojects.mcheli.entity.HelicopterEntity; import com.jnhprojects.mcheli.entity.PlaneEntity; import com.jnhprojects.mcheli.entity.TankEntity; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; import net.minecraft.server.level.ServerLevel; import net.minecraft.tags.BlockTags; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.block.Block; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.state.BlockState; -import net.minecraftforge.common.Tags; import org.jspecify.annotations.Nullable; /** Damage rules shared by bullets and explosions. */ @@ -52,12 +55,15 @@ public static boolean hurt(Entity target, DamageSource source, float damage, @Nu return target.hurtServer(level, source, damage * factor(def, target)); } + /** The convention tag every loader fills, so modded panes count too. */ + private static final TagKey GLASS_PANES = TagKey.create(Registries.BLOCK, Identifier.fromNamespaceAndPath("c", "glass_panes")); + /** * {@code BulletBreakableBlocks} with its default list: panes, grass, flowers, vines, wheat, * sugar cane and lily pads, which a round shoots through. */ public static boolean isBulletBreakable(BlockState state) { - return state.is(Tags.Blocks.GLASS_PANES) + return state.is(GLASS_PANES) || state.is(BlockTags.FLOWERS) || state.is(Blocks.SHORT_GRASS) || state.is(Blocks.TALL_GRASS) || state.is(Blocks.FERN) || state.is(Blocks.LARGE_FERN) From 815d12aea1debc8c92ed4541712a6721700598d6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:14 +0800 Subject: [PATCH 093/314] Move ExplosionEffect to the shared 26.3 sources --- .../main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java From b276a8867df018214cb4afeac08e9455341baa2c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:15 +0800 Subject: [PATCH 094/314] Move Explosions to the shared sources and read block resistance the vanilla way --- .../main/java/com/jnhprojects/mcheli/weapon/Explosions.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java (98%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java similarity index 98% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java index 41a7bbd..f7fe3e8 100644 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java @@ -183,6 +183,8 @@ private static final class Calculator extends ExplosionDamageCalculator { this.level = level; } + // Vanilla's own calculator reads the block's resistance the same deprecated way. + @SuppressWarnings("deprecation") @Override public Optional getBlockExplosionResistance(Explosion explosion, BlockGetter getter, BlockPos pos, BlockState block, FluidState fluid) { @@ -192,7 +194,7 @@ public Optional getBlockExplosionResistance(Explosion explosion, BlockGet if (block.isAir()) { return Optional.empty(); } - float resistance = block.getExplosionResistance(getter, pos, explosion); + float resistance = block.getBlock().getExplosionResistance(); return Optional.of(resistance * (level.getRandom().nextFloat() * 0.2F + 0.2F)); } From ae1f8cfe05a8f28ee81cbcce8b87c6763f9421ac Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:15 +0800 Subject: [PATCH 095/314] Move FlareDispenser to the shared 26.3 sources --- .../main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java From ed2b81536462b31ede673c1220899f07445ca4c4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:15 +0800 Subject: [PATCH 096/314] Move Guidance to the shared 26.3 sources --- .../src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java From b7896aeb195a599a035bad46eefae725da6b324a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:16 +0800 Subject: [PATCH 097/314] Move LegacyItems to the shared 26.3 sources --- .../src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java From 7bf7eeb4699c714954af00f840d82fdddaac60be Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:16 +0800 Subject: [PATCH 098/314] Move ModDamageTypes to the shared 26.3 sources --- .../main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java From 14275c18fa62d47a5a5f5d2003a105d46b20d2dd Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:17 +0800 Subject: [PATCH 099/314] Move Resupply to the shared 26.3 sources --- .../src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java From 8c341e98ecb3adb406300b6ad6e5b6fa72b9b023 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:17 +0800 Subject: [PATCH 100/314] Move ServerFiring to the shared 26.3 sources --- .../src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java From 0ff76ab090a7f2022049e90d0b85cc66b64b9360 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:18 +0800 Subject: [PATCH 101/314] Move Shot to the shared 26.3 sources --- .../src/main/java/com/jnhprojects/mcheli/weapon/Shot.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java From 0a229450fdecb8782bfe487edc0c6d0eadbae76b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:18 +0800 Subject: [PATCH 102/314] Move VehicleWeapons to the shared 26.3 sources --- .../main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java From d3f7223effe2362300282ca7abbf3bc06d87b6d1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:18 +0800 Subject: [PATCH 103/314] Move WeaponSet to the shared 26.3 sources --- .../src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java (100%) diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java similarity index 100% rename from forge-26.3/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java rename to minecraft-26.3/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java From d0237954b2bd95228c374439ce5cd05570bb0eec Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:19 +0800 Subject: [PATCH 104/314] Move assets/mcheli/items/helicopter.json to the shared 26.3 resources --- .../src/main/resources/assets/mcheli/items/helicopter.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/resources/assets/mcheli/items/helicopter.json (100%) diff --git a/forge-26.3/src/main/resources/assets/mcheli/items/helicopter.json b/minecraft-26.3/src/main/resources/assets/mcheli/items/helicopter.json similarity index 100% rename from forge-26.3/src/main/resources/assets/mcheli/items/helicopter.json rename to minecraft-26.3/src/main/resources/assets/mcheli/items/helicopter.json From 23acafdc9dabb8e3431d9c346901a3c63557b683 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:19 +0800 Subject: [PATCH 105/314] Move assets/mcheli/items/plane.json to the shared 26.3 resources --- .../src/main/resources/assets/mcheli/items/plane.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/resources/assets/mcheli/items/plane.json (100%) diff --git a/forge-26.3/src/main/resources/assets/mcheli/items/plane.json b/minecraft-26.3/src/main/resources/assets/mcheli/items/plane.json similarity index 100% rename from forge-26.3/src/main/resources/assets/mcheli/items/plane.json rename to minecraft-26.3/src/main/resources/assets/mcheli/items/plane.json From 5aa4270286f62b7040ae11597c62efd0d141a1ea Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:20 +0800 Subject: [PATCH 106/314] Move assets/mcheli/items/tank.json to the shared 26.3 resources --- .../src/main/resources/assets/mcheli/items/tank.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/resources/assets/mcheli/items/tank.json (100%) diff --git a/forge-26.3/src/main/resources/assets/mcheli/items/tank.json b/minecraft-26.3/src/main/resources/assets/mcheli/items/tank.json similarity index 100% rename from forge-26.3/src/main/resources/assets/mcheli/items/tank.json rename to minecraft-26.3/src/main/resources/assets/mcheli/items/tank.json From f89163c2fa6371b0b7feea73e9f577137d596558 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:20 +0800 Subject: [PATCH 107/314] Move assets/mcheli/items/vehicle.json to the shared 26.3 resources --- .../src/main/resources/assets/mcheli/items/vehicle.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/resources/assets/mcheli/items/vehicle.json (100%) diff --git a/forge-26.3/src/main/resources/assets/mcheli/items/vehicle.json b/minecraft-26.3/src/main/resources/assets/mcheli/items/vehicle.json similarity index 100% rename from forge-26.3/src/main/resources/assets/mcheli/items/vehicle.json rename to minecraft-26.3/src/main/resources/assets/mcheli/items/vehicle.json From 0232fc13a021cbc52a606a2cbef80e4a9939392e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:20 +0800 Subject: [PATCH 108/314] Move assets/mcheli/particles/glow.json to the shared 26.3 resources --- .../src/main/resources/assets/mcheli/particles/glow.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/resources/assets/mcheli/particles/glow.json (100%) diff --git a/forge-26.3/src/main/resources/assets/mcheli/particles/glow.json b/minecraft-26.3/src/main/resources/assets/mcheli/particles/glow.json similarity index 100% rename from forge-26.3/src/main/resources/assets/mcheli/particles/glow.json rename to minecraft-26.3/src/main/resources/assets/mcheli/particles/glow.json From dff93ccfbb9aca2e66eee7ed821aedd3bb1c4ca6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:21 +0800 Subject: [PATCH 109/314] Move assets/mcheli/textures/particle/glow.png to the shared 26.3 resources --- .../assets/mcheli/textures/particle/glow.png | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/resources/assets/mcheli/textures/particle/glow.png (100%) diff --git a/forge-26.3/src/main/resources/assets/mcheli/textures/particle/glow.png b/minecraft-26.3/src/main/resources/assets/mcheli/textures/particle/glow.png similarity index 100% rename from forge-26.3/src/main/resources/assets/mcheli/textures/particle/glow.png rename to minecraft-26.3/src/main/resources/assets/mcheli/textures/particle/glow.png From c191b6376cc8454513bfc0fb2c6b7c845ae1899b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:21 +0800 Subject: [PATCH 110/314] Move data/mcheli/damage_type/bullet.json to the shared 26.3 resources --- .../src/main/resources/data/mcheli/damage_type/bullet.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/resources/data/mcheli/damage_type/bullet.json (100%) diff --git a/forge-26.3/src/main/resources/data/mcheli/damage_type/bullet.json b/minecraft-26.3/src/main/resources/data/mcheli/damage_type/bullet.json similarity index 100% rename from forge-26.3/src/main/resources/data/mcheli/damage_type/bullet.json rename to minecraft-26.3/src/main/resources/data/mcheli/damage_type/bullet.json From 59ed9b2994cdc6f865425e2f4934d060fa30e586 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:22 +0800 Subject: [PATCH 111/314] Move data/minecraft/tags/damage_type/is_projectile.json to the shared 26.3 resources --- .../resources/data/minecraft/tags/damage_type/is_projectile.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json (100%) diff --git a/forge-26.3/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json b/minecraft-26.3/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json similarity index 100% rename from forge-26.3/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json rename to minecraft-26.3/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json From 2545f8b68caea30b681836e7607b48e6f2f96500 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:22 +0800 Subject: [PATCH 112/314] Move mcheli_logo.png to the shared 26.3 resources --- .../src/main/resources/mcheli_logo.png | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/resources/mcheli_logo.png (100%) diff --git a/forge-26.3/src/main/resources/mcheli_logo.png b/minecraft-26.3/src/main/resources/mcheli_logo.png similarity index 100% rename from forge-26.3/src/main/resources/mcheli_logo.png rename to minecraft-26.3/src/main/resources/mcheli_logo.png From 0a47e228a6a22038c4875b1d53f83aee50d43fd5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:22 +0800 Subject: [PATCH 113/314] Move pack.mcmeta to the shared 26.3 resources --- {forge-26.3 => minecraft-26.3}/src/main/resources/pack.mcmeta | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {forge-26.3 => minecraft-26.3}/src/main/resources/pack.mcmeta (100%) diff --git a/forge-26.3/src/main/resources/pack.mcmeta b/minecraft-26.3/src/main/resources/pack.mcmeta similarity index 100% rename from forge-26.3/src/main/resources/pack.mcmeta rename to minecraft-26.3/src/main/resources/pack.mcmeta From e460ec8dbf0d7a54e45b50cce2a82416fec21daa Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:23 +0800 Subject: [PATCH 114/314] Make MCHeli the loader-free core that every entry point initialises --- .../java/com/jnhprojects/mcheli/MCHeli.java | 77 ------------------- .../java/com/jnhprojects/mcheli/MCHeli.java | 65 ++++++++++++++++ 2 files changed, 65 insertions(+), 77 deletions(-) delete mode 100644 forge-26.3/src/main/java/com/jnhprojects/mcheli/MCHeli.java create mode 100644 minecraft-26.3/src/main/java/com/jnhprojects/mcheli/MCHeli.java diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/MCHeli.java b/forge-26.3/src/main/java/com/jnhprojects/mcheli/MCHeli.java deleted file mode 100644 index bf183df..0000000 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/MCHeli.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.jnhprojects.mcheli; - -import com.jnhprojects.mcheli.client.ClientSetup; -import com.jnhprojects.mcheli.content.ContentRegistry; -import com.jnhprojects.mcheli.network.ModNetwork; -import com.jnhprojects.mcheli.network.PackSync; -import com.jnhprojects.mcheli.registry.ModCreativeTabs; -import com.jnhprojects.mcheli.registry.ModDataComponents; -import com.jnhprojects.mcheli.registry.ModEntities; -import com.jnhprojects.mcheli.registry.ModItems; -import com.jnhprojects.mcheli.registry.ModParticles; -import com.jnhprojects.mcheli.registry.ModRecipes; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.event.server.ServerAboutToStartEvent; -import net.minecraftforge.eventbus.api.bus.BusGroup; -import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.fml.config.ModConfig; -import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; -import net.minecraftforge.fml.loading.FMLEnvironment; -import net.minecraftforge.fml.loading.FMLPaths; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -@Mod(MCHeli.MOD_ID) -public final class MCHeli { - public static final String MOD_ID = "mcheli"; - public static final Logger LOGGER = LoggerFactory.getLogger(MCHeli.class); - - private static LegacyContent legacyContent; - private static ContentPacks packs; - private static ContentRegistry content; - - public MCHeli(FMLJavaModLoadingContext context) { - BusGroup modBus = context.getModBusGroup(); - - legacyContent = LegacyContent.prepare(FMLPaths.GAMEDIR.get()); - legacyContent.registerPacks(); - packs = new ContentPacks(FMLPaths.GAMEDIR.get(), legacyContent); - content = packs.load(false); - // The server config (allow list, size limits) exists from here on, before any world loads. - ServerAboutToStartEvent.BUS.addListener(event -> content.replaceWith(packs.load(true))); - context.registerConfig(ModConfig.Type.SERVER, ServerConfig.SPEC); - context.registerConfig(ModConfig.Type.CLIENT, ClientConfig.SPEC); - ModDataComponents.register(modBus); - ModEntities.register(modBus); - ModParticles.register(modBus); - ModItems.register(modBus, content); - ModCreativeTabs.register(modBus); - ModRecipes.register(modBus); - ModNetwork.register(); - PackSync.init(); - McheliCommand.register(); - - if (FMLEnvironment.dist == Dist.CLIENT) { - ClientSetup.init(modBus); - } - } - - /** - * Vehicle and weapon definitions, from the imported original content and the content packs. - * Available from mod construction onwards. Always the same object; a reload changes what it - * holds. - */ - public static ContentRegistry content() { - return content; - } - - /** Where the original vehicle content came from, and why it is missing if it is. */ - public static LegacyContent legacyContent() { - return legacyContent; - } - - /** The content packs in {@code mcheli/packs} and the ones in use. */ - public static ContentPacks packs() { - return packs; - } -} diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/MCHeli.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/MCHeli.java new file mode 100644 index 0000000..21d4185 --- /dev/null +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/MCHeli.java @@ -0,0 +1,65 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.registry.ModCreativeTabs; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.registry.ModItems; +import com.jnhprojects.mcheli.registry.ModParticles; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The mod, minus the loader. Each loader module's entry point installs its {@link Platform}, calls + * {@link #init()}, registers what {@code Registration} collected and wires its events to + * {@link CommonHooks} (and, on a client, {@code ClientHooks}). + */ +public final class MCHeli { + public static final String MOD_ID = "mcheli"; + public static final Logger LOGGER = LoggerFactory.getLogger(MCHeli.class); + + private static LegacyContent legacyContent; + private static ContentPacks packs; + private static ContentRegistry content; + + private MCHeli() { + } + + /** + * Imports the original content if needed, loads the definitions and declares the registry + * entries. The vehicle list must be known here: 1.0.0 had an item per vehicle, and those items + * are still registered so old worlds keep them. + */ + public static void init() { + Platform platform = Platform.get(); + LOGGER.info("MCHeli Rework {} on {}", platform.modVersion(), platform.loaderName()); + legacyContent = LegacyContent.prepare(platform.gameDir()); + packs = new ContentPacks(platform.gameDir(), legacyContent); + content = packs.load(false); + ModDataComponents.init(); + ModEntities.init(); + ModParticles.init(); + ModItems.init(content); + ModCreativeTabs.init(); + } + + /** + * Vehicle and weapon definitions, from the imported original content and the content packs. + * Available from mod construction onwards. Always the same object; a reload changes what it + * holds. + */ + public static ContentRegistry content() { + return content; + } + + /** Where the original vehicle content came from, and why it is missing if it is. */ + public static LegacyContent legacyContent() { + return legacyContent; + } + + /** The content packs in {@code mcheli/packs} and the ones in use. */ + public static ContentPacks packs() { + return packs; + } +} From f7b6bfc4816fdcc350fb8c1abf41fc66cb98e6ab Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:23 +0800 Subject: [PATCH 115/314] Turn ClientSetup into ClientHooks, which the loader modules call from their events --- .../mcheli/client/ClientSetup.java | 68 ------ .../mcheli/client/ClientHooks.java | 215 ++++++++++++++++++ 2 files changed, 215 insertions(+), 68 deletions(-) delete mode 100644 forge-26.3/src/main/java/com/jnhprojects/mcheli/client/ClientSetup.java create mode 100644 minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/ClientSetup.java b/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/ClientSetup.java deleted file mode 100644 index e29e911..0000000 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/client/ClientSetup.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.jnhprojects.mcheli.client; - -import com.jnhprojects.mcheli.MCHeli; -import com.jnhprojects.mcheli.client.hud.HudRenderer; -import com.jnhprojects.mcheli.client.hud.HudScripts; -import com.jnhprojects.mcheli.client.particle.GlowParticle; -import com.jnhprojects.mcheli.client.render.A10Renderer; -import com.jnhprojects.mcheli.client.render.AircraftRenderer; -import com.jnhprojects.mcheli.client.render.FlareRenderer; -import com.jnhprojects.mcheli.client.render.MeshCache; -import com.jnhprojects.mcheli.client.render.ProjectileRenderer; -import com.jnhprojects.mcheli.client.render.RiderPose; -import com.jnhprojects.mcheli.client.sound.EngineSoundPlayer; -import com.jnhprojects.mcheli.entity.ClientAccess; -import com.jnhprojects.mcheli.registry.ModEntities; -import com.jnhprojects.mcheli.registry.ModParticles; -import net.minecraft.client.Minecraft; -import net.minecraftforge.client.event.ClientPlayerNetworkEvent; -import net.minecraftforge.client.event.EntityRenderersEvent; -import net.minecraftforge.client.event.RegisterClientReloadListenersEvent; -import net.minecraftforge.client.event.RegisterKeyMappingsEvent; -import net.minecraftforge.client.event.RegisterParticleProvidersEvent; -import net.minecraftforge.eventbus.api.bus.BusGroup; - -/** Client only wiring. Kept out of {@code MCHeli} so dedicated servers never load these classes. */ -public final class ClientSetup { - private ClientSetup() { - } - - public static void init(BusGroup modBus) { - ClientAccess.install(() -> Minecraft.getInstance().player, ClientPacks.INSTANCE); - EntityRenderersEvent.RegisterRenderers.BUS.addListener(event -> { - event.registerEntityRenderer(ModEntities.HELICOPTER.get(), AircraftRenderer::new); - event.registerEntityRenderer(ModEntities.PLANE.get(), AircraftRenderer::new); - event.registerEntityRenderer(ModEntities.TANK.get(), AircraftRenderer::new); - event.registerEntityRenderer(ModEntities.VEHICLE.get(), AircraftRenderer::new); - event.registerEntityRenderer(ModEntities.BULLET.get(), ProjectileRenderer::new); - event.registerEntityRenderer(ModEntities.ROCKET.get(), ProjectileRenderer::new); - event.registerEntityRenderer(ModEntities.BOMB.get(), ProjectileRenderer::new); - event.registerEntityRenderer(ModEntities.AA_MISSILE.get(), ProjectileRenderer::new); - event.registerEntityRenderer(ModEntities.AS_MISSILE.get(), ProjectileRenderer::new); - event.registerEntityRenderer(ModEntities.AT_MISSILE.get(), ProjectileRenderer::new); - event.registerEntityRenderer(ModEntities.TV_MISSILE.get(), ProjectileRenderer::new); - event.registerEntityRenderer(ModEntities.TORPEDO.get(), ProjectileRenderer::new); - event.registerEntityRenderer(ModEntities.MARKER_ROCKET.get(), ProjectileRenderer::new); - event.registerEntityRenderer(ModEntities.DISPENSED_ITEM.get(), ProjectileRenderer::new); - event.registerEntityRenderer(ModEntities.FLARE.get(), FlareRenderer::new); - event.registerEntityRenderer(ModEntities.A10.get(), A10Renderer::new); - }); - RegisterParticleProvidersEvent.BUS.addListener(event -> event.registerSpriteSet(ModParticles.GLOW.get(), GlowParticle.Provider::new)); - RegisterKeyMappingsEvent.BUS.addListener(event -> KeyBindings.ALL.forEach(event::register)); - RegisterClientReloadListenersEvent.BUS.addListener(event -> { - event.registerReloadListener(MeshCache.INSTANCE); - event.registerReloadListener(HudScripts.INSTANCE); - }); - // Without the original zip there are no vehicles, so say so where the player will see it. - ClientPlayerNetworkEvent.LoggingIn.BUS.addListener(event -> - MCHeli.legacyContent().problem().ifPresent(event.getPlayer()::sendSystemMessage)); - ClientPlayerNetworkEvent.LoggingOut.BUS.addListener(event -> ClientPacks.INSTANCE.loggedOut()); - CrewController.init(); - WeaponController.init(); - VehiclePicker.init(); - WeaponHud.init(); - HudRenderer.init(); - EngineSoundPlayer.init(); - RiderPose.init(); - } -} diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java new file mode 100644 index 0000000..50b9bed --- /dev/null +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java @@ -0,0 +1,215 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.ClientConfig; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.hud.HudRenderer; +import com.jnhprojects.mcheli.client.hud.HudScripts; +import com.jnhprojects.mcheli.client.render.A10Renderer; +import com.jnhprojects.mcheli.client.render.AircraftRenderer; +import com.jnhprojects.mcheli.client.render.FlareRenderer; +import com.jnhprojects.mcheli.client.render.MeshCache; +import com.jnhprojects.mcheli.client.render.ProjectileRenderer; +import com.jnhprojects.mcheli.client.render.RiderPose; +import com.jnhprojects.mcheli.client.sound.EngineSoundPlayer; +import com.jnhprojects.mcheli.entity.ClientAccess; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.KeyMapping; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.resources.PreparableReloadListener; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; + +import java.util.List; +import java.util.Map; + +/** + * Client side counterpart of {@code CommonHooks}: what the mod does on client events. The loader + * module calls these from its own events. Kept out of the shared entry point so dedicated servers + * never load these classes. + */ +public final class ClientHooks { + /** The weapon HUD's layer, drawn above the vanilla HUD. */ + public static final Identifier HUD_LAYER = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "weapons"); + + private ClientHooks() { + } + + /** During mod construction, on a client only. */ + public static void init() { + ClientConfig.load(Platform.get().configDir()); + ClientAccess.install(() -> Minecraft.getInstance().player, ClientPacks.INSTANCE); + } + + /** How a loader registers an entity renderer. */ + public interface EntityRenderers { + void register(EntityType type, EntityRendererProvider provider); + } + + public static void registerEntityRenderers(EntityRenderers renderers) { + renderers.register(ModEntities.HELICOPTER.get(), AircraftRenderer::new); + renderers.register(ModEntities.PLANE.get(), AircraftRenderer::new); + renderers.register(ModEntities.TANK.get(), AircraftRenderer::new); + renderers.register(ModEntities.VEHICLE.get(), AircraftRenderer::new); + renderers.register(ModEntities.BULLET.get(), ProjectileRenderer::new); + renderers.register(ModEntities.ROCKET.get(), ProjectileRenderer::new); + renderers.register(ModEntities.BOMB.get(), ProjectileRenderer::new); + renderers.register(ModEntities.AA_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.AS_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.AT_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.TV_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.TORPEDO.get(), ProjectileRenderer::new); + renderers.register(ModEntities.MARKER_ROCKET.get(), ProjectileRenderer::new); + renderers.register(ModEntities.DISPENSED_ITEM.get(), ProjectileRenderer::new); + renderers.register(ModEntities.FLARE.get(), FlareRenderer::new); + renderers.register(ModEntities.A10.get(), A10Renderer::new); + } + + public static List keyMappings() { + return KeyBindings.ALL; + } + + /** Client resource reload listeners, by the id Fabric wants for each. */ + public static Map reloadListeners() { + return Map.of(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "meshes"), MeshCache.INSTANCE, + Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "hud_scripts"), HudScripts.INSTANCE); + } + + public static void clientTickEnd() { + CrewController.onClientTick(); + WeaponController.onClientTick(); + HudRenderer.tick(); + EngineSoundPlayer.tick(); + } + + /** Tilts the view with the hull and moves the eye into the cockpit or onto a TV missile. */ + public static void computeCameraAngles(CameraAngles angles) { + CrewController.onCameraAngles(angles); + } + + /** Sets the camera's rotation, roll included, the way Forge's {@code Camera#setRotation} does. */ + public interface CameraRotation { + void set(Camera camera, float yaw, float pitch, float roll); + } + + /** + * For loaders without Forge's camera angle event: called where Forge fires it, right after + * vanilla placed the camera for this frame. Starts from the camera's own angles and no roll, + * and only touches the rotation when something changed it. + */ + public static void computeCameraAngles(Camera camera, float partialTick, CameraRotation rotation) { + Angles angles = new Angles(camera, partialTick, camera.yRot(), camera.xRot()); + computeCameraAngles(angles); + if (angles.yaw != camera.yRot() || angles.pitch != camera.xRot() || angles.roll != 0.0F) { + rotation.set(camera, angles.yaw, angles.pitch, angles.roll); + } + } + + private static final class Angles implements CameraAngles { + private final Camera camera; + private final float partialTick; + private float yaw; + private float pitch; + private float roll; + + Angles(Camera camera, float partialTick, float yaw, float pitch) { + this.camera = camera; + this.partialTick = partialTick; + this.yaw = yaw; + this.pitch = pitch; + } + + @Override + public Camera camera() { + return camera; + } + + @Override + public float partialTick() { + return partialTick; + } + + @Override + public float yaw() { + return yaw; + } + + @Override + public float pitch() { + return pitch; + } + + @Override + public float roll() { + return roll; + } + + @Override + public void setYaw(float yaw) { + this.yaw = yaw; + } + + @Override + public void setPitch(float pitch) { + this.pitch = pitch; + } + + @Override + public void setRoll(float roll) { + this.roll = roll; + } + } + + /** Whether to skip drawing the first person hand this frame. */ + public static boolean hidesHand() { + return CrewController.hidesHand(); + } + + /** What happens to a press of the use key. */ + public enum UseKey { + /** Vanilla handles it. */ + PASS, + /** Taken, and the hand does not swing: it is a weapon trigger. */ + CONSUME, + /** Taken, and the hand swings as for a vanilla click: the player boards a vehicle. */ + CONSUME_AND_SWING + } + + public static UseKey onUseKey(InteractionHand hand) { + if (WeaponController.onUseItem()) { + return UseKey.CONSUME; + } + return VehiclePicker.onUseItem(hand) ? UseKey.CONSUME_AND_SWING : UseKey.PASS; + } + + /** The pick block key switches weapons in a seat that has any; true when it was taken. */ + public static boolean onPickBlockKey() { + return WeaponController.onPickBlock(); + } + + public static void beforeRenderLiving(LivingEntityRenderState state, PoseStack pose, float partialTicks) { + RiderPose.beforeRender(state, pose, partialTicks); + } + + public static void drawHud(GuiGraphicsExtractor graphics, DeltaTracker delta) { + WeaponHud.draw(graphics, delta); + } + + /** Without the original zip there are no vehicles, so say so where the player will see it. */ + public static void loggedIn(LocalPlayer player) { + MCHeli.legacyContent().problem().ifPresent(player::sendSystemMessage); + } + + public static void loggedOut() { + ClientPacks.INSTANCE.loggedOut(); + } +} From 0a3d0e0afc05859e3190e6e38485117368398de1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:23 +0800 Subject: [PATCH 116/314] Split VehicleIngredient into shared matching and a wrapper per loader --- .../mcheli/item/VehicleIngredient.java | 83 ------------------- .../mcheli/item/VehicleIngredient.java | 68 +++++++++++++++ 2 files changed, 68 insertions(+), 83 deletions(-) delete mode 100644 forge-26.3/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java create mode 100644 minecraft-26.3/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java b/forge-26.3/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java deleted file mode 100644 index 2012f9b..0000000 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.jnhprojects.mcheli.item; - -import com.jnhprojects.mcheli.MCHeli; -import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; -import com.jnhprojects.mcheli.registry.ModItems; -import com.mojang.serialization.Codec; -import com.mojang.serialization.MapCodec; -import net.minecraft.core.HolderSet; -import net.minecraft.network.RegistryFriendlyByteBuf; -import net.minecraft.network.codec.ByteBufCodecs; -import net.minecraft.network.codec.StreamCodec; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.ItemStackTemplate; -import net.minecraft.world.item.crafting.Ingredient; -import net.minecraft.world.item.crafting.display.SlotDisplay; -import net.minecraftforge.common.crafting.ingredients.AbstractIngredient; -import net.minecraftforge.common.crafting.ingredients.IIngredientSerializer; -import org.jspecify.annotations.Nullable; - -import java.util.Objects; - -/** - * Matches one vehicle, such as an AH-6 in the recipe that turns it into an AH-6X. Every vehicle of - * a kind shares an item, so a plain item ingredient would accept any of them. In JSON: - *

{"type": "mcheli:vehicle", "vehicle": "ah-6"}
- */ -public final class VehicleIngredient extends AbstractIngredient { - public static final MapCodec CODEC = - Codec.STRING.fieldOf("vehicle").xmap(VehicleIngredient::new, VehicleIngredient::vehicle); - public static final StreamCodec STREAM_CODEC = - ByteBufCodecs.STRING_UTF8.map(VehicleIngredient::new, VehicleIngredient::vehicle).cast(); - public static final IIngredientSerializer SERIALIZER = IIngredientSerializer.simple(CODEC, STREAM_CODEC); - - private final String vehicle; - - public VehicleIngredient(String vehicle) { - super(kindItems()); - this.vehicle = vehicle; - } - - @SuppressWarnings("deprecation") - private static HolderSet kindItems() { - return HolderSet.direct(Item::builtInRegistryHolder, - ModItems.HELICOPTER.get(), ModItems.PLANE.get(), ModItems.TANK.get(), ModItems.VEHICLE.get()); - } - - public String vehicle() { - return vehicle; - } - - @Override - public boolean test(@Nullable ItemStack stack) { - return stack != null && vehicle.equals(AircraftItem.vehicleOf(stack)); - } - - @Override - public SlotDisplay display() { - AircraftDefinition def = MCHeli.content().any(vehicle); - return def != null ? new SlotDisplay.ItemStackSlotDisplay(ItemStackTemplate.fromNonEmptyStack(AircraftItem.stackOf(def))) - : SlotDisplay.Empty.INSTANCE; - } - - @Override - public boolean isSimple() { - return false; - } - - @Override - public IIngredientSerializer serializer() { - return SERIALIZER; - } - - @Override - public boolean equals(Object o) { - return o instanceof VehicleIngredient other && vehicle.equals(other.vehicle); - } - - @Override - public int hashCode() { - return Objects.hash(vehicle); - } -} diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java new file mode 100644 index 0000000..c261904 --- /dev/null +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java @@ -0,0 +1,68 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.registry.ModItems; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.Identifier; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.ItemStackTemplate; +import net.minecraft.world.item.crafting.display.SlotDisplay; +import org.jspecify.annotations.Nullable; + +import java.util.function.Function; +import java.util.stream.Stream; + +/** + * Matches one vehicle, such as an AH-6 in the recipe that turns it into an AH-6X. Every vehicle of + * a kind shares an item, so a plain item ingredient would accept any of them. In JSON: + *
{"type": "mcheli:vehicle", "vehicle": "ah-6"}
+ * with the type also under {@code neoforge:ingredient_type} and {@code fabric:type}, since each + * loader names custom ingredients its own way. + * + *

This is the loader-independent half; each loader module wraps it in its own ingredient class. + */ +public final class VehicleIngredient { + public static final Identifier ID = Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "vehicle"); + + private VehicleIngredient() { + } + + /** The JSON fields, given a way to build the loader's ingredient from a vehicle name. */ + public static MapCodec codec(Function create, Function vehicle) { + return Codec.STRING.fieldOf("vehicle").xmap(create, vehicle); + } + + public static StreamCodec streamCodec(Function create, Function vehicle) { + return ByteBufCodecs.STRING_UTF8.map(create, vehicle).cast(); + } + + /** The items a vehicle can be: the generic item of each kind. */ + @SuppressWarnings("deprecation") + public static HolderSet kindItems() { + return HolderSet.direct(Item::builtInRegistryHolder, + ModItems.HELICOPTER.get(), ModItems.PLANE.get(), ModItems.TANK.get(), ModItems.VEHICLE.get()); + } + + public static Stream> items() { + return kindItems().stream(); + } + + public static boolean matches(String vehicle, @Nullable ItemStack stack) { + return stack != null && vehicle.equals(AircraftItem.vehicleOf(stack)); + } + + /** What recipe books show: the vehicle itself, or nothing while it is not loaded. */ + public static SlotDisplay display(String vehicle) { + AircraftDefinition def = MCHeli.content().any(vehicle); + return def != null ? new SlotDisplay.ItemStackSlotDisplay(ItemStackTemplate.fromNonEmptyStack(AircraftItem.stackOf(def))) + : SlotDisplay.Empty.INSTANCE; + } +} From a0cdfdae20600bf8c40f2aa99686e31e56f8716f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:24 +0800 Subject: [PATCH 117/314] List the packets as payloads for every loader to register --- .../mcheli/network/ModNetwork.java | 87 ------------------- .../mcheli/network/ModNetwork.java | 81 +++++++++++++++++ 2 files changed, 81 insertions(+), 87 deletions(-) delete mode 100644 forge-26.3/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java create mode 100644 minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java b/forge-26.3/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java deleted file mode 100644 index 705a2ad..0000000 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.jnhprojects.mcheli.network; - -import com.jnhprojects.mcheli.MCHeli; -import net.minecraft.network.RegistryFriendlyByteBuf; -import net.minecraft.network.codec.StreamCodec; -import net.minecraft.resources.Identifier; -import net.minecraft.server.MinecraftServer; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.server.level.ServerPlayer; -import net.minecraftforge.network.ChannelBuilder; -import net.minecraftforge.network.PacketDistributor; -import net.minecraftforge.network.SimpleChannel; - -public final class ModNetwork { - /** Bump whenever a packet layout changes. Clients and servers must match exactly. */ - private static final int PROTOCOL_VERSION = 6; - - private static final SimpleChannel CHANNEL = ChannelBuilder - .named(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "main")) - .networkProtocolVersion(PROTOCOL_VERSION) - .simpleChannel() - .play() - .serverbound() - .addMain(PilotStatePacket.class, - StreamCodec.ofMember(PilotStatePacket::encode, PilotStatePacket::decode), - PilotStatePacket::handle) - .addMain(CrewActionPacket.class, - StreamCodec.ofMember(CrewActionPacket::encode, CrewActionPacket::decode), - CrewActionPacket::handle) - .addMain(FireWeaponPacket.class, - StreamCodec.ofMember(FireWeaponPacket::encode, FireWeaponPacket::decode), - FireWeaponPacket::handle) - .addMain(BoardVehiclePacket.class, - StreamCodec.ofMember(BoardVehiclePacket::encode, BoardVehiclePacket::decode), - BoardVehiclePacket::handle) - .addMain(LockWarningPacket.class, - StreamCodec.ofMember(LockWarningPacket::encode, LockWarningPacket::decode), - LockWarningPacket::handle) - .addMain(PackRequestPacket.class, - StreamCodec.ofMember(PackRequestPacket::encode, PackRequestPacket::decode), - PackRequestPacket::handle) - .clientbound() - .addMain(WeaponStatePacket.class, - StreamCodec.ofMember(WeaponStatePacket::encode, WeaponStatePacket::decode), - WeaponStatePacket::handle) - .addMain(ExplosionEffectPacket.class, - StreamCodec.ofMember(ExplosionEffectPacket::encode, ExplosionEffectPacket::decode), - ExplosionEffectPacket::handle) - .addMain(PackListPacket.class, - StreamCodec.ofMember(PackListPacket::encode, PackListPacket::decode), - PackListPacket::handle) - .addMain(PackDataPacket.class, - StreamCodec.ofMember(PackDataPacket::encode, PackDataPacket::decode), - PackDataPacket::handle) - .build(); - - private ModNetwork() { - } - - /** Forces the channel to be built during mod construction. */ - public static void register() { - MCHeli.LOGGER.debug("Network channel {} v{}", CHANNEL.getName(), CHANNEL.getProtocolVersion()); - } - - public static void sendToServer(Object message) { - CHANNEL.send(message, PacketDistributor.SERVER.noArg()); - } - - public static void sendToPlayer(ServerPlayer player, Object message) { - CHANNEL.send(message, PacketDistributor.PLAYER.with(player)); - } - - public static void sendToAll(MinecraftServer server, Object message) { - for (ServerPlayer player : server.getPlayerList().getPlayers()) { - sendToPlayer(player, message); - } - } - - /** Sends to every player in {@code level} within {@code range} blocks of the point. */ - public static void sendNear(ServerLevel level, double x, double y, double z, double range, Object message) { - for (ServerPlayer player : level.players()) { - if (player.distanceToSqr(x, y, z) < range * range) { - sendToPlayer(player, message); - } - } - } -} diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java new file mode 100644 index 0000000..464780e --- /dev/null +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java @@ -0,0 +1,81 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; + +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * Our packets. Each one is a custom payload with its own id and the same encoding on every loader, + * so a client on one loader can talk to a server on another. The loader module registers the + * lists below with its own networking API; handlers always run on the main thread. + */ +public final class ModNetwork { + /** Bump whenever a packet layout changes. Clients and servers must match exactly. */ + public static final int PROTOCOL_VERSION = 6; + + /** Client to server. The handler gets the sending player. */ + public static final List> SERVERBOUND = List.of( + new Serverbound<>(PilotStatePacket.TYPE, PilotStatePacket.STREAM_CODEC, PilotStatePacket::handle), + new Serverbound<>(CrewActionPacket.TYPE, CrewActionPacket.STREAM_CODEC, CrewActionPacket::handle), + new Serverbound<>(FireWeaponPacket.TYPE, FireWeaponPacket.STREAM_CODEC, FireWeaponPacket::handle), + new Serverbound<>(BoardVehiclePacket.TYPE, BoardVehiclePacket.STREAM_CODEC, BoardVehiclePacket::handle), + new Serverbound<>(LockWarningPacket.TYPE, LockWarningPacket.STREAM_CODEC, LockWarningPacket::handle), + new Serverbound<>(PackRequestPacket.TYPE, PackRequestPacket.STREAM_CODEC, PackRequestPacket::handle)); + + /** Server to client. Handlers touch client classes, so they only ever run on a client. */ + public static final List> CLIENTBOUND = List.of( + new Clientbound<>(WeaponStatePacket.TYPE, WeaponStatePacket.STREAM_CODEC, WeaponStatePacket::handle), + new Clientbound<>(ExplosionEffectPacket.TYPE, ExplosionEffectPacket.STREAM_CODEC, ExplosionEffectPacket::handle), + new Clientbound<>(PackListPacket.TYPE, PackListPacket.STREAM_CODEC, PackListPacket::handle), + new Clientbound<>(PackDataPacket.TYPE, PackDataPacket.STREAM_CODEC, PackDataPacket::handle)); + + private ModNetwork() { + } + + static CustomPacketPayload.Type type(String name) { + return new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, name)); + } + + public static void sendToServer(CustomPacketPayload message) { + Platform.get().sendToServer(message); + } + + public static void sendToPlayer(ServerPlayer player, CustomPacketPayload message) { + Platform.get().sendToPlayer(player, message); + } + + public static void sendToAll(MinecraftServer server, CustomPacketPayload message) { + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + sendToPlayer(player, message); + } + } + + /** Sends to every player in {@code level} within {@code range} blocks of the point. */ + public static void sendNear(ServerLevel level, double x, double y, double z, double range, CustomPacketPayload message) { + for (ServerPlayer player : level.players()) { + if (player.distanceToSqr(x, y, z) < range * range) { + sendToPlayer(player, message); + } + } + } + + public record Serverbound(CustomPacketPayload.Type type, + StreamCodec codec, + BiConsumer handler) { + } + + public record Clientbound(CustomPacketPayload.Type type, + StreamCodec codec, + Consumer handler) { + } +} From 838c532b64ed102a6b4a26dbcdab1898a284763d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:24 +0800 Subject: [PATCH 118/314] Declare the entity types on Registration --- .../mcheli/registry/ModEntities.java | 112 ------------------ .../mcheli/registry/ModEntities.java | 108 +++++++++++++++++ 2 files changed, 108 insertions(+), 112 deletions(-) delete mode 100644 forge-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java create mode 100644 minecraft-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java b/forge-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java deleted file mode 100644 index 418f31e..0000000 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.jnhprojects.mcheli.registry; - -import com.jnhprojects.mcheli.MCHeli; -import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; -import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; -import com.jnhprojects.mcheli.content.aircraft.PlaneDefinition; -import com.jnhprojects.mcheli.content.aircraft.TankDefinition; -import com.jnhprojects.mcheli.entity.AircraftEntity; -import com.jnhprojects.mcheli.entity.GroundVehicleEntity; -import com.jnhprojects.mcheli.entity.HelicopterEntity; -import com.jnhprojects.mcheli.entity.PlaneEntity; -import com.jnhprojects.mcheli.entity.TankEntity; -import com.jnhprojects.mcheli.entity.projectile.A10Entity; -import com.jnhprojects.mcheli.entity.projectile.AAMissileEntity; -import com.jnhprojects.mcheli.entity.projectile.ASMissileEntity; -import com.jnhprojects.mcheli.entity.projectile.ATMissileEntity; -import com.jnhprojects.mcheli.entity.projectile.BombEntity; -import com.jnhprojects.mcheli.entity.projectile.BulletEntity; -import com.jnhprojects.mcheli.entity.projectile.DispensedItemEntity; -import com.jnhprojects.mcheli.entity.projectile.FlareEntity; -import com.jnhprojects.mcheli.entity.projectile.MarkerRocketEntity; -import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; -import com.jnhprojects.mcheli.entity.projectile.RocketEntity; -import com.jnhprojects.mcheli.entity.projectile.TorpedoEntity; -import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; -import net.minecraft.world.entity.EntityType; -import net.minecraft.world.entity.MobCategory; -import net.minecraftforge.eventbus.api.bus.BusGroup; -import net.minecraftforge.registries.DeferredRegister; -import net.minecraftforge.registries.ForgeRegistries; -import net.minecraftforge.registries.RegistryObject; - -/** - * One entity type per vehicle kind. The concrete vehicle is stored on the entity, the same approach - * the original took, so adding content never changes the entity registry. - */ -public final class ModEntities { - private static final DeferredRegister> ENTITY_TYPES = DeferredRegister.create(ForgeRegistries.ENTITY_TYPES, MCHeli.MOD_ID); - - public static final RegistryObject> HELICOPTER = register("helicopter", HelicopterEntity::new); - public static final RegistryObject> PLANE = register("plane", PlaneEntity::new); - public static final RegistryObject> TANK = register("tank", TankEntity::new); - public static final RegistryObject> VEHICLE = register("vehicle", GroundVehicleEntity::new); - - public static final RegistryObject> BULLET = projectile("bullet", BulletEntity::new); - public static final RegistryObject> ROCKET = projectile("rocket", RocketEntity::new); - public static final RegistryObject> BOMB = projectile("bomb", BombEntity::new); - public static final RegistryObject> AA_MISSILE = projectile("aa_missile", AAMissileEntity::new); - public static final RegistryObject> AS_MISSILE = projectile("as_missile", ASMissileEntity::new); - public static final RegistryObject> AT_MISSILE = projectile("at_missile", ATMissileEntity::new); - public static final RegistryObject> TV_MISSILE = projectile("tv_missile", TvMissileEntity::new); - public static final RegistryObject> TORPEDO = projectile("torpedo", TorpedoEntity::new); - public static final RegistryObject> MARKER_ROCKET = projectile("marker_rocket", MarkerRocketEntity::new); - public static final RegistryObject> DISPENSED_ITEM = projectile("dispensed_item", DispensedItemEntity::new); - public static final RegistryObject> FLARE = ENTITY_TYPES.register("flare", () -> EntityType.Builder - .of(FlareEntity::new, MobCategory.MISC) - .sized(1.0F, 1.0F) - .clientTrackingRange(16) - .updateInterval(2) - .fireImmune() - .noSave() - .build(ENTITY_TYPES.key("flare"))); - public static final RegistryObject> A10 = ENTITY_TYPES.register("a10", () -> EntityType.Builder - .of(A10Entity::new, MobCategory.MISC) - .sized(5.0F, 3.0F) - .clientTrackingRange(32) - .updateInterval(1) - .fireImmune() - .noSave() - .build(ENTITY_TYPES.key("a10"))); - - private ModEntities() { - } - - /** Projectiles are a metre cube like the original's, and never saved. */ - private static RegistryObject> projectile(String name, EntityType.EntityFactory factory) { - return ENTITY_TYPES.register(name, () -> EntityType.Builder.of(factory, MobCategory.MISC) - .sized(1.0F, 1.0F) - .clientTrackingRange(16) - .updateInterval(1) - .fireImmune() - .noSave() - .build(ENTITY_TYPES.key(name))); - } - - private static RegistryObject> register(String name, EntityType.EntityFactory factory) { - return ENTITY_TYPES.register(name, () -> EntityType.Builder.of(factory, MobCategory.MISC) - .sized(2.0F, 0.7F) - .clientTrackingRange(16) - .updateInterval(1) - .fireImmune() - .build(ENTITY_TYPES.key(name))); - } - - /** The entity type that hosts a definition of this kind. */ - public static EntityType typeFor(AircraftDefinition def) { - if (def instanceof HelicopterDefinition) { - return HELICOPTER.get(); - } - if (def instanceof PlaneDefinition) { - return PLANE.get(); - } - if (def instanceof TankDefinition) { - return TANK.get(); - } - return VEHICLE.get(); - } - - public static void register(BusGroup modBus) { - ENTITY_TYPES.register(modBus); - } -} diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java new file mode 100644 index 0000000..c16131e --- /dev/null +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java @@ -0,0 +1,108 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; +import com.jnhprojects.mcheli.content.aircraft.PlaneDefinition; +import com.jnhprojects.mcheli.content.aircraft.TankDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.GroundVehicleEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import com.jnhprojects.mcheli.entity.projectile.A10Entity; +import com.jnhprojects.mcheli.entity.projectile.AAMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ASMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ATMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.BombEntity; +import com.jnhprojects.mcheli.entity.projectile.BulletEntity; +import com.jnhprojects.mcheli.entity.projectile.DispensedItemEntity; +import com.jnhprojects.mcheli.entity.projectile.FlareEntity; +import com.jnhprojects.mcheli.entity.projectile.MarkerRocketEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.entity.projectile.RocketEntity; +import com.jnhprojects.mcheli.entity.projectile.TorpedoEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MobCategory; + +/** + * One entity type per vehicle kind. The concrete vehicle is stored on the entity, the same approach + * the original took, so adding content never changes the entity registry. + */ +public final class ModEntities { + public static final Registered> HELICOPTER = register("helicopter", HelicopterEntity::new); + public static final Registered> PLANE = register("plane", PlaneEntity::new); + public static final Registered> TANK = register("tank", TankEntity::new); + public static final Registered> VEHICLE = register("vehicle", GroundVehicleEntity::new); + + public static final Registered> BULLET = projectile("bullet", BulletEntity::new); + public static final Registered> ROCKET = projectile("rocket", RocketEntity::new); + public static final Registered> BOMB = projectile("bomb", BombEntity::new); + public static final Registered> AA_MISSILE = projectile("aa_missile", AAMissileEntity::new); + public static final Registered> AS_MISSILE = projectile("as_missile", ASMissileEntity::new); + public static final Registered> AT_MISSILE = projectile("at_missile", ATMissileEntity::new); + public static final Registered> TV_MISSILE = projectile("tv_missile", TvMissileEntity::new); + public static final Registered> TORPEDO = projectile("torpedo", TorpedoEntity::new); + public static final Registered> MARKER_ROCKET = projectile("marker_rocket", MarkerRocketEntity::new); + public static final Registered> DISPENSED_ITEM = projectile("dispensed_item", DispensedItemEntity::new); + public static final Registered> FLARE = Registration.register(Registries.ENTITY_TYPE, "flare", key -> EntityType.Builder + .of(FlareEntity::new, MobCategory.MISC) + .sized(1.0F, 1.0F) + .clientTrackingRange(16) + .updateInterval(2) + .fireImmune() + .noSave() + .build(key)); + public static final Registered> A10 = Registration.register(Registries.ENTITY_TYPE, "a10", key -> EntityType.Builder + .of(A10Entity::new, MobCategory.MISC) + .sized(5.0F, 3.0F) + .clientTrackingRange(32) + .updateInterval(1) + .fireImmune() + .noSave() + .build(key)); + + private ModEntities() { + } + + /** Projectiles are a metre cube like the original's, and never saved. */ + private static Registered> projectile(String name, EntityType.EntityFactory factory) { + return Registration.register(Registries.ENTITY_TYPE, name, key -> EntityType.Builder.of(factory, MobCategory.MISC) + .sized(1.0F, 1.0F) + .clientTrackingRange(16) + .updateInterval(1) + .fireImmune() + .noSave() + .build(key)); + } + + private static Registered> register(String name, EntityType.EntityFactory factory) { + return Registration.register(Registries.ENTITY_TYPE, name, key -> EntityType.Builder.of(factory, MobCategory.MISC) + .sized(2.0F, 0.7F) + .clientTrackingRange(16) + .updateInterval(1) + .fireImmune() + .build(key)); + } + + /** The entity type that hosts a definition of this kind. */ + public static EntityType typeFor(AircraftDefinition def) { + if (def instanceof HelicopterDefinition) { + return HELICOPTER.get(); + } + if (def instanceof PlaneDefinition) { + return PLANE.get(); + } + if (def instanceof TankDefinition) { + return TANK.get(); + } + return VEHICLE.get(); + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 917944050613a64aca0564de6107e72c2f4f3dac Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:25 +0800 Subject: [PATCH 119/314] Remove ModRecipes; the recipe book and ingredient moved to the hooks and loader modules --- .../mcheli/registry/ModRecipes.java | 42 ------------------- 1 file changed, 42 deletions(-) delete mode 100644 forge-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModRecipes.java diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModRecipes.java b/forge-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModRecipes.java deleted file mode 100644 index d96ba19..0000000 --- a/forge-26.3/src/main/java/com/jnhprojects/mcheli/registry/ModRecipes.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.jnhprojects.mcheli.registry; - -import com.jnhprojects.mcheli.MCHeli; -import com.jnhprojects.mcheli.item.VehicleIngredient; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.item.crafting.RecipeHolder; -import net.minecraftforge.common.crafting.ingredients.IIngredientSerializer; -import net.minecraftforge.event.entity.player.PlayerEvent; -import net.minecraftforge.eventbus.api.bus.BusGroup; -import net.minecraftforge.registries.DeferredRegister; -import net.minecraftforge.registries.ForgeRegistries; - -import java.util.List; - -/** - * The vehicle recipes live in {@code data/mcheli/recipe} of the imported content pack, generated - * from the definitions' {@code AddRecipe} lines. The original listed them on its drafting table; - * until that is ported, every MCHeli recipe goes into a player's recipe book when they join, so they can be found. - */ -public final class ModRecipes { - private static final DeferredRegister> INGREDIENTS = - DeferredRegister.create(ForgeRegistries.Keys.INGREDIENT_SERIALIZERS, MCHeli.MOD_ID); - - static { - INGREDIENTS.register("vehicle", () -> VehicleIngredient.SERIALIZER); - } - - private ModRecipes() { - } - - public static void register(BusGroup modBus) { - INGREDIENTS.register(modBus); - PlayerEvent.PlayerLoggedInEvent.BUS.addListener(event -> { - if (event.getEntity() instanceof ServerPlayer player) { - List> ours = player.level().getServer().getRecipeManager().getRecipes().stream() - .filter(holder -> holder.id().identifier().getNamespace().equals(MCHeli.MOD_ID)) - .toList(); - player.awardRecipes(ours); - } - }); - } -} From ca3ad8e04d6073e53e259ed3501eacf92ff5f6f3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:25 +0800 Subject: [PATCH 120/314] Add CommonHooks for the loader modules to call from server events --- .../com/jnhprojects/mcheli/CommonHooks.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 minecraft-26.3/src/main/java/com/jnhprojects/mcheli/CommonHooks.java diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/CommonHooks.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/CommonHooks.java new file mode 100644 index 0000000..d7b91d8 --- /dev/null +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/CommonHooks.java @@ -0,0 +1,70 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.network.PackSync; +import com.mojang.brigadier.CommandDispatcher; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.packs.repository.RepositorySource; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.level.storage.LevelResource; + +import java.util.List; + +/** + * What the mod does on game events that both sides see. Each loader module calls these from its + * own events; nothing here knows which loader that is. + */ +public final class CommonHooks { + private CommonHooks() { + } + + /** + * Adds the imported original content to the resource packs (client) and data packs (server). + * Without the original zip there is nothing to add. + */ + public static RepositorySource packSource() { + return onLoad -> { + // Fabric builds some pack repositories before our initializer has run. + LegacyContent content = MCHeli.legacyContent(); + if (content != null) { + content.pack().ifPresent(onLoad); + } + }; + } + + /** Before the worlds load: read the world's server config, then load the content packs it allows. */ + public static void serverAboutToStart(MinecraftServer server) { + ServerConfig.load(server.getWorldPath(LevelResource.ROOT)); + MCHeli.content().replaceWith(MCHeli.packs().load(true)); + } + + public static void serverStopped() { + ServerConfig.unload(); + } + + public static void serverTickEnd(MinecraftServer server) { + PackSync.pump(server); + } + + public static void registerCommands(CommandDispatcher dispatcher) { + McheliCommand.register(dispatcher); + } + + /** + * The vehicle recipes live in {@code data/mcheli/recipe} of the imported content pack. The + * original listed them on its drafting table; until that is ported, every MCHeli recipe goes + * into a player's recipe book when they join, so they can be found. + */ + public static void playerLoggedIn(ServerPlayer player) { + PackSync.onLogin(player); + List> ours = player.level().getServer().getRecipeManager().getRecipes().stream() + .filter(holder -> holder.id().identifier().getNamespace().equals(MCHeli.MOD_ID)) + .toList(); + player.awardRecipes(ours); + } + + public static void playerLoggedOut(ServerPlayer player) { + PackSync.onLogout(player); + } +} From b5c762e330af99f72df33cd351a1734f23598af2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:26 +0800 Subject: [PATCH 121/314] Add CameraAngles so the camera hook works without Forge's event --- .../mcheli/client/CameraAngles.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java new file mode 100644 index 0000000..ab62e12 --- /dev/null +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java @@ -0,0 +1,26 @@ +package com.jnhprojects.mcheli.client; + +import net.minecraft.client.Camera; + +/** + * The camera's angles for this frame, after vanilla placed the camera and before it renders, as + * the loader module hands them to {@link ClientHooks#computeCameraAngles}. Forge and NeoForge + * back this with their viewport event; Fabric with a mixin at the same spot. + */ +public interface CameraAngles { + Camera camera(); + + float partialTick(); + + float yaw(); + + float pitch(); + + float roll(); + + void setYaw(float yaw); + + void setPitch(float pitch); + + void setRoll(float roll); +} From 87d89717e9e5f22dc65e75b1a898cfb449322ca6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:26 +0800 Subject: [PATCH 122/314] Build the Forge module from the shared loader script --- forge-26.3/build.gradle | 69 +---------------------------------------- 1 file changed, 1 insertion(+), 68 deletions(-) diff --git a/forge-26.3/build.gradle b/forge-26.3/build.gradle index 70b4337..c0a19a2 100644 --- a/forge-26.3/build.gradle +++ b/forge-26.3/build.gradle @@ -5,21 +5,7 @@ plugins { id 'net.minecraftforge.gradle' } -version = mod_version -group = mod_group - -base { - archivesName = "mcheli-${minecraft_version}" -} - -java.toolchain.languageVersion = JavaLanguageVersion.of(java_version as int) - -// The shared code is compiled into this module rather than depended on, so the mod stays one -// jar and one module in development runs. The common project checks on its own that these -// sources need nothing from Minecraft. -def common = project(':common') -sourceSets.main.java.srcDir(common.file('src/main/java')) -sourceSets.main.resources.srcDir(common.file('src/main/resources')) +apply from: rootProject.file('gradle/loader.gradle') // The original MCHeli 1.0.3 download. Players supply their own copy; development runs use this // one (git-ignored, see docs/DEVELOPMENT.md). @@ -52,56 +38,3 @@ dependencies { implementation minecraft.dependency("net.minecraftforge:forge:${minecraft_version}-${forge_version}") annotationProcessor "net.minecraftforge:eventbus-validator:${eventbus_version}" } - -tasks.withType(JavaCompile).configureEach { - options.encoding = 'UTF-8' - options.compilerArgs.addAll(['-Xlint:deprecation', '-Xlint:unchecked']) -} - -def expandProps = [ - mod_id : mod_id, - mod_name : mod_name, - mod_version : version, - mod_authors : mod_authors, - mod_license : mod_license, - minecraft_version_range: minecraft_version_range, - forge_version_range : forge_version_range, - loader_version_range : loader_version_range, -] - -tasks.named('processResources', ProcessResources) { - inputs.properties(expandProps) - filesMatching(['META-INF/mods.toml', 'pack.mcmeta']) { - expand expandProps - } -} - -tasks.named('jar', Jar) { - // EMB4's content is never redistributed: players import it from their own copy of the original - // zip (LegacyContent). The repository no longer holds it, but the jar is packed from - // build/sourceSets/main, which can still have copies from older builds and from the IDE. - def shipped = ['assets/mcheli/items/helicopter.json', 'assets/mcheli/items/plane.json', - 'assets/mcheli/items/tank.json', 'assets/mcheli/items/vehicle.json', - // Our own: the night glow sprite (generated, no EMB4 content) and its particle definition. - 'assets/mcheli/particles/glow.json', 'assets/mcheli/textures/particle/glow.png'] - exclude { FileTreeElement element -> - String path = element.relativePath.pathString - !element.directory && path.startsWith('assets/mcheli/') && !path.startsWith('assets/mcheli/lang/') && - !shipped.contains(path) - } - exclude 'data/mcheli/recipe/**' - includeEmptyDirs = false - // The licence travels with every copy of the jar. - from(rootProject.file('LICENSE')) - manifest { - attributes([ - 'Specification-Title' : mod_id, - 'Specification-Vendor' : 'JnH Projects', - 'Specification-Version' : '1', - 'Implementation-Title' : mod_name, - 'Implementation-Version': version, - 'Implementation-Vendor' : 'JnH Projects', - 'Minecraft-Version' : minecraft_version, - ]) - } -} From 5ae417bb7816fed581385f760462d9072f867a35 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:26 +0800 Subject: [PATCH 123/314] Name the loader in the Forge module properties --- forge-26.3/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge-26.3/gradle.properties b/forge-26.3/gradle.properties index 286e31a..2c20a3d 100644 --- a/forge-26.3/gradle.properties +++ b/forge-26.3/gradle.properties @@ -1,8 +1,8 @@ # Minecraft 26.3 on Forge +loader=forge minecraft_version=26.3 minecraft_version_range=[26.3,26.4) forge_version=66.1.0 -forge_version_range=[66,) loader_version_range=[66,) eventbus_version=7.0.6 java_version=25 From 39f54e02143cafb6a2f7648b81ca0c753e88348e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:27 +0800 Subject: [PATCH 124/314] Use the loader version range for the Forge dependency --- forge-26.3/src/main/resources/META-INF/mods.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge-26.3/src/main/resources/META-INF/mods.toml b/forge-26.3/src/main/resources/META-INF/mods.toml index b6481de..1fdc3b4 100644 --- a/forge-26.3/src/main/resources/META-INF/mods.toml +++ b/forge-26.3/src/main/resources/META-INF/mods.toml @@ -21,7 +21,7 @@ MCHeli CurseForge page into the "mcheli" folder of the game directory. [[dependencies.${mod_id}]] modId="forge" mandatory=true - versionRange="${forge_version_range}" + versionRange="${loader_version_range}" ordering="NONE" side="BOTH" From 61d022b1989612a8cb2acdde773e21303ae2072d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:27 +0800 Subject: [PATCH 125/314] Add the Forge entry point --- .../jnhprojects/mcheli/forge/McheliForge.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java b/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java new file mode 100644 index 0000000..d9b6998 --- /dev/null +++ b/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java @@ -0,0 +1,68 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.CommonHooks; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.event.AddPackFindersEvent; +import net.minecraftforge.event.RegisterCommandsEvent; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.event.server.ServerAboutToStartEvent; +import net.minecraftforge.event.server.ServerStoppedEvent; +import net.minecraftforge.eventbus.api.bus.BusGroup; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.registries.ForgeRegistries; +import net.minecraftforge.registries.RegisterEvent; + +/** The Forge entry point: everything here only connects Forge to the shared code. */ +@Mod(MCHeli.MOD_ID) +public final class McheliForge { + + public McheliForge(FMLJavaModLoadingContext context) { + BusGroup modBus = context.getModBusGroup(); + Platform.install(new ForgePlatform()); + MCHeli.init(); + + RegisterEvent.getBus(modBus).addListener(event -> { + for (ResourceKey> registry : Registration.registries()) { + register(event, Registration.typed(registry)); + } + event.register(ForgeRegistries.Keys.INGREDIENT_SERIALIZERS, + helper -> helper.register(VehicleIngredient.ID, ForgeVehicleIngredient.SERIALIZER)); + }); + ForgeNetwork.register(); + + AddPackFindersEvent.BUS.addListener(event -> event.addRepositorySource(CommonHooks.packSource())); + ServerAboutToStartEvent.BUS.addListener(event -> CommonHooks.serverAboutToStart(event.getServer())); + ServerStoppedEvent.BUS.addListener(event -> CommonHooks.serverStopped()); + TickEvent.ServerTickEvent.Post.BUS.addListener(event -> CommonHooks.serverTickEnd(event.server())); + RegisterCommandsEvent.BUS.addListener(event -> CommonHooks.registerCommands(event.getDispatcher())); + PlayerEvent.PlayerLoggedInEvent.BUS.addListener(event -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedIn(player); + } + }); + PlayerEvent.PlayerLoggedOutEvent.BUS.addListener(event -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedOut(player); + } + }); + + if (Platform.get().isClient()) { + ForgeClient.init(); + } + } + + /** Only once Forge asks for that registry, with our entries in declaration order. */ + private static void register(RegisterEvent event, ResourceKey> registry) { + if (event.getRegistryKey().equals(registry)) { + event.register(registry, helper -> Registration.registerAll(registry, helper::register)); + } + } +} From 59f8d8d8e1fce1ac1308d19fa89f8a61a0498e68 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:27 +0800 Subject: [PATCH 126/314] Add the Forge platform --- .../mcheli/forge/ForgePlatform.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java b/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java new file mode 100644 index 0000000..d82312f --- /dev/null +++ b/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java @@ -0,0 +1,58 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.loading.FMLEnvironment; +import net.minecraftforge.fml.loading.FMLPaths; +import net.minecraftforge.network.PacketDistributor; + +import java.nio.file.Path; + +final class ForgePlatform implements Platform { + @Override + public String loaderName() { + return "forge"; + } + + @Override + public Path gameDir() { + return FMLPaths.GAMEDIR.get(); + } + + @Override + public Path configDir() { + return FMLPaths.CONFIGDIR.get(); + } + + @Override + public String modVersion() { + return ModList.getModContainerById(MCHeli.MOD_ID) + .map(c -> c.getModInfo().getVersion().toString()).orElse(""); + } + + @Override + public boolean isClient() { + return FMLEnvironment.dist == Dist.CLIENT; + } + + @Override + public void sendToServer(CustomPacketPayload payload) { + ForgeNetwork.channel().send(payload, PacketDistributor.SERVER.noArg()); + } + + @Override + public void sendToPlayer(ServerPlayer player, CustomPacketPayload payload) { + ForgeNetwork.channel().send(payload, PacketDistributor.PLAYER.with(player)); + } + + @Override + public CreativeModeTab.Builder creativeTab(ResourceKey after) { + return CreativeModeTab.builder().withTabsBefore(after); + } +} From 5c78b6059e7eef2ad75092ccc0663d73b978da38 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:28 +0800 Subject: [PATCH 127/314] Register the packets on a Forge payload channel --- .../mcheli/forge/ForgeNetwork.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java b/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java new file mode 100644 index 0000000..54a48e8 --- /dev/null +++ b/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java @@ -0,0 +1,58 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.network.ModNetwork; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; +import net.minecraftforge.network.Channel; +import net.minecraftforge.network.ChannelBuilder; +import net.minecraftforge.network.payload.PayloadFlow; + +/** + * Registers the shared packets on a Forge payload channel. Each packet travels under its own id, + * as on NeoForge and Fabric; the channel only adds Forge's version check. + */ +final class ForgeNetwork { + private static Channel channel; + + private ForgeNetwork() { + } + + static void register() { + PayloadFlow flow = ChannelBuilder + .named(Identifier.fromNamespaceAndPath(MCHeli.MOD_ID, "main")) + .networkProtocolVersion(ModNetwork.PROTOCOL_VERSION) + .payloadChannel() + .play() + .serverbound(); + for (ModNetwork.Serverbound packet : ModNetwork.SERVERBOUND) { + flow = serverbound(flow, packet); + } + flow = flow.clientbound(); + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + flow = clientbound(flow, packet); + } + channel = flow.build(); + } + + static Channel channel() { + return channel; + } + + private static PayloadFlow serverbound( + PayloadFlow flow, ModNetwork.Serverbound packet) { + return flow.add(packet.type(), packet.codec(), (msg, ctx) -> { + ctx.enqueueWork(() -> packet.handler().accept(msg, ctx.getSender())); + ctx.setPacketHandled(true); + }); + } + + private static PayloadFlow clientbound( + PayloadFlow flow, ModNetwork.Clientbound packet) { + return flow.add(packet.type(), packet.codec(), (msg, ctx) -> { + ctx.enqueueWork(() -> packet.handler().accept(msg)); + ctx.setPacketHandled(true); + }); + } +} From b798fb9e878f3c72bf6e1b5dcb65a03015acd50a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:28 +0800 Subject: [PATCH 128/314] Add the Forge vehicle ingredient --- .../mcheli/forge/ForgeVehicleIngredient.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java b/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java new file mode 100644 index 0000000..b39681a --- /dev/null +++ b/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java @@ -0,0 +1,61 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.mojang.serialization.MapCodec; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.display.SlotDisplay; +import net.minecraftforge.common.crafting.ingredients.AbstractIngredient; +import net.minecraftforge.common.crafting.ingredients.IIngredientSerializer; +import org.jspecify.annotations.Nullable; + +/** {@link VehicleIngredient} as a Forge ingredient, read from the {@code type} key. */ +final class ForgeVehicleIngredient extends AbstractIngredient { + static final MapCodec CODEC = VehicleIngredient.codec(ForgeVehicleIngredient::new, ForgeVehicleIngredient::vehicle); + static final StreamCodec STREAM_CODEC = + VehicleIngredient.streamCodec(ForgeVehicleIngredient::new, ForgeVehicleIngredient::vehicle); + static final IIngredientSerializer SERIALIZER = IIngredientSerializer.simple(CODEC, STREAM_CODEC); + + private final String vehicle; + + private ForgeVehicleIngredient(String vehicle) { + super(VehicleIngredient.kindItems()); + this.vehicle = vehicle; + } + + String vehicle() { + return vehicle; + } + + @Override + public boolean test(@Nullable ItemStack stack) { + return VehicleIngredient.matches(vehicle, stack); + } + + @Override + public SlotDisplay display() { + return VehicleIngredient.display(vehicle); + } + + @Override + public boolean isSimple() { + return false; + } + + @Override + public IIngredientSerializer serializer() { + return SERIALIZER; + } + + @Override + public boolean equals(Object o) { + return o instanceof ForgeVehicleIngredient other && vehicle.equals(other.vehicle); + } + + @Override + public int hashCode() { + return vehicle.hashCode(); + } +} From 8c41f1929a1230a43804cee59277b74bfd42e2c9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:28 +0800 Subject: [PATCH 129/314] Wire the Forge client events to the client hooks --- .../jnhprojects/mcheli/forge/ForgeClient.java | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java diff --git a/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java b/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java new file mode 100644 index 0000000..2cc5cb7 --- /dev/null +++ b/forge-26.3/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java @@ -0,0 +1,104 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.client.CameraAngles; +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.client.particle.GlowParticle; +import com.jnhprojects.mcheli.registry.ModParticles; +import net.minecraft.client.Camera; +import net.minecraftforge.client.event.AddGuiOverlayLayersEvent; +import net.minecraftforge.client.event.ClientPlayerNetworkEvent; +import net.minecraftforge.client.event.EntityRenderersEvent; +import net.minecraftforge.client.event.InputEvent; +import net.minecraftforge.client.event.RegisterClientReloadListenersEvent; +import net.minecraftforge.client.event.RegisterKeyMappingsEvent; +import net.minecraftforge.client.event.RegisterParticleProvidersEvent; +import net.minecraftforge.client.event.RenderHandEvent; +import net.minecraftforge.client.event.RenderLivingEvent; +import net.minecraftforge.client.event.ViewportEvent; +import net.minecraftforge.client.gui.overlay.ForgeLayeredDraw; +import net.minecraftforge.event.TickEvent; + +/** Client events on Forge. Only loaded on a physical client. */ +final class ForgeClient { + private ForgeClient() { + } + + static void init() { + ClientHooks.init(); + EntityRenderersEvent.RegisterRenderers.BUS.addListener(event -> ClientHooks.registerEntityRenderers(event::registerEntityRenderer)); + RegisterParticleProvidersEvent.BUS.addListener(event -> event.registerSpriteSet(ModParticles.GLOW.get(), GlowParticle.Provider::new)); + RegisterKeyMappingsEvent.BUS.addListener(event -> ClientHooks.keyMappings().forEach(event::register)); + RegisterClientReloadListenersEvent.BUS.addListener(event -> ClientHooks.reloadListeners().values().forEach(event::registerReloadListener)); + AddGuiOverlayLayersEvent.BUS.addListener(event -> event.getLayeredDraw() + .add(ForgeLayeredDraw.POST_SLEEP_STACK, ClientHooks.HUD_LAYER, ClientHooks::drawHud)); + + TickEvent.ClientTickEvent.Post.BUS.addListener(event -> ClientHooks.clientTickEnd()); + ViewportEvent.ComputeCameraAngles.BUS.addListener(event -> ClientHooks.computeCameraAngles(new Angles(event))); + RenderHandEvent.BUS.addListener(ForgeClient::hideHand); + RenderLivingEvent.Pre.BUS.addListener(event -> { + ClientHooks.beforeRenderLiving(event.getState(), event.getPoseStack(), event.getCameraState().cameraEntityPartialTicks); + return false; + }); + InputEvent.InteractionKeyMappingTriggered.UseItem.BUS.addListener(event -> switch (ClientHooks.onUseKey(event.getHand())) { + case PASS -> false; + case CONSUME -> { + event.setSwingHand(false); + yield true; + } + case CONSUME_AND_SWING -> true; + }); + InputEvent.InteractionKeyMappingTriggered.PickBlock.BUS.addListener(ForgeClient::pickBlock); + ClientPlayerNetworkEvent.LoggingIn.BUS.addListener(event -> ClientHooks.loggedIn(event.getPlayer())); + ClientPlayerNetworkEvent.LoggingOut.BUS.addListener(event -> ClientHooks.loggedOut()); + } + + private static boolean hideHand(RenderHandEvent event) { + return ClientHooks.hidesHand(); + } + + private static boolean pickBlock(InputEvent.InteractionKeyMappingTriggered.PickBlock event) { + return ClientHooks.onPickBlockKey(); + } + + private record Angles(ViewportEvent.ComputeCameraAngles event) implements CameraAngles { + @Override + public Camera camera() { + return event.getCamera(); + } + + @Override + public float partialTick() { + return event.getPartialTick(); + } + + @Override + public float yaw() { + return event.getYaw(); + } + + @Override + public float pitch() { + return event.getPitch(); + } + + @Override + public float roll() { + return event.getRoll(); + } + + @Override + public void setYaw(float yaw) { + event.setYaw(yaw); + } + + @Override + public void setPitch(float pitch) { + event.setPitch(pitch); + } + + @Override + public void setRoll(float roll) { + event.setRoll(roll); + } + } +} From a65111f7d90b2fa9cfcc2a28663f5bf46ea22dc8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:29 +0800 Subject: [PATCH 130/314] Add the NeoForge 26.3 module --- neoforge-26.3/build.gradle | 40 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 neoforge-26.3/build.gradle diff --git a/neoforge-26.3/build.gradle b/neoforge-26.3/build.gradle new file mode 100644 index 0000000..f57e988 --- /dev/null +++ b/neoforge-26.3/build.gradle @@ -0,0 +1,40 @@ +plugins { + id 'java' + id 'idea' + id 'eclipse' + id 'net.neoforged.moddev' +} + +apply from: rootProject.file('gradle/loader.gradle') + +// The original MCHeli 1.0.3 download. Players supply their own copy; development runs use this +// one (git-ignored, see docs/DEVELOPMENT.md). +def legacyZip = rootProject.file('Mod/MC1.7.10_mcheli_1.0.3.zip').absolutePath + +neoForge { + version = neoforge_version + + runs { + configureEach { + gameDirectory = rootProject.file('run') + systemProperty 'mcheli.legacyZip', legacyZip + } + client { + client() + } + server { + server() + programArgument '--nogui' + } + } + + mods { + mcheli { + sourceSet sourceSets.main + } + } +} + +repositories { + mavenCentral() +} From a2d0acc2cada173bca59def1645b849ce14b9577 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:29 +0800 Subject: [PATCH 131/314] Add the NeoForge 26.3 module properties --- neoforge-26.3/gradle.properties | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 neoforge-26.3/gradle.properties diff --git a/neoforge-26.3/gradle.properties b/neoforge-26.3/gradle.properties new file mode 100644 index 0000000..7645c93 --- /dev/null +++ b/neoforge-26.3/gradle.properties @@ -0,0 +1,7 @@ +# Minecraft 26.3 on NeoForge +loader=neoforge +minecraft_version=26.3 +minecraft_version_range=[26.3,26.4) +neoforge_version=26.3.0.99 +loader_version_range=[26.3,) +java_version=25 From 858c23bb760c4bc25b380df3b2b8378d153f19bc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:29 +0800 Subject: [PATCH 132/314] Add the NeoForge mod metadata --- .../resources/META-INF/neoforge.mods.toml | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 neoforge-26.3/src/main/resources/META-INF/neoforge.mods.toml diff --git a/neoforge-26.3/src/main/resources/META-INF/neoforge.mods.toml b/neoforge-26.3/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..c809591 --- /dev/null +++ b/neoforge-26.3/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,36 @@ +modLoader="javafml" +loaderVersion="[1,)" +license="${mod_license}" +issueTrackerURL="https://github.com/JnH-Projects/MCHeli-Rework-Forge/issues" + +[[mods]] +modId="${mod_id}" +version="${mod_version}" +displayName="${mod_name}" +displayURL="https://github.com/JnH-Projects/MCHeli-Rework-Forge" +iconFile="mcheli_logo.png" +authors="${mod_authors}" +credits="EMB4 created the original MCHeli mod and all of its vehicle content. Fureniku maintains the CurseForge project." +description=''' +Helicopters, planes, tanks and vehicles for modern Minecraft. +A ground-up rework of MCHeli 1.0.3 by EMB4, which stopped at Minecraft 1.7.10. +The vehicles come from your own copy of the original: put MC1.7.10_mcheli_1.0.3.zip from the +MCHeli CurseForge page into the "mcheli" folder of the game directory. +''' + +[[mixins]] +config="mcheli.mixins.json" + +[[dependencies.${mod_id}]] + modId="neoforge" + type="required" + versionRange="${loader_version_range}" + ordering="NONE" + side="BOTH" + +[[dependencies.${mod_id}]] + modId="minecraft" + type="required" + versionRange="${minecraft_version_range}" + ordering="NONE" + side="BOTH" From 0a41c5fd8c088bdbf1f4b0baf8587a09f2da6384 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:30 +0800 Subject: [PATCH 133/314] Add the NeoForge mixin config --- neoforge-26.3/src/main/resources/mcheli.mixins.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 neoforge-26.3/src/main/resources/mcheli.mixins.json diff --git a/neoforge-26.3/src/main/resources/mcheli.mixins.json b/neoforge-26.3/src/main/resources/mcheli.mixins.json new file mode 100644 index 0000000..19d6b73 --- /dev/null +++ b/neoforge-26.3/src/main/resources/mcheli.mixins.json @@ -0,0 +1,13 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "com.jnhprojects.mcheli.neoforge.mixin", + "compatibilityLevel": "JAVA_21", + "client": [ + "CameraInvoker", + "GameRendererMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} From 256dfd4e888436bf6e3a5e6337c394ad3e6fe77a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:30 +0800 Subject: [PATCH 134/314] Add the NeoForge entry point --- .../mcheli/neoforge/McheliNeoForge.java | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java diff --git a/neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java b/neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java new file mode 100644 index 0000000..734b0ed --- /dev/null +++ b/neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java @@ -0,0 +1,91 @@ +package com.jnhprojects.mcheli.neoforge; + +import com.jnhprojects.mcheli.CommonHooks; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.Registry; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.AddPackFindersEvent; +import net.neoforged.neoforge.event.RegisterCommandsEvent; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import net.neoforged.neoforge.event.server.ServerAboutToStartEvent; +import net.neoforged.neoforge.event.server.ServerStoppedEvent; +import net.neoforged.neoforge.event.tick.ServerTickEvent; +import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; +import net.neoforged.neoforge.network.registration.PayloadRegistrar; +import net.neoforged.neoforge.registries.NeoForgeRegistries; +import net.neoforged.neoforge.registries.RegisterEvent; + +/** The NeoForge entry point: everything here only connects NeoForge to the shared code. */ +@Mod(MCHeli.MOD_ID) +public final class McheliNeoForge { + + public McheliNeoForge(IEventBus modBus) { + Platform.install(new NeoForgePlatform()); + MCHeli.init(); + + modBus.addListener(RegisterEvent.class, event -> { + for (ResourceKey> registry : Registration.registries()) { + register(event, Registration.typed(registry)); + } + event.register(NeoForgeRegistries.Keys.INGREDIENT_TYPES, + helper -> helper.register(VehicleIngredient.ID, NeoForgeVehicleIngredient.TYPE)); + }); + modBus.addListener(RegisterPayloadHandlersEvent.class, McheliNeoForge::registerPackets); + modBus.addListener(AddPackFindersEvent.class, event -> event.addRepositorySource(CommonHooks.packSource())); + + NeoForge.EVENT_BUS.addListener(ServerAboutToStartEvent.class, event -> CommonHooks.serverAboutToStart(event.getServer())); + NeoForge.EVENT_BUS.addListener(ServerStoppedEvent.class, event -> CommonHooks.serverStopped()); + NeoForge.EVENT_BUS.addListener(ServerTickEvent.Post.class, event -> CommonHooks.serverTickEnd(event.getServer())); + NeoForge.EVENT_BUS.addListener(RegisterCommandsEvent.class, event -> CommonHooks.registerCommands(event.getDispatcher())); + NeoForge.EVENT_BUS.addListener(PlayerEvent.PlayerLoggedInEvent.class, event -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedIn(player); + } + }); + NeoForge.EVENT_BUS.addListener(PlayerEvent.PlayerLoggedOutEvent.class, event -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedOut(player); + } + }); + + if (Platform.get().isClient()) { + NeoForgeClient.init(modBus); + } + } + + /** Only once NeoForge asks for that registry, with our entries in declaration order. */ + private static void register(RegisterEvent event, ResourceKey> registry) { + if (event.getRegistryKey().equals(registry)) { + event.register(registry, helper -> Registration.registerAll(registry, helper::register)); + } + } + + /** Handlers run on the main thread, which is NeoForge's default. */ + private static void registerPackets(RegisterPayloadHandlersEvent event) { + PayloadRegistrar registrar = event.registrar(Integer.toString(ModNetwork.PROTOCOL_VERSION)); + for (ModNetwork.Serverbound packet : ModNetwork.SERVERBOUND) { + serverbound(registrar, packet); + } + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + clientbound(registrar, packet); + } + } + + private static void serverbound(PayloadRegistrar registrar, ModNetwork.Serverbound packet) { + registrar.playToServer(packet.type(), packet.codec(), + (msg, context) -> packet.handler().accept(msg, (ServerPlayer) context.player())); + } + + private static void clientbound(PayloadRegistrar registrar, ModNetwork.Clientbound packet) { + registrar.playToClient(packet.type(), packet.codec(), (msg, context) -> packet.handler().accept(msg)); + } +} From 5931f9e1d5d6712f26b9f471134905bf67a89682 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:31 +0800 Subject: [PATCH 135/314] Add the NeoForge platform --- .../mcheli/neoforge/NeoForgePlatform.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java diff --git a/neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java b/neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java new file mode 100644 index 0000000..6cd0660 --- /dev/null +++ b/neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java @@ -0,0 +1,59 @@ +package com.jnhprojects.mcheli.neoforge; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.fml.ModList; +import net.neoforged.fml.loading.FMLEnvironment; +import net.neoforged.fml.loading.FMLPaths; +import net.neoforged.neoforge.client.network.ClientPacketDistributor; +import net.neoforged.neoforge.network.PacketDistributor; + +import java.nio.file.Path; + +final class NeoForgePlatform implements Platform { + @Override + public String loaderName() { + return "neoforge"; + } + + @Override + public Path gameDir() { + return FMLPaths.GAMEDIR.get(); + } + + @Override + public Path configDir() { + return FMLPaths.CONFIGDIR.get(); + } + + @Override + public String modVersion() { + return ModList.get().getModContainerById(MCHeli.MOD_ID) + .map(c -> c.getModInfo().getVersion().toString()).orElse(""); + } + + @Override + public boolean isClient() { + return FMLEnvironment.getDist() == Dist.CLIENT; + } + + @Override + public void sendToServer(CustomPacketPayload payload) { + ClientPacketDistributor.sendToServer(payload); + } + + @Override + public void sendToPlayer(ServerPlayer player, CustomPacketPayload payload) { + PacketDistributor.sendToPlayer(player, payload); + } + + @Override + public CreativeModeTab.Builder creativeTab(ResourceKey after) { + return CreativeModeTab.builder().withTabsBefore(after); + } +} From 7d94fac3f46d61bd82384eaf711848c0a76477ab Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:31 +0800 Subject: [PATCH 136/314] Add the NeoForge vehicle ingredient --- .../neoforge/NeoForgeVehicleIngredient.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java diff --git a/neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java b/neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java new file mode 100644 index 0000000..195be00 --- /dev/null +++ b/neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java @@ -0,0 +1,43 @@ +package com.jnhprojects.mcheli.neoforge; + +import com.jnhprojects.mcheli.item.VehicleIngredient; +import net.minecraft.core.Holder; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.display.SlotDisplay; +import net.neoforged.neoforge.common.crafting.ICustomIngredient; +import net.neoforged.neoforge.common.crafting.IngredientType; + +import java.util.stream.Stream; + +/** {@link VehicleIngredient} as a NeoForge ingredient, read from the {@code neoforge:ingredient_type} key. */ +record NeoForgeVehicleIngredient(String vehicle) implements ICustomIngredient { + static final IngredientType TYPE = new IngredientType<>( + VehicleIngredient.codec(NeoForgeVehicleIngredient::new, NeoForgeVehicleIngredient::vehicle), + VehicleIngredient.streamCodec(NeoForgeVehicleIngredient::new, NeoForgeVehicleIngredient::vehicle)); + + @Override + public boolean test(ItemStack stack) { + return VehicleIngredient.matches(vehicle, stack); + } + + @Override + public Stream> items() { + return VehicleIngredient.items(); + } + + @Override + public boolean isSimple() { + return false; + } + + @Override + public IngredientType getType() { + return TYPE; + } + + @Override + public SlotDisplay display() { + return VehicleIngredient.display(vehicle); + } +} From 8905e1b5dd869ae0392927e4e7334fd10d045fa7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:31 +0800 Subject: [PATCH 137/314] Wire the NeoForge client events to the client hooks --- .../mcheli/neoforge/NeoForgeClient.java | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java diff --git a/neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java b/neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java new file mode 100644 index 0000000..1cd8698 --- /dev/null +++ b/neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java @@ -0,0 +1,64 @@ +package com.jnhprojects.mcheli.neoforge; + +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.client.particle.GlowParticle; +import com.jnhprojects.mcheli.registry.ModParticles; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.client.event.AddClientReloadListenersEvent; +import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent; +import net.neoforged.neoforge.client.event.ClientTickEvent; +import net.neoforged.neoforge.client.event.EntityRenderersEvent; +import net.neoforged.neoforge.client.event.InputEvent; +import net.neoforged.neoforge.client.event.RegisterGuiLayersEvent; +import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent; +import net.neoforged.neoforge.client.event.RegisterParticleProvidersEvent; +import net.neoforged.neoforge.client.event.RenderHandEvent; +import net.neoforged.neoforge.client.event.RenderLivingEvent; +import net.neoforged.neoforge.client.gui.VanillaGuiLayers; +import net.neoforged.neoforge.common.NeoForge; + +/** + * Client events on NeoForge. Only loaded on a physical client. The camera hook is a mixin + * ({@code GameRendererMixin}): NeoForge's camera event fires before vanilla positions the camera, + * so a cockpit eye set from it would be overwritten. + */ +final class NeoForgeClient { + private NeoForgeClient() { + } + + static void init(IEventBus modBus) { + ClientHooks.init(); + modBus.addListener(EntityRenderersEvent.RegisterRenderers.class, + event -> ClientHooks.registerEntityRenderers(event::registerEntityRenderer)); + modBus.addListener(RegisterParticleProvidersEvent.class, + event -> event.registerSpriteSet(ModParticles.GLOW.get(), GlowParticle.Provider::new)); + modBus.addListener(RegisterKeyMappingsEvent.class, event -> ClientHooks.keyMappings().forEach(event::register)); + modBus.addListener(AddClientReloadListenersEvent.class, event -> ClientHooks.reloadListeners().forEach(event::addListener)); + modBus.addListener(RegisterGuiLayersEvent.class, + event -> event.registerAbove(VanillaGuiLayers.SLEEP_OVERLAY, ClientHooks.HUD_LAYER, ClientHooks::drawHud)); + + NeoForge.EVENT_BUS.addListener(ClientTickEvent.Post.class, event -> ClientHooks.clientTickEnd()); + NeoForge.EVENT_BUS.addListener(RenderHandEvent.class, event -> event.setCanceled(ClientHooks.hidesHand())); + NeoForge.EVENT_BUS.addListener(RenderLivingEvent.Pre.class, + event -> ClientHooks.beforeRenderLiving(event.getRenderState(), event.getPoseStack(), event.getPartialTick())); + NeoForge.EVENT_BUS.addListener(InputEvent.InteractionKeyMappingTriggered.class, NeoForgeClient::onInteractionKey); + NeoForge.EVENT_BUS.addListener(ClientPlayerNetworkEvent.LoggingIn.class, event -> ClientHooks.loggedIn(event.getPlayer())); + NeoForge.EVENT_BUS.addListener(ClientPlayerNetworkEvent.LoggingOut.class, event -> ClientHooks.loggedOut()); + } + + private static void onInteractionKey(InputEvent.InteractionKeyMappingTriggered event) { + if (event.isUseItem()) { + switch (ClientHooks.onUseKey(event.getHand())) { + case PASS -> { + } + case CONSUME -> { + event.setSwingHand(false); + event.setCanceled(true); + } + case CONSUME_AND_SWING -> event.setCanceled(true); + } + } else if (event.isPickBlock() && ClientHooks.onPickBlockKey()) { + event.setCanceled(true); + } + } +} From 4c12d1e83dfdd216e4f1b4ab957ab773161a4d1a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:32 +0800 Subject: [PATCH 138/314] Hook the camera where Forge fires its camera event on NeoForge --- .../neoforge/mixin/GameRendererMixin.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererMixin.java diff --git a/neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererMixin.java b/neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererMixin.java new file mode 100644 index 0000000..a076be4 --- /dev/null +++ b/neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererMixin.java @@ -0,0 +1,39 @@ +package com.jnhprojects.mcheli.neoforge.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.renderer.GameRenderer; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * The camera hook, at the two places Forge fires its camera angle event: once vanilla has placed + * the camera for the frame, and again before the camera is copied into the render state. + */ +@Mixin(GameRenderer.class) +abstract class GameRendererMixin { + @Shadow + @Final + private Camera mainCamera; + + @Inject(method = "update", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;update(Lnet/minecraft/client/DeltaTracker;)V", + shift = At.Shift.AFTER)) + private void mcheli$afterCameraUpdate(DeltaTracker deltaTracker, CallbackInfo ci) { + ClientHooks.computeCameraAngles(mainCamera, deltaTracker.getGameTimeDeltaPartialTick(true), GameRendererMixin::setRotation); + } + + @Inject(method = "extractCamera", at = @At("HEAD")) + private void mcheli$beforeExtractCamera(DeltaTracker deltaTracker, float worldPartialTicks, CallbackInfo ci) { + ClientHooks.computeCameraAngles(mainCamera, worldPartialTicks, GameRendererMixin::setRotation); + } + + /** NeoForge turns the camera the other way round its roll axis than Forge does. */ + private static void setRotation(Camera camera, float yaw, float pitch, float roll) { + ((CameraInvoker) camera).mcheli$setRotation(yaw, pitch, -roll); + } +} From 10325977cb4d80295f1dc5d1db7816fd69c1b73f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:32 +0800 Subject: [PATCH 139/314] Reach NeoForge's camera rotation with roll --- .../mcheli/neoforge/mixin/CameraInvoker.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/CameraInvoker.java diff --git a/neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/CameraInvoker.java b/neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/CameraInvoker.java new file mode 100644 index 0000000..4c289d0 --- /dev/null +++ b/neoforge-26.3/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/CameraInvoker.java @@ -0,0 +1,12 @@ +package com.jnhprojects.mcheli.neoforge.mixin; + +import net.minecraft.client.Camera; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +/** NeoForge adds a roll to {@code Camera#setRotation}, but leaves it protected. */ +@Mixin(Camera.class) +public interface CameraInvoker { + @Invoker("setRotation") + void mcheli$setRotation(float yRot, float xRot, float roll); +} From 438bb7337dd9e3ef83f239cc5f27a6cee98b672c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:32 +0800 Subject: [PATCH 140/314] Add the Fabric 26.3 module --- fabric-26.3/build.gradle | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 fabric-26.3/build.gradle diff --git a/fabric-26.3/build.gradle b/fabric-26.3/build.gradle new file mode 100644 index 0000000..c0890b2 --- /dev/null +++ b/fabric-26.3/build.gradle @@ -0,0 +1,30 @@ +plugins { + id 'java' + id 'idea' + id 'eclipse' + id 'net.fabricmc.fabric-loom' +} + +apply from: rootProject.file('gradle/loader.gradle') + +// The original MCHeli 1.0.3 download. Players supply their own copy; development runs use this +// one (git-ignored, see docs/DEVELOPMENT.md). +def legacyZip = rootProject.file('Mod/MC1.7.10_mcheli_1.0.3.zip').absolutePath + +loom { + runs { + configureEach { + runDir = '../run' + vmArg "-Dmcheli.legacyZip=${legacyZip}" + } + server { + programArgs '--nogui' + } + } +} + +dependencies { + minecraft "com.mojang:minecraft:${minecraft_version}" + implementation "net.fabricmc:fabric-loader:${fabric_loader_version}" + implementation "net.fabricmc.fabric-api:fabric-api:${fabric_api_version}" +} From 013c76820e8bb9fef1c103e6d640ab28b1c4b5fd Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:33 +0800 Subject: [PATCH 141/314] Add the Fabric 26.3 module properties --- fabric-26.3/gradle.properties | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 fabric-26.3/gradle.properties diff --git a/fabric-26.3/gradle.properties b/fabric-26.3/gradle.properties new file mode 100644 index 0000000..3831c00 --- /dev/null +++ b/fabric-26.3/gradle.properties @@ -0,0 +1,8 @@ +# Minecraft 26.3 on Fabric +loader=fabric +minecraft_version=26.3 +minecraft_version_range=~26.3 +fabric_loader_version=0.20.1 +loader_version_range=>=0.20.0 +fabric_api_version=0.165.0+26.3 +java_version=25 From 2c6f0a59200908041d08f22856c94c3722084496 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:33 +0800 Subject: [PATCH 142/314] Add the Fabric mod metadata --- .../src/main/resources/fabric.mod.json | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 fabric-26.3/src/main/resources/fabric.mod.json diff --git a/fabric-26.3/src/main/resources/fabric.mod.json b/fabric-26.3/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..8346f4c --- /dev/null +++ b/fabric-26.3/src/main/resources/fabric.mod.json @@ -0,0 +1,27 @@ +{ + "schemaVersion": 1, + "id": "${mod_id}", + "version": "${mod_version}", + "name": "${mod_name}", + "description": "Helicopters, planes, tanks and vehicles for modern Minecraft. A ground-up rework of MCHeli 1.0.3 by EMB4, which stopped at Minecraft 1.7.10. The vehicles come from your own copy of the original: put MC1.7.10_mcheli_1.0.3.zip from the MCHeli CurseForge page into the \"mcheli\" folder of the game directory.", + "authors": ["JayMar921 (rework)", "EMB4 (original author)", "Fureniku (CurseForge maintainer)"], + "contact": { + "homepage": "https://github.com/JnH-Projects/MCHeli-Rework-Forge", + "issues": "https://github.com/JnH-Projects/MCHeli-Rework-Forge/issues", + "sources": "https://github.com/JnH-Projects/MCHeli-Rework-Forge" + }, + "license": "LicenseRef-MCHeli-Rework-1.0", + "icon": "mcheli_logo.png", + "environment": "*", + "entrypoints": { + "main": ["com.jnhprojects.mcheli.fabric.McheliFabric"], + "client": ["com.jnhprojects.mcheli.fabric.McheliFabricClient"] + }, + "mixins": ["mcheli.mixins.json"], + "depends": { + "fabricloader": "${loader_version_range}", + "fabric-api": "*", + "minecraft": "${minecraft_version_range}", + "java": ">=${java_version}" + } +} From e469c402c66fa886314804771a4c31798e987f92 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:33 +0800 Subject: [PATCH 143/314] Add the Fabric mixin config --- .../src/main/resources/mcheli.mixins.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 fabric-26.3/src/main/resources/mcheli.mixins.json diff --git a/fabric-26.3/src/main/resources/mcheli.mixins.json b/fabric-26.3/src/main/resources/mcheli.mixins.json new file mode 100644 index 0000000..1b14b27 --- /dev/null +++ b/fabric-26.3/src/main/resources/mcheli.mixins.json @@ -0,0 +1,19 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "com.jnhprojects.mcheli.fabric.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [ + "PackRepositoryMixin" + ], + "client": [ + "CameraMixin", + "GameRendererMixin", + "LivingEntityRendererMixin", + "MinecraftMixin", + "RuntimeSoundInstanceMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} From 2403e159f78de11a20f15a831e1c093d0462cecc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:34 +0800 Subject: [PATCH 144/314] Add the Fabric entry point --- .../mcheli/fabric/McheliFabric.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java diff --git a/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java new file mode 100644 index 0000000..e0f1f29 --- /dev/null +++ b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java @@ -0,0 +1,73 @@ +package com.jnhprojects.mcheli.fabric; + +import com.jnhprojects.mcheli.CommonHooks; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registration; +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; + +/** + * The Fabric entry point: everything here only connects Fabric to the shared code. The imported + * content pack is added by {@code PackRepositoryMixin}, since Fabric has no pack finder event. + */ +public final class McheliFabric implements ModInitializer { + + @Override + public void onInitialize() { + Platform.install(new FabricPlatform()); + MCHeli.init(); + + for (ResourceKey> registry : Registration.registries()) { + register(Registration.typed(registry)); + } + CustomIngredientSerializer.register(FabricVehicleIngredient.SERIALIZER); + registerPackets(); + + ServerLifecycleEvents.SERVER_STARTING.register(CommonHooks::serverAboutToStart); + ServerLifecycleEvents.SERVER_STOPPED.register(server -> CommonHooks.serverStopped()); + ServerTickEvents.END_SERVER_TICK.register(CommonHooks::serverTickEnd); + CommandRegistrationCallback.EVENT.register((dispatcher, context, selection) -> CommonHooks.registerCommands(dispatcher)); + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> CommonHooks.playerLoggedIn(handler.getPlayer())); + ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> CommonHooks.playerLoggedOut(handler.getPlayer())); + } + + @SuppressWarnings("unchecked") + private static void register(ResourceKey> key) { + Registry registry = (Registry) BuiltInRegistries.REGISTRY.getValue(key.identifier()); + if (registry == null) { + throw new IllegalStateException("No registry " + key.identifier()); + } + Registration.registerAll(key, (id, value) -> Registry.register(registry, id, value)); + } + + /** Client handlers are registered by {@link McheliFabricClient}; handlers run on the main thread. */ + private static void registerPackets() { + for (ModNetwork.Serverbound packet : ModNetwork.SERVERBOUND) { + serverbound(packet); + } + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + clientbound(packet); + } + } + + private static void clientbound(ModNetwork.Clientbound packet) { + PayloadTypeRegistry.clientboundPlay().register(packet.type(), packet.codec()); + } + + private static void serverbound(ModNetwork.Serverbound packet) { + PayloadTypeRegistry.serverboundPlay().register(packet.type(), packet.codec()); + ServerPlayNetworking.registerGlobalReceiver(packet.type(), (msg, context) -> packet.handler().accept(msg, context.player())); + } +} From 95fef44b75e11f170fc65906c3290742a9cb4a8f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:34 +0800 Subject: [PATCH 145/314] Add the Fabric client entry point --- .../mcheli/fabric/McheliFabricClient.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java diff --git a/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java new file mode 100644 index 0000000..76ff11c --- /dev/null +++ b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java @@ -0,0 +1,48 @@ +package com.jnhprojects.mcheli.fabric; + +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.client.particle.GlowParticle; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.registry.ModParticles; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.keymapping.v1.KeyMappingHelper; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.client.particle.v1.ParticleProviderRegistry; +import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry; +import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry; +import net.fabricmc.fabric.api.client.rendering.v1.hud.VanillaHudElements; +import net.fabricmc.fabric.api.resource.v1.ResourceLoader; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.packs.PackType; + +/** + * Client side of the Fabric entry point. The camera, hand, key and rider hooks are mixins (see + * {@code com.jnhprojects.mcheli.fabric.mixin}), since Fabric API has no events for them. + */ +public final class McheliFabricClient implements ClientModInitializer { + + // EntityRendererRegistry is deprecated in favour of a vanilla method that is not public yet. + @SuppressWarnings("deprecation") + @Override + public void onInitializeClient() { + ClientHooks.init(); + ClientHooks.registerEntityRenderers(EntityRendererRegistry::register); + ParticleProviderRegistry.getInstance().register(ModParticles.GLOW.get(), GlowParticle.Provider::new); + ClientHooks.keyMappings().forEach(KeyMappingHelper::registerKeyMapping); + ClientHooks.reloadListeners().forEach(ResourceLoader.get(PackType.CLIENT_RESOURCES)::registerReloadListener); + HudElementRegistry.attachElementAfter(VanillaHudElements.SLEEP, ClientHooks.HUD_LAYER, ClientHooks::drawHud); + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + clientbound(packet); + } + + ClientTickEvents.END_CLIENT_TICK.register(client -> ClientHooks.clientTickEnd()); + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> ClientHooks.loggedIn(client.player)); + ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> ClientHooks.loggedOut()); + } + + private static void clientbound(ModNetwork.Clientbound packet) { + ClientPlayNetworking.registerGlobalReceiver(packet.type(), (msg, context) -> packet.handler().accept(msg)); + } +} From 087a93e89888cf7ce3a64d219a2c4255d091ee0c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:34 +0800 Subject: [PATCH 146/314] Add the Fabric platform --- .../mcheli/fabric/FabricPlatform.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java diff --git a/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java new file mode 100644 index 0000000..0e59c8d --- /dev/null +++ b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java @@ -0,0 +1,59 @@ +package com.jnhprojects.mcheli.fabric; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.fabricmc.api.EnvType; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.creativetab.v1.FabricCreativeModeTab; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; + +import java.nio.file.Path; + +final class FabricPlatform implements Platform { + @Override + public String loaderName() { + return "fabric"; + } + + @Override + public Path gameDir() { + return FabricLoader.getInstance().getGameDir(); + } + + @Override + public Path configDir() { + return FabricLoader.getInstance().getConfigDir(); + } + + @Override + public String modVersion() { + return FabricLoader.getInstance().getModContainer(MCHeli.MOD_ID) + .map(c -> c.getMetadata().getVersion().getFriendlyString()).orElse(""); + } + + @Override + public boolean isClient() { + return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT; + } + + @Override + public void sendToServer(CustomPacketPayload payload) { + ClientPlayNetworking.send(payload); + } + + @Override + public void sendToPlayer(ServerPlayer player, CustomPacketPayload payload) { + ServerPlayNetworking.send(player, payload); + } + + /** Fabric orders tabs by registration, which already puts ours one after another. */ + @Override + public CreativeModeTab.Builder creativeTab(ResourceKey after) { + return FabricCreativeModeTab.builder(); + } +} From da03a37386d53f427fc745cd86fb8f539f855126 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:35 +0800 Subject: [PATCH 147/314] Add the Fabric vehicle ingredient --- .../fabric/FabricVehicleIngredient.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java diff --git a/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java new file mode 100644 index 0000000..ecc8b27 --- /dev/null +++ b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java @@ -0,0 +1,65 @@ +package com.jnhprojects.mcheli.fabric; + +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.mojang.serialization.MapCodec; +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredient; +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer; +import net.minecraft.core.Holder; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.Identifier; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.display.SlotDisplay; + +import java.util.stream.Stream; + +/** {@link VehicleIngredient} as a Fabric ingredient, read from the {@code fabric:type} key. */ +record FabricVehicleIngredient(String vehicle) implements CustomIngredient { + static final CustomIngredientSerializer SERIALIZER = new CustomIngredientSerializer<>() { + private final MapCodec codec = + VehicleIngredient.codec(FabricVehicleIngredient::new, FabricVehicleIngredient::vehicle); + private final StreamCodec streamCodec = + VehicleIngredient.streamCodec(FabricVehicleIngredient::new, FabricVehicleIngredient::vehicle); + + @Override + public Identifier getIdentifier() { + return VehicleIngredient.ID; + } + + @Override + public MapCodec getCodec() { + return codec; + } + + @Override + public StreamCodec getStreamCodec() { + return streamCodec; + } + }; + + @Override + public boolean test(ItemStack stack) { + return VehicleIngredient.matches(vehicle, stack); + } + + @Override + public Stream> items() { + return VehicleIngredient.items(); + } + + @Override + public boolean requiresTesting() { + return true; + } + + @Override + public CustomIngredientSerializer getSerializer() { + return SERIALIZER; + } + + @Override + public SlotDisplay display() { + return VehicleIngredient.display(vehicle); + } +} From 55f559c36612e22affb5a7955681bc7b649983d8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:35 +0800 Subject: [PATCH 148/314] Hook the camera, and hide the hand, where Forge does on Fabric --- .../fabric/mixin/GameRendererMixin.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java diff --git a/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java new file mode 100644 index 0000000..7f67f18 --- /dev/null +++ b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java @@ -0,0 +1,46 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.renderer.GameRenderer; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * The camera hook, at the two places Forge fires its camera angle event (once vanilla has placed + * the camera for the frame, and again before the camera is copied into the render state), and + * the first person hand, which is hidden in some seats. + */ +@Mixin(GameRenderer.class) +abstract class GameRendererMixin { + @Shadow + @Final + private Camera mainCamera; + + @Inject(method = "update", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;update(Lnet/minecraft/client/DeltaTracker;)V", + shift = At.Shift.AFTER)) + private void mcheli$afterCameraUpdate(DeltaTracker deltaTracker, CallbackInfo ci) { + ClientHooks.computeCameraAngles(mainCamera, deltaTracker.getGameTimeDeltaPartialTick(true), GameRendererMixin::setRotation); + } + + @Inject(method = "extractCamera", at = @At("HEAD")) + private void mcheli$beforeExtractCamera(DeltaTracker deltaTracker, float worldPartialTicks, CallbackInfo ci) { + ClientHooks.computeCameraAngles(mainCamera, worldPartialTicks, GameRendererMixin::setRotation); + } + + @Inject(method = "renderItemInHand", at = @At("HEAD"), cancellable = true) + private void mcheli$renderItemInHand(CallbackInfo ci) { + if (ClientHooks.hidesHand()) { + ci.cancel(); + } + } + + private static void setRotation(Camera camera, float yaw, float pitch, float roll) { + ((RollableCamera) camera).mcheli$setRotation(yaw, pitch, roll); + } +} From faa6e84d19b53a8336f72f893693c5bc01c9a318 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:36 +0800 Subject: [PATCH 149/314] Give the vanilla camera a roll on Fabric --- .../mcheli/fabric/mixin/CameraMixin.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java diff --git a/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java new file mode 100644 index 0000000..32f7075 --- /dev/null +++ b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java @@ -0,0 +1,57 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import net.minecraft.client.Camera; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.joml.Vector3fc; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +/** + * Vanilla's camera has no roll. This is {@code Camera#setRotation} with the roll Forge patches in, + * so a banked aircraft tilts the view the same on every loader. + */ +@Mixin(Camera.class) +abstract class CameraMixin implements RollableCamera { + @Shadow + @Final + private static Vector3fc FORWARDS; + @Shadow + @Final + private static Vector3fc UP; + @Shadow + @Final + private static Vector3fc LEFT; + + @Shadow + private float xRot; + @Shadow + private float yRot; + @Shadow + @Final + private Vector3f forwards; + @Shadow + @Final + private Vector3f up; + @Shadow + @Final + private Vector3f left; + @Shadow + @Final + private Quaternionf rotation; + @Shadow + private int matrixPropertiesDirty; + + @Override + public void mcheli$setRotation(float yRot, float xRot, float roll) { + this.xRot = xRot; + this.yRot = yRot; + this.rotation.rotationYXZ((float) Math.PI - yRot * (float) (Math.PI / 180.0), -xRot * (float) (Math.PI / 180.0), + roll * (float) (Math.PI / 180.0)); + FORWARDS.rotate(this.rotation, this.forwards); + UP.rotate(this.rotation, this.up); + LEFT.rotate(this.rotation, this.left); + this.matrixPropertiesDirty |= 3; + } +} From 87e0e0a2e779da294e2419b5851d1b223588e090 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:36 +0800 Subject: [PATCH 150/314] Add the camera roll interface for Fabric --- .../jnhprojects/mcheli/fabric/mixin/RollableCamera.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RollableCamera.java diff --git a/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RollableCamera.java b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RollableCamera.java new file mode 100644 index 0000000..f31feae --- /dev/null +++ b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RollableCamera.java @@ -0,0 +1,7 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +/** Implemented on {@code Camera} by {@link CameraMixin}. */ +public interface RollableCamera { + /** Like vanilla's {@code setRotation}, with a roll in degrees as Forge applies it. */ + void mcheli$setRotation(float yRot, float xRot, float roll); +} From 427b6541d8b47f8bc3900ba10c57e70e4bcb88c3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:36 +0800 Subject: [PATCH 151/314] Take the use and pick block keys on Fabric --- .../mcheli/fabric/mixin/MinecraftMixin.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java diff --git a/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java new file mode 100644 index 0000000..c7c2695 --- /dev/null +++ b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java @@ -0,0 +1,47 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.phys.HitResult; +import org.jspecify.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** The use and pick block keys, which Forge and NeoForge let mods take through an input event. */ +@Mixin(Minecraft.class) +abstract class MinecraftMixin { + @Shadow + public @Nullable LocalPlayer player; + @Shadow + public @Nullable HitResult hitResult; + @Shadow + private int rightClickDelay; + + @Inject(method = "startUseItem", at = @At("HEAD"), cancellable = true) + private void mcheli$startUseItem(CallbackInfo ci) { + if (player == null) { + return; + } + ClientHooks.UseKey result = ClientHooks.onUseKey(InteractionHand.MAIN_HAND); + if (result != ClientHooks.UseKey.PASS) { + // Vanilla sets the same delay before it looks at the hands. + rightClickDelay = 4; + if (result == ClientHooks.UseKey.CONSUME_AND_SWING) { + player.swing(InteractionHand.MAIN_HAND, player.getMainHandItem().getInteractAnimation(), false); + } + ci.cancel(); + } + } + + @Inject(method = "pickBlockOrEntity", at = @At("HEAD"), cancellable = true) + private void mcheli$pickBlockOrEntity(CallbackInfo ci) { + if (hitResult != null && hitResult.getType() != HitResult.Type.MISS && ClientHooks.onPickBlockKey()) { + ci.cancel(); + } + } +} From 2c3f2fc851c137a6a1bd9ae091f10d079e9333f8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:37 +0800 Subject: [PATCH 152/314] Tilt riders with their vehicle on Fabric --- .../mixin/LivingEntityRendererMixin.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java diff --git a/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java new file mode 100644 index 0000000..aec4e49 --- /dev/null +++ b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java @@ -0,0 +1,25 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.LivingEntityRenderer; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** Tilts riders with their vehicle, where Forge fires its living entity render event. */ +@Mixin(LivingEntityRenderer.class) +abstract class LivingEntityRendererMixin { + + @Inject(method = "submit(Lnet/minecraft/client/renderer/entity/state/LivingEntityRenderState;Lcom/mojang/blaze3d/vertex/PoseStack;" + + "Lnet/minecraft/client/renderer/SubmitNodeCollector;Lnet/minecraft/client/renderer/state/level/CameraRenderState;)V", + at = @At("HEAD")) + private void mcheli$beforeSubmit(LivingEntityRenderState state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera, + CallbackInfo ci) { + ClientHooks.beforeRenderLiving(state, pose, camera.cameraEntityPartialTicks); + } +} From 98d251c56fb192004e7461ecc45ca584df26a6f6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:37 +0800 Subject: [PATCH 153/314] Add the imported content pack to every pack repository on Fabric --- .../fabric/mixin/PackRepositoryMixin.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java diff --git a/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java new file mode 100644 index 0000000..965719b --- /dev/null +++ b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java @@ -0,0 +1,25 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.CommonHooks; +import net.minecraft.server.packs.repository.PackRepository; +import net.minecraft.server.packs.repository.RepositorySource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +import java.util.Arrays; + +/** + * Adds the imported original content to every pack repository, client resources and server data + * alike, which is what Forge and NeoForge do through their pack finder event. + */ +@Mixin(PackRepository.class) +abstract class PackRepositoryMixin { + + @ModifyVariable(method = "", at = @At("HEAD"), argsOnly = true) + private static RepositorySource[] mcheli$addImportedContent(RepositorySource[] sources) { + RepositorySource[] withOurs = Arrays.copyOf(sources, sources.length + 1); + withOurs[sources.length] = CommonHooks.packSource(); + return withOurs; + } +} From b6615747dd5b6216cced15e5768b95e4a19b99c3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:37 +0800 Subject: [PATCH 154/314] Stream content pack sounds through Fabric's sound hook --- .../mixin/RuntimeSoundInstanceMixin.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java diff --git a/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java new file mode 100644 index 0000000..f085c68 --- /dev/null +++ b/fabric-26.3/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java @@ -0,0 +1,20 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.sound.RuntimeSoundInstance; +import net.fabricmc.fabric.api.client.sound.v1.FabricSoundInstance; +import net.minecraft.client.sounds.AudioStream; +import net.minecraft.client.sounds.SoundBufferLibrary; +import net.minecraft.resources.Identifier; +import org.spongepowered.asm.mixin.Mixin; + +import java.util.concurrent.CompletableFuture; + +/** Streams content pack sounds from disk through Fabric's sound instance hook. */ +@Mixin(RuntimeSoundInstance.class) +abstract class RuntimeSoundInstanceMixin implements FabricSoundInstance { + + @Override + public CompletableFuture getAudioStream(SoundBufferLibrary loader, Identifier id, boolean repeatInstantly) { + return ((RuntimeSoundInstance) (Object) this).openStream(loader, id, repeatInstantly); + } +} From 9d868a679f0123ef83c8bfe4452ad77fd597f2a6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:38 +0800 Subject: [PATCH 155/314] Describe the loader layer, the hooks and the per-loader mixins --- docs/ARCHITECTURE.md | 140 ++++++++++++++++++++++++++++--------------- 1 file changed, 91 insertions(+), 49 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 84bed2f..b32f6ae 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,7 +1,7 @@ # Architecture This document explains how MCHeli Rework is organized and why. It reflects the code as of -`1.1.0-beta.2`. +`1.1.0-beta.3`. ## Goals @@ -17,15 +17,19 @@ This document explains how MCHeli Rework is organized and why. It reflects the c ``` ┌────────────────────────────────────────────────────────────────┐ + │ forge / neoforge / fabric entry point, event wiring, packet │ loader module + │ channel, vehicle ingredient type, mixins │ (forge-26.3, ...) + ├────────────────────────────────────────────────────────────────┤ │ client renderer, mesh cache, runtime assets, crew │ client only │ controller, sound, HUD renderer, pack download │ - ├────────────────────────────────────────────────────────────────┤ platform module - │ entity AircraftEntity and one subclass per kind │ (forge-26.3) + ├────────────────────────────────────────────────────────────────┤ minecraft- + │ entity AircraftEntity and one subclass per kind │ (minecraft-26.3) │ item AircraftItem, VehicleIngredient │ - │ network PilotStatePacket, CrewActionPacket, PackSync │ Minecraft + Forge + │ network packets, ModNetwork, PackSync │ Minecraft, no loader │ particle GlowParticleOptions │ │ registry entities, items, components, particles, tabs │ - │ (root) MCHeli, configs, McheliCommand, ContentPacks │ + │ platform Platform, Registration │ + │ (root) MCHeli, CommonHooks, configs, McheliCommand │ ├────────────────────────────────────────────────────────────────┤ │ content Definition, DefinitionReader, ContentRegistry, │ common module │ aircraft/* definitions, recipe conversion, │ @@ -34,46 +38,73 @@ This document explains how MCHeli Rework is organized and why. It reflects the c │ hud HudScript, Expression, HudVariable │ │ physics AircraftRotation, GroundImpact │ │ effect NightGlow │ + │ config ConfigSpec (the TOML config format) │ └────────────────────────────────────────────────────────────────┘ ``` -Arrows only point down. The bottom half lives in the `common` Gradle module, which is compiled -and tested with nothing from Minecraft or Forge on its classpath (only the JDK, SLF4J, JSpecify -and JOML), so a stray import fails the build; `CommonBoundaryTest` also scans the sources. The top -half is a platform module, one per Minecraft version, which compiles `common`'s sources together -with its own into one jar. Other versions and loaders (phases 7 and 8) add platform modules -and leave `common` alone; see [DEVELOPMENT.md](DEVELOPMENT.md#common-and-platform-modules). - -`client` is only referenced from `MCHeli` behind a `Dist.CLIENT` check, so a dedicated server never -loads a client class. - -Package root: `com.jnhprojects.mcheli`. Mod id and asset namespace: `mcheli`, the same as the -original, so existing resource paths and content packs keep working. +Arrows only point down. The bottom layer lives in the `common` Gradle module, which is compiled +and tested with nothing from Minecraft or a loader on its classpath (only the JDK, SLF4J, +JSpecify and JOML), so a stray import fails the build; `CommonBoundaryTest` also scans the +sources. The middle layer, `minecraft-`, is everything that touches Minecraft but no +loader. The top layer is a thin module per loader and Minecraft version, which compiles both +lower layers together with its own glue into one jar. See +[DEVELOPMENT.md](DEVELOPMENT.md#three-layers). + +The shared code reaches the loader in two ways only: + +- **`Platform`**, installed by the loader module before anything else runs: the game and config + folders, the mod version, whether this is a client, sending a packet, and a creative tab + builder (vanilla's is not public on Fabric, and only Forge and NeoForge order tabs). +- **`Registration`**, a list of registry entries (`Registered`, like Forge's + `RegistryObject`) that the shared registry classes declare during `MCHeli.init`. Forge and + NeoForge register them from their `RegisterEvent`; Fabric registers them straight away. + +Everything else goes the other way: the loader module calls `CommonHooks` (server start and stop, +ticks, player join and leave, commands, the imported pack) and `client.ClientHooks` (renderers, +keys, reload listeners, HUD layer, ticks, camera, hand, use and pick block keys, rider pose) from +its own events. Where a loader has no event, the loader module adds a mixin at the place Forge +fires its event, so the shared code sees the same thing on every loader: + +| Hook | Forge | NeoForge | Fabric | +|------|-------|----------|--------| +| Camera angles and cockpit eye | `ViewportEvent.ComputeCameraAngles` | mixin on `GameRenderer` (NeoForge's event fires before vanilla sets the camera position) | mixin on `GameRenderer`, plus roll on `Camera` | +| Hide the hand | `RenderHandEvent` | `RenderHandEvent` | mixin on `GameRenderer.renderItemInHand` | +| Use and pick block keys | `InputEvent.InteractionKeyMappingTriggered` | the same | mixin on `Minecraft` | +| Rider tilt | `RenderLivingEvent.Pre` | `RenderLivingEvent.Pre` | mixin on `LivingEntityRenderer.submit` | +| Imported content pack | `AddPackFindersEvent` | `AddPackFindersEvent` | mixin on the `PackRepository` constructor | +| Streamed sounds | `SoundInstance.getStream` | `SoundInstance.getStream` | mixin adding `getAudioStream` to `RuntimeSoundInstance` | + +NeoForge rolls the camera the other way round from Forge, so its mixin negates the roll. + +`client` is only referenced from the loader modules behind a physical client check, so a +dedicated server never loads a client class. + +Package root: `com.jnhprojects.mcheli`, with the loader glue in `.forge`, `.neoforge` and +`.fabric`. Mod id and asset namespace: `mcheli`, the same as the original, so existing resource +paths and content packs keep working. ## Startup ``` -MCHeli constructor - ├─ LegacyContent.prepare import the original zip into mcheli/imported - ├─ LegacyContent.registerPacks that folder as a required resource + data pack - ├─ ContentPacks.load(false) the import plus every pack in mcheli/packs - ├─ registerConfig(SERVER, ServerConfig.SPEC) serverconfig/mcheli-server.toml - ├─ registerConfig(CLIENT, ClientConfig.SPEC) config/mcheli-client.toml - ├─ ModDataComponents.register mcheli:vehicle - ├─ ModEntities.register one entity type per kind - ├─ ModParticles.register mcheli:glow - ├─ ModItems.register(content) one item per kind, plus the 1.0.0 items - ├─ ModCreativeTabs.register - ├─ ModRecipes.register mcheli:vehicle ingredient, recipe book - ├─ ModNetwork.register - ├─ PackSync.init pack list on join, downloads - ├─ McheliCommand.register /mcheli - └─ ClientSetup.init (client only) renderers, keys, reload listener, controller +loader entry point (McheliForge, McheliNeoForge, McheliFabric) + ├─ Platform.install + ├─ MCHeli.init + │ ├─ LegacyContent.prepare import the original zip into mcheli/imported + │ ├─ ContentPacks.load(false) the import plus every pack in mcheli/packs + │ ├─ ModDataComponents.init mcheli:vehicle + │ ├─ ModEntities.init one entity type per kind + │ ├─ ModParticles.init mcheli:glow + │ ├─ ModItems.init(content) one item per kind, plus the 1.0.0 items + │ └─ ModCreativeTabs.init + ├─ register Registration's entries, and the mcheli:vehicle ingredient type + ├─ register ModNetwork's packets + ├─ wire events to CommonHooks imported pack, server config, packs, commands + └─ client only: ClientHooks.init and client events client config, renderers, keys, HUD, input ``` -Definitions are still parsed in the constructor, but only because the 1.0.0 items have to be -registered before Forge freezes the registries (see [Vehicle items](#vehicle-items)); nothing else -depends on them existing that early any more. The files are read straight from the imported +Definitions are still parsed during mod construction, but only because the 1.0.0 items have to be +registered before the loader freezes the registries (see [Vehicle items](#vehicle-items)); nothing +else depends on them existing that early any more. The files are read straight from the imported folder and the content packs (see [Assets](#assets) and [Content packs](#content-packs)). Models and textures, by contrast, go through the resource manager, so resource packs can replace them. @@ -112,8 +143,8 @@ number moves, rebuilding seats, parts and weapons while keeping the ammunition i ### Vehicle items -1.0.0 registered one item per vehicle at startup, and Forge freezes the item registry before a -world loads, so a vehicle added later could never get an item. Since 1.1.0 there is one item per +1.0.0 registered one item per vehicle at startup, and the loaders freeze the item registry before +a world loads, so a vehicle added later could never get an item. Since 1.1.0 there is one item per kind (`mcheli:helicopter`, `mcheli:plane`, `mcheli:tank`, `mcheli:vehicle`). A stack names its vehicle in the `mcheli:vehicle` data component and points its `minecraft:item_model` component at the item model the import wrote for that vehicle, so it looks and reads as before. @@ -127,9 +158,12 @@ themselves with the generic stack (count, name and other components kept) as soo a player's inventory. A stack in a chest converts when a player takes it out. Recipes craft the generic item with both components set. A vehicle used as an ingredient (the -AH-6X recipe takes an AH-6) becomes a `{"type": "mcheli:vehicle", "vehicle": "ah-6"}` ingredient, -`VehicleIngredient`, registered as a Forge ingredient serializer, which only matches stacks of that -vehicle. +AH-6X recipe takes an AH-6) becomes a `{"type": "mcheli:vehicle", "vehicle": "ah-6"}` ingredient that only matches stacks of +that vehicle. Each loader names custom ingredients under its own key (`type` on Forge, +`neoforge:ingredient_type` on NeoForge, `fabric:type` on Fabric), so the import writes all three +and each loader ignores the other two; a world keeps working when it moves to another loader. +`VehicleIngredient` holds the matching, and each loader module wraps it in its own ingredient +class. ### Reloading content @@ -190,19 +224,25 @@ instead. `RuntimeAssets.setRoots` swaps the folder list in one write, the pack l ### Server config and command -`ServerConfig` is a Forge `SERVER` config, `serverconfig/mcheli-server.toml` in each world: -explosions breaking blocks and starting fires, a damage factor for vehicles, infinite ammunition, -and a list of disabled vehicles. Only the server applies these rules; the getters return the +`ServerConfig` is `serverconfig/mcheli-server.toml` in each world, read when the world starts and +on `/mcheli reload`: explosions breaking blocks and starting fires, a damage factor for vehicles, +infinite ammunition, and a list of disabled vehicles. Only the server applies these rules; the getters return the defaults (the 1.0.0 behaviour) when no world is loaded. `McheliCommand` registers `/mcheli` (content status), `/mcheli list [kind]`, and for operators `/mcheli give` and `/mcheli reload`. +Both configs use `config.ConfigSpec` from `common`, a small TOML reader and writer with the layout +Forge's config spec wrote, so files from 1.0.0 and the first betas keep their values on every +loader. A missing key or bad value falls back to its default, and the file is written back +complete with comments. Unlike Forge's, it does not watch the file: edits apply on the next world +start or `/mcheli reload`. + Recipes are data in 26.x, so the definitions' `AddRecipe` and `AddShapelessRecipe` lines are turned into `data/mcheli/recipe/.json` during the import by `content.recipe.LegacyRecipe`, following `MCH_ItemRecipe`. `LegacyItems` maps the 1.7.10 item names and damage values: dyes and wool by colour, the common `c:` tags where an item became a family (iron, glass, dyes) so other mods' materials count, vanilla tags for logs, planks and boats. As in 1.7.10, a pattern letter the key does not define is an empty slot. `RecipeDataTest` converts the whole set and spot checks -the output. `ModRecipes` puts every MCHeli recipe in a player's recipe book on login, standing in +the output. `CommonHooks.playerLoggedIn` puts every MCHeli recipe in a player's recipe book on login, standing in for the drafting table's recipe list. ## Models @@ -423,7 +463,7 @@ camera facing halos and tracer ribbons for `ProjectileRenderer` (rounds and rock particle that `ExplosionEffect` spawns for the flash, the embers and the lingering crash fire. How bright a glow gets comes from `effect.NightGlow` in `common`: the block light, sky light and the sky darkening at that spot, so it is faint at noon and full at night, in caves and under -water. `ClientConfig`, a Forge `CLIENT` config, turns it off per player. +water. `ClientConfig` (`config/mcheli-client.toml`) turns it off per player. **CrewController** also hides the first person hand through `RenderHandEvent` wherever the original did: for the pilot, and for anyone looking through a camera or with a weapon selected. @@ -504,16 +544,18 @@ of the content packs in use. ours so the texture manager takes over again. - **Sounds**: `RuntimeSoundInstance` (the engine loop extends it) plays through the sound manager when a resource pack defines the event, and otherwise streams the OGG file a runtime root holds - for it through Forge's `SoundInstance.getStream` hook, with no `sounds.json` entry and no reload. + for it through the loader's sound stream hook (`SoundInstance.getStream` on Forge and NeoForge, + `getAudioStream` on Fabric), with no `sounds.json` entry and no reload. -`LegacyContent` adds the folder through `AddPackFindersEvent` for both pack types, as a required +`CommonHooks.packSource` adds the folder (through `AddPackFindersEvent`, or a mixin on Fabric) for +both pack types, as a required pack at the bottom of the list: it is enabled in every world, cannot be turned off, and regular resource packs still override it. Minecraft merges lang files across packs, so the item names from the import and the rework's own keys in the jar's lang files sit side by side; `LegacyImporterTest` checks they never share a key. Without a zip or an earlier import, `ContentRegistry` is empty: the game starts with no vehicles -and `ClientSetup` tells the player in the chat where the zip goes. A zip that fails to import is +and `ClientHooks.loggedIn` tells the player in the chat where the zip goes. A zip that fails to import is logged and reported the same way. The repository holds none of the original files. The unit tests get them the way players do: From 8d3c0244850c6eade0702a3936ce60def40a70ae Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:38 +0800 Subject: [PATCH 156/314] Document the three layers, the new toolchain and the loader run tasks --- docs/DEVELOPMENT.md | 101 +++++++++++++++++++++++++++----------------- 1 file changed, 63 insertions(+), 38 deletions(-) diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 36fa136..c5db596 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -5,65 +5,89 @@ | Tool | Version | Notes | |------|---------|-------| | JDK | 25 | Gradle provisions one through the Foojay resolver if none is installed. | -| Gradle | 9.7.1 | Use the wrapper (`./gradlew`). | -| ForgeGradle | 7.x | Sets up Minecraft through the Mavenizer on first build. | -| Forge | 66.1.0 for Minecraft 26.3 | Mappings are Mojang's official names. | +| Gradle | 9.8.2 | Use the wrapper (`./gradlew`). Loom 1.19 needs 9.8. | +| ForgeGradle | 7.x | Forge modules. Sets up Minecraft through the Mavenizer on first build. | +| ModDevGradle | 2.0.x | NeoForge modules. | +| Fabric Loom | 1.19 | Fabric modules: `net.fabricmc.fabric-loom` for 26.x, `-remap` for 1.21.1. | +| Forge | 66.1.0 for Minecraft 26.3 | | +| NeoForge | 26.3.0.99 for Minecraft 26.3 | | +| Fabric | Loader 0.20.1, Fabric API 0.165.0+26.3 | | -The first build downloads and decompiles Minecraft, which takes a few minutes. Avoid running a -second Gradle build (for example an IDE import) while that is in progress: both write the same -cache and can corrupt each other's output. If setup fails with a zip or EOF error, wait for the -other process to finish and build again. +All three use Mojang's official names, so the shared code reads the same on every loader. + +The first build downloads and decompiles Minecraft once per loader, which takes a few minutes. +Avoid running a second Gradle build (for example an IDE import) while that is in progress: both +write the same caches and can corrupt each other's output. If setup fails with a zip or EOF error, +wait for the other process to finish and build again. ## Common tasks ``` -./gradlew build # compile and test every module, one jar per Minecraft version in build/release -./gradlew :common:test # unit tests (they all live in common) -./gradlew :forge-26.3:runClient # start a development client from ./run -./gradlew :forge-26.3:runServer # start a development dedicated server from ./run +./gradlew build # compile and test every module, one jar per loader and version in build/release +./gradlew :common:test # unit tests (they all live in common) +./gradlew :forge-26.3:runClient # development client from ./run +./gradlew :neoforge-26.3:runClient +./gradlew :fabric-26.3:runClient +./gradlew ::runServer # development dedicated server from ./run ``` Shared properties live in the root `gradle.properties`: `mod_version`, the mod metadata, the list -of platform modules (`platforms`) and the library versions `common` compiles against. Each -platform module has its own `gradle.properties` with its Minecraft, loader and Java versions. -`mods.toml` and `pack.mcmeta` are filled in from them at build time. +of loader modules (`platforms`) and the library versions `common` compiles against. Each loader +module has its own `gradle.properties` with its loader, Minecraft and Java versions. +`mods.toml`, `neoforge.mods.toml`, `fabric.mod.json` and `pack.mcmeta` are filled in from them +at build time. What every loader module shares (source folders, jar contents, manifest, file +names) is in `gradle/loader.gradle`. ## Repository layout ``` common/ version-free code, built and tested without Minecraft - src/main/java content, model, hud, physics (see ARCHITECTURE.md) + src/main/java content, model, hud, physics, config (see ARCHITECTURE.md) src/main/resources lang files shared by every platform src/test/java all unit tests, including the boundary test -forge-26.3/ the Minecraft 26.3 / Forge 66 platform module - src/main/java rendering, networking, registration, entities, events - src/main/resources mods.toml, pack.mcmeta, item models, damage type data - gradle.properties Minecraft, Forge and Java versions for this module +minecraft-26.3/ code shared by every loader on Minecraft 26.3 (not a Gradle project) + src/main/java entities, rendering, networking, registries, HUD, sounds, hooks + src/main/resources pack.mcmeta, item models, damage type data, mod icon +forge-26.3/ Forge 66 glue: entry point, events, payload channel, ingredient +neoforge-26.3/ NeoForge glue, plus a camera mixin +fabric-26.3/ Fabric glue, plus mixins for what Fabric API has no event for +gradle/loader.gradle what every loader module shares docs/ documentation and content format guides .github/workflows CI and release pipelines ``` -### Common and platform modules +### Three layers -1.1.0 brings older Minecraft versions, so the code is split in two: +1.1.0 runs on several Minecraft versions and three loaders, so the code is split in three: - **`common`** holds everything that does not depend on the game version: definition parsing, the content registry, model loading, the HUD expression language, recipe conversion, rotation - and impact maths. Its classpath has only the JDK, SLF4J, JSpecify and JOML (all shipped by every - supported Minecraft version), so a stray Minecraft import fails to compile, and - `CommonBoundaryTest` names the offending line. -- **A platform module per Minecraft version** (`forge-26.3` today) holds the glue: entities, - rendering, networking, registration, config, commands. It compiles `common`'s sources together - with its own, so each version ships as one jar and runs as one module in development. - -A feature that is pure logic goes in `common` with a unit test; only the part that touches -Minecraft goes in the platform module. Adding a version means adding a module directory, listing -it in `platforms`, and porting the platform code; `common` should not need to change. The run -folder `./run` is shared for now; later modules will get their own. - -IDEs import the project as three Gradle projects (root, `common`, `forge-26.3`). After pulling the -split, re-import the Gradle project so the IDE picks up the new source folders and run -configurations; the old root-level `run*.launch` files point at a project that no longer exists. + and impact maths, the config file format. Its classpath has only the JDK, SLF4J, JSpecify and + JOML (all shipped by every supported Minecraft version), so a stray Minecraft import fails to + compile, and `CommonBoundaryTest` names the offending line. +- **`minecraft-`** holds everything that touches Minecraft but not a loader: entities, + rendering, networking, the HUD, sounds, commands, config. It talks to the loader through + `platform.Platform` (paths, sending packets, creative tab builders) and `platform.Registration` + (registry entries), and exposes `CommonHooks` and `client.ClientHooks` for the loader to call + from its events. It is not a Gradle project of its own: every loader module on that version + compiles it, and since Fabric has none of Forge's classes and Forge none of Fabric's, a loader + API that slips in breaks a build. +- **A loader module per loader and version** (`forge-26.3`, `neoforge-26.3`, `fabric-26.3`) is + the thin glue: the entry point, registering what `Registration` collected, wiring events to the + hooks, the packet channel, the vehicle ingredient type, and mixins where the loader has no event + (camera roll, hand, use and pick block keys, rider pose, the imported content pack on Fabric). + +Each loader module compiles `common` and its `minecraft-` sources together with its own, +so each loader and version ships as one jar and runs as one module in development. + +A feature that is pure logic goes in `common` with a unit test; the part that touches Minecraft +goes in `minecraft-`; only what needs a loader API goes in the loader modules. The run +folder `./run` is shared by every module; worlds made by one loader open on the others. + +IDEs import the project as one Gradle project per module. A file under `minecraft-26.3` belongs +to all three loader modules, so the IDE shows it under whichever it picked; it compiles under all +of them on the command line. After pulling the split, re-import the Gradle project; the old +`forge-26.3` run configurations still work, the NeoForge and Fabric ones are new. `Mod/` holds the original `MC1.7.10_mcheli_1.0.3.zip`. It is git-ignored. Download it from the [MCHeli CurseForge page](https://www.curseforge.com/minecraft/mc-mods/mcheli-minecraft-helicopter-mod/files) @@ -105,8 +129,9 @@ Releases are cut from tags. The release workflow refuses to publish when the tag `.github/workflows/release.yml` then builds and tests the tag, extracts the matching `CHANGELOG.md` section as release notes, and publishes a GitHub release with one jar per -Minecraft version (`mcheli--.jar`, gathered by the `releaseJars` task into -`build/release`). One tag always releases every platform module at the same mod version. Versions +loader and Minecraft version (`mcheli---.jar`, such as +`mcheli-fabric-26.3-1.1.0-beta.3.jar`, gathered by the `releaseJars` task into `build/release`). +One tag always releases every loader module at the same mod version. Versions containing a hyphen (`-alpha.N`, `-beta.N`, `-rc.N`) are marked as pre-releases. Every release, pre-releases included, is mirrored to the public repository and tagged there too. From 33af39528fed21f605c0f766fbd0c6293f45fe09 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:38 +0800 Subject: [PATCH 157/314] Add a smoke test for every jar and checks across loaders --- docs/TESTING.md | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/docs/TESTING.md b/docs/TESTING.md index a2e0d64..3b19d2f 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -5,13 +5,47 @@ hand with this plan before a pre-release is tagged. ## Setup -1. `./gradlew build`, then copy `build/release/mcheli-26.3-.jar` into the `mods` folder of - the Forge 26.3 profile and `MC1.7.10_mcheli_1.0.3.zip` into its `mcheli` folder. - Alternatively run `./gradlew :forge-26.3:runClient`, which imports the zip from `Mod/`. +1. `./gradlew build`, then copy the jar for the loader under test from `build/release` + (`mcheli--26.3-.jar`) into the `mods` folder of a 26.3 profile for that loader, + and `MC1.7.10_mcheli_1.0.3.zip` into its `mcheli` folder. Fabric also needs Fabric API. + Alternatively run `./gradlew :-26.3:runClient`, which imports the zip from `Mod/`. 2. Use a launcher profile with its own game directory. Mods for other loaders or Minecraft - versions in the same `mods` folder will prevent Forge from starting. + versions in the same `mods` folder will prevent the game from starting. 3. Create a creative world with cheats enabled. +The phase checklists below were written on Forge and are run in full there. Every other jar gets +the smoke test, which touches each piece of loader glue once. + +## Smoke test (every jar) + +Run this for each jar in `build/release`: Forge, NeoForge and Fabric on every Minecraft version. + +- [ ] The game reaches the title screen. The log has `MCHeli Rework on ` and no + `mcheli` errors; on the first start it also has `Imported ... files` from the zip. +- [ ] The four MCHeli creative tabs list the vehicles with icons and names, after the vanilla tabs. +- [ ] Fly a helicopter (`ah-64`): it takes off, banks and lands. In first person the view rolls + with the hull and the eye sits in the cockpit; the hand is hidden in the pilot seat. +- [ ] Other players and the rider model tilt with the hull (second player or F5). +- [ ] Drive a tank (`m1a2`): it moves, the turret follows the view, the gun fires. +- [ ] Fire a missile from a helicopter or jet: the use key fires without swinging the arm, the pick + block key switches weapons, the weapon HUD draws, the missile hits and explodes. +- [ ] Right-clicking the hull outside the main hit box boards the vehicle (the arm swings). +- [ ] Engine and rotor sounds play, including a content pack sound (the example pack). +- [ ] `config/mcheli-client.toml` exists; setting `nightGlow = false` and restarting removes the + glow. +- [ ] Join a dedicated server running the same jar: the server log has no `mcheli` errors, the + client downloads the server's content packs, `/mcheli status` and `/mcheli reload` work, + `serverconfig/mcheli-server.toml` is in the world folder. +- [ ] Crafting: an AH-6X can be crafted from an AH-6 and redstone (the vehicle ingredient), and + the recipe book lists MCHeli recipes. + +### Across loaders + +- [ ] A world made on Forge 26.3 with vehicles in it opens on NeoForge and Fabric 26.3 with the + vehicles and items intact, and back. +- [ ] A Fabric client joins a NeoForge server (both with the mod) and downloads its packs. +- [ ] A `serverconfig/mcheli-server.toml` written by 1.1.0-beta.2 (Forge) keeps its values. + ## Phase 1 checklist ### Loading From 46eae3c3a5aa93ae26631a0343f03df4d5c5ab4b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:39 +0800 Subject: [PATCH 158/314] Tick the phase 7 items done for 26.3 --- docs/ROADMAP.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 1aa0b3b..edbcb4e 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -210,19 +210,19 @@ The external content packs from phase 4, in the package format the workshop will [supported versions](#supported-versions)). This phase splits the code so the loaders share almost everything, adds NeoForge and Fabric on 26.3, then brings all three to 1.21.1. -- [ ] Three layers: `common` (pure Java, as now), shared code per Minecraft version (rendering, +- [x] Three layers: `common` (pure Java, as now), shared code per Minecraft version (rendering, entities, sounds, HUD, which are the same on every loader), and a thin module per loader (registration, events, networking, config) -- [ ] NeoForge and Fabric on 26.3, with the same features and checklists as Forge +- [x] NeoForge and Fabric on 26.3, with the same features and checklists as Forge - [ ] 1.21.1 (Java 21) on Forge, NeoForge and Fabric: older rendering (no render states), older networking, item models from the item JSON instead of `item_model` - [ ] Content packs and their sync work across loaders on the same Minecraft version (a Fabric client on a NeoForge server) - [ ] Legacy importer checked on 1.21.1 (pack format numbers and the asset layout differ) -- [ ] Jar names carry the loader and the Minecraft version, such as +- [x] Jar names carry the loader and the Minecraft version, such as `mcheli-fabric-1.21.1-1.1.0-beta.3.jar`; the release workflow publishes every jar -- [ ] CI builds and tests every module on every pull request -- [ ] [TESTING.md](TESTING.md) gets a short smoke test per jar: import, fly a helicopter, drive a +- [x] CI builds and tests every module on every pull request +- [x] [TESTING.md](TESTING.md) gets a short smoke test per jar: import, fly a helicopter, drive a tank, fire a missile, join a dedicated server ## Phase 8: 1.20.1 (`1.1.0-beta.4`) From ffe4e5d358cc348ff74b16e3f67940ec059801f3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:39 +0800 Subject: [PATCH 159/314] List NeoForge and Fabric in the requirements and install steps --- README.md | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 48dbd8e..2be7aac 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,8 @@ MCHeli Rework icon -Helicopters, planes, tanks and ground vehicles for **Minecraft 26.3** on **Forge 66.1**. +Helicopters, planes, tanks and ground vehicles for **Minecraft 26.3** on **Forge**, **NeoForge** +and **Fabric**. MCHeli Rework is a ground-up port of MCHeli 1.0.3, a mod that stopped at Minecraft 1.7.10. Every system is rewritten for the modern game, while the original vehicle definitions, models, @@ -127,21 +128,25 @@ Reserved original, the code is source available rather than open source. | | Version | |---|---------| | Minecraft | 26.3 | -| Forge | 66.1.0 or newer in the 66.x line | +| Loader | Forge 66.1.0 or newer in the 66.x line, NeoForge 26.3.0.99 or newer, or Fabric Loader 0.20 with Fabric API | | Java | 25 (bundled with the Minecraft launcher) | ## Installing -1. Install Forge 66.1.0 for Minecraft 26.3. -2. Download `mcheli-26.3-.jar` from the [releases](https://github.com/JnH-Projects/MCHeli-Rework-Forge/releases). -3. Drop it into the `mods` folder of your Forge 26.3 profile. +1. Install Forge, NeoForge or Fabric for Minecraft 26.3. On Fabric, also install + [Fabric API](https://modrinth.com/mod/fabric-api). +2. Download the jar for your loader from the + [releases](https://github.com/JnH-Projects/MCHeli-Rework-Forge/releases): + `mcheli-forge-26.3-.jar`, `mcheli-neoforge-26.3-.jar` or + `mcheli-fabric-26.3-.jar`. +3. Drop it into the `mods` folder of that profile. 4. Download `MC1.7.10_mcheli_1.0.3.zip` from the [MCHeli CurseForge page](https://www.curseforge.com/minecraft/mc-mods/mcheli-minecraft-helicopter-mod/files) and put it, still zipped, into a folder called `mcheli` next to `mods` (create it if needed): ``` / - mods/mcheli-26.3-.jar + mods/mcheli--26.3-.jar mcheli/MC1.7.10_mcheli_1.0.3.zip ``` @@ -151,8 +156,10 @@ be switched off. The zip can be deleted afterwards. Dedicated servers need the z place. Without it the game still starts, but there are no vehicles, and the chat says where the zip goes. -Use a separate game directory for the Forge 26.3 profile. Mods built for other loaders or older -versions in a shared `mods` folder will stop the game from starting. +Use a separate game directory for each loader and version. Mods built for other loaders or older +versions in a shared `mods` folder will stop the game from starting. Worlds and the `mcheli` +folder can move between loaders: the imported content, content packs and configs are the same +files on all three. ## Controls @@ -279,8 +286,8 @@ adds a small helicopter, the Rework Scout. To make your own, see ./gradlew build ``` -The jars, one per Minecraft version, end up in `build/release`. Java 25 is required; Gradle -downloads one automatically if it is not installed. `./gradlew :forge-26.3:runClient` and -`:forge-26.3:runServer` import the content from +The jars, one per loader and Minecraft version, end up in `build/release`. Java 25 is required; +Gradle downloads one automatically if it is not installed. `./gradlew :-26.3:runClient` +and `:-26.3:runServer` (`forge`, `neoforge` or `fabric`) import the content from `Mod/MC1.7.10_mcheli_1.0.3.zip`, which you have to download yourself. See [DEVELOPMENT.md](docs/DEVELOPMENT.md) for details. From 715ddcdc4fdfbce0113ada8321675f7878bfe9e6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Thu, 29 Oct 2026 23:44:39 +0800 Subject: [PATCH 160/314] Note the NeoForge and Fabric jars under Unreleased --- CHANGELOG.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ce96ad..09f2653 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,41 @@ All notable changes to this project are documented here. The format follows [Semantic Versioning](https://semver.org/). Pre-releases (`1.0.0-alpha.N`) mark the phases on the way to 1.0.0; see [docs/ROADMAP.md](docs/ROADMAP.md). +## [Unreleased] + +Phase 7 of the road to 1.1.0: the mod on NeoForge and Fabric as well as Forge. + +### Added + +- NeoForge and Fabric jars for Minecraft 26.3, with the same features as the Forge one. Fabric + needs Fabric API. Worlds, the `mcheli` folder, content packs and configs carry over between + loaders. +- The log names the loader the mod runs on, next to the version. + +### Changed + +- Jar names carry the loader: `mcheli-forge-26.3-.jar`, `mcheli-neoforge-26.3-.jar` + and `mcheli-fabric-26.3-.jar`. +- The configs are read by the mod itself instead of Forge's config system, which Fabric does not + have. The files, their place and their keys stay the same, so existing settings are kept. Edits + to `serverconfig/mcheli-server.toml` now apply on the next world start or `/mcheli reload` + instead of while the server runs; `/mcheli reload` now reads the server config again too. +- The original content is imported again on the first start, since the recipes now name the + vehicle ingredient for every loader. +- Every packet travels under its own id (`mcheli:pilot_state` and so on) with the same encoding on + every loader. Clients and servers need the same version, since the network protocol changed. +- Glass panes that bullets shoot through come from the `c:glass_panes` tag, which every loader + fills. + +### Developers + +- The code is split in three layers: `common` (no Minecraft), `minecraft-26.3` (Minecraft, no + loader) and a thin module per loader (`forge-26.3`, `neoforge-26.3`, `fabric-26.3`). The shared + code reaches the loader through `Platform` and `Registration`, and the loaders call + `CommonHooks` and `ClientHooks` from their events. Where NeoForge or Fabric has no event, a + mixin hooks in where Forge fires its own. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). +- Gradle 9.8.2, for Fabric Loom 1.19. NeoForge modules build with ModDevGradle. + ## [1.1.0-beta.2] - 2026-10-29 Phase 6 of the road to 1.1.0: content packs that servers send to their players, night glow and From 2ef890f7d2bf5a2df8a0833e147169ff823e9cd6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:24 +0800 Subject: [PATCH 161/314] Ignore the per-version run folders --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b7d0f53..a36e87d 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ eclipse/ # Run directories run/ +run-*/ run-data/ logs/ *.log From c74e9b554845c65e8dd683ede156174b6801b8c0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:25 +0800 Subject: [PATCH 162/314] Build the Forge, NeoForge and Fabric modules for 1.21.1 too --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 7c676d0..a239d67 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mod_license=MCHeli Rework Licence 1.0 (see LICENSE) # Platform modules built by this project, one per Minecraft version. Each has its own # gradle.properties with the Minecraft, loader and Java versions. -platforms=forge-26.3,neoforge-26.3,fabric-26.3 +platforms=forge-26.3,neoforge-26.3,fabric-26.3,forge-1.21.1,neoforge-1.21.1,fabric-1.21.1 # Libraries the common module compiles against. Every supported Minecraft version ships them. slf4j_version=2.0.17 From d33a97890b09ed27c14c2b63e26d71d25642ee83 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:25 +0800 Subject: [PATCH 163/314] Put JSpecify on the compile classpath, which 1.21.1 does not ship --- gradle/loader.gradle | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/gradle/loader.gradle b/gradle/loader.gradle index 1e856ee..023e7d3 100644 --- a/gradle/loader.gradle +++ b/gradle/loader.gradle @@ -22,6 +22,15 @@ sourceSets.main.resources.srcDir(project(':common').file('src/main/resources')) sourceSets.main.java.srcDir(new File(shared, 'src/main/java')) sourceSets.main.resources.srcDir(new File(shared, 'src/main/resources')) +repositories { + mavenCentral() +} + +dependencies { + // Annotations only; Minecraft ships JSpecify from 1.21.6 on, so older versions need it to compile. + compileOnly "org.jspecify:jspecify:${jspecify_version}" +} + tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' options.release = java_version as int From c06deb22165c6ca8fff421c9d6262fec169f9d1d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:25 +0800 Subject: [PATCH 164/314] Add ImportTarget for the pack layout of each Minecraft version --- .../mcheli/content/ImportTarget.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 common/src/main/java/com/jnhprojects/mcheli/content/ImportTarget.java diff --git a/common/src/main/java/com/jnhprojects/mcheli/content/ImportTarget.java b/common/src/main/java/com/jnhprojects/mcheli/content/ImportTarget.java new file mode 100644 index 0000000..0753c4f --- /dev/null +++ b/common/src/main/java/com/jnhprojects/mcheli/content/ImportTarget.java @@ -0,0 +1,19 @@ +package com.jnhprojects.mcheli.content; + +/** + * The pack layout {@link LegacyImporter} writes. Resource and data formats changed between the + * supported Minecraft versions, so each version's module asks for its own. + */ +public enum ImportTarget { + /** + * Minecraft 26.x: every vehicle's item model has an item model definition, a crafted stack names + * it in the {@code item_model} component, and recipe ingredients are plain ids and {@code #tags}. + */ + MODERN, + /** + * Minecraft 1.21.1: there is no {@code item_model} component, so each kind's item model picks + * the vehicle's model by {@code custom_model_data} (see {@link LegacyImporter#ICONS_FILE}), and + * recipe ingredients are {@code {"item": ...}} and {@code {"tag": ...}} objects. + */ + V1_21_1 +} From b3d310b5daaac83f6c31598a6df1da6a2d6bb58b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:26 +0800 Subject: [PATCH 165/314] Write 1.21.1 recipes with object ingredients and custom model data --- .../mcheli/content/recipe/LegacyRecipe.java | 59 +++++++++++++++---- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/common/src/main/java/com/jnhprojects/mcheli/content/recipe/LegacyRecipe.java b/common/src/main/java/com/jnhprojects/mcheli/content/recipe/LegacyRecipe.java index 9e9af5c..25d9e5a 100644 --- a/common/src/main/java/com/jnhprojects/mcheli/content/recipe/LegacyRecipe.java +++ b/common/src/main/java/com/jnhprojects/mcheli/content/recipe/LegacyRecipe.java @@ -1,6 +1,7 @@ package com.jnhprojects.mcheli.content.recipe; import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.content.ImportTarget; import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; import java.util.ArrayList; @@ -52,18 +53,28 @@ private LegacyRecipe() { * @throws InvalidRecipeException with the reason when the line cannot become a recipe */ public static String toJson(Vehicle result, String line, boolean shaped, Predicate modItems) { + return toJson(result, line, shaped, modItems, ImportTarget.MODERN); + } + + /** As above, in the recipe format of {@code target}. */ + public static String toJson(Vehicle result, String line, boolean shaped, Predicate modItems, ImportTarget target) { String[] s = line.trim().split("\\s*,\\s*"); - return shaped ? shaped(result, s, modItems) : shapeless(result, s, modItems); + return shaped ? shaped(result, s, modItems, target) : shapeless(result, s, modItems, target); } /** * A crafted vehicle: the generic item of its kind ({@code mcheli:helicopter}) carrying the * definition name, and the item model the import wrote for it. + * + * @param icon the vehicle's {@code custom_model_data} on 1.21.1, 0 for none */ - public record Vehicle(String kind, String name) { + public record Vehicle(String kind, String name, int icon) { + public Vehicle(String kind, String name) { + this(kind, name, 0); + } } - private static String shaped(Vehicle result, String[] s, Predicate modItems) { + private static String shaped(Vehicle result, String[] s, Predicate modItems, ImportTarget target) { int idx = 0; int count = 1; if (s.length > 0 && isNumber(s[0])) { @@ -102,7 +113,7 @@ private static String shaped(Vehicle result, String[] s, Predicate modIt if (idx < s.length && isNumber(s[idx])) { damage = Integer.parseInt(s[idx++]); } - key.put(Character.toUpperCase(symbol.charAt(0)), ingredient(name, damage, modItems)); + key.put(Character.toUpperCase(symbol.charAt(0)), ingredient(name, damage, modItems, target)); } // 1.7.10 left a pattern letter the key does not define as an empty slot; two bundled @@ -137,11 +148,11 @@ private static String shaped(Vehicle result, String[] s, Predicate modIt json.append(" \"").append(rows.get(i)).append('"').append(i + 1 < rows.size() ? ",\n" : "\n"); } json.append(" ],\n"); - appendResult(json, result, count); + appendResult(json, result, count, target); return json.toString(); } - private static String shapeless(Vehicle result, String[] s, Predicate modItems) { + private static String shapeless(Vehicle result, String[] s, Predicate modItems, ImportTarget target) { List ingredients = new ArrayList<>(); for (int idx = 0; idx < s.length; idx++) { if (s[idx].isEmpty()) { @@ -152,7 +163,7 @@ private static String shapeless(Vehicle result, String[] s, Predicate mo if (idx + 1 < s.length && isNumber(s[idx + 1])) { damage = Integer.parseInt(s[++idx]); } - ingredients.add(ingredient(name, damage, modItems)); + ingredients.add(ingredient(name, damage, modItems, target)); } if (ingredients.isEmpty() || ingredients.size() > 9) { throw new InvalidRecipeException("a shapeless recipe needs one to nine ingredients"); @@ -163,23 +174,31 @@ private static String shapeless(Vehicle result, String[] s, Predicate mo json.append(" ").append(ingredients.get(i)).append(i + 1 < ingredients.size() ? ",\n" : "\n"); } json.append(" ],\n"); - appendResult(json, result, 1); + appendResult(json, result, 1, target); return json.toString(); } - private static void appendResult(StringBuilder json, Vehicle result, int count) { + private static void appendResult(StringBuilder json, Vehicle result, int count, ImportTarget target) { json.append(" \"result\": {\n \"id\": \"mcheli:").append(result.kind()).append('"'); if (count > 1) { json.append(",\n \"count\": ").append(count); } json.append(",\n \"components\": {\n") .append(" \"").append(VEHICLE_COMPONENT).append("\": \"").append(result.name()).append("\",\n") - .append(" \"minecraft:item_model\": \"mcheli:").append(result.name()).append("\"\n") + .append(" ").append(model(result, target)).append('\n') .append(" }\n }\n}\n"); } + /** How the crafted stack points at the vehicle's item model. */ + private static String model(Vehicle result, ImportTarget target) { + return switch (target) { + case MODERN -> "\"minecraft:item_model\": \"mcheli:" + result.name() + '"'; + case V1_21_1 -> "\"minecraft:custom_model_data\": " + result.icon(); + }; + } + /** @return the ingredient as a JSON value: a quoted item id or tag, or an object for a vehicle */ - private static String ingredient(String name, int damage, Predicate modItems) { + private static String ingredient(String name, int damage, Predicate modItems, ImportTarget target) { String lower = name.trim().toLowerCase(Locale.ROOT); if (lower.startsWith("mcheli:")) { if (!modItems.test(lower)) { @@ -195,7 +214,11 @@ private static String ingredient(String name, int damage, Predicate modI if (mapped == null) { throw new InvalidRecipeException("no modern item for " + lower + (damage != 0 ? " with damage " + damage : "")); } - return '"' + mapped + '"'; + if (target == ImportTarget.MODERN) { + return '"' + mapped + '"'; + } + // 1.21.1 only reads ingredients as objects. + return mapped.startsWith("#") ? "{\"tag\": \"" + mapped.substring(1) + "\"}" : "{\"item\": \"" + mapped + "\"}"; } private static boolean isNumber(String s) { @@ -226,6 +249,15 @@ public static Map fileNames(String vehicle, List lines) * that could not become a recipe, as {@code ": "} */ public static Generated generate(ContentRegistry content) { + return generate(content, ImportTarget.MODERN, Map.of()); + } + + /** + * As above, in the recipe format of {@code target}. + * + * @param icons each vehicle's {@code custom_model_data}, for 1.21.1 + */ + public static Generated generate(ContentRegistry content, ImportTarget target, Map icons) { Set items = new TreeSet<>(); for (AircraftDefinition def : content.all()) { items.add("mcheli:" + def.name()); @@ -237,7 +269,8 @@ public static Generated generate(ContentRegistry content) { for (Map.Entry e : fileNames(def.name(), def.recipes).entrySet()) { try { files.put(e.getKey() + ".json", - toJson(new Vehicle(def.kind(), def.name()), e.getValue(), def.shapedRecipe, items::contains)); + toJson(new Vehicle(def.kind(), def.name(), icons.getOrDefault(def.name(), 0)), e.getValue(), def.shapedRecipe, + items::contains, target)); } catch (InvalidRecipeException ex) { skipped.add(e.getKey() + ": " + ex.getMessage()); } From 00122eebd319c89f9def00d2733b106aeb385cf4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:26 +0800 Subject: [PATCH 166/314] Import for a target version and write the 1.21.1 kind models and icon list --- .../mcheli/content/LegacyImporter.java | 62 +++++++++++++++++-- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/common/src/main/java/com/jnhprojects/mcheli/content/LegacyImporter.java b/common/src/main/java/com/jnhprojects/mcheli/content/LegacyImporter.java index c17001f..1713018 100644 --- a/common/src/main/java/com/jnhprojects/mcheli/content/LegacyImporter.java +++ b/common/src/main/java/com/jnhprojects/mcheli/content/LegacyImporter.java @@ -1,5 +1,6 @@ package com.jnhprojects.mcheli.content; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; import com.jnhprojects.mcheli.content.recipe.LegacyRecipe; import java.io.IOException; @@ -44,11 +45,17 @@ public final class LegacyImporter { /** * Bump whenever the output changes, so existing installs import again. 2: recipes craft the - * generic vehicle items of 1.1.0. 3: vehicle ingredients name - * their type for every loader. + * generic vehicle items of 1.1.0. 3: vehicle ingredients name their type for every loader, and + * the output depends on the {@link ImportTarget}. */ public static final int FORMAT = 3; + /** + * Written at the pack root for {@link ImportTarget#V1_21_1}: each original vehicle's + * {@code custom_model_data}, as {@code name=index}, which the mod puts on the vehicle's stack. + */ + public static final String ICONS_FILE = "item_icons.properties"; + private static final String ASSET_ROOT = "mcheli/assets/mcheli/"; private static final Set SKIPPED_EXTENSIONS = Set.of("xcf"); private static final Set DEFINITION_FOLDERS = Set.of("helicopters", "planes", "tanks", "vehicles", "throwable"); @@ -56,6 +63,8 @@ public final class LegacyImporter { private static final Pattern LANG_TILE = Pattern.compile("^tile\\.(.+)\\.name$"); private static final Pattern LANG_ACHIEVEMENT = Pattern.compile("^achievement\\.McHeli(\\w+?)(\\.desc)?$"); + private final ImportTarget target; + private final Path packRoot; private final Path assetsOut; private final Path recipesOut; private final Map> translations = new TreeMap<>(); @@ -63,7 +72,9 @@ public final class LegacyImporter { private final Set itemTextures = new TreeSet<>(); private int copied; - private LegacyImporter(Path packRoot) { + private LegacyImporter(Path packRoot, ImportTarget target) { + this.target = target; + this.packRoot = packRoot; this.assetsOut = packRoot.resolve("assets/mcheli"); this.recipesOut = packRoot.resolve("data/mcheli/recipe"); } @@ -74,7 +85,12 @@ private LegacyImporter(Path packRoot) { * @throws IOException when the zip cannot be read or holds no {@code mcheli/assets/mcheli} folder */ public static Summary run(Path zip, Path packRoot) throws IOException { - return new LegacyImporter(packRoot).importFrom(zip); + return run(zip, packRoot, ImportTarget.MODERN); + } + + /** As above, in the layout of {@code target}. */ + public static Summary run(Path zip, Path packRoot, ImportTarget target) throws IOException { + return new LegacyImporter(packRoot, target).importFrom(zip); } private Summary importFrom(Path zipPath) throws IOException { @@ -98,7 +114,9 @@ private Summary importFrom(Path zipPath) throws IOException { writeItemModels(); writeTranslations(); - LegacyRecipe.Generated recipes = LegacyRecipe.generate(ContentRegistry.load(assetsOut)); + ContentRegistry content = ContentRegistry.load(assetsOut); + Map icons = target == ImportTarget.V1_21_1 ? writeKindModels(content) : Map.of(); + LegacyRecipe.Generated recipes = LegacyRecipe.generate(content, target, icons); for (Map.Entry e : recipes.files().entrySet()) { write(recipesOut.resolve(e.getKey()), e.getValue()); } @@ -258,6 +276,40 @@ private void writeItemModels() throws IOException { } } + /** + * 1.21.1: one item model per kind, which shows a barrier unless the stack's + * {@code custom_model_data} picks a vehicle. Vehicles are numbered from 1 in name order within + * their kind; one without an item texture keeps the barrier. + * + * @return each vehicle's number, also written to {@link #ICONS_FILE} + */ + private Map writeKindModels(ContentRegistry content) throws IOException { + Map> byKind = new TreeMap<>(); + for (AircraftDefinition def : content.all()) { + byKind.computeIfAbsent(def.kind(), k -> new ArrayList<>()).add(def.name()); + } + Map icons = new TreeMap<>(); + for (Map.Entry> e : byKind.entrySet()) { + List names = e.getValue(); + names.sort(null); + StringBuilder json = new StringBuilder("{\n \"parent\": \"minecraft:item/barrier\",\n \"overrides\": [\n"); + for (int i = 0; i < names.size(); i++) { + String name = names.get(i); + icons.put(name, i + 1); + String model = itemTextures.contains(name) ? "mcheli:item/" + name : "minecraft:item/barrier"; + json.append(" {\"predicate\": {\"custom_model_data\": ").append(i + 1).append("}, \"model\": \"") + .append(model).append("\"}").append(i + 1 < names.size() ? "," : "").append('\n'); + } + write(assetsOut.resolve("models/item/" + e.getKey() + ".json"), json.append(" ]\n}\n").toString()); + } + StringBuilder list = new StringBuilder("# Written by MCHeli Rework: the custom_model_data of each vehicle's icon\n"); + for (Map.Entry e : icons.entrySet()) { + list.append(e.getKey()).append('=').append(e.getValue()).append('\n'); + } + write(packRoot.resolve(ICONS_FILE), list.toString()); + return icons; + } + private void writeTranslations() throws IOException { for (Map.Entry> e : translations.entrySet()) { StringBuilder json = new StringBuilder("{\n"); From 3d5946f1ce13255b77ff377ec57d5510ec87dd21 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:27 +0800 Subject: [PATCH 167/314] Test the recipe format of each import target --- .../recipe/LegacyRecipeTargetTest.java | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 common/src/test/java/com/jnhprojects/mcheli/content/recipe/LegacyRecipeTargetTest.java diff --git a/common/src/test/java/com/jnhprojects/mcheli/content/recipe/LegacyRecipeTargetTest.java b/common/src/test/java/com/jnhprojects/mcheli/content/recipe/LegacyRecipeTargetTest.java new file mode 100644 index 0000000..01ce208 --- /dev/null +++ b/common/src/test/java/com/jnhprojects/mcheli/content/recipe/LegacyRecipeTargetTest.java @@ -0,0 +1,67 @@ +package com.jnhprojects.mcheli.content.recipe; + +import com.jnhprojects.mcheli.content.ImportTarget; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** The recipe format differs per Minecraft version; these need no original zip. */ +class LegacyRecipeTargetTest { + private static final LegacyRecipe.Vehicle AH_6X = new LegacyRecipe.Vehicle("helicopter", "ah-6x", 7); + private static final String LINE = "\"RXR\", X, mcheli:ah-6, R, redstone"; + + @Test + void modernRecipesUseStringsAndTheItemModel() { + assertEquals(""" + { + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "R": "#c:dusts/redstone", + "X": {"type": "mcheli:vehicle", "neoforge:ingredient_type": "mcheli:vehicle", "fabric:type": "mcheli:vehicle", "vehicle": "ah-6"} + }, + "pattern": [ + "RXR" + ], + "result": { + "id": "mcheli:helicopter", + "components": { + "mcheli:vehicle": "ah-6x", + "minecraft:item_model": "mcheli:ah-6x" + } + } + } + """, LegacyRecipe.toJson(AH_6X, LINE, true, "mcheli:ah-6"::equals, ImportTarget.MODERN)); + } + + @Test + void minecraft1211RecipesUseObjectsAndCustomModelData() { + assertEquals(""" + { + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "R": {"tag": "c:dusts/redstone"}, + "X": {"type": "mcheli:vehicle", "neoforge:ingredient_type": "mcheli:vehicle", "fabric:type": "mcheli:vehicle", "vehicle": "ah-6"} + }, + "pattern": [ + "RXR" + ], + "result": { + "id": "mcheli:helicopter", + "components": { + "mcheli:vehicle": "ah-6x", + "minecraft:custom_model_data": 7 + } + } + } + """, LegacyRecipe.toJson(AH_6X, LINE, true, "mcheli:ah-6"::equals, ImportTarget.V1_21_1)); + } + + @Test + void minecraft1211ShapelessIngredientsAreObjectsToo() { + String json = LegacyRecipe.toJson(AH_6X, "iron_block,coal", false, name -> false, ImportTarget.V1_21_1); + assertTrue(json.contains(" {\"tag\": \"c:storage_blocks/iron\"},\n {\"item\": \"minecraft:coal\"}\n"), json); + } +} From 2f2ecfa7a4a9fc53c4a39121e6d335bd417a6dd9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:27 +0800 Subject: [PATCH 168/314] Import in the 26.x layout and record the target in the marker --- .../src/main/java/com/jnhprojects/mcheli/LegacyContent.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/LegacyContent.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/LegacyContent.java index db29fdf..495bb50 100644 --- a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/LegacyContent.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/LegacyContent.java @@ -1,5 +1,6 @@ package com.jnhprojects.mcheli; +import com.jnhprojects.mcheli.content.ImportTarget; import com.jnhprojects.mcheli.content.LegacyImporter; import net.minecraft.network.chat.Component; import net.minecraft.server.packs.PackLocationInfo; @@ -43,6 +44,8 @@ public final class LegacyContent { private static final String PACK_ID = "mcheli_content"; private static final String MARKER = "import.properties"; + /** The pack layout this Minecraft version reads. */ + private static final ImportTarget TARGET = ImportTarget.MODERN; private final Path folder; private final Path packRoot; @@ -114,7 +117,7 @@ private void importIfNeeded() throws IOException { MCHeli.LOGGER.info("Importing MCHeli content from {}", zip); Path staging = folder.resolve("imported.tmp"); deleteTree(staging); - LegacyImporter.Summary summary = LegacyImporter.run(zip, staging); + LegacyImporter.Summary summary = LegacyImporter.run(zip, staging, TARGET); try (OutputStream out = Files.newOutputStream(staging.resolve(MARKER))) { source.store(out, "Written by MCHeli Rework. Delete this folder to import again."); } @@ -205,6 +208,7 @@ private static void deleteQuietly(Path root) { private static Properties describe(Path zip) throws IOException { Properties p = new Properties(); p.setProperty("format", Integer.toString(LegacyImporter.FORMAT)); + p.setProperty("target", TARGET.name()); p.setProperty("source", zip.toAbsolutePath().normalize().toString()); p.setProperty("size", Long.toString(Files.size(zip))); p.setProperty("modified", Long.toString(Files.getLastModifiedTime(zip).toMillis())); From 73bdc82b880b43fbe524f0c9538a86ac2facc98f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:27 +0800 Subject: [PATCH 169/314] Sort the RiderPose imports --- .../java/com/jnhprojects/mcheli/client/render/RiderPose.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java index 60c1e9c..9ddb169 100644 --- a/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java +++ b/minecraft-26.3/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java @@ -2,9 +2,9 @@ import com.jnhprojects.mcheli.entity.AircraftEntity; import com.jnhprojects.mcheli.physics.AircraftRotation; +import com.mojang.blaze3d.vertex.PoseStack; import net.minecraft.client.Minecraft; import net.minecraft.client.multiplayer.ClientLevel; -import com.mojang.blaze3d.vertex.PoseStack; import net.minecraft.client.renderer.entity.state.AvatarRenderState; import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; import net.minecraft.world.entity.Entity; From f93c40bc01d2f5e248b5ba359980b94616c335c0 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:28 +0800 Subject: [PATCH 170/314] Add ClientConfig to the shared 1.21.1 sources --- .../com/jnhprojects/mcheli/ClientConfig.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ClientConfig.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ClientConfig.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ClientConfig.java new file mode 100644 index 0000000..ff25884 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ClientConfig.java @@ -0,0 +1,59 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.config.ConfigSpec; + +import java.io.IOException; +import java.nio.file.Path; + +/** + * Per player look and feel, in {@code config/mcheli-client.toml}. Nothing here changes how the + * game plays, so each player picks for themselves and the server never sees it. + */ +public final class ClientConfig { + public static final ConfigSpec SPEC; + + private static final ConfigSpec.BooleanValue NIGHT_GLOW; + private static final ConfigSpec.IntValue PACK_CACHE_SIZE; + + static { + ConfigSpec.Builder builder = ConfigSpec.builder(); + + builder.push("effects"); + NIGHT_GLOW = builder + .comment("Whether tracers, rocket motors, flares and explosions glow. The glow is faint by day", + "and strongest at night, in caves and under water.") + .define("nightGlow", true); + builder.pop(); + + builder.push("packs"); + PACK_CACHE_SIZE = builder + .comment("How much the content packs downloaded from servers may take up in mcheli/cache, in megabytes.", + "The packs used longest ago are deleted first; the current server's packs are always kept.", + "A single pack larger than this is not downloaded.") + .defineInRange("cacheSize", 256, 16, 16384); + builder.pop(); + + SPEC = builder.build(); + } + + private ClientConfig() { + } + + /** Reads {@code config/mcheli-client.toml}, writing it with the defaults if it is new. */ + public static void load(Path configDir) { + Path file = configDir.resolve(MCHeli.MOD_ID + "-client.toml"); + try { + SPEC.load(file); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not read {}; using the defaults", file, e); + } + } + + public static boolean nightGlow() { + return SPEC.isLoaded() ? NIGHT_GLOW.get() : NIGHT_GLOW.getDefault(); + } + + public static long packCacheBytes() { + return (long) (SPEC.isLoaded() ? PACK_CACHE_SIZE.get() : PACK_CACHE_SIZE.getDefault()) << 20; + } +} From 4352ec6860f6e7bffa36abfcdee2d750777678aa Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:28 +0800 Subject: [PATCH 171/314] Read recipe ids the 1.21.1 way --- .../com/jnhprojects/mcheli/CommonHooks.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/CommonHooks.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/CommonHooks.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/CommonHooks.java new file mode 100644 index 0000000..d42f522 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/CommonHooks.java @@ -0,0 +1,70 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.network.PackSync; +import com.mojang.brigadier.CommandDispatcher; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.packs.repository.RepositorySource; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.level.storage.LevelResource; + +import java.util.List; + +/** + * What the mod does on game events that both sides see. Each loader module calls these from its + * own events; nothing here knows which loader that is. + */ +public final class CommonHooks { + private CommonHooks() { + } + + /** + * Adds the imported original content to the resource packs (client) and data packs (server). + * Without the original zip there is nothing to add. + */ + public static RepositorySource packSource() { + return onLoad -> { + // Fabric builds some pack repositories before our initializer has run. + LegacyContent content = MCHeli.legacyContent(); + if (content != null) { + content.pack().ifPresent(onLoad); + } + }; + } + + /** Before the worlds load: read the world's server config, then load the content packs it allows. */ + public static void serverAboutToStart(MinecraftServer server) { + ServerConfig.load(server.getWorldPath(LevelResource.ROOT)); + MCHeli.content().replaceWith(MCHeli.packs().load(true)); + } + + public static void serverStopped() { + ServerConfig.unload(); + } + + public static void serverTickEnd(MinecraftServer server) { + PackSync.pump(server); + } + + public static void registerCommands(CommandDispatcher dispatcher) { + McheliCommand.register(dispatcher); + } + + /** + * The vehicle recipes live in {@code data/mcheli/recipe} of the imported content pack. The + * original listed them on its drafting table; until that is ported, every MCHeli recipe goes + * into a player's recipe book when they join, so they can be found. + */ + public static void playerLoggedIn(ServerPlayer player) { + PackSync.onLogin(player); + List> ours = player.level().getServer().getRecipeManager().getRecipes().stream() + .filter(holder -> holder.id().getNamespace().equals(MCHeli.MOD_ID)) + .toList(); + player.awardRecipes(ours); + } + + public static void playerLoggedOut(ServerPlayer player) { + PackSync.onLogout(player); + } +} From 5f9af1c35ad2fa0ec8d09034cda3e3bcf950614d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:29 +0800 Subject: [PATCH 172/314] Add ContentPacks to the shared 1.21.1 sources --- .../com/jnhprojects/mcheli/ContentPacks.java | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ContentPacks.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ContentPacks.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ContentPacks.java new file mode 100644 index 0000000..c378489 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ContentPacks.java @@ -0,0 +1,151 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.content.pack.PackFolder; +import com.jnhprojects.mcheli.content.pack.PackProblem; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * The content packs in {@code /mcheli/packs}, loaded together with the imported 1.0.3 + * content, which always comes first. + * + *

At startup every pack in the folder is loaded, so the vehicles exist before any world does. + * When a server starts (single player included) and on {@code /mcheli reload}, the folder is + * scanned again and the server config applied: the allow list, the size limit per pack and the + * limit for all packs together. The packs that pass are what players get sent. + * + *

A zip pack is read in place, so packs stay open while their definitions and assets are in + * use. The set a reload replaces is closed on the reload after that, so a client in the same game + * that is still drawing from it never reads from a closed zip. + */ +public final class ContentPacks { + private static final int PROBLEMS_IN_CHAT = 8; + + private final Path folder; + private final LegacyContent legacy; + private volatile List active = List.of(); + private volatile List problems = List.of(); + private List retired = List.of(); + + ContentPacks(Path gameDir, LegacyContent legacy) { + this.folder = gameDir.resolve(MCHeli.MOD_ID).resolve("packs"); + this.legacy = legacy; + } + + /** {@code /mcheli/packs}. */ + public Path folder() { + return folder; + } + + /** The packs whose definitions are loaded, in load order. The import is not among them. */ + public List active() { + return active; + } + + /** Everything that went wrong in the last load, packs left out included. */ + public List problems() { + return problems; + } + + /** + * Scans the folder and loads the import and the packs. Never throws; whatever goes wrong is + * logged and kept in {@link #problems()}. + * + * @param serverRules whether to apply the server config; off at startup, when no world exists + * @return the new content, for the caller to swap in + */ + public synchronized ContentRegistry load(boolean serverRules) { + try { + Files.createDirectories(folder); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not create {}", folder, e); + } + + PackFolder.Scan scan = PackFolder.scan(folder); + List found = new ArrayList<>(scan.problems()); + List accepted = new ArrayList<>(); + long total = 0; + for (ContentPack pack : scan.packs()) { + long size; + String refusal; + try { + size = pack.archiveSize(); + refusal = serverRules ? refusal(pack, size, total) : null; + } catch (IOException e) { + size = 0; + refusal = "left out, could not be read: " + e; + } + if (refusal == null) { + accepted.add(pack); + total += size; + } else { + found.add(new PackProblem(pack.id(), refusal)); + close(List.of(pack)); + } + } + for (PackProblem problem : found) { + MCHeli.LOGGER.warn("Content pack {}", problem); + } + + List sources = new ArrayList<>(); + sources.add(new ContentSource(ContentSource.ORIGINAL, legacy.assets())); + for (ContentPack pack : accepted) { + sources.add(pack.contentSource()); + } + ContentRegistry registry = ContentRegistry.load(sources); + found.addAll(registry.problems()); + + close(retired); + retired = active; + active = List.copyOf(accepted); + problems = List.copyOf(found); + MCHeli.LOGGER.info("Content packs in use: {}", active.isEmpty() ? "none" : active); + return registry; + } + + private static @Nullable String refusal(ContentPack pack, long size, long total) { + if (!ServerConfig.isPackAllowed(pack.id())) { + return "left out, not on the server's allow list (packs.allowList in mcheli-server.toml)"; + } + if (size > ServerConfig.maxPackBytes()) { + return "left out, %.1f MB is more than the server allows for one pack (packs.maxPackSize, %d MB)" + .formatted(size / 1048576.0, ServerConfig.maxPackBytes() >> 20); + } + if (total + size > ServerConfig.maxTotalPackBytes()) { + return "left out, it would take the packs past the server's total limit (packs.maxTotalSize, %d MB)" + .formatted(ServerConfig.maxTotalPackBytes() >> 20); + } + return null; + } + + /** Sends the problems of the last load as chat lines, the first few of them. */ + public void reportProblems(Consumer out) { + List list = problems; + for (int i = 0; i < Math.min(list.size(), PROBLEMS_IN_CHAT); i++) { + out.accept(Component.translatable("message.mcheli.pack.problem", list.get(i).pack(), list.get(i).message())); + } + if (list.size() > PROBLEMS_IN_CHAT) { + out.accept(Component.translatable("message.mcheli.pack.more_problems", list.size() - PROBLEMS_IN_CHAT)); + } + } + + private static void close(List packs) { + for (ContentPack pack : packs) { + try { + pack.close(); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not close content pack {}", pack, e); + } + } + } +} From 11d9fc6c65b4c0a14848c91bc536abdeca1a39de Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:29 +0800 Subject: [PATCH 173/314] Add ContentReload to the shared 1.21.1 sources --- .../com/jnhprojects/mcheli/ContentReload.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ContentReload.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ContentReload.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ContentReload.java new file mode 100644 index 0000000..7846b7b --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ContentReload.java @@ -0,0 +1,28 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.network.PackSync; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.storage.LevelResource; + +/** + * Reads the vehicle and weapon definitions from disk again while the game runs, for + * {@code /mcheli reload}: the server config, then the imported content and the content packs. + * Vehicles in the world pick up their new definition on their next tick (see + * {@code AircraftEntity#definition}). Every player gets the new pack list, downloads what changed + * and rebuilds their content the same way. + */ +public final class ContentReload { + private ContentReload() { + } + + /** @return the newly loaded content, already in use */ + public static ContentRegistry reload(MinecraftServer server) { + ServerConfig.load(server.getWorldPath(LevelResource.ROOT)); + ContentRegistry fresh = MCHeli.packs().load(true); + MCHeli.content().replaceWith(fresh); + MCHeli.LOGGER.info("Content reloaded: {} vehicles, {} weapons", fresh.all().size(), fresh.weapons().size()); + PackSync.sendListToAll(server); + return fresh; + } +} From b6858b703b007cf067ee99f45435338d8fb03210 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:29 +0800 Subject: [PATCH 174/314] Import in the 1.21.1 layout and read the vehicle icon numbers --- .../com/jnhprojects/mcheli/LegacyContent.java | 275 ++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/LegacyContent.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/LegacyContent.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/LegacyContent.java new file mode 100644 index 0000000..12a5862 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/LegacyContent.java @@ -0,0 +1,275 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ImportTarget; +import com.jnhprojects.mcheli.content.LegacyImporter; +import net.minecraft.network.chat.Component; +import net.minecraft.server.packs.PackLocationInfo; +import net.minecraft.server.packs.PackSelectionConfig; +import net.minecraft.server.packs.PathPackResources; +import net.minecraft.server.packs.repository.Pack; +import net.minecraft.server.packs.repository.PackCompatibility; +import net.minecraft.server.packs.repository.PackSource; +import net.minecraft.world.flag.FeatureFlagSet; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.nio.file.FileSystemException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.Properties; +import java.util.stream.Stream; + +/** + * The original vehicles, models, textures and sounds, imported from the player's own copy of + * MCHeli 1.0.3. + * + *

That content is EMB4's and is not redistributed with this mod. Players download the original + * zip from the MCHeli CurseForge page and put it in {@code /mcheli}. On start the zip is + * converted once by {@link LegacyImporter} into {@code mcheli/imported}, which then serves as a + * required resource pack and data pack. The zip is converted again only when it changes or the + * importer's output format does; once imported it may be deleted. + * + *

Development runs point the {@code mcheli.legacyZip} system property at the zip instead. + */ +public final class LegacyContent { + /** The file name the original download has. Any zip with the same layout works. */ + public static final String ZIP_NAME = "MC1.7.10_mcheli_1.0.3.zip"; + + private static final String PACK_ID = "mcheli_content"; + private static final String MARKER = "import.properties"; + /** The pack layout this Minecraft version reads. */ + private static final ImportTarget TARGET = ImportTarget.V1_21_1; + + private final Path folder; + private final Path packRoot; + private @Nullable Component problem; + private volatile @Nullable Map icons; + + private LegacyContent(Path folder) { + this.folder = folder; + this.packRoot = folder.resolve("imported"); + } + + /** Imports the zip if needed. Never throws; a missing or broken zip leaves the mod without content. */ + static LegacyContent prepare(Path gameDir) { + LegacyContent content = new LegacyContent(gameDir.resolve(MCHeli.MOD_ID)); + try { + content.importIfNeeded(); + } catch (IOException | UncheckedIOException e) { + MCHeli.LOGGER.error("Could not import the original MCHeli content", e); + content.problem = Component.translatable("message.mcheli.content.import_failed", e.getMessage()); + } + return content; + } + + /** The imported {@code assets/mcheli} folder. It does not exist when nothing was imported. */ + public Path assets() { + return packRoot.resolve("assets").resolve(MCHeli.MOD_ID); + } + + /** + * The {@code custom_model_data} that shows this original vehicle's icon, from the list the + * import wrote ({@link LegacyImporter#ICONS_FILE}); empty for a vehicle it did not import. + */ + public OptionalInt iconIndex(String vehicle) { + Map map = icons; + if (map == null) { + map = readIcons(); + icons = map; + } + Integer index = map.get(vehicle); + return index != null ? OptionalInt.of(index) : OptionalInt.empty(); + } + + private Map readIcons() { + Properties list = new Properties(); + try (InputStream in = Files.newInputStream(packRoot.resolve(LegacyImporter.ICONS_FILE))) { + list.load(in); + } catch (IOException e) { + return Map.of(); + } + Map map = new HashMap<>(); + for (String name : list.stringPropertyNames()) { + try { + map.put(name, Integer.parseInt(list.getProperty(name).trim())); + } catch (NumberFormatException ignored) { + // A hand edited list; that vehicle shows its kind's model. + } + } + return Map.copyOf(map); + } + + /** Why the vehicles are missing, for the player; empty when the content is in place. */ + public Optional problem() { + return Optional.ofNullable(problem); + } + + /** + * The imported folder as a pack, for both the client resource packs and the server data packs. + * Empty when nothing was imported. + */ + public Optional pack() { + if (!Files.isDirectory(packRoot)) { + return Optional.empty(); + } + PackLocationInfo location = new PackLocationInfo(PACK_ID, + Component.translatable("pack.mcheli.content"), PackSource.BUILT_IN, Optional.empty()); + Pack.Metadata metadata = new Pack.Metadata(Component.translatable("pack.mcheli.content.description"), + PackCompatibility.COMPATIBLE, FeatureFlagSet.of(), List.of()); + // Required, so it is enabled in every world and cannot be switched off. It sits at the + // bottom so that regular resource packs can still replace its textures and models. + return Optional.of(new Pack(location, new PathPackResources.PathResourcesSupplier(packRoot), metadata, + new PackSelectionConfig(true, Pack.Position.BOTTOM, false))); + } + + private void importIfNeeded() throws IOException { + Path zip = findZip(); + if (zip == null) { + if (Files.isDirectory(packRoot)) { + MCHeli.LOGGER.info("Using the MCHeli content imported earlier into {}", packRoot); + } else { + MCHeli.LOGGER.error("MCHeli content not found. Download {} from the MCHeli CurseForge page and put it in {}", + ZIP_NAME, folder.toAbsolutePath()); + problem = Component.translatable("message.mcheli.content.missing", ZIP_NAME, folder.toAbsolutePath().toString()); + } + return; + } + + Properties source = describe(zip); + if (source.equals(readMarker())) { + return; + } + + MCHeli.LOGGER.info("Importing MCHeli content from {}", zip); + Path staging = folder.resolve("imported.tmp"); + deleteTree(staging); + LegacyImporter.Summary summary = LegacyImporter.run(zip, staging, TARGET); + try (OutputStream out = Files.newOutputStream(staging.resolve(MARKER))) { + source.store(out, "Written by MCHeli Rework. Delete this folder to import again."); + } + swapIn(staging); + + MCHeli.LOGGER.info("Imported {} files, {} sounds, {} item models and {} recipes into {}", + summary.files(), summary.sounds(), summary.itemModels(), summary.recipes(), packRoot); + for (String skipped : summary.skippedRecipes()) { + MCHeli.LOGGER.info("Recipe left out: {}", skipped); + } + } + + /** + * Replaces the previous import with the fresh one in {@code staging}. + * + *

On Windows a folder that was just deleted keeps its name for a moment while another + * program (a file watcher, a virus scanner) still has a handle in it, and renaming onto that + * name fails with "access denied". So the old import is moved out of the way rather than + * deleted, the renames are retried for a few seconds, and the old copy is deleted last, on a + * best effort basis; a copy left behind is removed on the next import. + */ + private void swapIn(Path staging) throws IOException { + Path old = folder.resolve("imported.old"); + deleteQuietly(old); + if (Files.exists(packRoot)) { + try { + moveWithRetry(packRoot, old); + } catch (IOException e) { + deleteTree(packRoot); + } + } + moveWithRetry(staging, packRoot); + deleteQuietly(old); + } + + private static void moveWithRetry(Path from, Path to) throws IOException { + for (int attempt = 1; ; attempt++) { + try { + Files.move(from, to); + return; + } catch (FileSystemException e) { + if (attempt >= 15) { + throw e; + } + try { + Thread.sleep(50L * attempt); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw e; + } + } + } + } + + private static void deleteQuietly(Path root) { + try { + deleteTree(root); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not delete {}; it is removed on the next import", root, e); + } + } + + private @Nullable Path findZip() throws IOException { + String configured = System.getProperty("mcheli.legacyZip"); + if (configured != null && !configured.isBlank()) { + Path zip = Path.of(configured); + if (Files.isRegularFile(zip)) { + return zip; + } + MCHeli.LOGGER.warn("mcheli.legacyZip points to {}, which does not exist", zip); + } + + Files.createDirectories(folder); + if (Files.isRegularFile(folder.resolve(ZIP_NAME))) { + return folder.resolve(ZIP_NAME); + } + // Browsers rename repeated downloads, so take any zip, the same way every start. + try (Stream files = Files.list(folder)) { + return files.filter(p -> p.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(".zip")) + .filter(Files::isRegularFile) + .sorted() + .findFirst() + .orElse(null); + } + } + + /** What the current import was made from. Comparing size and time is enough to notice a new zip. */ + private static Properties describe(Path zip) throws IOException { + Properties p = new Properties(); + p.setProperty("format", Integer.toString(LegacyImporter.FORMAT)); + p.setProperty("target", TARGET.name()); + p.setProperty("source", zip.toAbsolutePath().normalize().toString()); + p.setProperty("size", Long.toString(Files.size(zip))); + p.setProperty("modified", Long.toString(Files.getLastModifiedTime(zip).toMillis())); + return p; + } + + private Properties readMarker() throws IOException { + Properties p = new Properties(); + Path marker = packRoot.resolve(MARKER); + if (Files.isRegularFile(marker)) { + try (InputStream in = Files.newInputStream(marker)) { + p.load(in); + } + } + return p; + } + + private static void deleteTree(Path root) throws IOException { + if (!Files.exists(root)) { + return; + } + try (Stream paths = Files.walk(root)) { + for (Path p : paths.sorted(Comparator.reverseOrder()).toList()) { + Files.delete(p); + } + } + } +} From 8a2051e50a280164e61a33c20c21a12bbeb3c0d1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:30 +0800 Subject: [PATCH 175/314] Add MCHeli to the shared 1.21.1 sources --- .../java/com/jnhprojects/mcheli/MCHeli.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/MCHeli.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/MCHeli.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/MCHeli.java new file mode 100644 index 0000000..21d4185 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/MCHeli.java @@ -0,0 +1,65 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.registry.ModCreativeTabs; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.registry.ModItems; +import com.jnhprojects.mcheli.registry.ModParticles; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The mod, minus the loader. Each loader module's entry point installs its {@link Platform}, calls + * {@link #init()}, registers what {@code Registration} collected and wires its events to + * {@link CommonHooks} (and, on a client, {@code ClientHooks}). + */ +public final class MCHeli { + public static final String MOD_ID = "mcheli"; + public static final Logger LOGGER = LoggerFactory.getLogger(MCHeli.class); + + private static LegacyContent legacyContent; + private static ContentPacks packs; + private static ContentRegistry content; + + private MCHeli() { + } + + /** + * Imports the original content if needed, loads the definitions and declares the registry + * entries. The vehicle list must be known here: 1.0.0 had an item per vehicle, and those items + * are still registered so old worlds keep them. + */ + public static void init() { + Platform platform = Platform.get(); + LOGGER.info("MCHeli Rework {} on {}", platform.modVersion(), platform.loaderName()); + legacyContent = LegacyContent.prepare(platform.gameDir()); + packs = new ContentPacks(platform.gameDir(), legacyContent); + content = packs.load(false); + ModDataComponents.init(); + ModEntities.init(); + ModParticles.init(); + ModItems.init(content); + ModCreativeTabs.init(); + } + + /** + * Vehicle and weapon definitions, from the imported original content and the content packs. + * Available from mod construction onwards. Always the same object; a reload changes what it + * holds. + */ + public static ContentRegistry content() { + return content; + } + + /** Where the original vehicle content came from, and why it is missing if it is. */ + public static LegacyContent legacyContent() { + return legacyContent; + } + + /** The content packs in {@code mcheli/packs} and the ones in use. */ + public static ContentPacks packs() { + return packs; + } +} From e88468dde02ed78610a17a3459ff802352937486 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:30 +0800 Subject: [PATCH 176/314] Check permissions and hand out items the 1.21.1 way --- .../com/jnhprojects/mcheli/McheliCommand.java | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/McheliCommand.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/McheliCommand.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/McheliCommand.java new file mode 100644 index 0000000..f70a747 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/McheliCommand.java @@ -0,0 +1,135 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +/** + * {@code /mcheli}: what content is loaded, and operator tools to hand out vehicles and reload the + * definitions and content packs. + *

+ *   /mcheli                              content source and counts
+ *   /mcheli list [helicopter|plane|...]  loaded vehicles
+ *   /mcheli packs                        content packs in use, and their problems
+|plane|...]  loaded vehicles
+ *   /mcheli give <players> <vehicle> [count]   (operators)
+ *   /mcheli reload                       re-read the definitions and packs (operators)
+ * 
+ */ +public final class McheliCommand { + private static final List KINDS = List.of("helicopter", "plane", "tank", "vehicle"); + private static final DynamicCommandExceptionType UNKNOWN_VEHICLE = + new DynamicCommandExceptionType(name -> Component.translatable("commands.mcheli.unknown_vehicle", name)); + + private McheliCommand() { + } + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register(Commands.literal(MCHeli.MOD_ID) + .executes(McheliCommand::status) + .then(Commands.literal("list") + .executes(ctx -> list(ctx, null)) + .then(Commands.argument("kind", StringArgumentType.word()) + .suggests((ctx, builder) -> SharedSuggestionProvider.suggest(KINDS, builder)) + .executes(ctx -> list(ctx, StringArgumentType.getString(ctx, "kind"))))) + .then(Commands.literal("packs") + .executes(McheliCommand::packs)) + .then(Commands.literal("give") + .requires(source -> source.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .then(Commands.argument("targets", EntityArgument.players()) + .then(Commands.argument("vehicle", StringArgumentType.word()) + .suggests((ctx, builder) -> SharedSuggestionProvider.suggest( + MCHeli.content().all().stream().map(AircraftDefinition::name), builder)) + .executes(ctx -> give(ctx, 1)) + .then(Commands.argument("count", IntegerArgumentType.integer(1, 64)) + .executes(ctx -> give(ctx, IntegerArgumentType.getInteger(ctx, "count"))))))) + .then(Commands.literal("reload") + .requires(source -> source.hasPermission(Commands.LEVEL_GAMEMASTERS)) + .executes(McheliCommand::reload))); + } + + private static int status(CommandContext ctx) { + ContentRegistry content = MCHeli.content(); + CommandSourceStack source = ctx.getSource(); + MCHeli.legacyContent().problem().ifPresent(source::sendFailure); + source.sendSuccess(() -> Component.translatable("commands.mcheli.status", + content.helicopters().size(), content.planes().size(), content.tanks().size(), content.vehicles().size(), + content.weapons().size(), MCHeli.legacyContent().assets().toString()), false); + List packs = MCHeli.packs().active(); + source.sendSuccess(() -> Component.translatable("commands.mcheli.status.packs", packs.size(), + packs.stream().map(ContentPack::id).collect(Collectors.joining(", "))), false); + return content.all().size(); + } + + private static int packs(CommandContext ctx) { + CommandSourceStack source = ctx.getSource(); + List packs = MCHeli.packs().active(); + if (packs.isEmpty()) { + source.sendSuccess(() -> Component.translatable("commands.mcheli.packs.none", MCHeli.packs().folder().toString()), false); + } else { + source.sendSuccess(() -> Component.translatable("commands.mcheli.packs.header", packs.size(), MCHeli.packs().folder().toString()), false); + } + ContentRegistry content = MCHeli.content(); + for (ContentPack pack : packs) { + long vehicles = content.all().stream().filter(def -> def.pack().equals(pack.id())).count(); + long weapons = content.weapons().stream().filter(def -> def.pack().equals(pack.id())).count(); + source.sendSuccess(() -> Component.translatable("commands.mcheli.packs.entry", pack.id(), pack.manifest().version(), + pack.manifest().name(), vehicles, weapons, ContentPack.shortHash(pack.hash())), false); + } + MCHeli.packs().reportProblems(source::sendFailure); + return packs.size(); + } + + private static int list(CommandContext ctx, String kind) { + List defs = MCHeli.content().all().stream() + .filter(def -> kind == null || def.kind().equals(kind)) + .toList(); + String names = defs.stream().map(AircraftDefinition::name).collect(Collectors.joining(", ")); + ctx.getSource().sendSuccess(() -> Component.translatable("commands.mcheli.list", defs.size(), names), false); + return defs.size(); + } + + private static int give(CommandContext ctx, int count) throws CommandSyntaxException { + String name = StringArgumentType.getString(ctx, "vehicle"); + AircraftDefinition def = MCHeli.content().any(name); + if (def == null) { + throw UNKNOWN_VEHICLE.create(name); + } + Collection targets = EntityArgument.getPlayers(ctx, "targets"); + for (ServerPlayer player : targets) { + // Vehicle items do not stack, so each one is a separate stack. + for (int i = 0; i < count; i++) { + player.getInventory().placeItemBackInInventory(AircraftItem.stackOf(def)); + } + } + Component vehicle = AircraftItem.stackOf(def).getHoverName(); + ctx.getSource().sendSuccess(() -> Component.translatable("commands.mcheli.give", count, vehicle, targets.size()), true); + return targets.size(); + } + + private static int reload(CommandContext ctx) { + CommandSourceStack source = ctx.getSource(); + ContentRegistry fresh = ContentReload.reload(source.getServer()); + source.sendSuccess(() -> Component.translatable("commands.mcheli.reload.success", + fresh.all().size(), fresh.weapons().size(), MCHeli.packs().active().size()), true); + MCHeli.packs().reportProblems(source::sendFailure); + return fresh.all().size(); + } +} From 942b43c88304f3cbe4368e4bf10c911840d47d65 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:31 +0800 Subject: [PATCH 177/314] Add ModSounds to the shared 1.21.1 sources --- .../com/jnhprojects/mcheli/ModSounds.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ModSounds.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ModSounds.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ModSounds.java new file mode 100644 index 0000000..5bbb322 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ModSounds.java @@ -0,0 +1,47 @@ +package com.jnhprojects.mcheli; + +import net.minecraft.core.Holder; +import net.minecraft.network.protocol.game.ClientboundSoundPacket; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Sound events from {@code assets/mcheli/sounds.json}. + * + *

The events are not put in the registry. Content files name their sounds freely and a content + * pack may bring its own, so they are created on demand as direct holders, which the sound packet + * and the sound manager both accept. + */ +public final class ModSounds { + private static final Map EVENTS = new ConcurrentHashMap<>(); + + private ModSounds() { + } + + public static SoundEvent get(String name) { + return EVENTS.computeIfAbsent(name, n -> SoundEvent.createVariableRangeEvent(ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, n))); + } + + /** Plays {@code name} at the entity for everyone nearby. Call on the server. */ + public static void playAt(Entity entity, String name, float volume, float pitch) { + entity.level().playSound(null, entity.getX(), entity.getY(), entity.getZ(), get(name), SoundSource.NEUTRAL, volume, pitch); + } + + /** Plays {@code name} at a position for everyone nearby. Call on the server. */ + public static void playAt(Level level, double x, double y, double z, String name, float volume, float pitch) { + level.playSound(null, x, y, z, get(name), SoundSource.NEUTRAL, volume, pitch); + } + + /** Plays {@code name} for one player only, such as a hit confirmation. */ + public static void playTo(ServerPlayer player, String name, float volume, float pitch) { + player.connection.send(new ClientboundSoundPacket(Holder.direct(get(name)), SoundSource.PLAYERS, + player.getX(), player.getY(), player.getZ(), volume, pitch, player.getRandom().nextLong())); + } +} From 0c3ef1ad17a87b5928a6a94fce51933c1933812a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:31 +0800 Subject: [PATCH 178/314] Add ServerConfig to the shared 1.21.1 sources --- .../com/jnhprojects/mcheli/ServerConfig.java | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ServerConfig.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ServerConfig.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ServerConfig.java new file mode 100644 index 0000000..eef7dc3 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/ServerConfig.java @@ -0,0 +1,145 @@ +package com.jnhprojects.mcheli; + +import com.jnhprojects.mcheli.config.ConfigSpec; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; + +/** + * Server rules, the part of the original {@code mcheli.cfg} that decides how the game plays. + * The file lives per world in {@code serverconfig/mcheli-server.toml}, where Forge kept it, and is + * never sent to clients (only the server applies these rules). It is read when the world starts + * and again on {@code /mcheli reload}. + * + *

The values only exist while a world is loaded. Before that, and on clients, the getters return + * the defaults, which match 1.0.0. + */ +public final class ServerConfig { + public static final ConfigSpec SPEC; + + private static final ConfigSpec.BooleanValue EXPLOSIONS_BREAK_BLOCKS; + private static final ConfigSpec.BooleanValue EXPLOSIONS_START_FIRES; + private static final ConfigSpec.DoubleValue VEHICLE_DAMAGE_FACTOR; + private static final ConfigSpec.BooleanValue INFINITE_AMMO; + private static final ConfigSpec.StringListValue DISABLED_VEHICLES; + private static final ConfigSpec.StringListValue PACK_ALLOW_LIST; + private static final ConfigSpec.IntValue MAX_PACK_SIZE; + private static final ConfigSpec.IntValue MAX_TOTAL_SIZE; + + static { + ConfigSpec.Builder builder = ConfigSpec.builder(); + + builder.push("explosions"); + EXPLOSIONS_BREAK_BLOCKS = builder + .comment("Whether weapons and crashing vehicles break blocks. The mobGriefing game rule also applies.", + "Original: Explosion_DestroyBlock") + .define("breakBlocks", true); + EXPLOSIONS_START_FIRES = builder + .comment("Whether incendiary weapons and crashes set blocks on fire. Original: Explosion_FlamingBlock") + .define("startFires", true); + builder.pop(); + + builder.push("vehicles"); + VEHICLE_DAMAGE_FACTOR = builder + .comment("Multiplies the damage every vehicle takes, after armour. 0.5 makes them twice as tough.") + .defineInRange("damageFactor", 1.0, 0.0, 100.0); + DISABLED_VEHICLES = builder + .comment("Vehicles that cannot be placed, by definition name (the file name, such as \"ah-64\").", + "Vehicles already in the world stay.") + .defineList("disabled", List.of()); + builder.pop(); + + builder.push("weapons"); + INFINITE_AMMO = builder + .comment("Whether vehicle weapons never run out of ammunition. Original: InfinityAmmo") + .define("infiniteAmmo", false); + builder.pop(); + + builder.comment("Content packs from /mcheli/packs. Players get the packs from the server when they join.", + "Changes apply when the world starts or after /mcheli reload.") + .push("packs"); + PACK_ALLOW_LIST = builder + .comment("Pack ids the server loads, such as \"rework-example\". Empty loads every pack in the folder.") + .defineList("allowList", List.of()); + MAX_PACK_SIZE = builder + .comment("Largest pack the server loads and sends to players, in megabytes.") + .defineInRange("maxPackSize", 8, 1, 512); + MAX_TOTAL_SIZE = builder + .comment("Most the server loads and sends in all, in megabytes. Packs are taken in id order until", + "the next one would go over.") + .defineInRange("maxTotalSize", 64, 1, 4096); + builder.pop(); + + SPEC = builder.build(); + } + + private ServerConfig() { + } + + /** Reads the world's file, writing it with the defaults if it is new. */ + public static void load(Path worldDir) { + Path file = worldDir.resolve("serverconfig").resolve(MCHeli.MOD_ID + "-server.toml"); + try { + SPEC.load(file); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not read {}; using the defaults", file, e); + } + } + + /** The world closed: back to the defaults. */ + public static void unload() { + SPEC.unload(); + } + + public static boolean explosionsBreakBlocks() { + return SPEC.isLoaded() ? EXPLOSIONS_BREAK_BLOCKS.get() : EXPLOSIONS_BREAK_BLOCKS.getDefault(); + } + + public static boolean explosionsStartFires() { + return SPEC.isLoaded() ? EXPLOSIONS_START_FIRES.get() : EXPLOSIONS_START_FIRES.getDefault(); + } + + public static float vehicleDamageFactor() { + double factor = SPEC.isLoaded() ? VEHICLE_DAMAGE_FACTOR.get() : VEHICLE_DAMAGE_FACTOR.getDefault(); + return (float) factor; + } + + public static boolean infiniteAmmo() { + return SPEC.isLoaded() ? INFINITE_AMMO.get() : INFINITE_AMMO.getDefault(); + } + + /** Whether the allow list lets the server load this pack. An empty list allows every pack. */ + public static boolean isPackAllowed(String id) { + if (!SPEC.isLoaded() || PACK_ALLOW_LIST.get().isEmpty()) { + return true; + } + for (String allowed : PACK_ALLOW_LIST.get()) { + if (allowed.trim().toLowerCase(Locale.ROOT).equals(id)) { + return true; + } + } + return false; + } + + public static long maxPackBytes() { + return (long) (SPEC.isLoaded() ? MAX_PACK_SIZE.get() : MAX_PACK_SIZE.getDefault()) << 20; + } + + public static long maxTotalPackBytes() { + return (long) (SPEC.isLoaded() ? MAX_TOTAL_SIZE.get() : MAX_TOTAL_SIZE.getDefault()) << 20; + } + + public static boolean isDisabled(String vehicle) { + if (!SPEC.isLoaded()) { + return false; + } + for (String name : DISABLED_VEHICLES.get()) { + if (name.trim().toLowerCase(Locale.ROOT).equals(vehicle)) { + return true; + } + } + return false; + } +} From 9b3cc719cd1f0e1c0a479f72e075c9a5a31dbb45 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:32 +0800 Subject: [PATCH 179/314] Add CameraAngles to the shared 1.21.1 sources --- .../mcheli/client/CameraAngles.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java new file mode 100644 index 0000000..ab62e12 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/CameraAngles.java @@ -0,0 +1,26 @@ +package com.jnhprojects.mcheli.client; + +import net.minecraft.client.Camera; + +/** + * The camera's angles for this frame, after vanilla placed the camera and before it renders, as + * the loader module hands them to {@link ClientHooks#computeCameraAngles}. Forge and NeoForge + * back this with their viewport event; Fabric with a mixin at the same spot. + */ +public interface CameraAngles { + Camera camera(); + + float partialTick(); + + float yaw(); + + float pitch(); + + float roll(); + + void setYaw(float yaw); + + void setPitch(float pitch); + + void setRoll(float roll); +} From 4536ee8710b1f42de11707ebdb0f0747ab34e5fd Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:32 +0800 Subject: [PATCH 180/314] Add ClientContentReload to the shared 1.21.1 sources --- .../mcheli/client/ClientContentReload.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java new file mode 100644 index 0000000..70bcc23 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/ClientContentReload.java @@ -0,0 +1,28 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.client.render.MeshCache; +import com.jnhprojects.mcheli.registry.ModCreativeTabs; +import net.minecraft.client.Minecraft; +import net.minecraft.world.item.CreativeModeTab; + +/** + * Makes the client show the content it now holds: after the server's pack list was applied, which + * also happens after {@code /mcheli reload}. + */ +final class ClientContentReload { + private ClientContentReload() { + } + + /** Drops cached models and textures so edited and new files load, and rebuilds the creative tabs. */ + static void refresh() { + Minecraft mc = Minecraft.getInstance(); + MeshCache.INSTANCE.clear(); + RuntimeAssets.refreshTextures(); + if (mc.level != null && mc.player != null) { + boolean operator = mc.options.operatorItemsTab().get() && mc.player.canUseGameMasterBlocks(); + CreativeModeTab.ItemDisplayParameters params = + new CreativeModeTab.ItemDisplayParameters(mc.level.enabledFeatures(), operator, mc.level.registryAccess()); + ModCreativeTabs.all().forEach(tab -> tab.buildContents(params)); + } + } +} From e73acd9652c1694e4ac97914f45d65e7f73c2399 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:32 +0800 Subject: [PATCH 181/314] Hand the rider hook the entity and the HUD hook GuiGraphics on 1.21.1 --- .../mcheli/client/ClientHooks.java | 215 ++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java new file mode 100644 index 0000000..6963150 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/ClientHooks.java @@ -0,0 +1,215 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.ClientConfig; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.hud.HudRenderer; +import com.jnhprojects.mcheli.client.hud.HudScripts; +import com.jnhprojects.mcheli.client.render.A10Renderer; +import com.jnhprojects.mcheli.client.render.AircraftRenderer; +import com.jnhprojects.mcheli.client.render.FlareRenderer; +import com.jnhprojects.mcheli.client.render.MeshCache; +import com.jnhprojects.mcheli.client.render.ProjectileRenderer; +import com.jnhprojects.mcheli.client.render.RiderPose; +import com.jnhprojects.mcheli.client.sound.EngineSoundPlayer; +import com.jnhprojects.mcheli.entity.ClientAccess; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.KeyMapping; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.PreparableReloadListener; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; + +import java.util.List; +import java.util.Map; + +/** + * Client side counterpart of {@code CommonHooks}: what the mod does on client events. The loader + * module calls these from its own events. Kept out of the shared entry point so dedicated servers + * never load these classes. + */ +public final class ClientHooks { + /** The weapon HUD's layer, drawn above the vanilla HUD. */ + public static final ResourceLocation HUD_LAYER = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "weapons"); + + private ClientHooks() { + } + + /** During mod construction, on a client only. */ + public static void init() { + ClientConfig.load(Platform.get().configDir()); + ClientAccess.install(() -> Minecraft.getInstance().player, ClientPacks.INSTANCE); + } + + /** How a loader registers an entity renderer. */ + public interface EntityRenderers { + void register(EntityType type, EntityRendererProvider provider); + } + + public static void registerEntityRenderers(EntityRenderers renderers) { + renderers.register(ModEntities.HELICOPTER.get(), AircraftRenderer::new); + renderers.register(ModEntities.PLANE.get(), AircraftRenderer::new); + renderers.register(ModEntities.TANK.get(), AircraftRenderer::new); + renderers.register(ModEntities.VEHICLE.get(), AircraftRenderer::new); + renderers.register(ModEntities.BULLET.get(), ProjectileRenderer::new); + renderers.register(ModEntities.ROCKET.get(), ProjectileRenderer::new); + renderers.register(ModEntities.BOMB.get(), ProjectileRenderer::new); + renderers.register(ModEntities.AA_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.AS_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.AT_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.TV_MISSILE.get(), ProjectileRenderer::new); + renderers.register(ModEntities.TORPEDO.get(), ProjectileRenderer::new); + renderers.register(ModEntities.MARKER_ROCKET.get(), ProjectileRenderer::new); + renderers.register(ModEntities.DISPENSED_ITEM.get(), ProjectileRenderer::new); + renderers.register(ModEntities.FLARE.get(), FlareRenderer::new); + renderers.register(ModEntities.A10.get(), A10Renderer::new); + } + + public static List keyMappings() { + return KeyBindings.ALL; + } + + /** Client resource reload listeners, by the id Fabric wants for each. */ + public static Map reloadListeners() { + return Map.of(ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "meshes"), MeshCache.INSTANCE, + ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "hud_scripts"), HudScripts.INSTANCE); + } + + public static void clientTickEnd() { + CrewController.onClientTick(); + WeaponController.onClientTick(); + HudRenderer.tick(); + EngineSoundPlayer.tick(); + } + + /** Tilts the view with the hull and moves the eye into the cockpit or onto a TV missile. */ + public static void computeCameraAngles(CameraAngles angles) { + CrewController.onCameraAngles(angles); + } + + /** Sets the camera's rotation, roll included, the way Forge's {@code Camera#setRotation} does. */ + public interface CameraRotation { + void set(Camera camera, float yaw, float pitch, float roll); + } + + /** + * For loaders without Forge's camera angle event: called where Forge fires it, right after + * vanilla placed the camera for this frame. Starts from the camera's own angles and no roll, + * and only touches the rotation when something changed it. + */ + public static void computeCameraAngles(Camera camera, float partialTick, CameraRotation rotation) { + Angles angles = new Angles(camera, partialTick, camera.getYRot(), camera.getXRot()); + computeCameraAngles(angles); + if (angles.yaw != camera.getYRot() || angles.pitch != camera.getXRot() || angles.roll != 0.0F) { + rotation.set(camera, angles.yaw, angles.pitch, angles.roll); + } + } + + private static final class Angles implements CameraAngles { + private final Camera camera; + private final float partialTick; + private float yaw; + private float pitch; + private float roll; + + Angles(Camera camera, float partialTick, float yaw, float pitch) { + this.camera = camera; + this.partialTick = partialTick; + this.yaw = yaw; + this.pitch = pitch; + } + + @Override + public Camera camera() { + return camera; + } + + @Override + public float partialTick() { + return partialTick; + } + + @Override + public float yaw() { + return yaw; + } + + @Override + public float pitch() { + return pitch; + } + + @Override + public float roll() { + return roll; + } + + @Override + public void setYaw(float yaw) { + this.yaw = yaw; + } + + @Override + public void setPitch(float pitch) { + this.pitch = pitch; + } + + @Override + public void setRoll(float roll) { + this.roll = roll; + } + } + + /** Whether to skip drawing the first person hand this frame. */ + public static boolean hidesHand() { + return CrewController.hidesHand(); + } + + /** What happens to a press of the use key. */ + public enum UseKey { + /** Vanilla handles it. */ + PASS, + /** Taken, and the hand does not swing: it is a weapon trigger. */ + CONSUME, + /** Taken, and the hand swings as for a vanilla click: the player boards a vehicle. */ + CONSUME_AND_SWING + } + + public static UseKey onUseKey(InteractionHand hand) { + if (WeaponController.onUseItem()) { + return UseKey.CONSUME; + } + return VehiclePicker.onUseItem(hand) ? UseKey.CONSUME_AND_SWING : UseKey.PASS; + } + + /** The pick block key switches weapons in a seat that has any; true when it was taken. */ + public static boolean onPickBlockKey() { + return WeaponController.onPickBlock(); + } + + public static void beforeRenderLiving(LivingEntity entity, PoseStack pose, float partialTicks) { + RiderPose.beforeRender(entity, pose, partialTicks); + } + + public static void drawHud(GuiGraphics graphics, DeltaTracker delta) { + WeaponHud.draw(graphics, delta); + } + + /** Without the original zip there are no vehicles, so say so where the player will see it. */ + public static void loggedIn(LocalPlayer player) { + MCHeli.legacyContent().problem().ifPresent(player::sendSystemMessage); + } + + public static void loggedOut() { + ClientPacks.INSTANCE.loggedOut(); + } +} From 8f07b4fae6f6ebeca5c0ed5abcc71f9a597901de Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:33 +0800 Subject: [PATCH 182/314] Add ClientPacks to the shared 1.21.1 sources --- .../mcheli/client/ClientPacks.java | 325 ++++++++++++++++++ 1 file changed, 325 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java new file mode 100644 index 0000000..13f2bad --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/ClientPacks.java @@ -0,0 +1,325 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.ClientConfig; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.content.pack.PackException; +import com.jnhprojects.mcheli.entity.ClientAccess; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.PackDataPacket; +import com.jnhprojects.mcheli.network.PackListPacket; +import com.jnhprojects.mcheli.network.PackRequestPacket; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import com.jnhprojects.mcheli.platform.Platform; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +/** + * The client half of the content pack sync (the server half is {@code PackSync}). + * + *

When the server sends its pack list, every pack this client has neither in its own + * {@code mcheli/packs} nor in {@code mcheli/cache} is requested. Each arrives in chunks, is checked + * against its hash and kept as {@code mcheli/cache/.zip}. Once all are there, the content is + * rebuilt from the client's own import plus exactly the server's packs, in the server's order, so + * the vehicle a player flies (which their client simulates) has the server's definition. + * + *

In single player, and for the host of a LAN game, client and server share one registry, so + * only the client's view of the server's packs is refreshed. + * + *

The cache is capped by {@code packs.cacheSize} in the client config: after a download, the + * packs used longest ago go first. The current server's packs are always kept. + */ +final class ClientPacks implements ClientAccess.PackReceiver { + static final ClientPacks INSTANCE = new ClientPacks(); + + private static final Pattern CACHED = Pattern.compile("[0-9a-f]{64}\\.zip"); + + private final Path cache = Platform.get().gameDir().resolve(MCHeli.MOD_ID).resolve("cache"); + private List wanted = List.of(); + private final Map downloads = new LinkedHashMap<>(); + /** Packs this class opened for the current server; closed when the next list replaces them. */ + private List mounted = List.of(); + private int downloaded; + + private ClientPacks() { + } + + @Override + public void onPackList(PackListPacket packet) { + if (Minecraft.getInstance().hasSingleplayerServer()) { + useRoots(MCHeli.packs().active()); + ClientContentReload.refresh(); + return; + } + + wanted = packet.packs(); + downloads.clear(); + downloaded = 0; + long cap = ClientConfig.packCacheBytes(); + List request = new ArrayList<>(); + long bytes = 0; + for (PackListPacket.Entry entry : wanted) { + if (find(entry) != null) { + continue; + } + if (entry.size() > cap || entry.size() <= 0) { + say(Component.translatable("message.mcheli.pack.too_large", entry.id(), mb(entry.size()), cap >> 20)); + continue; + } + downloads.put(entry.hash(), new Download(entry)); + request.add(entry.hash()); + bytes += entry.size(); + } + + if (request.isEmpty()) { + apply(); + } else { + say(Component.translatable("message.mcheli.pack.downloading", request.size(), mb(bytes))); + ModNetwork.sendToServer(new PackRequestPacket(request)); + } + } + + @Override + public void onPackData(PackDataPacket packet) { + Download download = downloads.get(packet.hash()); + if (download == null) { + return; + } + if (!download.accept(packet)) { + fail(download, "the server sent a chunk out of order"); + return; + } + if (!download.complete()) { + return; + } + downloads.remove(packet.hash()); + try { + store(download); + downloaded++; + } catch (IOException | PackException e) { + fail(download, e.getMessage()); + return; + } + if (downloads.isEmpty()) { + apply(); + } + } + + /** Leaving a server: its packs are no longer needed. */ + void loggedOut() { + downloads.clear(); + wanted = List.of(); + RuntimeAssets.setRoots(List.of()); + close(mounted); + mounted = List.of(); + } + + private void fail(Download download, String reason) { + downloads.remove(download.entry.hash()); + MCHeli.LOGGER.error("Content pack {} could not be downloaded: {}", download.entry.id(), reason); + say(Component.translatable("message.mcheli.pack.failed", download.entry.id(), reason)); + if (downloads.isEmpty()) { + apply(); + } + } + + /** Writes a finished download to the cache once its content matches the hash it was listed with. */ + private void store(Download download) throws IOException, PackException { + String hash = download.entry.hash(); + Files.createDirectories(cache); + Path part = cache.resolve(hash + ".download.zip"); + Files.write(part, download.data); + try (ContentPack pack = ContentPack.open(part)) { + if (!pack.hash().equals(hash)) { + throw new PackException("its files do not match the hash the server listed"); + } + } catch (IOException | PackException e) { + Files.deleteIfExists(part); + throw e; + } + Files.move(part, cache.resolve(hash + ".zip"), StandardCopyOption.REPLACE_EXISTING); + } + + /** Rebuilds the content from the import and the server's packs that are now on disk. */ + private void apply() { + List opened = new ArrayList<>(); + List sources = new ArrayList<>(); + sources.add(new ContentSource(ContentSource.ORIGINAL, MCHeli.legacyContent().assets())); + for (PackListPacket.Entry entry : wanted) { + Path file = find(entry); + if (file == null) { + continue; + } + try { + ContentPack pack = ContentPack.open(file); + if (!pack.hash().equals(entry.hash())) { + pack.close(); + MCHeli.LOGGER.warn("Content pack {} changed on disk and no longer matches the server's", file); + continue; + } + opened.add(pack); + sources.add(pack.contentSource()); + if (file.startsWith(cache)) { + Files.setLastModifiedTime(file, FileTime.from(Instant.now())); + } + } catch (IOException | PackException e) { + MCHeli.LOGGER.error("Could not open content pack {}", file, e); + } + } + + ContentRegistry registry = ContentRegistry.load(sources); + MCHeli.content().replaceWith(registry); + List old = mounted; + mounted = List.copyOf(opened); + useRoots(mounted); + close(old); + ClientContentReload.refresh(); + trimCache(); + if (downloaded > 0) { + say(Component.translatable("message.mcheli.pack.ready", downloaded)); + downloaded = 0; + } + } + + /** The pack on disk with the entry's content: one of the player's own packs, or a cached download. */ + private @Nullable Path find(PackListPacket.Entry entry) { + for (ContentPack own : MCHeli.packs().active()) { + if (own.hash().equals(entry.hash())) { + return own.source(); + } + } + Path cached = cache.resolve(entry.hash() + ".zip"); + return Files.isRegularFile(cached) ? cached : null; + } + + /** Deletes the cached packs used longest ago until the cache fits its cap. */ + private void trimCache() { + Set keep = new HashSet<>(); + for (PackListPacket.Entry entry : wanted) { + keep.add(entry.hash() + ".zip"); + } + List files; + try (Stream list = Files.list(cache)) { + files = list.filter(p -> CACHED.matcher(p.getFileName().toString()).matches()) + .sorted(Comparator.comparing(ClientPacks::lastUsed)) + .toList(); + } catch (IOException e) { + return; + } + long total = 0; + for (Path file : files) { + total += size(file); + } + long cap = ClientConfig.packCacheBytes(); + for (Path file : files) { + if (total <= cap) { + break; + } + if (keep.contains(file.getFileName().toString())) { + continue; + } + try { + long size = size(file); + Files.delete(file); + total -= size; + MCHeli.LOGGER.info("Removed {} from the content pack cache", file.getFileName()); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not remove {} from the content pack cache", file, e); + } + } + } + + private static FileTime lastUsed(Path file) { + try { + return Files.getLastModifiedTime(file); + } catch (IOException e) { + return FileTime.fromMillis(0); + } + } + + private static long size(Path file) { + try { + return Files.size(file); + } catch (IOException e) { + return 0; + } + } + + /** The pack loaded last overrides the others, so it is searched first. */ + private static void useRoots(List packs) { + List roots = new ArrayList<>(); + for (ContentPack pack : packs) { + roots.addFirst(pack.assets()); + } + RuntimeAssets.setRoots(roots); + } + + private static void close(List packs) { + for (ContentPack pack : packs) { + try { + pack.close(); + } catch (IOException e) { + MCHeli.LOGGER.warn("Could not close content pack {}", pack, e); + } + } + } + + private static void say(Component message) { + Minecraft mc = Minecraft.getInstance(); + if (mc.player != null) { + mc.player.sendSystemMessage(message); + } else { + MCHeli.LOGGER.info(message.getString()); + } + } + + private static String mb(long bytes) { + return "%.1f".formatted(bytes / 1048576.0); + } + + /** One pack arriving from the server. */ + private static final class Download { + private final PackListPacket.Entry entry; + private byte @Nullable [] data; + private int received; + + Download(PackListPacket.Entry entry) { + this.entry = entry; + } + + boolean accept(PackDataPacket packet) { + if (packet.total() != entry.size() || packet.offset() != received + || received + packet.data().length > entry.size()) { + return false; + } + if (data == null) { + data = new byte[entry.size()]; + } + System.arraycopy(packet.data(), 0, data, received, packet.data().length); + received += packet.data().length; + return true; + } + + boolean complete() { + return data != null && received == data.length; + } + } +} From 7727309c7424600a5a1973b3fec90062923a4b42 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:33 +0800 Subject: [PATCH 183/314] Read the movement keys from 1.21.1's player input --- .../mcheli/client/CrewController.java | 444 ++++++++++++++++++ 1 file changed, 444 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/CrewController.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/CrewController.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/CrewController.java new file mode 100644 index 0000000..18c514c --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/CrewController.java @@ -0,0 +1,444 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.hud.HudRenderer; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.CameraPosition; +import com.jnhprojects.mcheli.content.aircraft.SeatDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import com.jnhprojects.mcheli.entity.GroundVehicleEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.network.CrewActionPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.PilotStatePacket; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.Input; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.util.Mth; +import net.minecraft.world.phys.Vec3; +import org.joml.Vector3f; +import org.jspecify.annotations.Nullable; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; + +/** + * Turns local player input into vehicle control, for whichever seat the player occupies. + * + *

Keys are sampled once per client tick. The mouse is handled every frame, the way the + * original did it: for helicopters and planes, vanilla first turns the player's view, then the + * change is fed to the aircraft and the view is snapped back onto the aircraft's nose. Everyone + * else looks around freely, within the limits of their seat, and turrets follow their view. + * + *

Gunner seats and camera positions move the first person eye to the point the definition + * gives, which is how the original's camera entity behaved. Every other seat keeps its eye above + * the seat along the hull's up axis, so it tilts with the vehicle. + */ +final class CrewController { + /** Two presses of jump within this many ticks eject the player. */ + private static final int EJECT_WINDOW = 10; + + private static boolean jumpWasDown; + private static int lastJumpPress = -100; + private static int ticks; + private static boolean tracking; + private static float lastYaw; + private static float lastPitch; + + private static @Nullable AircraftEntity viewVehicle; + private static int viewSeat = -1; + private static int cameraIndex; + private static boolean gunnerSight; + + private static @Nullable MethodHandle setCameraPosition; + private static boolean cameraLookupFailed; + + private CrewController() { + } + + /** + * Like the original, the first person hand stays hidden in a vehicle: always for the pilot, + * and for anyone else who looks through a camera or has a weapon to aim. + */ + static boolean hidesHand() { + LocalPlayer player = Minecraft.getInstance().player; + AircraftEntity vehicle = vehicleOf(player); + if (vehicle == null) { + return false; + } + AircraftDefinition def = vehicle.definition(); + int seat = vehicle.seatOf(player); + if (seat < 0 || seat >= def.seatCount()) { + return false; + } + VehicleWeapons weapons = vehicle.weapons(); + boolean armed = weapons != null && weapons.selected(seat) >= 0; + return seat == 0 || armed || tvCamera(vehicle, seat) != null || eyePosition(vehicle, def, seat) != null; + } + + private static @Nullable AircraftEntity vehicleOf(@Nullable LocalPlayer player) { + return player != null && player.getVehicle() instanceof AircraftEntity vehicle && vehicle.definition() != null ? vehicle : null; + } + + static void onClientTick() { + ticks++; + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + AircraftEntity vehicle = vehicleOf(player); + if (vehicle == null) { + jumpWasDown = false; + tracking = false; + viewVehicle = null; + viewSeat = -1; + return; + } + int seat = vehicle.seatOf(player); + if (vehicle != viewVehicle || seat != viewSeat) { + // New vehicle or seat: start from the default view. + viewVehicle = vehicle; + viewSeat = seat; + cameraIndex = 0; + gunnerSight = false; + tracking = false; + } + + Input keys = player.input; + boolean jumpPressed = keys.jumping && !jumpWasDown; + jumpWasDown = keys.jumping; + boolean pilot = seat == 0 && vehicle.getControllingPassenger() == player; + + if (pilot) { + AircraftEntity.ControlInput input = vehicle.input(); + input.throttleUp = keys.up; + input.throttleDown = keys.down; + input.left = keys.left; + input.right = keys.right; + input.freeLook = isFreeLook(vehicle); + input.brake = vehicle instanceof TankEntity && keys.jumping; + if (input.brake) { + // The hand brake also stops pivot turns once the vehicle has come to rest. + input.throttleUp = false; + input.throttleDown = false; + double dx = vehicle.getX() - vehicle.xo; + double dz = vehicle.getZ() - vehicle.zo; + if (vehicle.visibleThrottle() <= 0.03F && dx * dx + dz * dz < 0.01) { + input.left = false; + input.right = false; + } + } + if (jumpPressed && vehicle instanceof HelicopterEntity heli) { + heli.toggleHovering(); + } + ModNetwork.sendToServer(new PilotStatePacket(vehicle.getId(), vehicle.visibleThrottle(), vehicle.reverse(), + vehicle.roll(), vehicle.isHoveringMode(), input.write())); + } + + // Eject with a double tap, except for helicopter pilots whose jump key toggles hover. + if (jumpPressed && vehicle.canEject(player) && !(pilot && vehicle instanceof HelicopterEntity)) { + if (ticks - lastJumpPress <= EJECT_WINDOW) { + send(vehicle, CrewActionPacket.Action.EJECT); + lastJumpPress = -100; + } else { + lastJumpPress = ticks; + } + } + + while (KeyBindings.SWITCH_SEAT.consumeClick()) { + send(vehicle, CrewActionPacket.Action.SWITCH_SEAT); + } + while (KeyBindings.SWITCH_CAMERA.consumeClick()) { + switchCamera(vehicle, seat); + } + while (KeyBindings.LANDING_GEAR.consumeClick()) { + if (pilot) { + send(vehicle, CrewActionPacket.Action.LANDING_GEAR); + } + } + while (KeyBindings.FOLD.consumeClick()) { + if (pilot) { + send(vehicle, CrewActionPacket.Action.FOLD); + } + } + while (KeyBindings.VTOL.consumeClick()) { + if (pilot) { + send(vehicle, CrewActionPacket.Action.VTOL); + } + } + while (KeyBindings.RACK_LOAD.consumeClick()) { + if (pilot) { + send(vehicle, CrewActionPacket.Action.RACK_LOAD); + } + } + while (KeyBindings.RACK_UNLOAD.consumeClick()) { + if (pilot) { + send(vehicle, CrewActionPacket.Action.RACK_UNLOAD); + } + } + } + + private static void send(AircraftEntity vehicle, CrewActionPacket.Action action) { + ModNetwork.sendToServer(new CrewActionPacket(vehicle.getId(), action)); + } + + /** Pilot cycles the camera positions; gunners that may leave their sight toggle it. */ + private static void switchCamera(AircraftEntity vehicle, int seat) { + AircraftDefinition def = vehicle.definition(); + if (def == null || seat < 0 || seat >= def.seatCount()) { + return; + } + SeatDefinition s = def.seats.get(seat); + if (seat == 0) { + switchPilotView(vehicle, def); + } else if (s.gunner() && s.switchGunner()) { + gunnerSight = !gunnerSight; + } + } + + /** + * The pilot's switch mode key, after {@code KeySwitchMode}: leave gunner mode, otherwise step + * through the extra camera positions, otherwise enter gunner mode where the vehicle allows it, + * otherwise go to the first extra camera. Unlike the original, leaving gunner mode never + * depends on the plane still meeting the conditions for entering it. + */ + private static void switchPilotView(AircraftEntity vehicle, AircraftDefinition def) { + int cameras = def.cameraPositions.size(); + if (vehicle.isGunnerMode()) { + send(vehicle, CrewActionPacket.Action.GUNNER_MODE); + cameraIndex = 0; + } else if (cameraIndex > 0) { + cameraIndex = (cameraIndex + 1) % cameras; + } else if (vehicle.canSwitchGunnerMode()) { + send(vehicle, CrewActionPacket.Action.GUNNER_MODE); + } else if (cameras >= 2) { + cameraIndex = 1; + } + } + + /** + * Free look for the pilot. Helicopters and planes look around while the key is held; tanks + * start from their {@code DefaultFreelook} setting and the key swaps it while held. + */ + private static boolean isFreeLook(AircraftEntity vehicle) { + boolean held = KeyBindings.FREE_LOOK.isDown(); + if (vehicle instanceof TankEntity) { + AircraftDefinition def = vehicle.definition(); + return def != null && def.defaultFreelook != held; + } + return held; + } + + static void onCameraAngles(CameraAngles event) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + AircraftEntity vehicle = vehicleOf(player); + if (vehicle == null || mc.isPaused()) { + return; + } + AircraftDefinition def = vehicle.definition(); + int seat = vehicle.seatOf(player); + if (seat < 0 || seat >= def.seatCount()) { + return; + } + float partialTicks = event.partialTick(); + boolean firstPerson = mc.options.getCameraType().isFirstPerson(); + + if (seat == 0 && vehicle.steersWithMouse() && !vehicle.isGunnerMode() && vehicle.getControllingPassenger() == player) { + steerWithMouse(vehicle, player, event, partialTicks, firstPerson); + } else { + tracking = false; + constrainView(vehicle, def, def.seats.get(seat), seat, player); + } + + TvMissileEntity missile = tvCamera(vehicle, seat); + if (!(firstPerson && missile != null)) { + // Every seat of every kind tilts with the hull, measured along the way the rider looks. + event.setRoll(AircraftRotation.cameraRoll(vehicle.getYRot(partialTicks), vehicle.getXRot(partialTicks), + vehicle.roll(partialTicks), event.yaw(), event.pitch())); + } + if (firstPerson && missile != null) { + placeCamera(event.camera(), missile.getPosition(partialTicks)); + } else if (firstPerson) { + Vector3 eye = eyePosition(vehicle, def, seat); + moveCamera(event.camera(), vehicle, def, eye != null ? eye : seatEye(vehicle, def, seat, player), seat, partialTicks); + } + } + + /** + * The normal eye, in hull space. Vanilla puts it straight above the rider in world space, + * which leaves the head outside the cockpit once the hull banks or pitches; measured along the + * hull's own up axis it stays where the rider model's head is drawn. + */ + private static Vector3 seatEye(AircraftEntity vehicle, AircraftDefinition def, int seat, LocalPlayer player) { + Vector3 pos = def.seats.get(seat).position(); + double up = player.getEyeHeight() - player.getVehicleAttachmentPoint(vehicle).y; + return new Vector3(pos.x(), pos.y() + up, pos.z()); + } + + /** The phase one pilot model: mouse movement becomes aircraft rotation, the view stays on the nose. */ + private static void steerWithMouse(AircraftEntity vehicle, LocalPlayer player, CameraAngles event, + float partialTicks, boolean firstPerson) { + boolean freeLook = KeyBindings.FREE_LOOK.isDown(); + boolean ownPitch = vehicle.isHovering(); + if (!freeLook) { + if (tracking) { + float dYaw = Mth.wrapDegrees(player.getYRot() - lastYaw); + float dPitch = player.getXRot() - lastPitch; + vehicle.applyPilotLook(dYaw, dPitch); + HudRenderer.onPilotLook(dYaw, dPitch); + } + lockView(player, vehicle.getYRot(), vehicle.yRotO, ownPitch ? null : vehicle.getXRot(), vehicle.xRotO); + lastYaw = player.getYRot(); + lastPitch = player.getXRot(); + tracking = true; + } else { + // Re-acquire the nose smoothly once free look is released. + tracking = false; + } + if (firstPerson && !freeLook) { + event.setYaw(vehicle.getYRot(partialTicks)); + if (!ownPitch) { + event.setPitch(vehicle.getXRot(partialTicks)); + } + } + } + + /** Seat limits for everyone who does not fly with the mouse. */ + private static void constrainView(AircraftEntity vehicle, AircraftDefinition def, SeatDefinition seat, int index, LocalPlayer player) { + boolean freeLookHeld = KeyBindings.FREE_LOOK.isDown(); + if (seat.fixedRotation() && !freeLookHeld) { + lockView(player, vehicle.getYRot() + seat.fixedYaw(), vehicle.yRotO + seat.fixedYaw(), + vehicle.getXRot() + seat.fixedPitch(), vehicle.xRotO + seat.fixedPitch()); + return; + } + boolean pilot = index == 0; + if (pilot && vehicle instanceof TankEntity && !isFreeLook(vehicle)) { + lockView(player, vehicle.getYRot(), vehicle.yRotO, vehicle.getXRot(), vehicle.xRotO); + return; + } + float min; + float max; + if (pilot && (vehicle instanceof TankEntity || vehicle instanceof GroundVehicleEntity)) { + min = def.minRotationPitch; + max = def.maxRotationPitch; + } else if (seat.gunner() || (pilot && vehicle.isGunnerMode())) { + min = seat.minPitch(); + max = seat.maxPitch(); + } else { + return; + } + // Limits are relative to the hull's tilt along the view direction. + double rel = Math.toRadians(Mth.wrapDegrees(player.getYRot() - vehicle.getYRot())); + float tilt = (float) (vehicle.getXRot() * Math.cos(rel) + vehicle.roll() * Math.sin(rel)); + float pitch = Mth.clamp(player.getXRot(), tilt + min, tilt + max); + pitch = Mth.clamp(pitch, -90.0F, 90.0F); + if (pitch != player.getXRot()) { + player.setXRot(pitch); + player.xRotO = pitch; + } + } + + private static void lockView(LocalPlayer player, float yaw, float yawO, @Nullable Float pitch, float pitchO) { + player.setYRot(yaw); + player.yRotO = yawO; + player.setYHeadRot(yaw); + player.setYBodyRot(yaw); + if (pitch != null) { + player.setXRot(pitch); + player.xRotO = pitchO; + } + } + + /** + * Where the first person eye goes, in hull space, or {@code null} for the normal eye. Mirrors + * the original's choice between the pilot's camera positions and a gunner seat's sight. + */ + private static @Nullable Vector3 eyePosition(AircraftEntity vehicle, AircraftDefinition def, int seat) { + SeatDefinition s = def.seats.get(seat); + boolean sight = s.gunner() && (!s.switchGunner() || gunnerSight); + CameraPosition camera = null; + if (seat == 0) { + if (cameraIndex > 0 && cameraIndex < def.cameraPositions.size()) { + camera = def.cameraPositions.get(cameraIndex); + } else if (def.uav || def.alwaysCameraView || sight || vehicle.isGunnerMode() || vehicle instanceof GroundVehicleEntity) { + camera = def.cameraPositions.getFirst(); + } + } else if (sight) { + camera = s.camera() != null && s.camera() != CameraPosition.DEFAULT ? s.camera() : def.cameraPositions.getFirst(); + } + // A camera at the origin means the file never set one; the normal eye is better there. + if (camera == null || camera.position().equals(Vector3.ZERO)) { + return null; + } + return camera.position(); + } + + private static void moveCamera(Camera camera, AircraftEntity vehicle, AircraftDefinition def, Vector3 eye, int seat, float partialTicks) { + MethodHandle setter = cameraSetter(); + if (setter == null) { + return; + } + SeatDefinition s = def.seats.get(seat); + Vector3 local = s.rotatingSeat() ? vehicle.onTurret(def, eye, partialTicks) : eye; + Vector3f offset = AircraftRotation.toWorld(vehicle.getYRot(partialTicks), vehicle.getXRot(partialTicks), vehicle.roll(partialTicks), + local.x(), local.y(), local.z()); + placeCamera(camera, new Vec3(Mth.lerp(partialTicks, vehicle.xo, vehicle.getX()) + offset.x, + Mth.lerp(partialTicks, vehicle.yo, vehicle.getY()) + offset.y, + Mth.lerp(partialTicks, vehicle.zo, vehicle.getZ()) + offset.z)); + } + + /** + * The TV missile this seat is flying, whose nose camera replaces the eye. Like the original, + * only while the seat has a TV missile selected in its guided mode. + */ + static @Nullable TvMissileEntity tvCamera(AircraftEntity vehicle, int seat) { + TvMissileEntity missile = vehicle.tvMissile(); + VehicleWeapons weapons = vehicle.weapons(); + WeaponSet set = weapons != null ? weapons.current(seat) : null; + if (missile == null || set == null || set.definition().type != WeaponType.TV_MISSILE || set.mode() != 0) { + return null; + } + return missile; + } + + private static void placeCamera(Camera camera, Vec3 at) { + MethodHandle setter = cameraSetter(); + if (setter == null) { + return; + } + try { + setter.invoke(camera, at); + } catch (Throwable t) { + MCHeli.LOGGER.warn("Could not move the camera; the first person eye stays where vanilla puts it", t); + setCameraPosition = null; + cameraLookupFailed = true; + } + } + + private static Method accessible(Method method) { + method.setAccessible(true); + return method; + } + + /** {@code Camera.setPosition} is protected. Looked up once; on failure the feature turns itself off. */ + private static @Nullable MethodHandle cameraSetter() { + if (setCameraPosition == null && !cameraLookupFailed) { + try { + setCameraPosition = MethodHandles.lookup().unreflect( + accessible(Camera.class.getDeclaredMethod("setPosition", Vec3.class))); + } catch (Exception e) { + MCHeli.LOGGER.warn("Camera.setPosition is not accessible; the first person eye stays where vanilla puts it", e); + cameraLookupFailed = true; + } + } + return setCameraPosition; + } +} From c73cca8988cf757b14f5aff7a35e2a1fc7474e3d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:33 +0800 Subject: [PATCH 184/314] Name the key category with a translation key on 1.21.1 --- .../mcheli/client/KeyBindings.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java new file mode 100644 index 0000000..863771b --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/KeyBindings.java @@ -0,0 +1,43 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.MCHeli; +import com.mojang.blaze3d.platform.InputConstants; +import net.minecraft.client.KeyMapping; + +import java.util.List; + +/** + * Mod key bindings. Throttle, steering, hover, brake and eject reuse the vanilla movement and jump + * keys (W/S, A/D and Space by default), which matches the original defaults. Firing uses the + * vanilla use key and the pick block button switches weapons, the right and middle mouse buttons + * of the original. The rest keep the original's letters where vanilla leaves them free. + */ +public final class KeyBindings { + public static final String CATEGORY = "key.category." + MCHeli.MOD_ID + ".main"; + + /** Hold to look around without steering. The original used Left Ctrl, which now clashes with sprint. */ + public static final KeyMapping FREE_LOOK = new KeyMapping("key.mcheli.free_look", InputConstants.KEY_LALT, CATEGORY); + /** Cycles the pilot's camera positions, or toggles the gunner sight in seats that allow it. */ + public static final KeyMapping SWITCH_CAMERA = new KeyMapping("key.mcheli.switch_camera", InputConstants.KEY_H, CATEGORY); + public static final KeyMapping LANDING_GEAR = new KeyMapping("key.mcheli.landing_gear", InputConstants.KEY_B, CATEGORY); + /** Folds hatches and wings, sweeps variable geometry wings. */ + public static final KeyMapping FOLD = new KeyMapping("key.mcheli.fold", InputConstants.KEY_Z, CATEGORY); + /** The original used F, which vanilla now uses to swap hands. */ + public static final KeyMapping VTOL = new KeyMapping("key.mcheli.vtol", InputConstants.KEY_V, CATEGORY); + public static final KeyMapping SWITCH_SEAT = new KeyMapping("key.mcheli.switch_seat", InputConstants.KEY_R, CATEGORY); + public static final KeyMapping RACK_LOAD = new KeyMapping("key.mcheli.rack_load", InputConstants.KEY_J, CATEGORY); + public static final KeyMapping RACK_UNLOAD = new KeyMapping("key.mcheli.rack_unload", InputConstants.KEY_U, CATEGORY); + /** The second weapon switch key of the original; the first is the vanilla pick block button. */ + public static final KeyMapping SWITCH_WEAPON = new KeyMapping("key.mcheli.switch_weapon", InputConstants.KEY_G, CATEGORY); + public static final KeyMapping WEAPON_MODE = new KeyMapping("key.mcheli.weapon_mode", InputConstants.KEY_X, CATEGORY); + /** The original used V, which VTOL took over. */ + public static final KeyMapping FLARE = new KeyMapping("key.mcheli.flare", InputConstants.KEY_C, CATEGORY); + /** The original resupplied from the vehicle's inventory screen, which does not exist yet. */ + public static final KeyMapping RESUPPLY = new KeyMapping("key.mcheli.resupply", InputConstants.KEY_N, CATEGORY); + + public static final List ALL = List.of(FREE_LOOK, SWITCH_CAMERA, LANDING_GEAR, FOLD, VTOL, SWITCH_SEAT, RACK_LOAD, RACK_UNLOAD, + SWITCH_WEAPON, WEAPON_MODE, FLARE, RESUPPLY); + + private KeyBindings() { + } +} From 5c19a6d7366448c19446cb47984fe796f13dc954 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:34 +0800 Subject: [PATCH 185/314] Upload runtime textures the 1.21.1 way --- .../mcheli/client/RuntimeAssets.java | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java new file mode 100644 index 0000000..43dcb6d --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/RuntimeAssets.java @@ -0,0 +1,141 @@ +package com.jnhprojects.mcheli.client; + +import com.mojang.blaze3d.platform.NativeImage; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Vehicle textures, models and sounds that can change while the game runs, without a resource + * reload (F3 + T), which would stall the game for every pack loaded and is not available on a + * server at all. + * + *

Lookups try the resource manager first, so resource packs keep the last word, then the + * runtime roots: folders laid out like {@code assets/mcheli} that content can add at any time. + * Content packs and, later, the workshop register their folders here. + *

    + *
  • Textures found only in a runtime root are uploaded as a {@link DynamicTexture} the first + * time a vehicle draws them. {@link #refreshTextures} uploads every vehicle texture again + * from disk, which is how a content reload shows edited textures.
  • + *
  • Models are read through {@link #open} by the mesh cache, which a reload clears.
  • + *
  • Sounds found only in a runtime root are streamed from the file + * ({@link #soundFile}); see {@code RuntimeSoundInstance}.
  • + *
+ */ +public final class RuntimeAssets { + private static final Logger LOGGER = LoggerFactory.getLogger(RuntimeAssets.class); + + /** Searched in order, so the pack loaded last comes first. Replaced as a whole. */ + private static volatile List roots = List.of(); + /** Every vehicle texture asked for, and whether we uploaded it ourselves. */ + private static final Map TEXTURES = new ConcurrentHashMap<>(); + + private RuntimeAssets() { + } + + /** + * Sets the folders laid out like {@code assets/mcheli} that are searched after the resource + * packs, the first one first. Content packs set theirs whenever the packs in use change. + */ + public static void setRoots(List folders) { + roots = List.copyOf(folders); + } + + /** Opens an {@code mcheli} asset from the resource packs, or else from a runtime root. */ + public static @Nullable InputStream open(ResourceLocation id) throws IOException { + Optional resource = Minecraft.getInstance().getResourceManager().getResource(id); + if (resource.isPresent()) { + return resource.get().open(); + } + Path file = find(id.getPath()); + return file != null ? Files.newInputStream(file) : null; + } + + /** + * A texture ready to bind. The first request for a texture no resource pack has, but a runtime + * root does, uploads it. Cheap after the first call, so renderers can call it every frame. + */ + public static ResourceLocation texture(ResourceLocation id) { + TEXTURES.computeIfAbsent(id, key -> { + if (Minecraft.getInstance().getResourceManager().getResource(key).isPresent()) { + return false; + } + return upload(key); + }); + return id; + } + + /** + * Uploads every texture vehicles have drawn so far again, read fresh from disk. Resource pack + * textures are replaced too, until the next resource reload loads them the normal way. One that + * no longer exists anywhere, because its pack is gone, is released. + */ + public static void refreshTextures() { + for (Map.Entry entry : TEXTURES.entrySet()) { + boolean ours = upload(entry.getKey()); + if (!ours && entry.getValue()) { + Minecraft.getInstance().getTextureManager().release(entry.getKey()); + } + entry.setValue(ours); + } + } + + private static boolean upload(ResourceLocation id) { + try (InputStream in = open(id)) { + if (in == null) { + return false; + } + NativeImage image = NativeImage.read(in); + Minecraft.getInstance().getTextureManager().register(id, new DynamicTexture(image)); + return true; + } catch (IOException e) { + LOGGER.warn("Could not load texture {}", id, e); + return false; + } + } + + /** + * Drops the textures we uploaded, so the texture manager loads them again the normal way. + * Called on resource reload. + */ + public static void clearTextures() { + TEXTURES.forEach((id, ours) -> { + if (ours) { + Minecraft.getInstance().getTextureManager().release(id); + } + }); + TEXTURES.clear(); + } + + /** + * The OGG file for a sound event of ours that no resource pack defines, such as {@code heli} + * from {@code sounds/heli.ogg} in a runtime root. Numbered variants ({@code heli1.ogg}) count. + */ + public static @Nullable Path soundFile(String event) { + Path file = find("sounds/" + event + ".ogg"); + return file != null ? file : find("sounds/" + event + "1.ogg"); + } + + private static @Nullable Path find(String path) { + for (Path root : roots) { + Path file = root.resolve(path).normalize(); + if (file.startsWith(root) && Files.isRegularFile(file)) { + return file; + } + } + return null; + } +} From ed3e4c42f63c3d74ad843f8789c2187aa2ee3bab Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:34 +0800 Subject: [PATCH 186/314] Add VehiclePicker to the shared 1.21.1 sources --- .../mcheli/client/VehiclePicker.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java new file mode 100644 index 0000000..9507412 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/VehiclePicker.java @@ -0,0 +1,68 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.BoardVehiclePacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.EntityHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.Optional; + +/** + * Right-clicking anywhere on a vehicle's hull boards it. + * + *

Vanilla only picks an entity by its main hit box, which for most MCHeli vehicles is the + * default two by 0.7 block box at their feet; the hull's real shape lives in its extra + * {@code BoundingBox} lines. In the original those boxes were clickable too. Inflating the main box + * instead would change how the vehicle collides with terrain, so the use key looks for them here. + */ +final class VehiclePicker { + private VehiclePicker() { + } + + static boolean onUseItem(InteractionHand hand) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + if (player == null || hand != InteractionHand.MAIN_HAND || player.getVehicle() instanceof AircraftEntity) { + return false; + } + // Vanilla already handles a click on the main box. + if (mc.hitResult instanceof EntityHitResult hit && hit.getEntity() instanceof AircraftEntity) { + return false; + } + Hit hit = pick(player, mc.hitResult); + if (hit == null) { + return false; + } + ModNetwork.sendToServer(new BoardVehiclePacket(hit.vehicle.getId(), hit.location)); + return true; + } + + private record Hit(AircraftEntity vehicle, Vec3 location) { + } + + /** The nearest vehicle hull along the view, closer than the block or entity vanilla picked. */ + private static @Nullable Hit pick(Player player, @Nullable HitResult vanilla) { + double range = player.entityInteractionRange(); + Vec3 eye = player.getEyePosition(); + Vec3 to = eye.add(player.getViewVector(1.0F).scale(range)); + double limit = vanilla != null && vanilla.getType() != HitResult.Type.MISS ? eye.distanceToSqr(vanilla.getLocation()) : range * range; + Hit best = null; + for (AircraftEntity vehicle : player.level().getEntitiesOfClass(AircraftEntity.class, new AABB(eye, to).inflate(16.0))) { + Optional at = vehicle.clipHitBoxes(eye, to, 0.2); + vehicle.consumeHitBoxFactor(); + if (at.isPresent() && eye.distanceToSqr(at.get()) < limit) { + limit = eye.distanceToSqr(at.get()); + best = new Hit(vehicle, at.get()); + } + } + return best; + } +} From 4ebfc49a549423614e74732c5b5c189f1d3c3a45 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:35 +0800 Subject: [PATCH 187/314] Read the camera and screen the 1.21.1 way --- .../mcheli/client/WeaponController.java | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java new file mode 100644 index 0000000..f5e6111 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/WeaponController.java @@ -0,0 +1,190 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.CrewActionPacket; +import com.jnhprojects.mcheli.network.FireWeaponPacket; +import com.jnhprojects.mcheli.network.LockWarningPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.weapon.Guidance; +import com.jnhprojects.mcheli.weapon.Shot; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * The crew member's trigger: fires, switches weapons and modes, drops flares and resupplies. + * + *

Like the original, the client fires first. Its copy of the weapon decides whether the shot + * can happen (ready, loaded, locked on, a target in sight) and works out the option values; only + * then is the server asked, which fires the real projectile. + */ +final class WeaponController { + private WeaponController() { + } + + private record Seat(AircraftEntity vehicle, VehicleWeapons weapons, int seat) { + } + + private static @Nullable Seat seat(@Nullable LocalPlayer player) { + if (player == null || !(player.getVehicle() instanceof AircraftEntity vehicle) || vehicle.definition() == null) { + return null; + } + VehicleWeapons weapons = vehicle.weapons(); + int seat = vehicle.seatOf(player); + if (weapons == null || seat < 0 || seat >= vehicle.definition().seatCount()) { + return null; + } + return new Seat(vehicle, weapons, seat); + } + + /** The use key is the trigger while the seat has a weapon, so it must not also use items. */ + static boolean onUseItem() { + Seat s = seat(Minecraft.getInstance().player); + return s != null && s.weapons.selected(s.seat) >= 0; + } + + static boolean onPickBlock() { + Seat s = seat(Minecraft.getInstance().player); + if (s == null || s.weapons.sets().isEmpty()) { + return false; + } + send(s.vehicle, Minecraft.getInstance().player != null && Minecraft.getInstance().player.isShiftKeyDown() + ? CrewActionPacket.Action.PREVIOUS_WEAPON : CrewActionPacket.Action.NEXT_WEAPON); + click(); + return true; + } + + static void onClientTick() { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + Seat s = seat(player); + if (s == null) { + return; + } + while (KeyBindings.SWITCH_WEAPON.consumeClick()) { + send(s.vehicle, player.isShiftKeyDown() ? CrewActionPacket.Action.PREVIOUS_WEAPON : CrewActionPacket.Action.NEXT_WEAPON); + click(); + } + while (KeyBindings.WEAPON_MODE.consumeClick()) { + WeaponSet set = s.weapons.current(s.seat); + if (set != null && set.switchMode()) { + send(s.vehicle, CrewActionPacket.Action.WEAPON_MODE); + click(); + } + } + while (KeyBindings.FLARE.consumeClick()) { + if (s.seat <= 1 && s.weapons.flares.use()) { + send(s.vehicle, CrewActionPacket.Action.FLARE); + click(); + } + } + while (KeyBindings.RESUPPLY.consumeClick()) { + send(s.vehicle, CrewActionPacket.Action.RESUPPLY); + } + + if (mc.screen == null && mc.options.keyUse.isDown() && !s.vehicle.isDestroyed()) { + fire(s, player); + } + } + + private static void fire(Seat s, LocalPlayer player) { + int id = s.weapons.selected(s.seat); + if (id < 0 || s.weapons.operatorSeat(id) != s.seat) { + return; + } + Shot shot = new Shot(s.vehicle, player); + boolean fired = s.weapons.use(player, s.seat, shot, (set, mount, sh) -> clientShot(s, id, set, sh, player)); + if (fired) { + ModNetwork.sendToServer(new FireWeaponPacket(s.vehicle.getId(), id, shot.option1, shot.option2)); + } + } + + /** The client half of each weapon's {@code shot}: can it fire, and with which options. */ + private static boolean clientShot(Seat s, int id, WeaponSet set, Shot shot, LocalPlayer player) { + WeaponDefinition w = set.definition(); + return switch (w.type) { + case MACHINE_GUN_2, ROCKET, MARKER_ROCKET -> { + shot.option1 = set.mode(); + yield true; + } + case AA_MISSILE, AT_MISSILE -> { + Guidance g = s.weapons.guidance(id); + if (g == null) { + yield false; + } + boolean locked = g.lock(player, viewPoint(player)); + if (g.shouldNotifyTarget() && g.target() != null) { + ModNetwork.sendToServer(new LockWarningPacket(g.target().getId())); + player.level().playLocalSound(player, ModSounds.get("lockon"), SoundSource.PLAYERS, 1.0F, 1.0F); + } + if (!locked || g.lastLocked() == null) { + yield false; + } + shot.option1 = g.lastLocked().getId(); + shot.option2 = set.mode(); + yield true; + } + case TV_MISSILE -> { + shot.option1 = set.mode(); + shot.option2 = 0; + yield true; + } + case AS_MISSILE -> landAhead(s.vehicle, player, 1.62, 200.0, false) != null; + case CAS -> landAhead(s.vehicle, player, 2.0, 80.0, false) != null; + case TORPEDO -> !w.guidedTorpedo || landAhead(player, player, 0.0, 100.0, true) != null; + case DUMMY, SMOKE -> false; + case null -> false; + default -> true; + }; + } + + /** + * Where the player sees from: in first person the camera, which CrewController moves to gunner + * sights and launcher cameras; otherwise the player's eyes. + */ + private static Vec3 viewPoint(LocalPlayer player) { + Minecraft mc = Minecraft.getInstance(); + return mc.options.getCameraType().isFirstPerson() ? mc.gameRenderer.getMainCamera().getPosition() : player.getEyePosition(); + } + + /** A block along the player's view: dry ground for strike weapons, water for guided torpedoes. */ + private static @Nullable BlockHitResult landAhead(Entity origin, LocalPlayer player, double eyeHeight, double range, boolean water) { + Vec3 from = origin.position().add(0.0, eyeHeight, 0.0); + Vec3 to = from.add(Vec3.directionFromRotation(player.getXRot(), player.getYRot()).scale(range)); + BlockHitResult hit = player.level().clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, player)); + if (hit.getType() != HitResult.Type.BLOCK) { + return null; + } + boolean wet = false; + BlockPos pos = hit.getBlockPos(); + for (Direction d : Direction.values()) { + wet |= player.level().getFluidState(pos.relative(d)).is(FluidTags.WATER); + } + return wet == water ? hit : null; + } + + private static void send(AircraftEntity vehicle, CrewActionPacket.Action action) { + ModNetwork.sendToServer(new CrewActionPacket(vehicle.getId(), action)); + } + + private static void click() { + LocalPlayer player = Minecraft.getInstance().player; + if (player != null) { + player.level().playLocalSound(player, SoundEvents.UI_BUTTON_CLICK.value(), SoundSource.PLAYERS, 0.3F, 1.0F); + } + } +} From d25c7d7ea689edc48d96f53e90e5fb3785efde91 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:35 +0800 Subject: [PATCH 188/314] Draw the weapon HUD with 1.21.1's GuiGraphics --- .../jnhprojects/mcheli/client/WeaponHud.java | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java new file mode 100644 index 0000000..5b9c40c --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/WeaponHud.java @@ -0,0 +1,123 @@ +package com.jnhprojects.mcheli.client; + +import com.jnhprojects.mcheli.client.hud.HudRenderer; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.weapon.Guidance; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.network.chat.Component; + +/** + * The vehicle HUD layer. In first person it runs the seat's HUD script (the {@code HUD} line of + * the definition, or the kind's default), or {@code tv_missile} while flying a TV missile, as the + * original did; a seat whose HUD is {@code none} gets nothing. Where no script draws (third + * person, a destroyed vehicle, a script that does not exist) it shows a plain text readout of hit + * points, ammunition, reload, heat, lock and flares. + */ +final class WeaponHud { + private static final int WHITE = 0xFFFFFFFF; + private static final int AMBER = 0xFFFFC040; + private static final int RED = 0xFFFF5050; + private static final int GREEN = 0xFF60FF60; + private static final String NO_HUD = "none"; + + private WeaponHud() { + } + + /** A pilot in gunner mode gets the gunner seat's HUD, as in the original. */ + private static int hudSeat(AircraftEntity vehicle, int seat) { + return seat == 0 && vehicle.isGunnerMode() ? 1 : seat; + } + + static void draw(GuiGraphics g, DeltaTracker delta) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + if (player == null || !(player.getVehicle() instanceof AircraftEntity vehicle)) { + return; + } + AircraftDefinition def = vehicle.definition(); + VehicleWeapons weapons = vehicle.weapons(); + int seat = vehicle.seatOf(player); + if (def == null || weapons == null || seat < 0 || seat >= def.seatCount()) { + return; + } + if (mc.options.getCameraType().isFirstPerson() && !vehicle.isDestroyed()) { + String hud = CrewController.tvCamera(vehicle, seat) != null ? "tv_missile" + : hudSeat(vehicle, seat) < def.huds.size() ? def.huds.get(hudSeat(vehicle, seat)) : null; + if (NO_HUD.equals(hud)) { + // The original's way of saying a seat has no HUD at all. + return; + } + if (hud != null && HudRenderer.draw(g, vehicle, def, player, seat, hud, delta.getGameTimeDeltaPartialTick(false))) { + return; + } + } + Font font = mc.font; + int x = g.guiWidth() - 160; + int y = g.guiHeight() - 92; + + float hp = (float) vehicle.hp() / def.maxHp; + int hpColor = vehicle.isDestroyed() ? RED : hp < 0.5F ? AMBER : GREEN; + g.drawString(font, Component.translatable("hud.mcheli.hp", vehicle.hp(), def.maxHp), x, y, hpColor); + y += 11; + + WeaponSet set = weapons.current(seat); + if (set != null) { + g.drawString(font, set.displayName(), x, y, WHITE); + y += 11; + Component ammo = vehicle.hasInfiniteAmmo(player) && set.magazineSize() > 0 + ? Component.translatable("hud.mcheli.ammo_infinite", set.ammo()) + : set.magazineSize() > 0 + ? Component.translatable("hud.mcheli.ammo", set.ammo(), set.restAmmo()) + : Component.translatable("hud.mcheli.ammo_none"); + g.drawString(font, ammo, x, y, set.ammo() == 0 && set.magazineSize() > 0 ? RED : WHITE); + y += 11; + Component status = null; + int color = AMBER; + if (set.isReloading()) { + status = Component.translatable("hud.mcheli.reloading"); + } else if (set.isOverheated()) { + status = Component.translatable("hud.mcheli.overheat"); + color = RED; + } else if (set.isInPreparation()) { + status = Component.translatable("hud.mcheli.preparing"); + } else if (set.magazineSize() > 0 && set.ammo() == 0 && set.restAmmo() == 0) { + status = Component.translatable("hud.mcheli.resupply", KeyBindings.RESUPPLY.getTranslatedKeyMessage()); + color = RED; + } + Guidance lock = weapons.guidance(weapons.selected(seat)); + if (status == null && lock != null && lock.lockCount() > 0) { + status = lock.isLocked() + ? Component.translatable("hud.mcheli.locked") + : Component.translatable("hud.mcheli.locking", lock.lockCount() * 100 / Math.max(lock.lockCountMax(), 1)); + color = lock.isLocked() ? RED : AMBER; + } + if (status != null) { + g.drawString(font, status, x, y, color); + y += 11; + } + if (set.definition().maxHeatCount > 0) { + int width = 100; + int filled = Math.min(width, set.currentHeat * width / set.definition().maxHeatCount); + g.fill(x, y + 2, x + width, y + 5, 0x80000000); + g.fill(x, y + 2, x + filled, y + 5, set.isOverheated() ? RED : AMBER); + y += 8; + } + } else if (!weapons.sets().isEmpty()) { + g.drawString(font, Component.translatable("hud.mcheli.no_weapon"), x, y, WHITE); + y += 11; + } + if (seat <= 1 && weapons.flares.isFitted()) { + Component flare = vehicle.isFlareActive() ? Component.translatable("hud.mcheli.flare_active") + : weapons.flares.isPreparing() ? Component.translatable("hud.mcheli.flare_wait") + : Component.translatable("hud.mcheli.flare_ready", KeyBindings.FLARE.getTranslatedKeyMessage()); + g.drawString(font, flare, x, y, vehicle.isFlareActive() ? GREEN : WHITE); + } + } +} From 7e49257af65053e08aa5b979ae567f402b13b365 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:35 +0800 Subject: [PATCH 189/314] Draw HUD scripts with 1.21.1's GuiGraphics and pose stack --- .../mcheli/client/hud/HudCanvas.java | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java new file mode 100644 index 0000000..b752e0a --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudCanvas.java @@ -0,0 +1,164 @@ +package com.jnhprojects.mcheli.client.hud; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.resources.ResourceLocation; + +/** + * The drawing primitives of {@code MCH_HudItem}, on the 1.21.1 GUI. + * + *

The original drew GL lines, points and quads at fractional GUI coordinates. The GUI now only + * takes whole numbers, but every element keeps a copy of the pose, so each primitive is a unit + * square stretched and turned into place. Lines are one GUI unit thick, which is what + * {@code glLineWidth(scaleFactor)} gave; blips are two units square. + */ +final class HudCanvas { + private static final float LINE_WIDTH = 1.0F; + private static final float POINT_SIZE = 2.0F; + + private final GuiGraphics g; + private final Font font; + + HudCanvas(GuiGraphics g, Font font) { + this.g = g; + this.font = font; + } + + PoseStack pose() { + return g.pose(); + } + + /** Turns everything drawn until the matching {@code pop} by {@code degrees} about a point. */ + void pushRotation(double x, double y, float degrees) { + PoseStack pose = g.pose(); + pose.pushPose(); + pose.translate((float) x, (float) y, 0.0F); + pose.mulPose(Axis.ZP.rotation((float) Math.toRadians(degrees))); + pose.translate((float) -x, (float) -y, 0.0F); + } + + void pop() { + g.pose().popPose(); + } + + void line(double x0, double y0, double x1, double y1, int color) { + double dx = x1 - x0; + double dy = y1 - y0; + double length = Math.sqrt(dx * dx + dy * dy); + if (length < 1.0E-4) { + return; + } + PoseStack pose = g.pose(); + pose.pushPose(); + pose.translate((float) x0, (float) y0, 0.0F); + pose.mulPose(Axis.ZP.rotation((float) Math.atan2(dy, dx))); + pose.translate(0.0F, -LINE_WIDTH / 2.0F, 0.0F); + pose.scale((float) length, LINE_WIDTH, 1.0F); + g.fill(0, 0, 1, 1, color); + pose.popPose(); + } + + /** {@code GL_LINES}: x, y pairs taken two points at a time. */ + void lines(double[] p, int color) { + for (int i = 0; i + 3 < p.length; i += 4) { + line(p[i], p[i + 1], p[i + 2], p[i + 3], color); + } + } + + /** {@code GL_LINE_STRIP}. */ + void strip(double[] p, int color) { + for (int i = 0; i + 3 < p.length; i += 2) { + line(p[i], p[i + 1], p[i + 2], p[i + 3], color); + } + } + + /** {@code GL_LINE_LOOP}. */ + void loop(double[] p, int color) { + strip(p, color); + if (p.length >= 4) { + line(p[p.length - 2], p[p.length - 1], p[0], p[1], color); + } + } + + /** + * {@code GL_LINES} with {@code glLineStipple(factor, pattern)}: each segment starts the 16 bit + * pattern afresh, lowest bit first, every bit covering {@code factor} GUI units. + */ + void stippled(double[] p, int color, int factor, int pattern) { + double step = Math.max(factor, 1); + for (int i = 0; i + 3 < p.length; i += 4) { + double dx = p[i + 2] - p[i]; + double dy = p[i + 3] - p[i + 1]; + double length = Math.sqrt(dx * dx + dy * dy); + if (length < 1.0E-4) { + continue; + } + double ux = dx / length; + double uy = dy / length; + double runStart = -1.0; + int bit = 0; + for (double t = 0.0; t < length; t += step, bit = (bit + 1) & 15) { + boolean on = (pattern >> bit & 1) != 0; + if (on && runStart < 0.0) { + runStart = t; + } else if (!on && runStart >= 0.0) { + line(p[i] + ux * runStart, p[i + 1] + uy * runStart, p[i] + ux * t, p[i + 1] + uy * t, color); + runStart = -1.0; + } + } + if (runStart >= 0.0) { + line(p[i] + ux * runStart, p[i + 1] + uy * runStart, p[i + 2], p[i + 3], color); + } + } + } + + void rect(double x0, double y0, double x1, double y1, int color) { + double left = Math.min(x0, x1); + double top = Math.min(y0, y1); + double width = Math.abs(x1 - x0); + double height = Math.abs(y1 - y0); + if (width <= 0.0 || height <= 0.0) { + return; + } + PoseStack pose = g.pose(); + pose.pushPose(); + pose.translate((float) left, (float) top, 0.0F); + pose.scale((float) width, (float) height, 1.0F); + g.fill(0, 0, 1, 1, color); + pose.popPose(); + } + + void point(double x, double y, int color) { + rect(x - POINT_SIZE / 2.0, y - POINT_SIZE / 2.0, x + POINT_SIZE / 2.0, y + POINT_SIZE / 2.0, color); + } + + /** + * {@code drawTexture}: texels {@code u, v, uWidth, vHeight} of a {@code textureWidth} by + * {@code textureHeight} image, drawn into a box and turned about its centre. Drawn untinted, + * as the original reset the colour to white first. + */ + void texture(ResourceLocation texture, double left, double top, double width, double height, double u, double v, + double uWidth, double vHeight, float rotation, int textureWidth, int textureHeight) { + if (width == 0.0 || height == 0.0) { + return; + } + PoseStack pose = g.pose(); + pose.pushPose(); + pose.translate((float) (left + width / 2.0), (float) (top + height / 2.0), 0.0F); + pose.mulPose(Axis.ZP.rotation((float) Math.toRadians(rotation))); + pose.translate((float) (-width / 2.0), (float) (-height / 2.0), 0.0F); + pose.scale((float) width, (float) height, 1.0F); + g.blit(texture, 0, 0, 1, 1, (float) u, (float) v, (int) Math.round(uWidth), (int) Math.round(vHeight), textureWidth, textureHeight); + pose.popPose(); + } + + void text(String text, int x, int y, int color, boolean centered) { + if (centered) { + g.drawCenteredString(font, text, x, y, color); + } else { + g.drawString(font, text, x, y, color); + } + } +} From 3d8abb1c26f8f3d105d50c96e4d36d2b90ed10ac Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:36 +0800 Subject: [PATCH 190/314] Draw HUD scripts with 1.21.1's GuiGraphics --- .../mcheli/client/hud/HudRenderer.java | 354 ++++++++++++++++++ 1 file changed, 354 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java new file mode 100644 index 0000000..458ee88 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudRenderer.java @@ -0,0 +1,354 @@ +package com.jnhprojects.mcheli.client.hud; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.hud.Expression; +import com.jnhprojects.mcheli.hud.HudCommand; +import com.jnhprojects.mcheli.hud.HudScript; +import com.jnhprojects.mcheli.hud.HudVariable; +import com.jnhprojects.mcheli.hud.TextArg; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashSet; +import java.util.IllegalFormatException; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +/** + * Runs HUD scripts, the drawing half of {@code MCH_Hud} and the {@code MCH_HudItem} subclasses. + * + *

An {@code If} hides every line up to its {@code EndIf}; {@code Exit} ends the current script + * only, so a {@code Call}er carries on; the colour carries into called scripts and over to the next + * frame. A script cannot call itself, directly or through another, the same guard the original had. + * A line that fails to format is skipped and reported once, where the original crashed. + */ +public final class HudRenderer { + private static final Logger LOGGER = LoggerFactory.getLogger(HudRenderer.class); + private static final int AZIMUTH_RED = 0xFFFF0000; + private static final String[] AZIMUTH_8 = {"S", "SW", "W", "NW", "N", "NE", "E", "SE"}; + /** {@code glLineStipple} pattern of the original's dashed lines. */ + private static final int DASHES = 0xCCCC; + + private static final HudState STATE = new HudState(); + private static final Set REPORTED = new HashSet<>(); + + private final HudCanvas canvas; + private final AircraftEntity vehicle; + private final AircraftDefinition def; + private final LocalPlayer player; + private final double centerX; + private final double centerY; + private final Set running = new HashSet<>(); + + private HudRenderer(HudCanvas canvas, AircraftEntity vehicle, AircraftDefinition def, LocalPlayer player, double width, double height) { + this.canvas = canvas; + this.vehicle = vehicle; + this.def = def; + this.player = player; + this.centerX = width / 2.0; + this.centerY = height / 2.0; + } + + public static void tick() { + LocalPlayer player = Minecraft.getInstance().player; + STATE.tick(player != null && player.getVehicle() instanceof AircraftEntity vehicle ? vehicle : null); + } + + /** Fed by the pilot's mouse steering; moves the control stick indicator. */ + public static void onPilotLook(float dYaw, float dPitch) { + STATE.onPilotLook(dYaw, dPitch); + } + + /** + * Draws a HUD script for the local player's seat. + * + * @return false if no script by that name is loaded, so the caller can draw something else + */ + public static boolean draw(GuiGraphics g, AircraftEntity vehicle, AircraftDefinition def, LocalPlayer player, + int seat, String hud, float partialTicks) { + HudScript script = HudScripts.INSTANCE.get(hud); + if (script == null) { + return false; + } + double width = g.guiWidth(); + double height = g.guiHeight(); + STATE.update(vehicle, def, player, seat, width, height, partialTicks); + HudRenderer renderer = new HudRenderer(new HudCanvas(g, Minecraft.getInstance().font), vehicle, def, player, width, height); + renderer.run(script); + return true; + } + + private double eval(Expression e) { + return e.eval(STATE.vars); + } + + private double x(Expression e) { + return centerX + eval(e); + } + + private double y(Expression e) { + return centerY + eval(e); + } + + private double[] points(List coordinates) { + double[] out = new double[coordinates.size()]; + for (int i = 0; i + 1 < out.length; i += 2) { + out[i] = x(coordinates.get(i)); + out[i + 1] = y(coordinates.get(i + 1)); + } + return out; + } + + private void run(HudScript script) { + if (!running.add(script.name())) { + return; + } + try { + boolean hidden = false; + for (HudCommand command : script.commands()) { + if (command instanceof HudCommand.If c) { + hidden = eval(c.condition()) == 0.0; + continue; + } + if (command instanceof HudCommand.EndIf) { + hidden = false; + continue; + } + if (hidden) { + continue; + } + if (command instanceof HudCommand.Exit) { + return; + } + execute(script, command); + } + } finally { + running.remove(script.name()); + } + } + + private void execute(HudScript script, HudCommand command) { + int color = STATE.color; + switch (command) { + case HudCommand.Color c -> { + STATE.color = (int) (long) eval(c.argb()); + STATE.vars.set(HudVariable.COLOR, STATE.color & 0xFFFFFFFFL); + } + case HudCommand.Call c -> { + HudScript called = HudScripts.INSTANCE.get(c.hud()); + if (called != null) { + run(called); + } + } + case HudCommand.Text c -> text(script, c, color); + case HudCommand.Texture c -> { + ResourceLocation texture = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/gui/" + c.name() + ".png"); + int[] size = HudScripts.INSTANCE.textureSize(Minecraft.getInstance().getResourceManager(), texture); + canvas.texture(texture, x(c.left()), y(c.top()), eval(c.width()), eval(c.height()), + eval(c.u()), eval(c.v()), eval(c.uWidth()), eval(c.vHeight()), (float) eval(c.rotation()), size[0], size[1]); + } + case HudCommand.Rect c -> { + // The original truncated the size, not the position. + double left = x(c.left()); + double top = y(c.top()); + canvas.rect(left, top, left + (int) eval(c.width()), top + (int) eval(c.height()), color); + } + case HudCommand.Line c -> canvas.strip(points(c.points()), color); + case HudCommand.LineStipple c -> canvas.stippled(points(c.points()), color, (int) eval(c.factor()), (int) eval(c.pattern())); + case HudCommand.Radar c -> radar(c, color); + case HudCommand.Graduation c -> graduation(c, color); + case HudCommand.CameraRot c -> cameraRot(x(c.x()), y(c.y()), color); + case HudCommand.If c -> { + } + case HudCommand.EndIf c -> { + } + case HudCommand.Exit c -> { + } + } + } + + /** {@code MCH_HudItemString}: skipped while a weapon value is shown and no weapon (or magazine) applies. */ + private void text(HudScript script, HudCommand.Text c, int color) { + Object[] args = new Object[c.args().size()]; + for (int i = 0; i < args.length; i++) { + TextArg arg = c.args().get(i); + if ((arg.needsWeapon() && !STATE.hasWeapon()) || (arg.needsMagazine() && !STATE.hasMagazine())) { + return; + } + args[i] = STATE.text(arg, vehicle, def, player); + } + String text; + try { + text = String.format(Locale.ROOT, c.format(), args); + } catch (IllegalFormatException e) { + String key = script.name() + ':' + c.line(); + if (REPORTED.add(key)) { + LOGGER.warn("HUD {}.txt line {}: cannot format \"{}\": {}", script.name(), c.line(), c.format(), e.getMessage()); + } + return; + } + canvas.text(text, (int) x(c.x()), (int) y(c.y()), color, c.centered()); + } + + /** {@code MCH_HudItemRadar}: blips at half scale for a 64 unit box, turned with the view. */ + private void radar(HudCommand.Radar c, int color) { + List blips = c.enemies() ? STATE.enemyBlips : STATE.entityBlips; + if (blips.isEmpty()) { + return; + } + double rotation = Math.toRadians(eval(c.rotation())); + double left = x(c.left()); + double top = y(c.top()); + double w = eval(c.width()); + double h = eval(c.height()); + double cos = Math.cos(rotation); + double sin = Math.sin(rotation); + for (double[] blip : blips) { + double px = blip[0] / 2.0 * w / 64.0; + double py = blip[1] / 2.0 * h / 64.0; + double rx = px * cos - py * sin; + double ry = px * sin + py * cos; + if (rx > -w / 2.0 && rx < w / 2.0 && ry > -h / 2.0 && ry < h / 2.0) { + canvas.point(rx + left + w / 2.0, ry + top + h / 2.0, color); + } + } + } + + private void graduation(HudCommand.Graduation c, int color) { + int x = (int) x(c.x()); + int y = (int) y(c.y()); + double angle = eval(c.angle()); + canvas.pushRotation(x, y, (float) eval(c.roll())); + switch (c.kind()) { + case YAW -> headingTape(angle, color, x, y); + case PITCH_1 -> pitchScale(angle, color, x, y); + case PITCH_2 -> pitchLadder(angle, color, x, y, true); + case PITCH_3 -> pitchLadder(angle, color, x, y, false); + } + canvas.pop(); + } + + /** {@code drawCommonGraduationYaw}: a tick every degree, labels every three, compass points every 45. */ + private void headingTape(double playerYaw, int color, int posX, int posY) { + double yaw = HudState.rotate360(playerYaw); + posX -= 90; + double[] line = new double[76]; + int offset = (int) (yaw * 10.0) % 10; + int degree = (int) yaw - 9; + for (int i = 0; i < line.length / 4; i++, degree++) { + int azimuthX = posX + i * 10 - offset; + line[i * 4] = azimuthX; + line[i * 4 + 1] = posY; + line[i * 4 + 2] = azimuthX; + line[i * 4 + 3] = posY + (degree % 45 == 0 ? 15 : degree % 3 == 0 ? 10 : 5); + if (degree % 45 == 0) { + canvas.text(AZIMUTH_8[Math.floorMod(degree, 360) / 45], azimuthX, posY - 10, AZIMUTH_RED, true); + } else if (degree % 3 == 0) { + int heading = degree + 180; + if (heading < 0) { + heading += 360; + } + if (heading > 360) { + heading -= 360; + } + canvas.text(Integer.toString(heading), azimuthX, posY - 10, color, true); + } + } + canvas.lines(line, color); + } + + /** {@code drawCommonGraduationPitch1}: a scale of ticks on both sides that slides with the pitch. */ + private void pitchScale(double playerPitch, int color, int posX, int posY) { + int pitch = (int) playerPitch % 360; + int y = (int) (playerPitch * 10.0 % 10.0); + if (y < 0) { + y += 10; + } + int left = posX - 100; + int right = posX + 100; + posY -= 80; + double[] line = new double[144]; + int p = playerPitch < 0.0 && y != 0 ? pitch - 9 : pitch - 8; + for (int i = 0; i < line.length / 8; i++, p++) { + int outer = p % 3 == 0 ? 15 : 5; + double rowY = posY + i * 10 - y; + line[i * 8] = left - outer; + line[i * 8 + 1] = rowY; + line[i * 8 + 2] = left; + line[i * 8 + 3] = rowY; + line[i * 8 + 4] = right + outer; + line[i * 8 + 5] = rowY; + line[i * 8 + 6] = right; + line[i * 8 + 7] = rowY; + } + canvas.lines(line, color); + canvas.strip(new double[]{left - 25, posY - 90, left, posY - 90, left, posY + 90, left - 25, posY + 90}, color); + canvas.strip(new double[]{right + 25, posY - 90, right, posY - 90, right, posY + 90, right + 25, posY + 90}, color); + } + + /** + * {@code drawCommonGraduationPitch2}: a ladder with a rung every five degrees, dashed below + * the horizon. {@code wide} is {@code Pitch2} with five rungs; {@code Pitch3} has three, twice as far apart. + */ + private void pitchLadder(double playerPitch, int color, int posX, int posY, boolean wide) { + playerPitch = -playerPitch; + int pitchStep = (int) playerPitch / 5 * 5; + int start = wide ? 0 : 1; + int end = wide ? 5 : 4; + int spacing = wide ? 1 : 2; + double rest = playerPitch % 5.0; + double[] line = new double[8]; + for (int i = start; i < end; i++) { + int pitch = -(-pitchStep - 10 + i * 5); + int halfWidth = pitch != 0 ? 50 : 100; + int y = posY + (int) (-60 * spacing + rest * 6.0 * spacing + i * 30 * spacing); + line[0] = posX - halfWidth; + line[1] = y + (pitch == 0 ? 0 : pitch > 0 ? 2 : -2); + line[2] = posX - 50; + line[3] = y; + line[4] = posX + halfWidth; + line[5] = line[1]; + line[6] = posX + 50; + line[7] = y; + canvas.lines(line, color); + line[0] = posX - 50; + line[1] = y; + line[2] = posX - 30; + line[3] = y; + line[4] = posX + 50; + line[5] = y; + line[6] = posX + 30; + line[7] = y; + if (pitch >= 0) { + canvas.lines(line, color); + } else { + canvas.stippled(line, color, 1, DASHES); + } + if (pitch != 0) { + canvas.text(Integer.toString(pitch), posX - 60, y - 4, color, true); + canvas.text(Integer.toString(pitch), posX + 60, y - 4, color, true); + } + } + } + + /** {@code MCH_HudItemCameraRot}: a frame with a box showing where the crew's view points against the hull. */ + private void cameraRot(double posX, double posY, int color) { + canvas.loop(new double[]{posX - 21, posY - 11, posX + 21, posY - 11, posX + 21, posY + 11, posX - 21, posY + 11}, color); + canvas.stippled(new double[]{posX - 21, posY, posX, posY, posX + 21, posY, posX, posY, + posX, posY - 11, posX, posY, posX, posY + 11, posX, posY}, color, 1, DASHES); + float pitch = Mth.clamp(player.getXRot(), -30.0F, 70.0F); + pitch = (float) ((pitch - 20.0F) * 0.16); + float yaw = Mth.clamp(Mth.wrapDegrees(player.getYRot() - vehicle.getYRot()) * 2.0F, -50.0F, 50.0F); + yaw = (float) (yaw * 0.34); + canvas.loop(new double[]{posX + yaw - 3, posY + pitch - 2, posX + yaw + 3, posY + pitch - 2, + posX + yaw + 3, posY + pitch + 2, posX + yaw - 3, posY + pitch + 2}, color); + } +} From 34ba906ee8919463cb89e9bc00d0f579bc2f385e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:36 +0800 Subject: [PATCH 191/314] Add HudScripts to the shared 1.21.1 sources --- .../mcheli/client/hud/HudScripts.java | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java new file mode 100644 index 0000000..5a670db --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudScripts.java @@ -0,0 +1,86 @@ +package com.jnhprojects.mcheli.client.hud; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.hud.HudFormatException; +import com.jnhprojects.mcheli.hud.HudScript; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.ResourceManagerReloadListener; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; + +/** + * The HUD scripts in {@code assets/mcheli/hud}, read through the resource manager so resource + * packs can replace or add them, and reloaded with F3 + T. A script that fails to parse is logged + * and left out; the vehicles using it fall back to the plain readout. + */ +public final class HudScripts implements ResourceManagerReloadListener { + public static final HudScripts INSTANCE = new HudScripts(); + private static final Logger LOGGER = LoggerFactory.getLogger(HudScripts.class); + private static final String FOLDER = "hud"; + private static final int DEFAULT_TEXTURE_SIZE = 256; + + private Map scripts = Map.of(); + private final Map textureSizes = new HashMap<>(); + + private HudScripts() { + } + + public @Nullable HudScript get(String name) { + return scripts.get(name.toLowerCase(Locale.ROOT)); + } + + @Override + public void onResourceManagerReload(ResourceManager resources) { + Map loaded = new HashMap<>(); + resources.listResources(FOLDER, id -> id.getNamespace().equals(MCHeli.MOD_ID) && id.getPath().endsWith(".txt")) + .forEach((id, resource) -> { + String path = id.getPath(); + String name = path.substring(FOLDER.length() + 1, path.length() - 4).toLowerCase(Locale.ROOT); + try (Reader in = resource.openAsReader()) { + loaded.put(name, HudScript.parse(name, in)); + } catch (IOException | HudFormatException e) { + LOGGER.error("Skipping HUD {}: {}", id, e.getMessage()); + } + }); + scripts = Map.copyOf(loaded); + textureSizes.clear(); + LOGGER.info("Loaded {} HUD scripts", scripts.size()); + } + + /** + * Width and height of a GUI texture in pixels, read from the PNG header once. The original + * fell back to 256 when it could not tell, and so does this. + */ + int[] textureSize(ResourceManager resources, ResourceLocation texture) { + return textureSizes.computeIfAbsent(texture, id -> readSize(resources.getResource(id))); + } + + private static int[] readSize(Optional resource) { + if (resource.isPresent()) { + try (InputStream in = resource.get().open(); DataInputStream data = new DataInputStream(in)) { + // 8 byte signature, IHDR length and type, then width and height as big endian ints. + data.skipNBytes(16); + int width = data.readInt(); + int height = data.readInt(); + if (width > 0 && height > 0) { + return new int[]{width, height}; + } + } catch (IOException e) { + LOGGER.warn("Could not read the size of a HUD texture", e); + } + } + return new int[]{DEFAULT_TEXTURE_SIZE, DEFAULT_TEXTURE_SIZE}; + } +} From 3caa5f2d9566a75d51d1c9d1412a3adfd99fee54 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:37 +0800 Subject: [PATCH 192/314] Read build height, time and rotation the 1.21.1 way --- .../mcheli/client/hud/HudState.java | 353 ++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java new file mode 100644 index 0000000..eb89586 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/hud/HudState.java @@ -0,0 +1,353 @@ +package com.jnhprojects.mcheli.client.hud; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.hud.HudVariable; +import com.jnhprojects.mcheli.hud.TextArg; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.weapon.Guidance; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.monster.Enemy; +import net.minecraft.world.level.Level; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * What the HUD scripts read, the static state of {@code MCH_HudItem}. Ticked with the client, so + * the radar sweep, altitude and stick behave as in the original; {@link #update} then fills in the + * per frame values before a script runs. + * + *

Systems that arrive later in phase 4 read as their idle state for now: fuel is full, the + * camera is in normal view at zoom 1, the pilot gunner mode is off, the vehicle inventory is empty + * and the mortar distance is unknown. + */ +final class HudState { + private static final int RADAR_RANGE = 64; + private static final int ALTITUDE_INTERVAL = 10; + private static final double STICK_MAX = 40.0; + /** Roughly the mouse units of the original per degree of pilot look. */ + private static final double STICK_PER_DEGREE = 6.0; + private static final int STICK_SAMPLES = 4; + + final HudVariable.Values vars = new HudVariable.Values(); + final List entityBlips = new ArrayList<>(); + final List enemyBlips = new ArrayList<>(); + + private @Nullable AircraftEntity vehicle; + private int radarRotate; + private int radarRotateO; + private int altitude; + private int altitudeCountdown; + private double stickX; + private double stickY; + private final double[] stickXSamples = new double[STICK_SAMPLES]; + private final double[] stickYSamples = new double[STICK_SAMPLES]; + private int stickSample; + + /** Colour set by the last {@code Color} line. Like the original it carries over between frames. */ + int color = 0xFF000000; + + // Per frame, for the text arguments. + private @Nullable WeaponSet weapon; + private String weaponName = ""; + private String weaponAmmo = ""; + private String weaponRestAmmo = ""; + private float reloadPercent; + private float reloadSeconds; + + /** Mouse look of the pilot this frame, in degrees; drives the control stick indicator. */ + void onPilotLook(float dYaw, float dPitch) { + stickX += dYaw * STICK_PER_DEGREE; + stickY += dPitch * STICK_PER_DEGREE; + double length = Math.sqrt(stickX * stickX + stickY * stickY); + if (length > STICK_MAX) { + stickX *= STICK_MAX / length; + stickY *= STICK_MAX / length; + } + } + + void tick(@Nullable AircraftEntity riding) { + if (riding != vehicle) { + vehicle = riding; + radarRotate = 0; + radarRotateO = 0; + altitudeCountdown = 0; + entityBlips.clear(); + enemyBlips.clear(); + } + stickX *= 0.85; + stickY *= 0.85; + if (riding == null || riding.definition() == null) { + return; + } + if (riding.definition().entityRadar) { + radarRotateO = radarRotate; + radarRotate += radarSpeed(riding); + if (radarRotate >= 360) { + radarRotate = 0; + radarRotateO = 0; + sweepRadar(riding); + } + } + if (--altitudeCountdown <= 0) { + altitude = measureAltitude(riding); + altitudeCountdown = ALTITUDE_INTERVAL; + } + } + + /** Degrees per tick, from each kind's client tick handler. */ + private static int radarSpeed(AircraftEntity vehicle) { + return vehicle instanceof PlaneEntity || vehicle instanceof TankEntity ? 10 : 5; + } + + /** {@code MCH_Radar.updateXZ}: mobs within 64 blocks that are not buried under five or more blocks. */ + private void sweepRadar(AircraftEntity center) { + entityBlips.clear(); + enemyBlips.clear(); + Level level = center.level(); + for (Mob mob : level.getEntitiesOfClass(Mob.class, center.getBoundingBox().inflate(RADAR_RANGE))) { + double x = mob.getX() - center.getX(); + double z = mob.getZ() - center.getZ(); + if (x * x + z * z >= RADAR_RANGE * RADAR_RANGE || isBuried(level, mob)) { + continue; + } + (mob instanceof Enemy ? enemyBlips : entityBlips).add(new double[]{x, z}); + } + } + + private static boolean isBuried(Level level, Mob mob) { + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(mob.getBlockX(), Math.max(mob.getBlockY() + 1, level.getMinBuildHeight()), mob.getBlockZ()); + int blocks = 0; + for (; pos.getY() < level.getMaxBuildHeight() - 1; pos.move(0, 1, 0)) { + if (!level.getBlockState(pos).isAir() && ++blocks >= 5) { + return true; + } + } + return false; + } + + /** {@code updateAltitude}: blocks of air below the vehicle, looking at most 256 down. */ + private static int measureAltitude(AircraftEntity vehicle) { + Level level = vehicle.level(); + BlockPos.MutableBlockPos pos = vehicle.blockPosition().mutable(); + int top = pos.getY(); + for (int i = 0; i < 256 && top - i > level.getMinBuildHeight(); i++) { + pos.setY(top - i); + if (!level.getBlockState(pos).isAir()) { + return i; + } + } + return 0; + } + + float radarRotation(float partialTicks) { + float rot = radarRotate; + float prev = radarRotateO; + if (rot < prev) { + rot += 360.0F; + } + return prev + (rot - prev) * partialTicks; + } + + void update(AircraftEntity vehicle, AircraftDefinition def, LocalPlayer player, int seat, double width, double height, + float partialTicks) { + HudVariable.Values v = vars; + v.set(HudVariable.COLOR, color & 0xFFFFFFFFL); + v.set(HudVariable.CENTER_X, width / 2.0); + v.set(HudVariable.CENTER_Y, height / 2.0); + v.set(HudVariable.WIDTH, width); + v.set(HudVariable.HEIGHT, height); + v.set(HudVariable.TIME, Math.floorMod(vehicle.level().getDayTime(), 24000L)); + v.set(HudVariable.TEST_MODE, 0.0); + v.set(HudVariable.PLYR_YAW, Mth.wrapDegrees(player.getViewYRot(partialTicks))); + v.set(HudVariable.PLYR_PITCH, player.getViewXRot(partialTicks)); + v.set(HudVariable.YAW, Mth.wrapDegrees(vehicle.getYRot(partialTicks))); + v.set(HudVariable.PITCH, vehicle.getXRot(partialTicks)); + v.set(HudVariable.ROLL, Mth.wrapDegrees(vehicle.roll(partialTicks))); + v.set(HudVariable.ALTITUDE, altitude); + v.set(HudVariable.SEA_ALT, Math.max(vehicle.getY() - vehicle.level().getSeaLevel(), 0.0)); + v.set(HudVariable.HAVE_RADAR, def.entityRadar); + v.set(HudVariable.RADAR_ROT, radarRotation(partialTicks)); + v.set(HudVariable.HP, vehicle.hp()); + v.set(HudVariable.MAX_HP, def.maxHp); + v.set(HudVariable.HP_RTO, def.maxHp > 0 ? (double) vehicle.hp() / def.maxHp : 0.0); + v.set(HudVariable.THROTTLE, vehicle.visibleThrottle()); + v.set(HudVariable.POS_X, vehicle.getX()); + v.set(HudVariable.POS_Y, vehicle.getY()); + v.set(HudVariable.POS_Z, vehicle.getZ()); + // Only the pilot's client simulates the hull, so read motion from the position instead. + double mx = vehicle.getX() - vehicle.xo; + double my = vehicle.getY() - vehicle.yo; + double mz = vehicle.getZ() - vehicle.zo; + v.set(HudVariable.MOTION_X, mx); + v.set(HudVariable.MOTION_Y, my); + v.set(HudVariable.MOTION_Z, mz); + v.set(HudVariable.SPEED, Math.sqrt(mx * mx + my * my + mz * mz)); + v.set(HudVariable.FUEL, 1.0); + v.set(HudVariable.LOW_FUEL, 0.0); + updateStick(seat == 0 && vehicle.steersWithMouse()); + updateWeapon(vehicle, seat); + v.set(HudVariable.VTOL_STAT, vehicle.vtolMode()); + v.set(HudVariable.FREE_LOOK, seat == 0 && vehicle.input().freeLook); + v.set(HudVariable.GUNNER_MODE, vehicle.isGunnerMode(player)); + v.set(HudVariable.CAM_MODE, 0.0); + v.set(HudVariable.CAM_ZOOM, 1.0); + v.set(HudVariable.AUTO_PILOT, vehicle instanceof PlaneEntity && vehicle.isGunnerMode(player)); + VehicleWeapons weapons = vehicle.weapons(); + boolean flares = weapons != null && weapons.flares.isFitted(); + v.set(HudVariable.HAVE_FLARE, flares); + v.set(HudVariable.CAN_FLARE, flares && !weapons.flares.isPreparing() && !weapons.flares.isActive()); + v.set(HudVariable.INVENTORY, 0.0); + v.set(HudVariable.HOVERING, vehicle instanceof HelicopterEntity && vehicle.isHoveringMode()); + v.set(HudVariable.IS_UAV, def.uav); + v.set(HudVariable.UAV_FS, 0.0); + } + + /** The original averaged the last four stick positions, scaled to -1..1, with up positive. */ + private void updateStick(boolean pilot) { + stickXSamples[stickSample] = pilot ? stickX / STICK_MAX : 0.0; + stickYSamples[stickSample] = pilot ? -stickY / STICK_MAX : 0.0; + stickSample = (stickSample + 1) % STICK_SAMPLES; + double x = 0.0; + double y = 0.0; + for (int i = 0; i < STICK_SAMPLES; i++) { + x += stickXSamples[i]; + y += stickYSamples[i]; + } + vars.set(HudVariable.STICK_X, x / STICK_SAMPLES); + vars.set(HudVariable.STICK_Y, y / STICK_SAMPLES); + } + + /** {@code updateWeapon} and {@code updateVarMap_Weapon}. */ + private void updateWeapon(AircraftEntity vehicle, int seat) { + VehicleWeapons weapons = vehicle.weapons(); + WeaponSet set = weapons != null ? weapons.current(seat) : null; + weapon = set; + double reloadTime = 0.0; + double heat = 0.0; + double lock = 0.0; + int sightType = 0; + if (set != null) { + WeaponDefinition def = set.definition(); + weaponName = set.displayName(); + weaponAmmo = set.magazineSize() > 0 ? "%4d".formatted(set.ammo()) : ""; + weaponRestAmmo = set.magazineSize() > 0 ? "%4d".formatted(set.restAmmo()) : ""; + float wait; + if (def.delay > def.reloadTime) { + reloadSeconds = Math.abs(set.countWait); + wait = Math.min(Math.abs((float) set.countWait / Math.max(def.delay, 1)), 1.0F); + } else { + reloadSeconds = set.countReloadWait; + wait = (float) set.countReloadWait / Math.max(def.reloadTime, 1); + } + reloadTime = wait; + reloadSeconds /= 20.0F; + reloadPercent = (1.0F - wait) * 100.0F; + if (def.maxHeatCount > 0) { + heat = Math.min((double) set.currentHeat / def.maxHeatCount, 1.0); + } + Guidance guidance = weapons.guidance(weapons.selected(seat)); + if (def.sight == WeaponDefinition.Sight.LOCK && guidance != null && guidance.lockCountMax() > 0) { + lock = (double) guidance.lockCount() / guidance.lockCountMax(); + sightType = 2; + } else if (def.sight == WeaponDefinition.Sight.ROCKET) { + sightType = 1; + } + vars.set(HudVariable.IS_HEAT_WPN, def.maxHeatCount > 0); + vars.set(HudVariable.RELOADING, set.isInPreparation()); + vars.set(HudVariable.DSP_MT_DIST, def.displayMortarDistance); + } else { + vars.set(HudVariable.IS_HEAT_WPN, 0.0); + vars.set(HudVariable.RELOADING, 0.0); + vars.set(HudVariable.DSP_MT_DIST, 0.0); + } + vars.set(HudVariable.RELOAD_TIME, reloadTime); + vars.set(HudVariable.WPN_HEAT, heat); + vars.set(HudVariable.SIGHT_TYPE, sightType); + vars.set(HudVariable.LOCK, lock); + vars.set(HudVariable.MT_DIST, -1.0); + } + + boolean hasWeapon() { + return weapon != null; + } + + boolean hasMagazine() { + return weapon != null && weapon.magazineSize() > 0; + } + + /** A {@code DrawString} argument, typed the way the original passed it to {@code String.format}. */ + @Nullable Object text(TextArg arg, AircraftEntity vehicle, AircraftDefinition def, LocalPlayer player) { + int worldTime = (int) Math.floorMod(vehicle.level().getDayTime() + 6000L, 24000L); + return switch (arg) { + case NONE -> null; + // The vehicle inventory screen and its key come later in phase 4. + case KEY_GUI -> ""; + case NAME -> def.displayName; + case ALTITUDE -> altitude; + case DATE -> new Date(); + case MC_THOR -> worldTime / 1000; + case MC_TMIN -> worldTime % 1000 * 36 / 10 / 60; + case MC_TSEC -> worldTime % 1000 * 36 / 10 % 60; + case MAX_HP -> def.maxHp; + case HP -> vehicle.hp(); + case HP_PER -> def.maxHp > 0 ? 100.0 * vehicle.hp() / def.maxHp : 0.0; + case POS_X -> vehicle.getX(); + case POS_Y -> vehicle.getY(); + case POS_Z -> vehicle.getZ(); + case MOTION_X -> vars.get(HudVariable.MOTION_X); + case MOTION_Y -> vars.get(HudVariable.MOTION_Y); + case MOTION_Z -> vars.get(HudVariable.MOTION_Z); + case INVENTORY -> 0; + case WPN_NAME -> weaponName; + case WPN_AMMO -> weaponAmmo; + case WPN_RM_AMMO -> weaponRestAmmo; + case RELOAD_PER -> reloadPercent; + case RELOAD_SEC -> reloadSeconds; + case MORTAR_DIST -> -1.0F; + case MC_VER -> "26.3"; + case MOD_VER -> Platform.get().modVersion(); + case MOD_NAME -> "MCHeli Rework"; + case YAW -> rotate360(vehicle.getYRot() + 180.0); + case PITCH -> -vehicle.getXRot(); + case ROLL -> Mth.wrapDegrees(vehicle.roll()); + case PLYR_YAW -> rotate360(player.getYRot() + 180.0); + case PLYR_PITCH -> -player.getXRot(); + case TVM_POS_X, TVM_POS_Y, TVM_POS_Z, TVM_DIFF -> tvMissile(arg, vehicle); + case CAM_ZOOM -> 1.0F; + case UAV_DIST -> 0.0; + case THROTTLE -> vehicle.visibleThrottle() * 100.0; + }; + } + + private static double tvMissile(TextArg arg, AircraftEntity vehicle) { + TvMissileEntity missile = vehicle.tvMissile(); + if (missile == null) { + return 0.0; + } + return switch (arg) { + case TVM_POS_X -> missile.getX(); + case TVM_POS_Y -> missile.getY(); + case TVM_POS_Z -> missile.getZ(); + default -> missile.position().distanceTo(vehicle.position()); + }; + } + + static double rotate360(double degrees) { + double r = degrees % 360.0; + return r >= 0.0 ? r : r + 360.0; + } +} From 47fc1f19d5490571f63bda11c9b56fc1840b12ba Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:37 +0800 Subject: [PATCH 193/314] Port the glow particle to 1.21.1's texture sheet particles --- .../mcheli/client/particle/GlowParticle.java | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java new file mode 100644 index 0000000..4d854b2 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/particle/GlowParticle.java @@ -0,0 +1,105 @@ +package com.jnhprojects.mcheli.client.particle; + +import com.jnhprojects.mcheli.client.render.Glow; +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.minecraft.client.Camera; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.particle.Particle; +import net.minecraft.client.particle.ParticleProvider; +import net.minecraft.client.particle.ParticleRenderType; +import net.minecraft.client.particle.SpriteSet; +import net.minecraft.client.particle.TextureSheetParticle; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.core.BlockPos; +import net.minecraft.util.FastColor; +import net.minecraft.util.Mth; +import org.jspecify.annotations.Nullable; + +/** + * {@code mcheli:glow}: a ball of light added on top of the scene. A flash swells quickly and fades + * out; a fire holds its brightness, wavering, and only fades towards the end. How bright it gets + * depends on the light where it starts, so the same explosion is a flicker at noon and lights up + * the sky at night. + */ +public class GlowParticle extends TextureSheetParticle { + private final float size; + private final float peak; + private final boolean flicker; + private float waver = 1.0F; + + GlowParticle(ClientLevel level, double x, double y, double z, double xd, double yd, double zd, GlowParticleOptions options, + TextureAtlasSprite sprite, float strength) { + super(level, x, y, z); + setSprite(sprite); + setParticleSpeed(xd, yd, zd); + this.size = options.size(); + this.peak = strength; + this.flicker = options.flicker(); + this.quadSize = size; + this.lifetime = options.lifetime(); + this.friction = 0.9F; + this.gravity = 0.0F; + this.hasPhysics = false; + int color = options.color(); + setColor(FastColor.ARGB32.red(color) / 255.0F, FastColor.ARGB32.green(color) / 255.0F, FastColor.ARGB32.blue(color) / 255.0F); + this.alpha = 0.0F; + } + + @Override + public void tick() { + super.tick(); + if (flicker) { + waver = 0.7F + random.nextFloat() * 0.3F; + } + } + + @Override + public void render(VertexConsumer buffer, Camera camera, float partialTicks) { + float t = Mth.clamp((age + partialTicks) / lifetime, 0.0F, 1.0F); + if (flicker) { + // Burning: hold, then die down over the last third. + alpha = peak * waver * Math.min(1.0F, (1.0F - t) * 3.0F); + } else { + // A flash: brightest at once, gone quickly. + float left = 1.0F - t; + alpha = peak * left * left; + } + super.render(buffer, camera, partialTicks); + } + + @Override + public float getQuadSize(float partialTicks) { + float t = Mth.clamp((age + partialTicks) / lifetime, 0.0F, 1.0F); + return flicker ? size * (0.85F + 0.15F * waver) : size * (0.6F + 0.4F * Math.min(1.0F, t * 4.0F)); + } + + @Override + protected int getLightColor(float partialTicks) { + return LightTexture.FULL_BRIGHT; + } + + @Override + public ParticleRenderType getRenderType() { + return Glow.PARTICLE_LAYER; + } + + public static class Provider implements ParticleProvider { + private final SpriteSet sprites; + + public Provider(SpriteSet sprites) { + this.sprites = sprites; + } + + @Override + public @Nullable Particle createParticle(GlowParticleOptions options, ClientLevel level, double x, double y, double z, + double xd, double yd, double zd) { + float strength = Glow.strength(level, BlockPos.containing(x, y, z)); + if (strength <= 0.0F) { + return null; + } + return new GlowParticle(level, x, y, z, xd, yd, zd, options, sprites.get(level.random), strength); + } + } +} From d35ae30da5b406da9177002cca8db32fa3b3e9e1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:37 +0800 Subject: [PATCH 194/314] Draw the A-10 through 1.21.1's render call --- .../mcheli/client/render/A10Renderer.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java new file mode 100644 index 0000000..a212650 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/A10Renderer.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.entity.projectile.A10Entity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; + +/** The close air support A-10, {@code MCH_RenderA10}. */ +public class A10Renderer extends EntityRenderer { + private static final ResourceLocation TEXTURE = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/bullets/a10.png"); + + public A10Renderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 10.5F; + } + + @Override + public void render(A10Entity entity, float entityYaw, float partialTicks, PoseStack pose, MultiBufferSource buffers, int light) { + Mesh mesh = entity.isVisible() ? MeshCache.INSTANCE.get("", "a-10") : null; + if (mesh != null) { + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-entity.getYRot(partialTicks))); + pose.mulPose(Axis.XP.rotationDegrees(-entity.getXRot(partialTicks))); + MeshRenderer.emit(mesh.groups(), pose.last(), buffers.getBuffer(RenderType.entityCutout(TEXTURE)), light, OverlayTexture.NO_OVERLAY); + pose.popPose(); + } + super.render(entity, entityYaw, partialTicks, pose, buffers, light); + } + + @Override + public ResourceLocation getTextureLocation(A10Entity entity) { + return TEXTURE; + } +} From 5e41d86a2862c1ace1096e1074acfd49351455a5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:38 +0800 Subject: [PATCH 195/314] Keep the vehicle render state as plain data on 1.21.1 --- .../client/render/AircraftRenderState.java | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java new file mode 100644 index 0000000..626907f --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderState.java @@ -0,0 +1,67 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import org.jspecify.annotations.Nullable; + +/** + * Snapshot of a vehicle for one frame, filled in by {@link AircraftRenderer} before it draws, the + * way 26.x's render states work. Every angle is already interpolated for the frame's partial tick. + */ +public class AircraftRenderState { + /** Packed block and sky light at the vehicle. */ + public int lightCoords; + /** Definitions are immutable after loading, so the snapshot can share them. */ + public @Nullable AircraftDefinition definition; + public String texture = ""; + public float yaw; + public float pitch; + public float roll; + /** Helicopter main rotor or plane propeller angle. */ + public float rotorAngle; + public float throttle; + + // Part states, 0 to 90 degrees + public float canopy; + public float landingGear; + public float hatch; + public float nozzle; + public float wing; + + public float wheelRoll; + public float steer; + public final float[] roller = new float[2]; + /** Track run per side from 0 to 1, in link spacings. */ + public final float[] track = new float[2]; + public float[] rotatingParts = new float[0]; + + /** Per seat: occupant view relative to the hull, and whether anyone sits there. */ + public float[] seatYaw = new float[0]; + public float[] seatPitch = new float[0]; + public boolean[] seatOccupied = new boolean[0]; + /** Seat of the player at this client, or -1, and whether they look through their own eyes. */ + public int localSeat = -1; + public boolean localFirstPerson; + /** Shade for the unlit glass, from the light at the vehicle. */ + public int glassColor = 0xFFFFFFFF; + + /** Per {@code AddPartWeapon} line, the state of the weapon it belongs to; see {@link WeaponPartState}. */ + public WeaponPartState[] weaponParts = new WeaponPartState[0]; + /** Per weapon bay, 0 to 90 degrees. */ + public float[] bays = new float[0]; + + /** How one weapon part is drawn this frame. */ + public static final class WeaponPartState { + /** Whether the part's weapon exists; without one the part follows its operator's view. */ + public boolean hasWeapon; + /** Aim relative to the weapon's default direction. */ + public float yaw; + public float pitch; + public float defaultYaw; + /** How far a turret weapon lags behind the pilot's view. */ + public float turretLag; + public float recoil; + public float barrel; + /** A missile part whose round is on its way and not reloaded yet. */ + public boolean hidden; + } +} From 778a88c06561745b66b3e858a4c77685d3074bb2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:38 +0800 Subject: [PATCH 196/314] Draw vehicles through 1.21.1's render call and buffer source --- .../client/render/AircraftRenderer.java | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java new file mode 100644 index 0000000..29086b0 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/AircraftRenderer.java @@ -0,0 +1,227 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.part.WeaponPart; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.AnimationState; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PartState; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.culling.Frustum; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraft.world.phys.AABB; +import org.jspecify.annotations.Nullable; + +/** + * Draws any MCHeli vehicle from its MQO/OBJ model, following {@code MCH_RenderAircraft}: the hull, + * the kind's own moving parts (rotors, wings, gun mounts) and then the parts every kind can have. + */ +public class AircraftRenderer extends EntityRenderer { + /** Reused every frame; rendering is single threaded. */ + private final AircraftRenderState state = new AircraftRenderState(); + + public AircraftRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 2.0F; + } + + /** What 26.x splits off as the render state: everything the frame needs, read from the entity. */ + private void extractRenderState(T entity, AircraftRenderState state, float partialTicks, int light) { + state.lightCoords = light; + AircraftDefinition def = entity.definition(); + state.definition = def; + state.texture = entity.texture(); + state.yaw = entity.getYRot(partialTicks); + state.pitch = entity.getXRot(partialTicks); + state.roll = entity.roll(partialTicks); + if (def == null) { + return; + } + + state.rotorAngle = switch (entity) { + case HelicopterEntity heli -> heli.rotorAngle(partialTicks); + case PlaneEntity plane -> plane.rotorAngle(partialTicks); + default -> 0.0F; + }; + state.canopy = angle(entity.canopy(), partialTicks); + state.landingGear = angle(entity.landingGear(), partialTicks); + state.hatch = angle(entity.hatch(), partialTicks); + if (entity instanceof PlaneEntity plane) { + state.nozzle = angle(plane.nozzle(), partialTicks); + state.wing = angle(plane.wing(), partialTicks); + } else { + state.nozzle = 0.0F; + state.wing = 0.0F; + } + + AnimationState anim = entity.animation; + state.throttle = Mth.lerp(partialTicks, anim.prevThrottle, anim.throttle); + state.wheelRoll = Mth.lerp(partialTicks, anim.prevWheelRoll, anim.wheelRoll); + state.steer = Mth.lerp(partialTicks, anim.prevSteer, anim.steer); + for (int i = 0; i < 2; i++) { + state.roller[i] = Mth.lerp(partialTicks, anim.prevRoller[i], anim.roller[i]); + state.track[i] = lerpWrapped(anim.prevTrack[i], anim.track[i], partialTicks); + } + if (state.rotatingParts.length != anim.rotatingParts.length) { + state.rotatingParts = new float[anim.rotatingParts.length]; + } + for (int i = 0; i < anim.rotatingParts.length; i++) { + state.rotatingParts[i] = Mth.lerp(partialTicks, anim.prevRotatingParts[i], anim.rotatingParts[i]); + } + + int seats = def.seatAndRackCount(); + if (state.seatYaw.length != seats) { + state.seatYaw = new float[seats]; + state.seatPitch = new float[seats]; + state.seatOccupied = new boolean[seats]; + } + for (int i = 0; i < seats; i++) { + state.seatYaw[i] = anim.seatYaw(i, partialTicks); + state.seatPitch[i] = anim.seatPitch(i, partialTicks); + state.seatOccupied[i] = entity.occupant(i) != null; + } + Minecraft mc = Minecraft.getInstance(); + state.localSeat = mc.player != null ? entity.seatOf(mc.player) : -1; + state.localFirstPerson = mc.options.getCameraType().isFirstPerson(); + state.glassColor = glassColor(entity, state.lightCoords); + extractWeapons(entity, def, state, partialTicks); + } + + /** + * Glass is drawn without the light map, so shade it by hand: as bright as the brighter of block + * light and the sky light left after night or rain darkens it. + */ + private static int glassColor(AircraftEntity entity, int lightCoords) { + int sky = Math.max(LightTexture.sky(lightCoords) - entity.level().getSkyDarken(), 0); + int light = Math.max(LightTexture.block(lightCoords), sky); + int shade = (int) ((0.3F + 0.7F * light / 15.0F) * 255.0F); + return 0xFF000000 | shade << 16 | shade << 8 | shade; + } + + /** {@code renderWeapon}'s inputs: each part's weapon aim, recoil, barrel spin and reload state. */ + private static void extractWeapons(AircraftEntity entity, AircraftDefinition def, AircraftRenderState state, float partialTicks) { + VehicleWeapons weapons = entity.weapons(); + int parts = def.weaponParts.size(); + if (state.weaponParts.length != parts) { + state.weaponParts = new AircraftRenderState.WeaponPartState[parts]; + for (int i = 0; i < parts; i++) { + state.weaponParts[i] = new AircraftRenderState.WeaponPartState(); + } + } + WeaponSet previous = null; + int mount = 0; + for (int i = 0; i < parts; i++) { + WeaponPart part = def.weaponParts.get(i); + AircraftRenderState.WeaponPartState out = state.weaponParts[i]; + WeaponSet set = weapons != null && !part.weapons().isEmpty() ? weapons.byName(part.weapons().getFirst()) : null; + if (set != previous) { + mount = 0; + previous = set; + } + out.hasWeapon = set != null; + if (set == null) { + continue; + } + float yaw = set.rotationYaw; + float prevYaw = set.prevRotationYaw; + if (yaw - prevYaw > 180.0F) { + prevYaw += 360.0F; + } else if (yaw - prevYaw < -180.0F) { + prevYaw -= 360.0F; + } + out.yaw = Mth.lerp(partialTicks, prevYaw, yaw) - set.defaultRotationYaw; + out.pitch = Mth.lerp(partialTicks, set.prevRotationPitch, set.rotationPitch); + out.defaultYaw = set.defaultRotationYaw; + out.turretLag = set.rotationTurretYaw; + out.barrel = Mth.lerp(partialTicks, set.prevBarrel, set.barrel); + // A part shared by several weapons kicks back with whichever fired hardest. + float recoil = set.recoil(0, partialTicks); + for (String name : part.weapons()) { + WeaponSet other = weapons.byName(name); + if (other != null) { + recoil = Math.max(recoil, other.recoil(0, partialTicks)); + } + } + out.recoil = recoil; + out.hidden = part.missile() && weapons.isMountUsed(weapons.indexOf(set.name()), mount); + mount++; + } + if (state.bays.length != def.weaponBays.size()) { + state.bays = new float[def.weaponBays.size()]; + } + for (int i = 0; i < state.bays.length; i++) { + state.bays[i] = weapons != null ? weapons.bayAngle(i, partialTicks) : 0.0F; + } + } + + private static float angle(@Nullable PartState part, float partialTicks) { + return part != null ? part.angle(partialTicks) : 0.0F; + } + + /** Interpolates a value that wraps from 1 back to 0. */ + private static float lerpWrapped(float prev, float current, float partialTicks) { + float d = current - prev; + if (d > 0.5F) { + d -= 1.0F; + } else if (d < -0.5F) { + d += 1.0F; + } + float v = prev + d * partialTicks; + return v - Mth.floor(v); + } + + @Override + public boolean shouldRender(T entity, Frustum frustum, double camX, double camY, double camZ) { + return entity.shouldRender(camX, camY, camZ) && frustum.isVisible(cullingBox(entity)); + } + + /** The hit box is far smaller than most models, so cull on a generous box instead. */ + private static AABB cullingBox(AircraftEntity entity) { + AircraftDefinition def = entity.definition(); + double reach = def != null ? Math.max(8.0, def.markerWidth * 4.0) : 8.0; + return entity.getBoundingBox().inflate(reach, reach / 2.0, reach); + } + + @Override + public void render(T entity, float entityYaw, float partialTicks, PoseStack poseStack, MultiBufferSource buffers, int light) { + extractRenderState(entity, state, partialTicks, light); + AircraftDefinition def = state.definition; + Mesh mesh = def != null ? MeshCache.INSTANCE.get(def.folder(), def.name()) : null; + if (mesh != null) { + // Canopies and windshields are semi-transparent texels; PartRenderer draws those + // triangles blended, as the original did, and everything else as cutout. + poseStack.pushPose(); + poseStack.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + poseStack.mulPose(Axis.XP.rotationDegrees(state.pitch)); + poseStack.mulPose(Axis.ZP.rotationDegrees(state.roll)); + new PartRenderer(def, mesh, state, poseStack, buffers, texture(def.folder(), state.texture), state.lightCoords, + OverlayTexture.NO_OVERLAY, state.glassColor).submitAll(); + poseStack.popPose(); + } + super.render(entity, entityYaw, partialTicks, poseStack, buffers, light); + } + + /** Vehicles bind their own textures per part; this is only for vanilla's bookkeeping. */ + @Override + public ResourceLocation getTextureLocation(T entity) { + return texture(entity.definition() != null ? entity.definition().folder() : "", entity.texture()); + } + + private static ResourceLocation texture(String folder, String name) { + return RuntimeAssets.texture(ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/" + folder + "/" + name + ".png")); + } +} From a6f68737b2c80b6061e5fb00c2d2db463fd3961e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:38 +0800 Subject: [PATCH 197/314] Draw flare glows through 1.21.1's render call --- .../mcheli/client/render/FlareRenderer.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java new file mode 100644 index 0000000..4d577f0 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/FlareRenderer.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.entity.projectile.FlareEntity; +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; + +/** + * Flares and the burning debris of a crash were only their particle trails. Not in the original: + * a burning flare now glows, which is what makes a flare salvo light up the night sky. Smoke + * grenades stay invisible. + */ +public class FlareRenderer extends EntityRenderer { + private static final float RADIUS = 1.4F; + + public FlareRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.0F; + } + + @Override + public void render(FlareEntity entity, float entityYaw, float partialTicks, PoseStack pose, MultiBufferSource buffers, int light) { + float glow = entity.isSmoke() || entity.isInWater() ? 0.0F : Glow.strength(entity.level(), entity.blockPosition()); + // A cheap waver keyed to the entity, so neighbouring flares do not pulse in step. + float flicker = 0.8F + 0.2F * (float) Math.sin((entity.tickCount + partialTicks) * 1.7F + entity.getId()); + Quaternionf camera = entityRenderDispatcher.cameraOrientation(); + Glow.halo(pose, buffers, camera, Vec3.ZERO, RADIUS * flicker, GlowParticleOptions.FIRE, glow); + Glow.halo(pose, buffers, camera, Vec3.ZERO, RADIUS * 0.35F, GlowParticleOptions.FLASH, glow); + super.render(entity, entityYaw, partialTicks, pose, buffers, light); + } + + @Override + public ResourceLocation getTextureLocation(FlareEntity entity) { + return Glow.TEXTURE; + } +} From 057c853b00b7c0aa940150c5a8b39fa8a5aa218f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:39 +0800 Subject: [PATCH 198/314] Read texture alpha the 1.21.1 way --- .../mcheli/client/render/GlassSplit.java | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java new file mode 100644 index 0000000..f80e3ae --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/GlassSplit.java @@ -0,0 +1,145 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.mojang.blaze3d.platform.NativeImage; +import net.minecraft.resources.ResourceLocation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Sorts a model's triangles into opaque ones and see-through glass, by the alpha of the texture + * under each triangle. + * + *

Drawing a whole vehicle translucent made its glass write depth before the game drew water, + * so water behind a canopy vanished. Opaque triangles now draw as cutout, like any entity, and + * only the glass draws translucent, through a type that leaves the depth buffer alone. + */ +final class GlassSplit { + static final GlassSplit INSTANCE = new GlassSplit(); + private static final Logger LOGGER = LoggerFactory.getLogger(GlassSplit.class); + /** Texels between these alphas are glass; fully clear ones are cut out, solid ones opaque. */ + private static final int MIN_GLASS_ALPHA = 10; + private static final int MAX_GLASS_ALPHA = 245; + + record Split(List opaque, List glass) { + static final Split EMPTY = new Split(List.of(), List.of()); + } + + private record Alpha(int width, int height, byte[] values) { + int at(float u, float v) { + int x = Math.floorMod((int) Math.floor(u * width), width); + int y = Math.floorMod((int) Math.floor(v * height), height); + return values[y * width + x] & 0xFF; + } + } + + private final Map> textures = new HashMap<>(); + private final Map, Map> splits = new IdentityHashMap<>(); + + private GlassSplit() { + } + + /** Called from the render thread only. */ + Split split(List groups, ResourceLocation texture) { + if (groups.isEmpty()) { + return Split.EMPTY; + } + return splits.computeIfAbsent(groups, g -> new HashMap<>()).computeIfAbsent(texture, t -> compute(groups, t)); + } + + void clear() { + textures.clear(); + splits.clear(); + } + + private Split compute(List groups, ResourceLocation texture) { + Alpha alpha = textures.computeIfAbsent(texture, GlassSplit::load).orElse(null); + if (alpha == null) { + return new Split(groups, List.of()); + } + List opaque = new ArrayList<>(); + List glass = new ArrayList<>(); + for (MeshGroup group : groups) { + split(group, alpha, opaque, glass); + } + return new Split(List.copyOf(opaque), List.copyOf(glass)); + } + + private static void split(MeshGroup group, Alpha alpha, List opaque, List glass) { + float[] uv = group.uvs(); + int triangles = group.vertexCount() / 3; + boolean[] isGlass = new boolean[triangles]; + int glassCount = 0; + for (int t = 0; t < triangles; t++) { + int i = t * 6; + float u = (uv[i] + uv[i + 2] + uv[i + 4]) / 3.0F; + float v = (uv[i + 1] + uv[i + 3] + uv[i + 5]) / 3.0F; + int a = alpha.at(u, v); + if (a > MIN_GLASS_ALPHA && a < MAX_GLASS_ALPHA) { + isGlass[t] = true; + glassCount++; + } + } + if (glassCount == 0) { + opaque.add(group); + } else if (glassCount == triangles) { + glass.add(group); + } else { + opaque.add(pick(group, isGlass, false, triangles - glassCount)); + glass.add(pick(group, isGlass, true, glassCount)); + } + } + + private static MeshGroup pick(MeshGroup group, boolean[] isGlass, boolean wanted, int count) { + float[] p = new float[count * 9]; + float[] n = new float[count * 9]; + float[] uv = new float[count * 6]; + int out = 0; + for (int t = 0; t < isGlass.length; t++) { + if (isGlass[t] != wanted) { + continue; + } + System.arraycopy(group.positions(), t * 9, p, out * 9, 9); + System.arraycopy(group.normals(), t * 9, n, out * 9, 9); + System.arraycopy(group.uvs(), t * 6, uv, out * 6, 6); + out++; + } + return new MeshGroup(group.name(), p, n, uv); + } + + private static Optional load(ResourceLocation texture) { + try (InputStream in = RuntimeAssets.open(texture)) { + if (in == null) { + return Optional.empty(); + } + return Optional.of(read(in)); + } catch (IOException | RuntimeException e) { + LOGGER.warn("Could not read {} to find its glass; drawing it opaque", texture, e); + return Optional.empty(); + } + } + + private static Alpha read(InputStream in) throws IOException { + try (NativeImage image = NativeImage.read(in)) { + int w = image.getWidth(); + int h = image.getHeight(); + byte[] values = new byte[w * h]; + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + values[y * w + x] = (byte) (image.getPixelRGBA(x, y) >>> 24); + } + } + return new Alpha(w, h, values); + } + } +} From 92285c97da23b4acdcd83027de01156ced2ce463 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:39 +0800 Subject: [PATCH 199/314] Build the additive glow render and particle types for 1.21.1 --- .../mcheli/client/render/Glow.java | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java new file mode 100644 index 0000000..305050d --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/Glow.java @@ -0,0 +1,188 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.ClientConfig; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.effect.NightGlow; +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.Tesselator; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.blaze3d.vertex.VertexFormat; +import net.minecraft.client.particle.ParticleRenderType; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderStateShard; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.client.renderer.texture.TextureAtlas; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.FastColor; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; + +/** + * Not in the original: light that adds to the scene instead of covering it, for tracers, rocket + * motors, flares and fireballs. Everything here blends additively and leaves the depth buffer + * alone, so overlapping glows brighten each other and never cut holes in the smoke behind them. + */ +public final class Glow { + /** A white disc fading to nothing at the rim; also the {@code mcheli:glow} particle sprite. */ + public static final ResourceLocation TEXTURE = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/particle/glow.png"); + + /** The particle render type for {@code mcheli:glow}: additive, depth tested, no depth writes. */ + public static final ParticleRenderType PARTICLE_LAYER = new ParticleRenderType() { + @Override + public BufferBuilder begin(Tesselator tesselator, TextureManager textures) { + RenderSystem.depthMask(false); + RenderSystem.setShaderTexture(0, TextureAtlas.LOCATION_PARTICLES); + RenderSystem.enableBlend(); + RenderSystem.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE); + return tesselator.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.PARTICLE); + } + + @Override + public String toString() { + return "MCHELI_GLOW"; + } + }; + + private static final RenderType HALO = Types.halo(); + + private static final int FULL_BRIGHT = LightTexture.FULL_BRIGHT; + + private Glow() { + } + + /** + * How strongly something at {@code pos} glows: faint by day, full at night, in caves and + * under water. 0 when the player turned the glow off. + */ + public static float strength(Level level, BlockPos pos) { + if (!ClientConfig.nightGlow()) { + return 0.0F; + } + return NightGlow.strength(level.getBrightness(LightLayer.BLOCK, pos), level.getBrightness(LightLayer.SKY, pos), level.getSkyDarken()); + } + + /** + * A disc of light facing the camera, centred {@code offset} away from the current pose. + * + * @param camera the camera's orientation, from the entity render dispatcher + * @param color RGB; {@code strength} becomes the alpha, which scales the light added + */ + public static void halo(PoseStack pose, MultiBufferSource buffers, Quaternionf camera, Vec3 offset, float radius, int color, + float strength) { + if (strength <= 0.0F || radius <= 0.0F) { + return; + } + int argb = FastColor.ARGB32.color(alpha(strength), color); + pose.pushPose(); + pose.translate(offset.x, offset.y, offset.z); + pose.mulPose(camera); + PoseStack.Pose p = pose.last(); + VertexConsumer buffer = buffers.getBuffer(HALO); + // Both windings, so culling never hides it whichever way the quad ends up facing. + vertex(buffer, p, -radius, -radius, 0.0F, 0.0F, 1.0F, argb); + vertex(buffer, p, radius, -radius, 0.0F, 1.0F, 1.0F, argb); + vertex(buffer, p, radius, radius, 0.0F, 1.0F, 0.0F, argb); + vertex(buffer, p, -radius, radius, 0.0F, 0.0F, 0.0F, argb); + vertex(buffer, p, -radius, radius, 0.0F, 0.0F, 0.0F, argb); + vertex(buffer, p, radius, radius, 0.0F, 1.0F, 0.0F, argb); + vertex(buffer, p, radius, -radius, 0.0F, 1.0F, 1.0F, argb); + vertex(buffer, p, -radius, -radius, 0.0F, 0.0F, 1.0F, argb); + pose.popPose(); + } + + /** + * A tracer: a ribbon of light from the current pose back along {@code tail}, turned to face + * the camera, bright at the head and fading out towards the end. + * + * @param toCamera from the camera to the head, so the ribbon can be turned side on to the view + */ + public static void streak(PoseStack pose, MultiBufferSource buffers, Vec3 toCamera, Vec3 tail, float width, int color, float strength) { + if (strength <= 0.0F || tail.lengthSqr() < 1.0E-4) { + return; + } + Vec3 side = tail.cross(toCamera); + if (side.lengthSqr() < 1.0E-8) { + // Seen exactly end on; the halo at the head covers this. + return; + } + side = side.normalize().scale(width); + int head = FastColor.ARGB32.color(alpha(strength), color); + int end = FastColor.ARGB32.color(0, color); + float sx = (float) side.x; + float sy = (float) side.y; + float sz = (float) side.z; + float tx = (float) tail.x; + float ty = (float) tail.y; + float tz = (float) tail.z; + PoseStack.Pose p = pose.last(); + VertexConsumer buffer = buffers.getBuffer(HALO); + // The middle row of the disc texture gives the ribbon soft edges. + vertex(buffer, p, sx, sy, sz, 0.0F, 0.5F, head); + vertex(buffer, p, -sx, -sy, -sz, 1.0F, 0.5F, head); + vertex(buffer, p, tx - sx, ty - sy, tz - sz, 1.0F, 0.5F, end); + vertex(buffer, p, tx + sx, ty + sy, tz + sz, 0.0F, 0.5F, end); + vertex(buffer, p, tx + sx, ty + sy, tz + sz, 0.0F, 0.5F, end); + vertex(buffer, p, tx - sx, ty - sy, tz - sz, 1.0F, 0.5F, end); + vertex(buffer, p, -sx, -sy, -sz, 1.0F, 0.5F, head); + vertex(buffer, p, sx, sy, sz, 0.0F, 0.5F, head); + } + + private static int alpha(float strength) { + return Math.clamp(Math.round(strength * 255.0F), 0, 255); + } + + private static void vertex(VertexConsumer buffer, PoseStack.Pose pose, float x, float y, float z, float u, float v, int argb) { + buffer.addVertex(pose, x, y, z) + .setColor(argb) + .setUv(u, v) + .setOverlay(OverlayTexture.NO_OVERLAY) + .setLight(FULL_BRIGHT) + .setNormal(pose, 0.0F, 1.0F, 0.0F); + } + + /** + * Builds the halo render type. A subclass only to reach the protected render state shards; + * {@code RenderType.create} is not public on every loader, the constructor is. + */ + private abstract static class Types extends RenderType { + private Types() { + super("", DefaultVertexFormat.NEW_ENTITY, VertexFormat.Mode.QUADS, 0, false, false, () -> { + }, () -> { + }); + } + + /** The eyes shader (full bright, no light map), additive, depth tested without depth writes. */ + static RenderType halo() { + RenderStateShard[] shards = { + RENDERTYPE_EYES_SHADER, + new TextureStateShard(TEXTURE, false, false), + LIGHTNING_TRANSPARENCY, + COLOR_WRITE, + LEQUAL_DEPTH_TEST + }; + return new RenderType(MCHeli.MOD_ID + "_glow", DefaultVertexFormat.NEW_ENTITY, VertexFormat.Mode.QUADS, + TRANSIENT_BUFFER_SIZE, false, true, + () -> { + for (RenderStateShard shard : shards) { + shard.setupRenderState(); + } + }, + () -> { + for (RenderStateShard shard : shards) { + shard.clearRenderState(); + } + }) { + }; + } + } +} From c005034c90aa950867c36976b28d873b07b53a68 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:40 +0800 Subject: [PATCH 200/314] Add MeshCache to the shared 1.21.1 sources --- .../mcheli/client/render/MeshCache.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java new file mode 100644 index 0000000..1fc66d2 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/MeshCache.java @@ -0,0 +1,82 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.client.RuntimeAssets; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshLoader; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.ResourceManagerReloadListener; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Lazily loads vehicle models, from the resource packs (so they can replace them) or else from the + * runtime roots of {@link RuntimeAssets}. Models are parsed the first time a vehicle of that kind + * is drawn, and dropped on resource reload and on content reload ({@link #clear}). + */ +public final class MeshCache implements ResourceManagerReloadListener { + public static final MeshCache INSTANCE = new MeshCache(); + private static final Logger LOGGER = LoggerFactory.getLogger(MeshCache.class); + private static final String[] EXTENSIONS = {".mqo", ".obj"}; + + private final Map> meshes = new ConcurrentHashMap<>(); + + private MeshCache() { + } + + /** + * @param folder content folder, for example {@code helicopters}, or empty for the models root + * @param name definition name + * @return the mesh, or {@code null} if no model exists or it failed to load + */ + public @Nullable Mesh get(String folder, String name) { + return meshes.computeIfAbsent(folder + '/' + name, key -> load(folder, name, true)).orElse(null); + } + + /** Like {@link #get}, for optional models such as separate part files; a missing one is not logged. */ + public @Nullable Mesh find(String folder, String name) { + return meshes.computeIfAbsent(folder + '/' + name, key -> load(folder, name, false)).orElse(null); + } + + private static Optional load(String folder, String name, boolean required) { + for (String extension : EXTENSIONS) { + ResourceLocation id = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, + (folder.isEmpty() ? "models/" : "models/" + folder + "/") + name + extension); + try (InputStream in = RuntimeAssets.open(id)) { + if (in == null) { + continue; + } + Mesh mesh = MeshLoader.load(id.toString(), in); + LOGGER.debug("Loaded {}", mesh); + return Optional.of(mesh); + } catch (IOException | RuntimeException e) { + LOGGER.error("Failed to load model {}", id, e); + return Optional.empty(); + } + } + if (required) { + LOGGER.warn("No model found for {}/{}", folder, name); + } + return Optional.empty(); + } + + /** Forgets every model and glass split, so the next frame reads them again from disk. */ + public void clear() { + meshes.clear(); + GlassSplit.INSTANCE.clear(); + } + + @Override + public void onResourceManagerReload(ResourceManager resourceManager) { + clear(); + RuntimeAssets.clearTextures(); + } +} From fb37f06bb3e707baa956716ad6a029d9914a8d74 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:40 +0800 Subject: [PATCH 201/314] Add MeshRenderer to the shared 1.21.1 sources --- .../mcheli/client/render/MeshRenderer.java | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java new file mode 100644 index 0000000..47cc66e --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/MeshRenderer.java @@ -0,0 +1,88 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.model.MeshGroup; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import org.joml.Matrix4f; +import org.joml.Vector3f; + +import java.util.List; + +/** Streams {@link MeshGroup} triangles into an entity render type. */ +final class MeshRenderer { + private static final int WHITE = 0xFFFFFFFF; + + private MeshRenderer() { + } + + /** + * Entity render types draw quads, so each triangle is sent as a quad whose last corner repeats + * the third one. The GPU discards the degenerate half for free. + */ + static void emit(List groups, PoseStack.Pose pose, VertexConsumer buffer, int light, int overlay) { + emit(groups, pose, buffer, light, overlay, WHITE); + } + + /** As above, tinted with an ARGB colour, for bullets drawn in their {@code BulletColor}. */ + static void emit(List groups, PoseStack.Pose pose, VertexConsumer buffer, int light, int overlay, int color) { + for (MeshGroup group : groups) { + float[] p = group.positions(); + float[] n = group.normals(); + float[] uv = group.uvs(); + int vertices = group.vertexCount(); + for (int v = 0; v < vertices; v += 3) { + vertex(buffer, pose, p, n, uv, v, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 1, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 2, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 2, light, overlay, color); + } + } + } + + /** + * As above, but only the triangles that face the camera. No vanilla type is both culled and + * free of depth writes, so glass is culled here. Back to back glass faces would otherwise sort + * in an arbitrary order and swap places as the view moves. + * + *

Entities are submitted in world-aligned space with the camera at the origin, so a front + * face (counter-clockwise, as in the original's GL setup) is one whose normal points back + * towards the origin. + */ + static void emitFrontFaces(List groups, PoseStack.Pose pose, VertexConsumer buffer, int light, int overlay, int color) { + Matrix4f m = pose.pose(); + Vector3f a = new Vector3f(); + Vector3f b = new Vector3f(); + Vector3f c = new Vector3f(); + for (MeshGroup group : groups) { + float[] p = group.positions(); + float[] n = group.normals(); + float[] uv = group.uvs(); + int vertices = group.vertexCount(); + for (int v = 0; v < vertices; v += 3) { + m.transformPosition(p[v * 3], p[v * 3 + 1], p[v * 3 + 2], a); + m.transformPosition(p[v * 3 + 3], p[v * 3 + 4], p[v * 3 + 5], b); + m.transformPosition(p[v * 3 + 6], p[v * 3 + 7], p[v * 3 + 8], c); + b.sub(a); + c.sub(a); + if (b.cross(c).dot(a) >= 0.0F) { + continue; + } + vertex(buffer, pose, p, n, uv, v, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 1, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 2, light, overlay, color); + vertex(buffer, pose, p, n, uv, v + 2, light, overlay, color); + } + } + } + + private static void vertex(VertexConsumer buffer, PoseStack.Pose pose, float[] p, float[] n, float[] uv, int i, int light, int overlay, int color) { + int a = i * 3; + int t = i * 2; + buffer.addVertex(pose, p[a], p[a + 1], p[a + 2]) + .setColor(color) + .setUv(uv[t], uv[t + 1]) + .setOverlay(overlay) + .setLight(light) + .setNormal(pose, n[a], n[a + 1], n[a + 2]); + } +} From 4d9399b97cf8f92782c195bd289d8be50f7e8fbc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:40 +0800 Subject: [PATCH 202/314] Send vehicle parts to 1.21.1's buffer source --- .../mcheli/client/render/PartRenderer.java | 517 ++++++++++++++++++ 1 file changed, 517 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java new file mode 100644 index 0000000..c367447 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/PartRenderer.java @@ -0,0 +1,517 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; +import com.jnhprojects.mcheli.content.aircraft.PlaneDefinition; +import com.jnhprojects.mcheli.content.aircraft.RotorDefinition; +import com.jnhprojects.mcheli.content.aircraft.VehicleDefinition; +import com.jnhprojects.mcheli.content.aircraft.WeaponMount; +import com.jnhprojects.mcheli.content.aircraft.part.CameraPart; +import com.jnhprojects.mcheli.content.aircraft.part.CrawlerTrack; +import com.jnhprojects.mcheli.content.aircraft.part.LandingGearPart; +import com.jnhprojects.mcheli.content.aircraft.part.MovingPart; +import com.jnhprojects.mcheli.content.aircraft.part.PropellerPart; +import com.jnhprojects.mcheli.content.aircraft.part.RotatingPart; +import com.jnhprojects.mcheli.content.aircraft.part.ThrottlePart; +import com.jnhprojects.mcheli.content.aircraft.part.TrackRoller; +import com.jnhprojects.mcheli.content.aircraft.part.VehiclePart; +import com.jnhprojects.mcheli.content.aircraft.part.WeaponBayPart; +import com.jnhprojects.mcheli.content.aircraft.part.WeaponPart; +import com.jnhprojects.mcheli.content.aircraft.part.WheelPart; +import com.jnhprojects.mcheli.content.aircraft.part.WingPart; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.model.MeshGroup; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import org.joml.Vector3f; + +import java.util.List; + +/** + * Submits a vehicle's hull and moving parts for one frame. A port of the part routines in + * {@code MCH_RenderAircraft} and the per kind renderers; every transform is applied in hull space + * on top of the pose the caller already set up. + * + *

A part is looked up as {@code $name} inside the vehicle model, or as a separate + * {@code _} model file, the two places the original accepted. + */ +final class PartRenderer { + private static final Vector3 X_AXIS = new Vector3(1, 0, 0); + private static final Vector3 DOWN_AXIS = new Vector3(0, -1, 0); + + private final AircraftDefinition def; + private final Mesh mesh; + private final AircraftRenderState state; + private final PoseStack pose; + private final MultiBufferSource buffers; + private final ResourceLocation texture; + private final RenderType opaqueType; + private final RenderType glassType; + private final int light; + private final int overlay; + /** Glass draws unlit, so its colour carries the brightness of where the vehicle stands. */ + private final int glassColor; + + PartRenderer(AircraftDefinition def, Mesh mesh, AircraftRenderState state, PoseStack pose, MultiBufferSource buffers, + ResourceLocation texture, int light, int overlay, int glassColor) { + this.def = def; + this.mesh = mesh; + this.state = state; + this.pose = pose; + this.buffers = buffers; + this.texture = texture; + // Culled, as the original drew it. The models are full of back to back faces for thin + // panels, and drawing both sides of those makes them fight for the same depth. + this.opaqueType = RenderType.entityCutout(texture); + // The one entity translucent type that does not write depth, so water behind glass still + // draws. It does not cull, so MeshRenderer drops the back faces itself. + this.glassType = RenderType.entityTranslucentEmissive(texture); + this.light = light; + this.overlay = overlay; + this.glassColor = glassColor; + } + + void submitAll() { + switch (def) { + case PlaneDefinition plane -> { + nozzles(plane); + wings(plane); + propellers(plane); + submit(mesh.body()); + } + case HelicopterDefinition heli -> { + submit(mesh.body()); + rotors(heli); + } + case VehicleDefinition vehicle -> { + submit(mesh.body()); + for (VehiclePart part : vehicle.parts) { + vehiclePart(vehicle, part); + } + } + default -> submit(mesh.body()); + } + weapons(); + rotatingParts(); + movingParts(def.hatches, state.hatch); + trackRollers(); + crawlerTracks(); + steeringWheels(); + wheels(); + throttles(); + cameras(); + landingGear(); + weaponBays(); + movingParts(def.canopies, state.canopy); + } + + // Kind specific parts + + private void rotors(HelicopterDefinition heli) { + for (RotorDefinition rotor : heli.rotors) { + List blade = part(rotor.part()); + if (blade.isEmpty() || isZero(rotor.axis())) { + continue; + } + int step = rotor.bladeStep() > 0 ? rotor.bladeStep() : 1; + int blades = Math.max(rotor.blades(), 1); + for (int i = 0; i < blades; i++) { + pose.pushPose(); + Vector3 p = rotor.position(); + translate(p, 1.0F); + rotate(state.rotorAngle + i * step, rotor.axis()); + if (!rotor.legacyPivot()) { + translate(p, -1.0F); + } + submit(blade); + pose.popPose(); + } + } + } + + private void nozzles(PlaneDefinition plane) { + for (MovingPart nozzle : plane.nozzles) { + pose.pushPose(); + rotateAround(nozzle.position(), state.nozzle, nozzle.axis()); + submit(part(nozzle.part())); + pose.popPose(); + } + } + + private void wings(PlaneDefinition plane) { + for (WingPart wing : plane.wings) { + pose.pushPose(); + rotateAround(wing.position(), state.wing * wing.factor(), wing.axis()); + submit(part(wing.part())); + for (MovingPart pylon : wing.pylons()) { + pose.pushPose(); + rotateAround(pylon.position(), state.wing * pylon.factor(), pylon.axis()); + submit(part(pylon.part())); + pose.popPose(); + } + pose.popPose(); + } + } + + /** Tilt rotors follow the nozzle state, the blades spin with the engine. */ + private void propellers(PlaneDefinition plane) { + if (!plane.hasNozzleState()) { + return; + } + for (PropellerPart rotor : plane.propellers) { + pose.pushPose(); + rotateAround(rotor.position(), state.nozzle * rotor.factor(), rotor.axis()); + submit(part(rotor.part())); + for (PropellerPart.Blade blade : rotor.blades()) { + List groups = part(blade.part()); + pose.pushPose(); + rotateAround(blade.position(), state.rotorAngle, blade.axis()); + for (int i = 0; i < blade.count(); i++) { + rotateAround(blade.position(), blade.step(), blade.axis()); + submit(groups); + } + pose.popPose(); + } + pose.popPose(); + } + } + + /** {@code MCH_RenderVehicle.drawPart}: mount parts turn with the gunner's view. */ + private void vehiclePart(VehicleDefinition vehicle, VehiclePart part) { + pose.pushPose(); + if (part.yaw() || part.pitch() || part.type() == VehiclePart.TYPE_ROTATING_BARREL) { + translate(part.position(), 1.0F); + if (part.yaw()) { + rotate(seatYaw(0), DOWN_AXIS); + } + if (part.pitch()) { + rotate(Mth.clamp(seatPitch(0), vehicle.minRotationPitch, vehicle.maxRotationPitch), X_AXIS); + } + translate(part.position(), -1.0F); + } + for (VehiclePart child : part.children()) { + vehiclePart(vehicle, child); + } + boolean gunnerLooking = state.localSeat == 0 && state.localFirstPerson; + if (part.drawFirstPerson() || !gunnerLooking) { + submit(part(part.part())); + } + pose.popPose(); + } + + // Parts every kind can have + + /** + * Gun mounts, turrets and missiles, {@code renderWeapon}. Each follows its weapon's aim, kicks + * back as it fires, spins its barrels and, for missiles, disappears until reloaded. A part + * whose weapon is missing follows the view of the seat that would fire it. + */ + private void weapons() { + float turretYaw = seatYaw(0); + for (int i = 0; i < def.weaponParts.size(); i++) { + WeaponPart w = def.weaponParts.get(i); + AircraftRenderState.WeaponPartState ws = i < state.weaponParts.length ? state.weaponParts[i] : null; + boolean live = ws != null && ws.hasWeapon; + WeaponMount mount = mount(w); + int seat = mount != null ? mount.seatIndex() : 0; + if (w.hideInFirstPerson() && state.localFirstPerson && state.localSeat == seat) { + continue; + } + float defaultYaw = live ? ws.defaultYaw : mount != null ? mount.defaultYaw() : 0.0F; + float yaw; + float pitch; + if (live) { + yaw = ws.yaw; + pitch = ws.pitch; + } else { + yaw = Mth.wrapDegrees(seatYaw(seat) - defaultYaw); + pitch = seatPitch(seat); + } + + pose.pushPose(); + if (w.turret()) { + rotateAround(def.turretPosition, turretYaw, DOWN_AXIS); + } + translate(w.position(), 1.0F); + if (w.yaw()) { + rotate(w.turret() && !live ? yaw - turretYaw : yaw, DOWN_AXIS); + } + if (w.turret() && live) { + rotate(-(turretYaw - ws.turretLag), DOWN_AXIS); + } + // Side facing guns pitch around their own axis, which the original did by turning + // into the gun's frame and flipping the sign for guns pointing left or right. + boolean sideways = false; + if ((int) defaultYaw != 0) { + float t = Mth.wrapDegrees(defaultYaw); + sideways = (t >= 45.0F && t <= 135.0F) || (t <= -45.0F && t >= -135.0F); + rotate(-defaultYaw, DOWN_AXIS); + } + if (w.pitch()) { + rotate(sideways ? -pitch : pitch, X_AXIS); + } + if (live && w.recoil() != 0.0F) { + pose.translate(0.0F, 0.0F, w.recoil() * ws.recoil); + } + if (mount != null || live) { + rotate(defaultYaw, DOWN_AXIS); + } + if (live && w.rotatingBarrel()) { + rotate(ws.barrel, w.axis()); + } + translate(w.position(), -1.0F); + if (!w.missile() || !live || !ws.hidden) { + submit(part(w.part())); + for (WeaponPart.Child child : w.children()) { + pose.pushPose(); + translate(child.position(), 1.0F); + if (child.yaw()) { + rotate(yaw, DOWN_AXIS); + } + if (live && (int) defaultYaw != 0) { + rotate(-defaultYaw, DOWN_AXIS); + } + if (child.pitch()) { + rotate(sideways ? -pitch : pitch, X_AXIS); + } + if (live && child.recoil() != 0.0F) { + pose.translate(0.0F, 0.0F, -child.recoil() * ws.recoil); + } + if (live && (int) defaultYaw != 0) { + rotate(defaultYaw, DOWN_AXIS); + } + translate(child.position(), -1.0F); + submit(part(child.part())); + pose.popPose(); + } + } + pose.popPose(); + } + } + + private WeaponMount mount(WeaponPart part) { + if (part.weapons().isEmpty()) { + return null; + } + int group = def.weaponGroup(part.weapons().getFirst()); + return group >= 0 ? def.weaponGroups.get(group).mounts().getFirst() : null; + } + + private void rotatingParts() { + for (int i = 0; i < def.rotatingParts.size() && i < state.rotatingParts.length; i++) { + RotatingPart part = def.rotatingParts.get(i); + pose.pushPose(); + rotateAround(part.position(), state.rotatingParts[i], part.axis()); + submit(part(part.part())); + pose.popPose(); + } + } + + /** Hatches and canopies: swing by {@code angle * factor}, or slide along their offset. */ + private void movingParts(List parts, float angle) { + for (MovingPart part : parts) { + pose.pushPose(); + moveOrSwing(part, angle); + submit(part(part.part())); + pose.popPose(); + } + } + + private void moveOrSwing(MovingPart part, float angle) { + if (part.slide()) { + translate(part.position(), angle / 90.0F); + } else { + rotateAround(part.position(), angle * part.factor(), part.axis()); + } + } + + private void trackRollers() { + for (TrackRoller roller : def.trackRollers) { + pose.pushPose(); + rotateAround(roller.position(), state.roller[roller.side()], X_AXIS); + submit(part(roller.part())); + pose.popPose(); + } + } + + /** + * Each link slides towards the next one by the track's run, so the loop appears to move. + * The link part is modelled at the origin and placed at each point of the outline. + */ + private void crawlerTracks() { + for (CrawlerTrack track : def.crawlerTracks) { + List link = part(track.part()); + List links = track.links(); + int count = links.size() - 1; + if (link.isEmpty() || count < 1) { + continue; + } + float run = state.track[track.side()]; + for (int i = 0; i < count; i++) { + CrawlerTrack.Link cur = links.get(i); + CrawlerTrack.Link next = links.get((i + 1) % count); + float angleNext = next.angle(); + if (angleNext - cur.angle() < -180.0F) { + angleNext += 360.0F; + } else if (angleNext - cur.angle() > 180.0F) { + angleNext -= 360.0F; + } + pose.pushPose(); + pose.translate(0.0F, cur.y() + (next.y() - cur.y()) * run, cur.z() + (next.z() - cur.z()) * run); + pose.mulPose(Axis.XN.rotationDegrees(cur.angle() + (angleNext - cur.angle()) * run)); + submit(link); + pose.popPose(); + } + } + } + + private void steeringWheels() { + for (WheelPart wheel : def.steeringWheels) { + pose.pushPose(); + rotateAround(wheel.position(), state.steer * wheel.steerAngle(), wheel.axis()); + submit(part(wheel.part())); + pose.popPose(); + } + } + + private void wheels() { + for (WheelPart wheel : def.wheels) { + pose.pushPose(); + rotateAround(wheel.pivot(), state.steer * wheel.steerAngle(), wheel.axis()); + rotateAround(wheel.position(), state.wheelRoll, X_AXIS); + submit(part(wheel.part())); + pose.popPose(); + } + } + + private void throttles() { + for (ThrottlePart lever : def.throttleParts) { + pose.pushPose(); + rotateAround(lever.position(), state.throttle * lever.angle(), lever.axis()); + translate(lever.slide(), state.throttle); + submit(part(lever.part())); + pose.popPose(); + } + } + + /** Sensor turrets look where the gunner looks, or the pilot when the gunner seat is empty. */ + private void cameras() { + int seat = state.seatOccupied.length > 1 && state.seatOccupied[1] ? 1 : 0; + for (CameraPart camera : def.cameraParts) { + pose.pushPose(); + translate(camera.position(), 1.0F); + if (camera.yawSync()) { + rotate(seatYaw(seat), DOWN_AXIS); + } + if (camera.pitchSync()) { + rotate(seatPitch(seat), X_AXIS); + } + translate(camera.position(), -1.0F); + submit(part(camera.part())); + pose.popPose(); + } + } + + /** + * Gear legs swing up with the gear state; reversed parts close as the gear rises, and gear bay + * doors open fully halfway through and shut again. + */ + private void landingGear() { + float angle = state.landingGear; + float reverse = 90.0F - angle; + float door = Math.min(90.0F, 90.0F * Mth.sin(angle * 2.0F * Mth.DEG_TO_RAD) * 3.0F); + for (LandingGearPart gear : def.landingGear) { + pose.pushPose(); + translate(gear.position(), 1.0F); + if (gear.reverse()) { + rotate(reverse * gear.factor(), gear.axis()); + } else { + rotate((gear.hatch() ? door : angle) * gear.factor(), gear.axis()); + } + if (gear.secondAxis() != null) { + rotate((gear.reverse() ? reverse : angle) * gear.secondFactor(), gear.secondAxis()); + } + translate(gear.position(), -1.0F); + if (gear.slide() != null) { + float f = angle / 90.0F; + translate(gear.slide(), gear.reverse() ? 1.0F - f : f); + } + submit(part(gear.part())); + pose.popPose(); + } + } + + /** Bays swing open while a crew member has one of their weapons selected. */ + private void weaponBays() { + for (int i = 0; i < def.weaponBays.size(); i++) { + WeaponBayPart bay = def.weaponBays.get(i); + pose.pushPose(); + moveOrSwing(bay.door(), i < state.bays.length ? state.bays[i] : 0.0F); + submit(part(bay.door().part())); + pose.popPose(); + } + } + + // Helpers + + private float seatYaw(int seat) { + return seat >= 0 && seat < state.seatYaw.length ? state.seatYaw[seat] : 0.0F; + } + + private float seatPitch(int seat) { + return seat >= 0 && seat < state.seatPitch.length ? state.seatPitch[seat] : 0.0F; + } + + private List part(String name) { + List groups = mesh.part("$" + name); + if (!groups.isEmpty()) { + return groups; + } + Mesh separate = MeshCache.INSTANCE.find(def.folder(), def.name() + "_" + name); + return separate != null ? separate.groups() : List.of(); + } + + private void submit(List groups) { + if (groups.isEmpty()) { + return; + } + GlassSplit.Split split = GlassSplit.INSTANCE.split(groups, texture); + if (!split.opaque().isEmpty()) { + MeshRenderer.emit(split.opaque(), pose.last(), buffers.getBuffer(opaqueType), light, overlay); + } + if (!split.glass().isEmpty()) { + MeshRenderer.emitFrontFaces(split.glass(), pose.last(), buffers.getBuffer(glassType), light, overlay, glassColor); + } + } + + private void translate(Vector3 v, float scale) { + pose.translate((float) v.x() * scale, (float) v.y() * scale, (float) v.z() * scale); + } + + private void rotateAround(Vector3 pivot, float degrees, Vector3 axis) { + if (degrees == 0.0F || isZero(axis)) { + return; + } + translate(pivot, 1.0F); + rotate(degrees, axis); + translate(pivot, -1.0F); + } + + /** Like {@code glRotatef}: any axis length, a zero axis does nothing. */ + private void rotate(float degrees, Vector3 axis) { + if (degrees == 0.0F || isZero(axis)) { + return; + } + Vector3f a = new Vector3f((float) axis.x(), (float) axis.y(), (float) axis.z()).normalize(); + pose.mulPose(Axis.of(a).rotationDegrees(degrees)); + } + + private static boolean isZero(Vector3 v) { + return v.x() * v.x() + v.y() * v.y() + v.z() * v.z() < 1.0E-12; + } +} From e448d17fdc54c125857bb8a8c90842de99567dda Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:41 +0800 Subject: [PATCH 203/314] Draw projectiles and tracers through 1.21.1's render call --- .../client/render/ProjectileRenderer.java | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java new file mode 100644 index 0000000..5a34b6e --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/ProjectileRenderer.java @@ -0,0 +1,178 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.weapon.WeaponColor; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.projectile.AAMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ASMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ATMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.BombEntity; +import com.jnhprojects.mcheli.entity.projectile.BulletEntity; +import com.jnhprojects.mcheli.entity.projectile.MarkerRocketEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.entity.projectile.RocketEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.model.Mesh; +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Mth; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; + +/** + * Draws rounds, rockets, missiles and bombs from {@code models/bullets}, tinted with the weapon's + * {@code BulletColor} (its in-water colour below the surface) and always fully lit, as the + * original's {@code MCH_RenderBulletBase} did. + * + *

Not in the original: rounds trail a tracer and rocket motors burn with a glow, faint by day + * and bright at night, so a firefight in the dark can be followed by eye. Nothing glows under + * water. + */ +public class ProjectileRenderer extends EntityRenderer { + private static final int FULL_BRIGHT = 0xF000F0; + /** Warm tracer colour for rounds whose weapon keeps the default white tint. */ + private static final int TRACER = 0xFFC46A; + /** The longest tracer ribbon, in blocks; fast rounds cover far more than this per tick. */ + private static final double MAX_TRACER = 6.0; + /** How far behind the projectile's centre the motor glow sits, in blocks. */ + private static final double MOTOR_OFFSET = 0.6; + + public ProjectileRenderer(EntityRendererProvider.Context context) { + super(context); + this.shadowRadius = 0.0F; + } + + enum GlowKind { + NONE, + TRACER, + MOTOR + } + + /** What 26.x splits off as the render state. */ + static class State { + double x; + double y; + double z; + String model = ""; + float yaw; + float pitch; + int color = 0xFFFFFFFF; + boolean visible = true; + GlowKind glowKind = GlowKind.NONE; + float glow; + int glowColor = TRACER; + /** Distance covered over the last tick, which points the tracer and the motor glow. */ + Vec3 travel = Vec3.ZERO; + } + + private final State state = new State(); + + private static void extractRenderState(ProjectileEntity entity, State state, float partialTicks) { + state.x = Mth.lerp(partialTicks, entity.xo, entity.getX()); + state.y = Mth.lerp(partialTicks, entity.yo, entity.getY()); + state.z = Mth.lerp(partialTicks, entity.zo, entity.getZ()); + state.model = entity.model(); + state.yaw = entity.getYRot(partialTicks); + state.pitch = entity.getXRot(partialTicks); + state.visible = !(entity instanceof BombEntity bomb) || bomb.isVisible(); + WeaponDefinition w = entity.weapon(); + boolean inWater = inWater(entity.level(), entity); + WeaponColor c = w != null ? (inWater ? w.colorInWater : w.color) : WeaponColor.WHITE; + state.color = c.argb(); + + state.glowKind = inWater ? GlowKind.NONE : glowKind(entity); + if (state.glowKind != GlowKind.NONE) { + state.glow = Glow.strength(entity.level(), entity.blockPosition()); + state.travel = new Vec3(entity.getX() - entity.xo, entity.getY() - entity.yo, entity.getZ() - entity.zo); + if (state.glowKind == GlowKind.MOTOR) { + state.glowColor = GlowParticleOptions.FIRE; + } else { + // A weapon with its own tint keeps it for the tracer; the default white burns warm. + int tint = state.color & 0xFFFFFF; + state.glowColor = tint == 0xFFFFFF ? TRACER : tint; + } + } + } + + private static GlowKind glowKind(ProjectileEntity entity) { + if (entity instanceof BulletEntity) { + return GlowKind.TRACER; + } + if (entity instanceof RocketEntity || entity instanceof MarkerRocketEntity || entity instanceof AAMissileEntity + || entity instanceof ASMissileEntity || entity instanceof ATMissileEntity || entity instanceof TvMissileEntity) { + return GlowKind.MOTOR; + } + return GlowKind.NONE; + } + + /** The original looked for water at the round and up to two blocks above it. */ + private static boolean inWater(Level level, ProjectileEntity e) { + for (int y = 0; y < 3; y++) { + if (level.getFluidState(BlockPos.containing(e.getX() + 0.5, e.getY() + 1.5 - y, e.getZ() + 0.5)).is(FluidTags.WATER)) { + return true; + } + } + return false; + } + + @Override + public void render(ProjectileEntity entity, float entityYaw, float partialTicks, PoseStack pose, MultiBufferSource buffers, int light) { + extractRenderState(entity, state, partialTicks); + Mesh mesh = state.visible ? MeshCache.INSTANCE.find("bullets", state.model) : null; + if (mesh != null) { + ResourceLocation texture = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/bullets/" + state.model + ".png"); + pose.pushPose(); + pose.mulPose(Axis.YP.rotationDegrees(-state.yaw)); + pose.mulPose(Axis.XP.rotationDegrees(state.pitch)); + MeshRenderer.emit(mesh.groups(), pose.last(), buffers.getBuffer(RenderType.entityTranslucentCull(texture)), FULL_BRIGHT, + OverlayTexture.NO_OVERLAY, state.color); + pose.popPose(); + } + if (state.visible) { + submitGlow(state, pose, buffers); + } + super.render(entity, entityYaw, partialTicks, pose, buffers, light); + } + + @Override + public ResourceLocation getTextureLocation(ProjectileEntity entity) { + return ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "textures/bullets/" + entity.model() + ".png"); + } + + private Quaternionf camera() { + return entityRenderDispatcher.cameraOrientation(); + } + + private void submitGlow(State state, PoseStack pose, MultiBufferSource buffers) { + if (state.glowKind == GlowKind.NONE || state.glow <= 0.0F) { + return; + } + double speed = state.travel.length(); + Vec3 back = speed > 1.0E-3 ? state.travel.scale(-1.0 / speed) : Vec3.ZERO; + switch (state.glowKind) { + case TRACER -> { + Vec3 cameraPos = entityRenderDispatcher.camera.getPosition(); + Vec3 toCamera = new Vec3(state.x - cameraPos.x, state.y - cameraPos.y, state.z - cameraPos.z); + Glow.streak(pose, buffers, toCamera, back.scale(Math.min(speed, MAX_TRACER)), 0.08F, state.glowColor, state.glow); + Glow.halo(pose, buffers, camera(), Vec3.ZERO, 0.35F, state.glowColor, state.glow); + } + case MOTOR -> { + Vec3 nozzle = back.scale(MOTOR_OFFSET); + Glow.halo(pose, buffers, camera(), nozzle, 1.1F, state.glowColor, state.glow); + Glow.halo(pose, buffers, camera(), nozzle, 0.4F, GlowParticleOptions.FLASH, state.glow); + } + default -> { + } + } + } +} From cba8ee73485204a57b0c5ad941cabe599fae5abb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:41 +0800 Subject: [PATCH 204/314] Tilt riders from the entity, which 1.21.1 hands the render hook --- .../mcheli/client/render/RiderPose.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java new file mode 100644 index 0000000..7b2b587 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/render/RiderPose.java @@ -0,0 +1,40 @@ +package com.jnhprojects.mcheli.client.render; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; + +/** + * Tilts players riding a vehicle with its hull, as the original did from its render hook. + * + *

Vanilla keeps riders upright, so a banked plane left the pilot standing straight up through + * the canopy. The tilt is the hull's pitch and roll expressed in the world frame, applied about + * the rider's vehicle attachment point (the hip), which is the point that sits on the seat. The + * rider's own body yaw is applied by the renderer afterwards, so turning in the seat still works. + * + *

The entity render dispatcher pushes a pose around every entity, so nothing needs popping here. + */ +public final class RiderPose { + private RiderPose() { + } + + /** Called before a living entity is drawn, with the pose the renderer is about to use. */ + public static void beforeRender(LivingEntity rider, PoseStack pose, float partialTicks) { + if (!(rider instanceof Player) || !(rider.getVehicle() instanceof AircraftEntity vehicle)) { + return; + } + float yaw = vehicle.getYRot(partialTicks); + float pitch = vehicle.getXRot(partialTicks); + float roll = vehicle.roll(partialTicks); + if (pitch == 0.0F && roll == 0.0F) { + return; + } + Quaternionf tilt = AircraftRotation.orientation(yaw, pitch, roll).rotateY((float) Math.toRadians(yaw)); + Vec3 hip = rider.getVehicleAttachmentPoint(vehicle); + pose.rotateAround(tilt, (float) hip.x, (float) hip.y, (float) hip.z); + } +} From a74ece084a9dd5304430280704364d1a287bdf75 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:42 +0800 Subject: [PATCH 205/314] Add EngineSound to the shared 1.21.1 sources --- .../mcheli/client/sound/EngineSound.java | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java new file mode 100644 index 0000000..8f91e64 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSound.java @@ -0,0 +1,89 @@ +package com.jnhprojects.mcheli.client.sound; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.EngineSoundSource; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; + +/** + * The looping engine sound of one vehicle. Ported from {@code MCH_SoundUpdater}. + * + *

Volume and pitch follow the vehicle's throttle every tick. For listeners outside the vehicle + * the sound is placed between them and the vehicle, so that the 16 block range of a normal sound + * stretches to the definition's {@code SoundRange}, and the pitch bends slightly as the vehicle + * approaches or leaves, which is what the original did. + */ +final class EngineSound extends RuntimeSoundInstance { + private static final float MAX_DOPPLER = 0.2F; + + private final AircraftEntity vehicle; + private final EngineSoundSource source; + private float doppler; + private double lastDistance = -1.0; + + EngineSound(AircraftEntity vehicle, EngineSoundSource source, String sound) { + super(ModSounds.get(sound), SoundSource.NEUTRAL, RandomSource.create()); + this.vehicle = vehicle; + this.source = source; + this.looping = true; + this.delay = 0; + update(); + } + + @Override + public void tick() { + if (vehicle.isRemoved() || vehicle.definition() == null) { + stop(); + return; + } + update(); + if (volume <= 0.0F) { + stop(); + } + } + + private void update() { + AircraftDefinition def = vehicle.definition(); + LocalPlayer player = Minecraft.getInstance().player; + if (def == null || player == null) { + volume = 0.0F; + return; + } + volume = def.soundVolume * source.engineVolume(); + float pitch = def.soundPitch * source.enginePitch(); + boolean aboard = player.getRootVehicle() == vehicle.getRootVehicle(); + if (aboard) { + x = vehicle.getX(); + y = vehicle.getY(); + z = vehicle.getZ(); + } else { + double dx = vehicle.getX() - player.getX(); + double dz = vehicle.getZ() - player.getZ(); + double distance = Math.sqrt(dx * dx + dz * dz); + if (lastDistance >= 0.0) { + double closing = lastDistance - distance; + if (Math.abs(closing) > 0.3) { + doppler = Math.clamp(doppler + (float) (closing / 40.0), -MAX_DOPPLER, MAX_DOPPLER); + } + } + doppler *= 0.9F; + pitch += doppler; + lastDistance = distance; + + double scale = def.soundRange / 16.0; + x = player.getX() + dx / scale; + y = player.getY() + (vehicle.getY() - player.getY()) / scale; + z = player.getZ() + dz / scale; + } + this.pitch = Math.max(pitch, 0.0F); + } + + @Override + public boolean canStartSilent() { + return true; + } +} From bed665666337ab2e009424ad5ca1f7813ae2cc8e Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:42 +0800 Subject: [PATCH 206/314] Add EngineSoundPlayer to the shared 1.21.1 sources --- .../client/sound/EngineSoundPlayer.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java new file mode 100644 index 0000000..b0d46e3 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/sound/EngineSoundPlayer.java @@ -0,0 +1,65 @@ +package com.jnhprojects.mcheli.client.sound; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.EngineSoundSource; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.entity.Entity; + +import java.util.HashMap; +import java.util.Map; + +/** + * Starts engine loops for vehicles near the player and restarts them after they fall silent. + * The sounds themselves track their vehicle until it stops or disappears. + */ +public final class EngineSoundPlayer { + /** Ticks to wait before restarting a loop, as the original did, so a stuttering engine does not spam. */ + private static final int RESTART_DELAY = 20; + + private static final Map PLAYING = new HashMap<>(); + private static final Map COOLDOWN = new HashMap<>(); + + private EngineSoundPlayer() { + } + + public static void tick() { + Minecraft mc = Minecraft.getInstance(); + ClientLevel level = mc.level; + LocalPlayer player = mc.player; + if (level == null || player == null || mc.isPaused()) { + if (level == null) { + PLAYING.clear(); + COOLDOWN.clear(); + } + return; + } + PLAYING.values().removeIf(EngineSound::isStopped); + COOLDOWN.replaceAll((id, ticks) -> ticks - 1); + COOLDOWN.values().removeIf(ticks -> ticks <= 0); + + for (Entity entity : level.entitiesForRendering()) { + if (!(entity instanceof AircraftEntity vehicle) || !(entity instanceof EngineSoundSource source)) { + continue; + } + int id = vehicle.getId(); + AircraftDefinition def = vehicle.definition(); + if (def == null || PLAYING.containsKey(id) || COOLDOWN.containsKey(id)) { + continue; + } + String sound = def.soundOrDefault(); + double range = def.soundRange * 1.2; + if (sound.isEmpty() || vehicle.distanceToSqr(player) > range * range) { + continue; + } + if (def.soundVolume * source.engineVolume() > 0.01F) { + EngineSound loop = new EngineSound(vehicle, source, sound); + mc.getSoundManager().play(loop); + PLAYING.put(id, loop); + COOLDOWN.put(id, RESTART_DELAY); + } + } + } +} From 62b9983cfd6585e9564369b85442a6d3e26a6743 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:42 +0800 Subject: [PATCH 207/314] Resolve and stream runtime sounds the 1.21.1 way --- .../client/sound/RuntimeSoundInstance.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java new file mode 100644 index 0000000..398c8a8 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/client/sound/RuntimeSoundInstance.java @@ -0,0 +1,82 @@ +package com.jnhprojects.mcheli.client.sound; + +import com.jnhprojects.mcheli.client.RuntimeAssets; +import net.minecraft.Util; +import net.minecraft.client.resources.sounds.AbstractTickableSoundInstance; +import net.minecraft.client.resources.sounds.Sound; +import net.minecraft.client.sounds.AudioStream; +import net.minecraft.client.sounds.JOrbisAudioStream; +import net.minecraft.client.sounds.LoopingAudioStream; +import net.minecraft.client.sounds.SoundBufferLibrary; +import net.minecraft.client.sounds.SoundManager; +import net.minecraft.client.sounds.WeighedSoundEvents; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; +import net.minecraft.util.valueproviders.ConstantFloat; +import org.jspecify.annotations.Nullable; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +/** + * A sound that plays through the sound manager when a resource pack defines its event, and + * otherwise streams the OGG file a runtime root holds for it ({@link RuntimeAssets#soundFile}). + * Content added while the game runs can bring sounds this way without touching + * {@code sounds.json} or reloading resources. + */ +public abstract class RuntimeSoundInstance extends AbstractTickableSoundInstance { + private @Nullable Path file; + + protected RuntimeSoundInstance(SoundEvent event, SoundSource source, RandomSource random) { + super(event, source, random); + } + + @Override + public @Nullable WeighedSoundEvents resolve(SoundManager manager) { + WeighedSoundEvents events = manager.getSoundEvent(location); + if (events != null) { + sound = events.getSound(random); + return events; + } + file = RuntimeAssets.soundFile(location.getPath()); + if (file == null) { + // What vanilla does for an unknown event: play nothing. + sound = SoundManager.EMPTY_SOUND; + return null; + } + // Streamed, so the file is read as it plays and never cached as a buffer. + sound = new Sound(location, ConstantFloat.of(1.0F), ConstantFloat.of(1.0F), 1, Sound.Type.FILE, true, false, 16); + return new WeighedSoundEvents(location, null); + } + + /** + * Forge and NeoForge call this through their sound instance extension, which it overrides + * without saying so: Fabric has no such method and calls {@link #openStream} from a mixin. + */ + public CompletableFuture getStream(SoundBufferLibrary buffers, Sound sound, boolean looping) { + return openStream(buffers, sound.getPath(), looping); + } + + /** Streams the file found on disk, or the resource {@code path} when there is none. */ + public CompletableFuture openStream(SoundBufferLibrary buffers, ResourceLocation path, boolean looping) { + Path source = file; + if (source == null) { + return buffers.getStream(path, looping); + } + return CompletableFuture.supplyAsync(() -> { + try { + InputStream in = new BufferedInputStream(Files.newInputStream(source)); + return looping ? new LoopingAudioStream(JOrbisAudioStream::new, in) : new JOrbisAudioStream(in); + } catch (IOException e) { + throw new CompletionException(e); + } + }, Util.nonCriticalIoPool()); + } +} From 996fc533833152b458ac40d33b83ade82a57f666 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:43 +0800 Subject: [PATCH 208/314] Port vehicles to 1.21.1: Entity base, NBT saves, lerp, hurt and interactAt --- .../mcheli/entity/AircraftEntity.java | 2019 +++++++++++++++++ 1 file changed, 2019 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/AircraftEntity.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/AircraftEntity.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/AircraftEntity.java new file mode 100644 index 0000000..bd9c1c8 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/AircraftEntity.java @@ -0,0 +1,2019 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.ServerConfig; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.BoundingBoxDefinition; +import com.jnhprojects.mcheli.content.aircraft.RackDefinition; +import com.jnhprojects.mcheli.content.aircraft.RideRack; +import com.jnhprojects.mcheli.content.aircraft.SeatDefinition; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import com.jnhprojects.mcheli.physics.GroundImpact; +import com.jnhprojects.mcheli.weapon.Explosions; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.tags.DamageTypeTags; +import net.minecraft.tags.FluidTags; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.Mth; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.damagesource.DamageTypes; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.effect.MobEffects; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityDimensions; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.entity.Pose; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.joml.Vector3f; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +/** + * Behaviour shared by every MCHeli vehicle: which definition it uses, hit points, seats, moving + * parts and the pilot authority model. + * + *

Movement is simulated wherever {@link #isControlledByLocalInstance()} is true. With a player + * at the controls that is the pilot's client, exactly like vanilla boats, and the server checks + * the result through the regular vehicle move packet. Without a pilot the server simulates the + * vehicle on its own. Roll, throttle and key state are not part of the vanilla packet, so the + * pilot sends them separately and the server republishes them through synched data. + * + *

Seats are explicit. Vanilla only knows an ordered passenger list, so the seat each passenger + * occupies is kept in a synched seat map of entity ids. Seat zero is the pilot and decides who + * controls the vehicle; crew seats follow, then cargo racks. + */ +public abstract class AircraftEntity extends Entity { + // VehicleEntity's hurt wobble. Vanilla 1.21.1 keeps that class package private where it matters, + // so vehicles extend Entity and carry their own. + private static final EntityDataAccessor DATA_HURT_TIME = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_HURT_DIR = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_TYPE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_TEXTURE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_HP = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_THROTTLE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.FLOAT); + private static final EntityDataAccessor DATA_REVERSE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.FLOAT); + private static final EntityDataAccessor DATA_ROLL = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.FLOAT); + private static final EntityDataAccessor DATA_HOVERING = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_GUNNER_MODE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_INPUT = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BYTE); + private static final EntityDataAccessor DATA_PARTS = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SEATS = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_WEAPON_SELECTION = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_WEAPON_USE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_TV_MISSILE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_FLARE = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_SINKING = SynchedEntityData.defineId(AircraftEntity.class, EntityDataSerializers.BOOLEAN); + + // Bits of DATA_PARTS, the same assignment the original used for its part status word. + protected static final int PART_CANOPY = 0; + protected static final int PART_NOZZLE = 1; + protected static final int PART_LANDING_GEAR = 2; + protected static final int PART_WING = 3; + protected static final int PART_HATCH = 4; + + private static final int EMPTY = -1; + /** How far above its hit box the original drew a vehicle's model; see {@link #waterDepth}. */ + private static final float LEGACY_MODEL_LIFT = 0.35F; + /** How fast a cooked-off boat goes under, in blocks per tick (about 0.4 blocks a second). */ + private static final double SINK_SPEED = 0.02; + /** The bow-up pitch and the list a sinking boat settles into, in degrees. */ + private static final float SINK_PITCH = 14.0F; + private static final float SINK_LIST = 12.0F; + + protected final ControlInput input = new ControlInput(); + public final AnimationState animation = new AnimationState(); + + /** Throttle in [0, 1] as seen by the simulating side. Other sides read {@link #DATA_THROTTLE}. */ + protected double throttle; + /** Reverse throttle in [0, 0.6] for vehicles with {@code EnableBack}. */ + protected float reverse; + protected float roll; + public float rollO; + protected int modeSwitchCooldown; + private int ticksAlive; + + private @Nullable AircraftDefinition cachedDefinition; + private String cachedName = ""; + private int cachedGeneration; + + protected @Nullable PartState canopy; + protected @Nullable PartState landingGear; + protected @Nullable PartState hatch; + private final List parts = new ArrayList<>(); + + private int[] seatIds = new int[0]; + private int pendingSeat = EMPTY; + private final Map savedSeats = new HashMap<>(); + private final Map lastSeats = new HashMap<>(); + + private @Nullable VehicleWeapons weapons; + private int @Nullable [] savedAmmo; + private @Nullable TvMissileEntity tvMissile; + + // Damage model + /** Damage factor of the extra hit box the last projectile ray struck, {@code lastBBDamageFactor}. */ + private float hitBoxFactor = 1.0F; + private int timeSinceHit; + /** How long a wreck stays, burning, before it disappears: 30 seconds. */ + private static final int WRECK_TICKS = 600; + /** Ticks a server position update is eased in over. */ + private static final int LERP_STEPS = 3; + /** The wreck fire dies down over the last five seconds. */ + private static final int WRECK_FADE_TICKS = 100; + /** Ticks until a destroyed wreck disappears; above zero means destroyed. Server only. */ + private int despawnCount; + private int lerpSteps; + private double lerpX; + private double lerpY; + private double lerpZ; + private double lerpYRot; + private double lerpXRot; + /** Ticks the client has seen this vehicle as a wreck, for the fire to die down on time. */ + private int wreckTicks; + private float destroyedYaw; + private float destroyedPitch; + private float destroyedRoll; + private boolean exploded; + /** Blasts still to come while a wrecked boat cooks off, and ticks until the next one. */ + private int cookOffBlasts; + private int cookOffDelay; + private @Nullable Entity cookOffCause; + private int alertCount; + + // Crashes, measured from position so they work whichever side simulates the vehicle. + /** Blocks per tick into terrain, sideways, that count as a crash: about 25 m/s. */ + private static final double TERRAIN_CRASH_SPEED = 1.25; + /** Closing speed between two vehicles that counts as a crash: about 10 m/s. */ + private static final double VEHICLE_CRASH_SPEED = 0.5; + /** Highest point since a ground vehicle left the ground, or NaN while it is on it. */ + private double airborneFromY = Double.NaN; + private @Nullable Vec3 lastTickPos; + private Vec3 lastVelocity = Vec3.ZERO; + private int collisionCooldown; + + protected AircraftEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_HURT_TIME, 0); + builder.define(DATA_HURT_DIR, 1); + builder.define(DATA_TYPE, ""); + builder.define(DATA_TEXTURE, ""); + builder.define(DATA_HP, 0); + builder.define(DATA_THROTTLE, 0.0F); + builder.define(DATA_REVERSE, 0.0F); + builder.define(DATA_ROLL, 0.0F); + builder.define(DATA_HOVERING, false); + builder.define(DATA_GUNNER_MODE, false); + builder.define(DATA_INPUT, (byte) 0); + builder.define(DATA_PARTS, 0); + builder.define(DATA_SEATS, ""); + builder.define(DATA_WEAPON_SELECTION, ""); + builder.define(DATA_WEAPON_USE, 0); + builder.define(DATA_TV_MISSILE, 0); + builder.define(DATA_FLARE, false); + builder.define(DATA_SINKING, false); + } + + // Definition + + /** + * Looks the definition up by name. Returns {@code null} for vehicles whose content was removed. + * After a content reload the next call picks up the new definition object. + */ + public @Nullable AircraftDefinition definition() { + String name = typeName(); + int generation = MCHeli.content().generation(); + if (!name.equals(cachedName) || generation != cachedGeneration) { + boolean reloaded = name.equals(cachedName); + cachedName = name; + cachedGeneration = generation; + AircraftDefinition previous = cachedDefinition; + cachedDefinition = name.isEmpty() ? null : lookup(name); + if (cachedDefinition != null && cachedDefinition != previous) { + if (reloaded && weapons != null) { + // The same vehicle with new numbers: keep the rounds it carries. + savedAmmo = weapons.saveAmmo(); + } + onDefinitionChanged(cachedDefinition); + } + } + return cachedDefinition; + } + + /** Finds a definition of this entity's kind. */ + protected abstract @Nullable AircraftDefinition lookup(String name); + + /** Builds the per vehicle state that depends on the definition: seats, parts, animation arrays. */ + protected void onDefinitionChanged(AircraftDefinition def) { + if (seatIds.length != def.seatAndRackCount()) { + int[] resized = new int[def.seatAndRackCount()]; + Arrays.fill(resized, EMPTY); + System.arraycopy(seatIds, 0, resized, 0, Math.min(seatIds.length, resized.length)); + seatIds = resized; + } + parts.clear(); + canopy = def.canopies.isEmpty() ? null : addPart(PART_CANOPY, 3.5F, PartState.Sounds.CANOPY); + landingGear = def.landingGear.isEmpty() ? null : addPart(PART_LANDING_GEAR, 2.5F, PartState.Sounds.LANDING_GEAR); + hatch = def.hatches.isEmpty() ? null : addPart(PART_HATCH, 1.5F, PartState.Sounds.HATCH); + animation.resize(def); + weapons = new VehicleWeapons(this, def); + if (savedAmmo != null) { + weapons.loadAmmo(savedAmmo); + savedAmmo = null; + } + } + + protected PartState addPart(int bit, float speed, PartState.Sounds sounds) { + PartState state = new PartState(this, bit, speed, sounds); + // A vehicle that comes into view with a part already switched should not animate it. + state.force(state.status()); + parts.add(state); + return state; + } + + /** Sets which vehicle this is. Call once, right after construction, before adding to the level. */ + public void setType(AircraftDefinition definition) { + entityData.set(DATA_TYPE, definition.name()); + entityData.set(DATA_TEXTURE, definition.textures().getFirst()); + entityData.set(DATA_HP, definition.maxHp); + refreshDimensions(); + definition(); + } + + public String typeName() { + return entityData.get(DATA_TYPE); + } + + public String texture() { + String texture = entityData.get(DATA_TEXTURE); + return texture.isEmpty() ? typeName() : texture; + } + + public int hp() { + return entityData.get(DATA_HP); + } + + /** Whether the flight model holds a hover: in hover mode, or while the pilot mans the guns. */ + public boolean isHovering() { + return isHoveringMode() || isGunnerMode(); + } + + /** The hover mode the helicopter pilot switches with jump. */ + public boolean isHoveringMode() { + return entityData.get(DATA_HOVERING); + } + + protected void setHovering(boolean hovering) { + entityData.set(DATA_HOVERING, hovering); + } + + // Pilot gunner mode + + /** + * Pilot gunner mode ({@code EnableGunnerMode}): the pilot leaves the controls to man the + * weapons from the camera position. Helicopters hold a hover; planes level off and circle. + */ + public boolean isGunnerMode() { + return entityData.get(DATA_GUNNER_MODE); + } + + /** {@code canSwitchGunnerMode}: the kinds add their own attitude and speed limits. */ + public boolean canSwitchGunnerMode() { + AircraftDefinition def = definition(); + if (def == null || !def.gunnerMode || !isCanopyClosed() || isDestroyed() || isHoveringMode()) { + return false; + } + // Without ConcurrentGunnerMode a player in the gunner seat has the guns already. + return def.concurrentGunnerMode || !(occupant(1) instanceof Player); + } + + /** Server side. Leaving gunner mode is always allowed; entering it only when {@link #canSwitchGunnerMode}. */ + public void setGunnerMode(boolean on) { + if (!on || canSwitchGunnerMode()) { + entityData.set(DATA_GUNNER_MODE, on); + } + } + + /** Whether {@code entity} is the pilot and currently in gunner mode. */ + public boolean isGunnerMode(Entity entity) { + return isGunnerMode() && seatOf(entity) == 0; + } + + /** Drops gunner mode once its conditions no longer hold. Server side, every tick. */ + private void checkGunnerMode(AircraftDefinition def) { + if (isGunnerMode() && (occupant(0) == null || isDestroyed() + || (!def.concurrentGunnerMode && occupant(1) instanceof Player))) { + entityData.set(DATA_GUNNER_MODE, false); + } + } + + // Throttle, roll and input + + /** Throttle for animation and sound, valid on every side. */ + public float visibleThrottle() { + return isSimulatingHere() ? (float) throttle : entityData.get(DATA_THROTTLE); + } + + public float visibleReverse() { + return isSimulatingHere() ? reverse : entityData.get(DATA_REVERSE); + } + + public float roll() { + return isSimulatingHere() ? roll : entityData.get(DATA_ROLL); + } + + public float roll(float partialTicks) { + return Mth.rotLerp(partialTicks, rollO, roll()); + } + + public void setRoll(float roll) { + this.roll = roll; + } + + protected boolean isSimulatingHere() { + return isControlledByLocalInstance() && !isPassenger(); + } + + public ControlInput input() { + return input; + } + + /** + * Applies the pilot's view change for one frame. Only called on the pilot's client, and only + * for kinds that steer with the mouse. + * + * @param dYaw change of the player's yaw since the last frame, in degrees + * @param dPitch change of the player's pitch since the last frame, in degrees + */ + public void applyPilotLook(float dYaw, float dPitch) { + } + + /** Whether the pilot steers with the mouse, as in helicopters and planes. */ + public boolean steersWithMouse() { + return false; + } + + /** Server side entry point for the pilot's control packet. */ + public void acceptPilotState(float throttle, float reverse, float roll, boolean hovering, byte keys) { + this.throttle = Mth.clamp(throttle, 0.0F, 1.0F); + this.reverse = Mth.clamp(reverse, 0.0F, 1.0F); + this.roll = AircraftRotation.wrap(roll); + input.read(keys); + entityData.set(DATA_THROTTLE, (float) this.throttle); + entityData.set(DATA_REVERSE, this.reverse); + entityData.set(DATA_ROLL, this.roll); + entityData.set(DATA_HOVERING, hovering); + entityData.set(DATA_INPUT, keys); + } + + public float reverse() { + return reverse; + } + + // Tick + + @Override + public void tick() { + rollO = roll(); + if (getHurtTime() > 0) { + setHurtTime(getHurtTime() - 1); + } + super.tick(); + if (level().isClientSide()) { + tickLerp(); + } + ticksAlive++; + lastSeats.clear(); + if (modeSwitchCooldown > 0) { + modeSwitchCooldown--; + } + + AircraftDefinition def = definition(); + if (def == null) { + // Content for this vehicle no longer exists. Keep the entity inert rather than crash. + setDeltaMovement(Vec3.ZERO); + return; + } + if (timeSinceHit > 0) { + timeSinceHit--; + } + if (alertCount > 0) { + alertCount--; + } + boolean groundedBefore = onGround(); + double fallBefore = getDeltaMovement().y; + tickDestroyed(); + + if (isSimulatingHere()) { + if (!(getControllingPassenger() instanceof Player) || isDestroyed()) { + input.clear(); + } + if (entityData.get(DATA_SINKING)) { + sink(); + } else { + simulate(); + } + float[] kick = weapons != null ? weapons.recoilKick() : null; + if (kick != null) { + setXRot(getXRot() + kick[0]); + roll += kick[1]; + } + if (!level().isClientSide()) { + entityData.set(DATA_THROTTLE, (float) throttle); + entityData.set(DATA_REVERSE, reverse); + entityData.set(DATA_ROLL, roll); + entityData.set(DATA_INPUT, input.write()); + } + } else { + if (!isControlledByLocalInstance()) { + input.read(entityData.get(DATA_INPUT)); + } + setDeltaMovement(Vec3.ZERO); + } + for (PartState part : parts) { + part.tick(); + } + if (!level().isClientSide()) { + updatePartsServer(def); + checkGunnerMode(def); + } + animation.tick(this, def); + tickAnimation(); + if (weapons != null) { + weapons.tick(); + if (!level().isClientSide()) { + entityData.set(DATA_FLARE, weapons.flares.isActive()); + warnIncomingMissiles(); + } + } + if (level().isClientSide()) { + damageSmoke(def); + } else { + tickImpacts(def, groundedBefore, fallBefore); + } + } + + /** + * Server side crashes. A wreck explodes when it lands; an intact vehicle takes damage when it + * meets the ground steeply tilted ({@code onUpdate_CollisionGroundDamage}), sits under water + * without floats, flies into terrain, or rams another vehicle. + */ + private void tickImpacts(AircraftDefinition def, boolean groundedBefore, double fallBefore) { + Vec3 velocity = lastTickPos != null ? position().subtract(lastTickPos) : Vec3.ZERO; + Vec3 previous = lastVelocity; + lastTickPos = position(); + lastVelocity = velocity; + if (collisionCooldown > 0) { + collisionCooldown--; + } + double speed = Math.max(velocity.length(), previous.length()); + + if (isDestroyed()) { + boolean falling = fallBefore < -0.2 || previous.y < -0.2; + boolean landed = onGround() && !groundedBefore; + boolean smashed = horizontalCollision && previous.horizontalDistance() > 0.5; + if (!exploded && (landed && falling || smashed)) { + crashExplosion(); + } + return; + } + + if (isNearGround(3)) { + float roll = Math.abs(Mth.wrapDegrees(roll())); + float pitch = Math.abs(Mth.wrapDegrees(getXRot())); + if (roll > crashTilt() || pitch > crashTilt()) { + float damage = roll + pitch; + damage *= damage < 90.0F ? 0.4F * (float) velocity.length() : 0.4F; + if (damage > 1.0F && random.nextInt(4) == 0) { + hurt(damageSources().inWall(), damage); + } + } + } + if (ticksAlive % 30 == 0 && !def.floats && level().getFluidState( + BlockPos.containing(getX() + 0.5, getY() + 1.5 + def.submergedDamageHeight, getZ() + 0.5)).is(FluidTags.WATER)) { + hurt(damageSources().inWall(), Math.max(def.maxHp / 10, 1)); + } + + if (takesGroundImpacts()) { + groundImpacts(def, previous); + } + + // Not in the original: flying into terrain or another vehicle at speed is a crash. + if (horizontalCollision && previous.horizontalDistance() > TERRAIN_CRASH_SPEED && collisionCooldown == 0) { + collisionCooldown = 20; + hurt(damageSources().flyIntoWall(), crashDamage(previous.horizontalDistance())); + } + if (speed > VEHICLE_CRASH_SPEED && collisionCooldown == 0) { + for (AircraftEntity other : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(0.5), + o -> o != this && !o.isRemoved() && o.getVehicle() != this && getVehicle() != o)) { + double closing = velocity.subtract(other.lastVelocity).length(); + closing = Math.max(closing, previous.subtract(other.lastVelocity).length()); + if (closing > VEHICLE_CRASH_SPEED && other.collisionCooldown == 0) { + collisionCooldown = 20; + other.collisionCooldown = 20; + float damage = crashDamage(closing); + hurt(damageSources().flyIntoWall(), damage); + other.hurt(damageSources().flyIntoWall(), damage); + break; + } + } + } + } + + /** {@code getGiveDamageRot}: how far a vehicle may lean near the ground before it scrapes. */ + protected float crashTilt() { + return 40.0F; + } + + /** Crash damage grows with the square of the impact speed, like the energy involved. */ + private static float crashDamage(double speed) { + return (float) (speed * speed * 60.0); + } + + /** Whether hitting walls and falling hurt this vehicle. Tanks and cars, which live on the ground. */ + protected boolean takesGroundImpacts() { + return false; + } + + /** + * A ground vehicle that hits a wall or lands after a long drop takes a share of its hit points + * by the speed or the height, see {@link GroundImpact}. The same height costs a car and a tank + * the same share before armour. + */ + private void groundImpacts(AircraftDefinition def, Vec3 previous) { + if (horizontalCollision && previous.horizontalDistance() > GroundImpact.WALL_SPEED && collisionCooldown == 0) { + collisionCooldown = 20; + hurt(damageSources().flyIntoWall(), GroundImpact.wall(previous.horizontalDistance()) * def.maxHp); + } + if (isInWater() || isPassenger()) { + airborneFromY = Double.NaN; + } else if (!onGround()) { + airborneFromY = Double.isNaN(airborneFromY) ? getY() : Math.max(airborneFromY, getY()); + } else if (!Double.isNaN(airborneFromY)) { + double drop = airborneFromY - getY(); + airborneFromY = Double.NaN; + if (drop > GroundImpact.SAFE_DROP) { + hurt(damageSources().fall(), GroundImpact.drop(drop) * def.maxHp); + } + } + } + + /** {@code MCH_MissileDetector}: the crew hears a warning while a missile homes on them. */ + private void warnIncomingMissiles() { + if (alertCount > 0 || isDestroyed() || weapons == null || !weapons.flares.isFitted() || ticksAlive % 5 != 0) { + return; + } + boolean tracked = !level().getEntitiesOfClass(ProjectileEntity.class, getBoundingBox().inflate(60.0), + p -> p.target() != null && (p.target() == this || hasPassenger(p.target()))).isEmpty(); + if (tracked) { + alertCount = 20; + ModSounds.playAt(this, "alert", 1.0F, 1.0F); + } + } + + /** One tick of control and physics on the simulating side. */ + protected abstract void simulate(); + + /** Visual state that every side updates, for example rotor angles. */ + protected void tickAnimation() { + } + + /** Ticks since this entity instance was created, used to delay controls right after spawning. */ + protected int ticksAlive() { + return ticksAlive; + } + + // Moving parts + + int partBits() { + return entityData.get(DATA_PARTS); + } + + void setPartBit(int bit, boolean on) { + int bits = entityData.get(DATA_PARTS); + entityData.set(DATA_PARTS, on ? bits | (1 << bit) : bits & ~(1 << bit)); + } + + public @Nullable PartState canopy() { + return canopy; + } + + public @Nullable PartState landingGear() { + return landingGear; + } + + public @Nullable PartState hatch() { + return hatch; + } + + public boolean isCanopyClosed() { + return canopy == null || canopy.isOff(); + } + + protected void openCanopy() { + if (canopy != null && modeSwitchCooldown <= 0) { + canopy.set(true, true); + modeSwitchCooldown = 20; + } + } + + protected void closeCanopy() { + if (canopy != null && modeSwitchCooldown <= 0 && canopy.status()) { + canopy.set(false, true); + modeSwitchCooldown = 20; + } + } + + public boolean isLandingGearUp() { + return landingGear != null && landingGear.status(); + } + + /** Gear can be raised once the vehicle is at least ten blocks above anything solid. */ + public boolean canRaiseLandingGear() { + return landingGear != null && landingGear.angle() < 1.0F && !landingGear.status() && !hasBlockBelow(3, 10, true); + } + + public boolean canLowerLandingGear() { + return landingGear != null && landingGear.angle() >= 89.0F && landingGear.status(); + } + + public void toggleLandingGear() { + if (landingGear == null || modeSwitchCooldown > 0) { + return; + } + if (canRaiseLandingGear()) { + landingGear.set(true, true); + modeSwitchCooldown = 20; + } else if (canLowerLandingGear()) { + landingGear.set(false, true); + modeSwitchCooldown = 20; + } + } + + public boolean canFoldHatch() { + return hatch != null && modeSwitchCooldown <= 0 && hatch.isOff(); + } + + public boolean canUnfoldHatch() { + return hatch != null && modeSwitchCooldown <= 0 && hatch.isOn(); + } + + /** The fold key: hatches by default, planes fold or sweep their wings instead. */ + public void toggleFold() { + if (canFoldHatch()) { + hatch.set(true, true); + modeSwitchCooldown = 20; + } else if (canUnfoldHatch()) { + hatch.set(false, true); + modeSwitchCooldown = 20; + } + } + + /** Throttle below which the landing gear comes down by itself near the ground. */ + protected float autoLowerGearThrottle() { + return 0.8F; + } + + /** {@code updateParts}: automatic landing gear, plus whatever a kind adds. Server only. */ + protected void updatePartsServer(AircraftDefinition def) { + if (landingGear == null) { + return; + } + if (!landingGear.status() && landingGear.factor() <= 0.1F) { + // Amphibians retract the gear when they settle on water. + boolean onWater = isInWater() || isWaterBelow(20); + if (def.floats && onWater && (throttle <= 0.8 || onGround() || hasBlockBelow(3, 20, true))) { + landingGear.set(true, true); + } + } else if (landingGear.status() && landingGear.factor() >= 0.9F) { + boolean ground = hasBlockBelow(3, 10, true); + if (throttle < autoLowerGearThrottle() && ground) { + if (!def.floats || !isWaterBelow(150)) { + landingGear.set(false, true); + } + } else if (vtolMode() == 2 && ground) { + landingGear.set(false, true); + } + } + } + + /** 0 off, 1 tilting, 2 fully in VTOL mode. Only planes have one. */ + public int vtolMode() { + return 0; + } + + // Seats + + /** Seat (or rack) index of {@code entity}, or -1. */ + public int seatOf(Entity entity) { + int id = entity.getId(); + for (int i = 0; i < seatIds.length; i++) { + if (seatIds[i] == id) { + return i; + } + } + return EMPTY; + } + + public @Nullable Entity occupant(int seat) { + if (seat < 0 || seat >= seatIds.length || seatIds[seat] == EMPTY) { + return null; + } + int id = seatIds[seat]; + for (Entity passenger : getPassengers()) { + if (passenger.getId() == id) { + return passenger; + } + } + return null; + } + + public @Nullable SeatDefinition seat(Entity entity) { + AircraftDefinition def = definition(); + int seat = seatOf(entity); + return def != null && seat >= 0 && seat < def.seatCount() ? def.seats.get(seat) : null; + } + + private boolean isSeatFree(int seat) { + if (seat < 0 || seat >= seatIds.length || seatIds[seat] != EMPTY) { + return false; + } + AircraftDefinition def = definition(); + if (def != null) { + for (int[] group : def.exclusiveSeats) { + if (contains(group, seat)) { + for (int other : group) { + if (other != seat && other >= 0 && other < seatIds.length && seatIds[other] != EMPTY) { + return false; + } + } + } + } + } + return true; + } + + private static boolean contains(int[] values, int value) { + for (int v : values) { + if (v == value) { + return true; + } + } + return false; + } + + /** First crew seat a player boarding by hand would take. Drones keep their pilot seat for the remote. */ + private int firstFreeCrewSeat() { + AircraftDefinition def = definition(); + if (def == null) { + return EMPTY; + } + for (int i = def.uav ? 1 : 0; i < def.seatCount(); i++) { + if (isSeatFree(i)) { + return i; + } + } + return EMPTY; + } + + private void assignSeat(Entity entity, int seat) { + for (int i = 0; i < seatIds.length; i++) { + if (seatIds[i] == entity.getId()) { + seatIds[i] = EMPTY; + if (weapons != null) { + weapons.onLeft(i); + } + } + } + if (seat >= 0 && seat < seatIds.length) { + seatIds[seat] = entity.getId(); + } + entityData.set(DATA_SEATS, encodeSeats()); + if (weapons != null) { + weapons.onSeated(entity, seat); + weapons.resendAmmo(); + } + } + + private String encodeSeats() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < seatIds.length; i++) { + if (i > 0) { + sb.append(','); + } + sb.append(seatIds[i]); + } + return sb.toString(); + } + + private void decodeSeats(String encoded) { + if (encoded.isEmpty()) { + Arrays.fill(seatIds, EMPTY); + return; + } + String[] parts = encoded.split(","); + if (parts.length != seatIds.length) { + seatIds = new int[parts.length]; + } + for (int i = 0; i < parts.length; i++) { + try { + seatIds[i] = Integer.parseInt(parts[i]); + } catch (NumberFormatException e) { + seatIds[i] = EMPTY; + } + } + } + + /** Moves {@code entity} to the next free crew seat, wrapping around. Server only. */ + public void switchSeat(Entity entity) { + AircraftDefinition def = definition(); + int current = seatOf(entity); + if (def == null || current < 0 || current >= def.seatCount()) { + return; + } + int count = def.seatCount(); + seatIds[current] = EMPTY; + for (int step = 1; step < count; step++) { + int seat = (current + step) % count; + if (def.uav && seat == 0) { + continue; + } + if (isSeatFree(seat)) { + assignSeat(entity, seat); + if (seat == 0) { + closeCanopy(); + } + return; + } + } + assignSeat(entity, current); + } + + @Override + public @Nullable LivingEntity getControllingPassenger() { + // A wreck is nobody's to fly; the server lets it fall, so its crash is seen the same way by everyone. + return occupant(0) instanceof Player player && !isDestroyed() ? player : null; + } + + @Override + protected boolean canAddPassenger(Entity passenger) { + AircraftDefinition def = definition(); + if (def == null || !def.canRide) { + return false; + } + if (pendingSeat >= 0) { + return isSeatFree(pendingSeat); + } + // Cargo only boards through a rack, never into a crew seat. + return !(passenger instanceof AircraftEntity) && firstFreeCrewSeat() >= 0; + } + + @Override + protected void addPassenger(Entity passenger) { + super.addPassenger(passenger); + if (level().isClientSide()) { + return; + } + int seat = pendingSeat; + pendingSeat = EMPTY; + if (seat < 0) { + Integer saved = savedSeats.remove(passenger.getUUID()); + seat = saved != null && isSeatFree(saved) ? saved : firstFreeCrewSeat(); + } + assignSeat(passenger, seat); + } + + @Override + protected void removePassenger(Entity passenger) { + super.removePassenger(passenger); + if (level().isClientSide()) { + return; + } + int seat = seatOf(passenger); + if (seat >= 0) { + lastSeats.put(passenger.getId(), seat); + seatIds[seat] = EMPTY; + entityData.set(DATA_SEATS, encodeSeats()); + if (weapons != null) { + weapons.onLeft(seat); + } + } + if (seat == 0) { + input.clear(); + } + } + + @Override + protected Vec3 getPassengerAttachmentPoint(Entity passenger, EntityDimensions dimensions, float scale) { + AircraftDefinition def = definition(); + int seat = seatOf(passenger); + if (def == null || seat < 0 || seat >= def.seatAndRackCount()) { + return super.getPassengerAttachmentPoint(passenger, dimensions, scale); + } + Vector3f p = seatPosition(def, seat, 1.0F); + return new Vec3(p.x, p.y, p.z); + } + + /** World offset of a seat from the vehicle origin. Turret seats turn with the pilot's view. */ + public Vector3f seatPosition(AircraftDefinition def, int seat, float partialTicks) { + Vector3 pos; + boolean turret; + if (seat < def.seatCount()) { + SeatDefinition s = def.seats.get(seat); + pos = s.position(); + turret = s.rotatingSeat(); + } else { + RackDefinition rack = def.rack(seat); + pos = rack.position(); + turret = rack.rotatingSeat(); + } + return toWorldOffset(turret ? onTurret(def, pos, partialTicks) : pos); + } + + /** Rotates a hull-space point around {@code TurretPosition} by the pilot's view yaw. */ + public Vector3 onTurret(AircraftDefinition def, Vector3 pos, float partialTicks) { + Vector3 t = def.turretPosition; + Vector3f v = AircraftRotation.toWorld(animation.seatYaw(0, partialTicks), 0.0F, 0.0F, + pos.x() - t.x(), pos.y() - t.y(), pos.z() - t.z()); + return new Vector3(v.x + t.x(), v.y + t.y(), v.z + t.z()); + } + + /** Rotates a hull-space point into the world frame, relative to the vehicle origin. */ + public Vector3f toWorldOffset(Vector3 local) { + return AircraftRotation.toWorld(getYRot(), getXRot(), roll(), local.x(), local.y(), local.z()); + } + + public Vec3 toWorld(Vector3 local) { + Vector3f v = toWorldOffset(local); + return position().add(v.x, v.y, v.z); + } + + @Override + protected void positionRider(Entity passenger, MoveFunction moveFunction) { + super.positionRider(passenger, moveFunction); + AircraftDefinition def = definition(); + if (def != null && passenger instanceof AircraftEntity cargo) { + RackDefinition rack = def.rack(seatOf(passenger)); + float yaw = rack != null ? rack.yaw() : 0.0F; + float pitch = rack != null ? rack.pitch() : 0.0F; + cargo.setYRot(getYRot() + yaw); + cargo.setXRot(getXRot() + pitch); + cargo.roll = roll(); + cargo.setDeltaMovement(Vec3.ZERO); + } + } + + @Override + public Vec3 getDismountLocationForPassenger(LivingEntity passenger) { + AircraftDefinition def = definition(); + Integer seat = lastSeats.get(passenger.getId()); + if (def != null) { + Vector3 local; + if (def.unmountPosition != null) { + local = def.unmountPosition; + } else { + Vector3 seatPos = seat != null && seat < def.seatCount() ? def.seats.get(seat).position() : def.seats.getFirst().position(); + double x = seatPos.x() >= 0.0 ? seatPos.x() + 3.0 : seatPos.x() - 3.0; + local = new Vector3(x, 2.0, seatPos.z()); + } + Vec3 at = toWorld(local); + AABB box = passenger.getDimensions(Pose.STANDING).makeBoundingBox(at); + if (level().noCollision(passenger, box)) { + return at; + } + } + return super.getDismountLocationForPassenger(passenger); + } + + /** + * 1.21.1 hands the click position to {@code interactAt}, which the client sends before the + * plain {@code interact}; 26.x merged the two. + */ + @Override + public InteractionResult interactAt(Player player, Vec3 location, InteractionHand hand) { + InteractionResult result = super.interact(player, hand); + if (result != InteractionResult.PASS || player.isSecondaryUseActive()) { + return result; + } + AircraftDefinition def = definition(); + if (def == null || !def.canRide || player.getVehicle() == this) { + return InteractionResult.PASS; + } + boolean boardingPilot = !def.uav && isSeatFree(0); + if (boardingPilot && canopy != null && isCanopyClosed()) { + // The first click opens the canopy, the next one climbs in. + if (!level().isClientSide()) { + openCanopy(); + } + return InteractionResult.SUCCESS; + } + if (boardingPilot && modeSwitchCooldown > 0) { + return InteractionResult.PASS; + } + if (!level().isClientSide()) { + if (!player.startRiding(this)) { + return InteractionResult.PASS; + } + if (seatOf(player) == 0) { + closeCanopy(); + } + } + return InteractionResult.SUCCESS; + } + + // Racks + + /** Loads {@code cargo} into the given rack seat. Server only. */ + private boolean loadCargo(AircraftEntity cargo, int seat) { + if (!isSeatFree(seat) || cargo.isPassenger()) { + return false; + } + pendingSeat = seat; + boolean loaded = cargo.startRiding(this, true); + pendingSeat = EMPTY; + if (loaded) { + level().playSound(null, getX(), getY(), getZ(), SoundEvents.UI_BUTTON_CLICK, SoundSource.NEUTRAL, 1.0F, 1.0F); + } + return loaded; + } + + /** Whether {@code cargo} may ride rack {@code seat} of this vehicle. */ + public boolean acceptsCargo(AircraftEntity cargo, int seat) { + AircraftDefinition def = definition(); + AircraftDefinition cargoDef = cargo.definition(); + RackDefinition rack = def != null ? def.rack(seat) : null; + if (rack == null || cargoDef == null || cargo == this || isPassenger() || cargo.isPassenger()) { + return false; + } + // A vehicle that carries cargo itself cannot be loaded. + for (Entity p : cargo.getPassengers()) { + if (p instanceof AircraftEntity) { + return false; + } + } + if (rack.accepts(cargoDef.name(), cargoDef.kind())) { + return true; + } + for (RideRack rr : cargoDef.rideRacks) { + if (rr.carrier().equalsIgnoreCase(def.name()) && def.seatCount() + rr.rack() - 1 == seat) { + return true; + } + } + return false; + } + + private boolean isNearRackEntry(AircraftEntity cargo, RackDefinition rack) { + Vec3 entry = toWorld(rack.entry()); + float r = rack.range(); + return Math.abs(cargo.getX() - entry.x) <= r && Math.abs(cargo.getY() - entry.y) <= r && Math.abs(cargo.getZ() - entry.z) <= r; + } + + /** Carrier pilot loads every nearby vehicle that fits a free rack. Server only. */ + public void loadNearbyCargo() { + AircraftDefinition def = definition(); + if (def == null || def.racks.isEmpty() || throttle > 0.3 || !hasBlockBelow(1, 3, true)) { + return; + } + for (int seat = def.seatCount(); seat < def.seatAndRackCount(); seat++) { + if (!isSeatFree(seat)) { + continue; + } + RackDefinition rack = def.rack(seat); + Vec3 entry = toWorld(rack.entry()); + AABB area = new AABB(entry, entry).inflate(rack.range()); + for (AircraftEntity cargo : level().getEntitiesOfClass(AircraftEntity.class, area)) { + if (acceptsCargo(cargo, seat) && loadCargo(cargo, seat)) { + break; + } + } + } + } + + /** Drops the last loaded piece of cargo at its rack's entry point. Server only. */ + public void unloadCargo() { + AircraftDefinition def = definition(); + if (def == null) { + return; + } + for (int seat = def.seatAndRackCount() - 1; seat >= def.seatCount(); seat--) { + Entity cargo = occupant(seat); + if (cargo == null) { + continue; + } + RackDefinition rack = def.rack(seat); + Vector3 drop = rack.entry(); + if (cargo instanceof AircraftEntity) { + // Aircraft roll out well clear of the carrier's tail or nose. + drop = drop.add(0.0, 0.0, drop.z() >= centreZ(def) ? 12.0 : -12.0); + } + Vec3 at = toWorld(drop); + cargo.stopRiding(); + cargo.moveTo(at.x, at.y, at.z, getYRot() + rack.yaw(), 0.0F); + return; + } + } + + /** Leaves the rack this vehicle rides on, at the rack entry point. Server only. */ + public void leaveRack() { + if (!(getVehicle() instanceof AircraftEntity carrier)) { + return; + } + AircraftDefinition carrierDef = carrier.definition(); + RackDefinition rack = carrierDef != null ? carrierDef.rack(carrier.seatOf(this)) : null; + Vec3 at = rack != null ? carrier.toWorld(rack.entry()) : position(); + stopRiding(); + moveTo(at.x, at.y, at.z, getYRot(), 0.0F); + } + + /** + * Pilot of a vehicle near a free rack of a carrier boards that rack. Server only. + * + * @return whether a rack was found + */ + public boolean boardNearbyRack() { + if (isPassenger()) { + return false; + } + for (AircraftEntity carrier : level().getEntitiesOfClass(AircraftEntity.class, getBoundingBox().inflate(60.0))) { + AircraftDefinition def = carrier.definition(); + if (carrier == this || def == null) { + continue; + } + for (int seat = def.seatCount(); seat < def.seatAndRackCount(); seat++) { + if (carrier.isSeatFree(seat) && carrier.acceptsCargo(this, seat) && carrier.isNearRackEntry(this, def.rack(seat)) + && carrier.loadCargo(this, seat)) { + return true; + } + } + } + return false; + } + + private static double centreZ(AircraftDefinition def) { + if (def.boundingBoxes.isEmpty()) { + return 0.0; + } + double min = Double.MAX_VALUE; + double max = -Double.MAX_VALUE; + for (var box : def.boundingBoxes) { + min = Math.min(min, box.offset().z() - box.width() / 2.0); + max = Math.max(max, box.offset().z() + box.width() / 2.0); + } + return (min + max) / 2.0; + } + + // Ejection + + /** Seats zero and one of vehicles with {@code EnableEjectionSeat} can bail out. */ + public boolean canEject(Entity entity) { + AircraftDefinition def = definition(); + int seat = seatOf(entity); + return def != null && def.ejectionSeat && seat >= 0 && seat < 2 && !(seat == 0 && def.uav); + } + + /** + * Throws the occupant clear of the aircraft. Without the parachute item (a later phase) the + * crew member gets a few seconds of slow falling instead. Server only. + */ + public void eject(Entity entity) { + AircraftDefinition def = definition(); + int seat = seatOf(entity); + if (def == null || !canEject(entity)) { + return; + } + Vector3 seatPos = def.seats.get(seat).position(); + Vec3 out = toWorld(seatPos.add(0.0, 2.0, 0.0)); + Vector3f push = toWorldOffset(new Vector3(0.0, 2.0, 0.0)); + entity.stopRiding(); + entity.teleportTo(out.x, out.y, out.z); + entity.setDeltaMovement(getDeltaMovement().add(push.x + (random.nextFloat() - 0.5) * 0.1, push.y, + push.z + (random.nextFloat() - 0.5) * 0.1)); + entity.hurtMarked = true; + if (entity instanceof LivingEntity living) { + living.addEffect(new MobEffectInstance(MobEffects.SLOW_FALLING, 400)); + } + if (canopy != null && !canopy.status()) { + canopy.set(true, false); + modeSwitchCooldown = 40; + } + ModSounds.playAt(entity, "eject_seat", 5.0F, 1.0F); + } + + // Terrain helpers + + /** True if any solid block is within {@code depth} blocks below the vehicle. */ + protected boolean isNearGround(int depth) { + BlockPos.MutableBlockPos pos = blockPosition().mutable(); + for (int i = 0; i < depth; i++) { + pos.move(0, -1, 0); + if (!level().getBlockState(pos).getCollisionShape(level(), pos).isEmpty()) { + return true; + } + } + return false; + } + + /** + * Port of {@code MCH_Lib.getBlockY}: scans a {@code size} x {@code size} column from the + * vehicle's block down {@code depth} layers (the first layer being the vehicle's own). + * + * @param solidOnly ignore blocks without collision; fluids still count when this is false + */ + public boolean hasBlockBelow(int size, int depth, boolean solidOnly) { + int px = Mth.floor(getX() + 0.5); + int py = Mth.floor(getY() + 0.5); + int pz = Mth.floor(getZ() + 0.5); + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + for (int y = 0; y < depth; y++) { + for (int x = -size / 2; x <= size / 2; x++) { + for (int z = -size / 2; z <= size / 2; z++) { + pos.set(px + x, py - y, pz + z); + BlockState state = level().getBlockState(pos); + if (state.isAir()) { + continue; + } + if (!solidOnly || !state.getCollisionShape(level(), pos).isEmpty() || !state.getFluidState().isEmpty()) { + return true; + } + } + } + } + return false; + } + + /** True if the first non-air block within {@code depth} below is water. */ + protected boolean isWaterBelow(int depth) { + BlockPos.MutableBlockPos pos = blockPosition().mutable(); + for (int i = 0; i < depth; i++) { + BlockState state = level().getBlockState(pos); + if (!state.isAir()) { + return !state.getFluidState().isEmpty(); + } + pos.move(0, -1, 0); + } + return false; + } + + /** {@code getBlockY(this, 3, -2, false)}: standing on something that is neither air nor water. */ + protected boolean isOnSolidGround() { + int px = Mth.floor(getX() + 0.5); + int py = Mth.floor(getY() + 0.5); + int pz = Mth.floor(getZ() + 0.5); + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + for (int y = 0; y < 2; y++) { + for (int x = -1; x <= 1; x++) { + for (int z = -1; z <= 1; z++) { + BlockState state = level().getBlockState(pos.set(px + x, py - y, pz + z)); + if (!state.isAir()) { + return state.getFluidState().isEmpty(); + } + } + } + } + return false; + } + + /** + * Port of {@code getWaterDepth}: the share of the hit box that is in water, from 0 to 1, sampled + * in five horizontal slices. {@code FloatOffset} raises the slices, so a hull with an offset rides + * higher. The float physics only pushes up once this reaches 1, which sets the waterline. + * + *

The original drew the model {@value #LEGACY_MODEL_LIFT} above the bottom of the hit box + * (its {@code yOffset}, half the 0.7 default height); this port draws it at the bottom. The + * slices are lowered by the same amount so boats sit in the water as they did in 1.0.3. + */ + protected double waterDepth() { + if (entityData.get(DATA_SINKING)) { + return 0.0; + } + AircraftDefinition def = definition(); + float offset = (def != null ? def.floatOffset : 0.0F) - LEGACY_MODEL_LIFT; + AABB box = getBoundingBox(); + double height = box.maxY - box.minY; + double depth = 0.0; + for (int i = 0; i < 5; i++) { + double bottom = box.minY + height * i / 5.0 - 0.125 + offset; + double top = box.minY + height * (i + 1) / 5.0 - 0.125 + offset; + AABB slice = new AABB(box.minX, bottom, box.minZ, box.maxX, top, box.maxZ); + if (level().getBlockStates(slice).anyMatch(state -> state.getFluidState().is(FluidTags.WATER))) { + depth += 1.0 / 5.0; + } + } + return depth; + } + + /** {@code applyOnGroundPitch}: eases pitch towards {@code OnGroundPitch} and roll towards level. */ + protected void applyOnGroundPitch(float factor) { + AircraftDefinition def = definition(); + if (def != null) { + float ground = def.onGroundPitch; + setXRot((getXRot() - ground) * factor + ground); + } + roll *= factor; + } + + /** {@code StepHeight}: how tall a ledge the vehicle drives up without jumping. */ + @Override + public float maxUpStep() { + AircraftDefinition def = definition(); + return def != null ? def.stepHeight : 0.0F; + } + + @Override + public boolean isPickable() { + return !isRemoved(); + } + + @Override + public boolean canBeCollidedWith() { + return true; + } + + @Override + public boolean isPushable() { + return false; + } + + // Weapons + + public @Nullable VehicleWeapons weapons() { + definition(); + return weapons; + } + + public void publishWeaponSelection() { + if (weapons != null && !level().isClientSide()) { + entityData.set(DATA_WEAPON_SELECTION, weapons.encodeSelection()); + } + } + + public void publishWeaponUse(int stat) { + entityData.set(DATA_WEAPON_USE, stat); + } + + public int syncedWeaponUse() { + return entityData.get(DATA_WEAPON_USE); + } + + /** + * Whether {@code set} fires along the user's view instead of the hull: the shot direction and + * the muzzle positions are worked out as if the vehicle faced where the user looks. Static + * mounts do this; see {@link GroundVehicleEntity#aimsWithUser}. + */ + public boolean aimsWithUser(WeaponSet set) { + return false; + } + + /** {@code isInfinityAmmo}: creative players never run dry, nor does anyone when the server config says so. */ + public boolean hasInfiniteAmmo(@Nullable Entity user) { + return ServerConfig.infiniteAmmo() || user instanceof Player player && player.getAbilities().instabuild; + } + + /** Where the pilot looks relative to the hull: the turret's traverse. */ + public float turretYaw() { + return animation.seatYaw(0, 1.0F); + } + + public float stealth() { + AircraftDefinition def = definition(); + return def != null ? def.stealth : 0.0F; + } + + /** The TV missile the crew is flying, if it still exists. */ + public @Nullable TvMissileEntity tvMissile() { + if (level().isClientSide()) { + return level().getEntity(entityData.get(DATA_TV_MISSILE)) instanceof TvMissileEntity m && !m.isRemoved() ? m : null; + } + return tvMissile != null && !tvMissile.isRemoved() ? tvMissile : null; + } + + public void setTvMissile(@Nullable TvMissileEntity missile) { + tvMissile = missile; + entityData.set(DATA_TV_MISSILE, missile != null ? missile.getId() : 0); + } + + /** Whether the flares are burning right now, for the crew's display. */ + public boolean isFlareActive() { + return entityData.get(DATA_FLARE); + } + + /** Someone is locking on to this vehicle; its crew hears the warning. Server only. */ + public void warnLocked() { + if (alertCount == 0 && !isDestroyed() && weapons != null && weapons.flares.isFitted()) { + alertCount = 10; + for (int seat = 0; seat < 2; seat++) { + if (occupant(seat) instanceof ServerPlayer player) { + ModSounds.playTo(player, "locked", 1.0F, 1.0F); + } + } + } + } + + // Hit boxes + + /** + * Clips a ray against the main hit box and every {@code BoundingBox}, remembering the damage + * factor of the box hit first, as the original's composite bounding box did. + */ + public Optional clipHitBoxes(Vec3 from, Vec3 to, double inflate) { + hitBoxFactor = 1.0F; + Optional best = clip(getBoundingBox().inflate(inflate), from, to); + double bestDistance = best.map(from::distanceToSqr).orElse(Double.MAX_VALUE); + AircraftDefinition def = definition(); + if (def != null) { + for (BoundingBoxDefinition box : def.boundingBoxes) { + Vec3 c = toWorld(box.offset()); + double hw = box.width() / 2.0 + inflate; + double hh = box.height() / 2.0 + inflate; + Optional hit = clip(new AABB(c.x - hw, c.y - hh, c.z - hw, c.x + hw, c.y + hh, c.z + hw), from, to); + if (hit.isPresent() && from.distanceToSqr(hit.get()) < bestDistance) { + best = hit; + bestDistance = from.distanceToSqr(hit.get()); + hitBoxFactor = box.damageFactor(); + } + } + } + return best; + } + + /** A ray that starts inside a box hits it where it starts, as it does for vanilla entity picking. */ + private static Optional clip(AABB box, Vec3 from, Vec3 to) { + return box.contains(from) ? Optional.of(from) : box.clip(from, to); + } + + public float consumeHitBoxFactor() { + float f = hitBoxFactor; + hitBoxFactor = 1.0F; + return f; + } + + // Damage + + public boolean isDestroyed() { + return definition() != null && hp() <= 0; + } + + /** + * Port of the original's {@code attackEntityFrom}. Armour caps a hit at {@code ArmorMaxDamage}, + * scales it by {@code ArmorDamageFactor} and the struck box's factor, then subtracts + * {@code ArmorMinDamage}. A player punching an empty vehicle packs it up instead. + */ + @Override + public boolean hurt(DamageSource source, float damage) { + // 1.21.1 also calls this on the attacking client; only the server decides. + if (!(level() instanceof ServerLevel level)) { + return false; + } + float boxFactor = consumeHitBoxFactor(); + if (isRemoved() || isInvulnerableTo(source) || timeSinceHit > 0) { + return false; + } + if (source.is(DamageTypes.IN_FIRE) || source.is(DamageTypes.CACTUS)) { + return false; + } + boolean lava = source.is(DamageTypes.LAVA); + boolean onFire = source.is(DamageTypes.ON_FIRE); + if (lava) { + damage *= random.nextInt(8) + 2; + timeSinceHit = 2; + } + if (source.is(DamageTypeTags.IS_EXPLOSION)) { + timeSinceHit = 1; + } else if (source.getEntity() != null && hasPassenger(source.getEntity())) { + return false; + } + if (onFire) { + timeSinceHit = 10; + } + + boolean creative = false; + boolean sneaking = false; + boolean packUp = false; + if (source.getEntity() instanceof Player player) { + creative = player.getAbilities().instabuild; + sneaking = player.isShiftKeyDown(); + if (source.is(DamageTypes.PLAYER_ATTACK)) { + packUp = creative || !hasPlayerPassenger(); + } + ModSounds.playAt(this, "hit", 1.0F, 1.0F); + } else { + ModSounds.playAt(this, "helidmg", 1.0F, 0.9F + random.nextFloat() * 0.1F); + } + + if (!isDestroyed()) { + if (!packUp) { + AircraftDefinition def = definition(); + if (def != null && !lava && !onFire) { + damage = Math.min(damage, def.armorMaxDamage); + if (boxFactor <= 1.0F) { + damage *= boxFactor; + } + damage *= def.armorDamageFactor; + damage -= def.armorMinDamage; + if (damage <= 0.0F) { + return false; + } + if (boxFactor > 1.0F) { + damage *= boxFactor; + } + } + damage *= ServerConfig.vehicleDamageFactor(); + entityData.set(DATA_HP, Math.max(hp() - (int) damage, 0)); + } + setHurtDir(-getHurtDir()); + // No markHurt(): it sends the server's motion to every client, which snaps a vehicle + // its pilot is flying back a step and looks like knockback on every hit. + setHurtTime(10); + if (hp() <= 0 || packUp) { + if (!packUp) { + entityData.set(DATA_HP, 0); + destroyVehicle(); + timeSinceHit = 20; + if (isAfloat()) { + startCookOff(source.getEntity()); + } else if (source.is(DamageTypes.IN_WALL) || source.is(DamageTypes.FLY_INTO_WALL) || source.is(DamageTypes.FALL)) { + crashExplosion(); + } else { + Explosions.explode(level, getX(), getY(), getZ(), + new Explosions.Params(null, source.getEntity(), 2.0F, 2.0F, true, true, 5, false, null)); + } + } else { + // In creative the item only drops when sneaking, as with the original's defaults. + if (!creative || sneaking) { + spawnAtLocation(getPickResult() != null ? getPickResult() : ItemStack.EMPTY); + } + discard(); + } + } + } else if (packUp && creative) { + discard(); + } + return true; + } + + private boolean hasPlayerPassenger() { + for (Entity passenger : getPassengers()) { + if (passenger instanceof Player) { + return true; + } + } + return false; + } + + /** {@code destroyAircraft}: the wreck tumbles, burns and disappears after 30 seconds. Server only. */ + private void destroyVehicle() { + AircraftDefinition def = definition(); + despawnCount = def != null && def.targetDrone ? 50 : WRECK_TICKS; + destroyedPitch = random.nextFloat() - 0.5F; + destroyedRoll = (random.nextFloat() - 0.5F) * 0.5F; + destroyedYaw = 0.0F; + setHovering(false); + if (def != null && def.uav) { + Entity remote = occupant(0); + if (remote != null) { + remote.stopRiding(); + } + } + for (int seat = 0; seat < 2; seat++) { + Entity crew = occupant(seat); + if (crew != null && canEject(crew)) { + eject(crew); + } + } + } + + /** {@code explosionByCrash}: a wreck hitting the ground goes up in a fireball sized by its fuel. */ + private void crashExplosion() { + if (!(level() instanceof ServerLevel level)) { + return; + } + exploded = true; + AircraftDefinition def = definition(); + float size = Mth.clamp(def != null ? def.maxFuel / 400.0F : 2.0F, 1.0F, 15.0F); + Explosions.explode(level, getX(), getY(), getZ(), + new Explosions.Params(null, null, size, size >= 2.0F ? size * 0.5F : 1.0F, true, true, 5, false, null)); + if (!isInWater()) { + Explosions.crashFireball(level, getX(), getY(), getZ(), size); + } + } + + /** A floating vehicle sitting on the water, such as a patrol boat or a float plane at rest. */ + private boolean isAfloat() { + AircraftDefinition def = definition(); + return def != null && def.floats && waterDepth() > 0.0; + } + + /** + * A boat destroyed on the water does not go up in one crash fireball; its fuel and magazine + * cook off in three blasts over about five seconds, then the hull sinks. Not in the original, + * which only had the crash explosion. + */ + private void startCookOff(@Nullable Entity cause) { + exploded = true; + cookOffBlasts = 3; + cookOffDelay = 1; + cookOffCause = cause; + } + + private void tickCookOff(ServerLevel level) { + if (cookOffBlasts <= 0 || --cookOffDelay > 0) { + return; + } + AircraftDefinition def = definition(); + int blast = 4 - cookOffBlasts--; + float fuel = Mth.clamp(def != null ? def.maxFuel / 400.0F : 2.0F, 1.0F, 15.0F); + // The fuel tank first, then whatever the magazine still holds, then the hull goes up. + float size = switch (blast) { + case 1 -> fuel * 0.6F; + case 2 -> Math.max(munitionSize(), fuel * 0.4F); + default -> fuel; + }; + size = Mth.clamp(size, 1.0F, 15.0F); + Vec3 at = cookOffPoint(def); + Explosions.explode(level, at.x, at.y, at.z, + new Explosions.Params(null, cookOffCause, size, size >= 2.0F ? size * 0.5F : 1.0F, true, true, 5, false, null)); + // The blasts happen at the waterline, but the fire is above it, so show the full fireball. + Explosions.crashFireball(level, at.x, at.y, at.z, size); + if (cookOffBlasts > 0) { + // Three blasts inside five seconds: about 1.5 to 2.5 seconds apart. + cookOffDelay = 30 + random.nextInt(20); + } else { + entityData.set(DATA_SINKING, true); + cookOffCause = null; + } + } + + /** + * A cooked-off wreck going under, in place of the vehicle's own physics: it settles slowly, + * stern first with the bow raised and listing to one side, drifts to a stop and comes to rest + * on the bottom. Out of the water it simply falls. + */ + private void sink() { + Vec3 m = getDeltaMovement(); + double my = isInWater() ? Math.max(m.y - 0.003, -SINK_SPEED) : m.y - 0.04; + Vec3 motion = new Vec3(m.x * 0.9, my, m.z * 0.9); + move(MoverType.SELF, motion); + setDeltaMovement(onGround() ? motion.multiply(0.5, 0.0, 0.5) : motion); + if (!onGround()) { + // Which way it lists only has to be the same on every side, so take it from the id. + float list = (getId() & 1) == 0 ? SINK_LIST : -SINK_LIST; + setXRot(Mth.approach(getXRot(), -SINK_PITCH, 0.12F)); + roll = Mth.approach(roll, list, 0.15F); + } + } + + /** + * How big the ammunition aboard goes up: each weapon's {@code Explosion} scaled by how full its + * magazine and reserve still are. Guns and empty racks add nothing. + */ + private float munitionSize() { + if (weapons == null) { + return 0.0F; + } + float size = 0.0F; + for (WeaponSet set : weapons.sets()) { + int rounds = set.ammo() + set.restAmmo(); + if (rounds > 0 && set.definition().explosion > 0) { + size += set.definition().explosion * Math.min(1.0F, (float) rounds / Math.max(set.maxAmmo(), 1)); + } + } + return size; + } + + /** A random spot on the hull: one of its hit boxes, or the middle of the body. */ + private Vec3 cookOffPoint(@Nullable AircraftDefinition def) { + if (def == null || def.boundingBoxes.isEmpty() || random.nextInt(def.boundingBoxes.size() + 1) == 0) { + return position().add(0.0, def != null ? def.bodyHeight / 2.0 : 0.5, 0.0); + } + return toWorld(def.boundingBoxes.get(random.nextInt(def.boundingBoxes.size())).offset()); + } + + /** Destroyed state each tick: the wreck spins, sets its crew alight and explodes on impact. */ + private void tickDestroyed() { + if (!isDestroyed()) { + return; + } + input.clear(); + throttle *= 0.98; + if (this instanceof HelicopterEntity) { + destroyedYaw = Math.min(destroyedYaw + 0.3F, 15.0F); + setYRot(getYRot() + destroyedYaw * (float) throttle); + if (!isNearGround(3)) { + if (Math.abs(getXRot()) < 10.0F) { + setXRot(getXRot() + destroyedPitch); + } + roll += destroyedRoll; + } + } + if (!(level() instanceof ServerLevel level)) { + return; + } + tickCookOff(level); + if (ticksAlive % 20 == 0) { + AircraftDefinition def = definition(); + for (int seat = 0; seat < seatIds.length; seat++) { + Entity crew = occupant(seat); + if (crew != null && !(seat == 0 && def != null && def.uav)) { + crew.igniteForSeconds(5.0F); + } + } + } + if (despawnCount > 0 && --despawnCount <= 1) { + ejectPassengers(); + discard(); + } + } + + /** {@code onUpdate_Particle2}: smoke from a damaged hull, thicker the lower its hit points. */ + private void damageSmoke(AircraftDefinition def) { + float ratio = (float) hp() / def.maxHp; + if (ratio > 0.5F) { + return; + } + int sources = 1 + def.boundingBoxes.size(); + int pick = random.nextInt(sources); + Vec3 at = pick == 0 ? position().add(0.0, def.bodyHeight / 2.0, 0.0) : toWorld(def.boundingBoxes.get(pick - 1).offset()); + if (ratio < random.nextFloat() / 2.0F) { + level().addAlwaysVisibleParticle(ParticleTypes.LARGE_SMOKE, true, at.x, at.y, at.z, + (random.nextDouble() - 0.5) * 0.3, random.nextDouble() * 0.1, (random.nextDouble() - 0.5) * 0.3); + } + if (isDestroyed()) { + level().addAlwaysVisibleParticle(ParticleTypes.CAMPFIRE_COSY_SMOKE, true, at.x, at.y, at.z, 0.0, 0.07, 0.0); + if (random.nextInt(3) == 0) { + level().addParticle(ParticleTypes.FLAME, at.x + random.nextGaussian() * 0.5, at.y, at.z + random.nextGaussian() * 0.5, 0.0, 0.05, 0.0); + } + wreckFire(def); + } + } + + /** + * Not in the original: the wreck keeps burning until it disappears, with a fire glow that + * lights up the smoke at night and dies down over the last few seconds. Nothing burns under + * water. Client only. + */ + private void wreckFire(AircraftDefinition def) { + wreckTicks++; + if (isInWater()) { + return; + } + float left = Math.clamp((float) (WRECK_TICKS - wreckTicks) / WRECK_FADE_TICKS, 0.15F, 1.0F); + Vec3 centre = position().add(0.0, def.bodyHeight / 2.0, 0.0); + if (wreckTicks % 8 == 0) { + // Each glow outlives the gap to the next, so the fire never goes dark in between. + float size = Math.clamp(def.bodyWidth * 0.8F, 2.0F, 8.0F) * left; + level().addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.FIRE, size, 24), true, + centre.x, centre.y, centre.z, 0.0, 0.02, 0.0); + } + if (random.nextFloat() < left) { + double spread = def.bodyWidth * 0.4; + level().addParticle(ParticleTypes.FLAME, centre.x + random.nextGaussian() * spread, centre.y, + centre.z + random.nextGaussian() * spread, 0.0, 0.04, 0.0); + } + if (random.nextInt(10) == 0) { + level().addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.EMBER, 0.3F + random.nextFloat() * 0.3F, + 20 + random.nextInt(20)), true, centre.x, centre.y, centre.z, + (random.nextDouble() - 0.5) * 0.15, 0.1 + random.nextDouble() * 0.15, (random.nextDouble() - 0.5) * 0.15); + } + } + + @Override + public @Nullable ItemStack getPickResult() { + AircraftDefinition def = definition(); + return def != null ? AircraftItem.stackOf(def) : null; + } + + // Dimensions and syncing + + @Override + public EntityDimensions getDimensions(Pose pose) { + AircraftDefinition def = definition(); + return def != null ? EntityDimensions.scalable(def.bodyWidth, def.bodyHeight) : super.getDimensions(pose); + } + + @Override + public void onSyncedDataUpdated(EntityDataAccessor accessor) { + super.onSyncedDataUpdated(accessor); + if (DATA_TYPE.equals(accessor)) { + refreshDimensions(); + definition(); + } else if (DATA_SEATS.equals(accessor) && level().isClientSide()) { + decodeSeats(entityData.get(DATA_SEATS)); + } else if (DATA_WEAPON_SELECTION.equals(accessor) && level().isClientSide() && weapons() != null) { + weapons.decodeSelection(entityData.get(DATA_WEAPON_SELECTION), ClientAccess.localPlayer()); + } + } + + /** + * Server position updates are eased in over three ticks, as 26.x's linear interpolation does, + * with the step vanilla's boat uses on 1.21.1. + */ + @Override + public void lerpTo(double x, double y, double z, float yRot, float xRot, int steps) { + lerpX = x; + lerpY = y; + lerpZ = z; + lerpYRot = yRot; + lerpXRot = xRot; + lerpSteps = LERP_STEPS; + } + + @Override + public double lerpTargetX() { + return lerpSteps > 0 ? lerpX : getX(); + } + + @Override + public double lerpTargetY() { + return lerpSteps > 0 ? lerpY : getY(); + } + + @Override + public double lerpTargetZ() { + return lerpSteps > 0 ? lerpZ : getZ(); + } + + @Override + public float lerpTargetXRot() { + return lerpSteps > 0 ? (float) lerpXRot : getXRot(); + } + + @Override + public float lerpTargetYRot() { + return lerpSteps > 0 ? (float) lerpYRot : getYRot(); + } + + private void tickLerp() { + if (isControlledByLocalInstance()) { + lerpSteps = 0; + syncPacketPositionCodec(getX(), getY(), getZ()); + } + if (lerpSteps > 0) { + lerpPositionAndRotationStep(lerpSteps, lerpX, lerpY, lerpZ, lerpYRot, lerpXRot); + lerpSteps--; + } + } + + @Override + protected void addAdditionalSaveData(CompoundTag tag) { + tag.putString("Type", typeName()); + tag.putString("Texture", texture()); + tag.putInt("Hp", hp()); + tag.putFloat("Roll", roll); + tag.putBoolean("Hovering", isHoveringMode()); + tag.putBoolean("GunnerMode", isGunnerMode()); + tag.putInt("Parts", partBits()); + tag.putInt("DespawnCount", despawnCount); + tag.putInt("CookOff", cookOffBlasts); + tag.putBoolean("Sinking", entityData.get(DATA_SINKING)); + if (weapons != null) { + tag.putIntArray("WeaponsAmmo", weapons.saveAmmo()); + } + ListTag seats = new ListTag(); + for (int i = 0; i < seatIds.length; i++) { + Entity e = occupant(i); + seats.add(StringTag.valueOf(e != null ? e.getUUID().toString() : "")); + } + tag.put("Seats", seats); + } + + @Override + protected void readAdditionalSaveData(CompoundTag tag) { + entityData.set(DATA_TYPE, tag.getString("Type")); + entityData.set(DATA_TEXTURE, tag.getString("Texture")); + entityData.set(DATA_PARTS, tag.getInt("Parts")); + AircraftDefinition def = definition(); + entityData.set(DATA_HP, tag.contains("Hp") ? tag.getInt("Hp") : def != null ? def.maxHp : 1); + roll = tag.getFloat("Roll"); + setHovering(tag.getBoolean("Hovering")); + entityData.set(DATA_GUNNER_MODE, tag.getBoolean("GunnerMode")); + despawnCount = tag.getInt("DespawnCount"); + if (hp() <= 0 && despawnCount <= 0) { + despawnCount = WRECK_TICKS; + } + cookOffBlasts = tag.getInt("CookOff"); + cookOffDelay = cookOffBlasts > 0 ? 20 : 0; + entityData.set(DATA_SINKING, tag.getBoolean("Sinking")); + exploded |= cookOffBlasts > 0 || entityData.get(DATA_SINKING); + if (tag.contains("WeaponsAmmo", Tag.TAG_INT_ARRAY)) { + int[] ammo = tag.getIntArray("WeaponsAmmo"); + if (weapons != null) { + weapons.loadAmmo(ammo); + } else { + savedAmmo = ammo; + } + } + savedSeats.clear(); + ListTag seats = tag.getList("Seats", Tag.TAG_STRING); + for (int i = 0; i < seats.size(); i++) { + if (!seats.getString(i).isEmpty()) { + try { + savedSeats.put(UUID.fromString(seats.getString(i)), i); + } catch (IllegalArgumentException ignored) { + // A hand edited save; that occupant simply takes the first free seat. + } + } + } + for (PartState part : parts) { + part.force(part.status()); + } + refreshDimensions(); + } + + /** Raw pilot input, filled in by the client controller every tick. */ + public static final class ControlInput { + public boolean throttleUp; + public boolean throttleDown; + public boolean left; + public boolean right; + public boolean brake; + /** The pilot is looking around instead of steering; planes and tanks yaw with A/D then. */ + public boolean freeLook; + + public void clear() { + throttleUp = false; + throttleDown = false; + left = false; + right = false; + brake = false; + freeLook = false; + } + + public byte write() { + return (byte) ((throttleUp ? 1 : 0) | (throttleDown ? 2 : 0) | (left ? 4 : 0) | (right ? 8 : 0) + | (brake ? 16 : 0) | (freeLook ? 32 : 0)); + } + + public void read(byte bits) { + throttleUp = (bits & 1) != 0; + throttleDown = (bits & 2) != 0; + left = (bits & 4) != 0; + right = (bits & 8) != 0; + brake = (bits & 16) != 0; + freeLook = (bits & 32) != 0; + } + } + + public int getHurtTime() { + return entityData.get(DATA_HURT_TIME); + } + + public void setHurtTime(int ticks) { + entityData.set(DATA_HURT_TIME, ticks); + } + + public int getHurtDir() { + return entityData.get(DATA_HURT_DIR); + } + + public void setHurtDir(int dir) { + entityData.set(DATA_HURT_DIR, dir); + } + + /** The yaw for this frame, as 26.x's {@code Entity#getYRot(float)} gives it. */ + public float getYRot(float partialTicks) { + return partialTicks == 1.0F ? getYRot() : Mth.rotLerp(partialTicks, yRotO, getYRot()); + } + + /** The pitch for this frame, as 26.x's {@code Entity#getXRot(float)} gives it. */ + public float getXRot(float partialTicks) { + return partialTicks == 1.0F ? getXRot() : Mth.lerp(partialTicks, xRotO, getXRot()); + } +} From 5dc9de15d56507d56941c4d19692eabf8999ceb5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:43 +0800 Subject: [PATCH 209/314] Add AnimationState to the shared 1.21.1 sources --- .../mcheli/entity/AnimationState.java | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java new file mode 100644 index 0000000..eda5ed7 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/AnimationState.java @@ -0,0 +1,202 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.RackDefinition; +import com.jnhprojects.mcheli.content.aircraft.part.RotatingPart; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; + +/** + * Purely visual state that every side advances once per tick: wheels, tracks, spinning parts and + * where each crew member is looking relative to the hull. + * + *

Ported from {@code updatePartWheel}, {@code updatePartCrawlerTrack} and + * {@code onUpdate_PartRotation}. Nothing in here affects physics, so it is never synced; each + * client derives it from the synced throttle, input and crew rotations. + */ +public final class AnimationState { + /** Road wheel roll in degrees. */ + public float wheelRoll; + public float prevWheelRoll; + /** Steering from -1 (full right) to 1 (full left). */ + public float steer; + public float prevSteer; + /** Track roller angle per side, index 1 is positive X. */ + public final float[] roller = new float[2]; + public final float[] prevRoller = new float[2]; + /** Track run per side, in link spacings from 0 to 1. */ + public final float[] track = new float[2]; + public final float[] prevTrack = new float[2]; + private final float[] trackSpeed = new float[2]; + public float[] rotatingParts = new float[0]; + public float[] prevRotatingParts = new float[0]; + public float throttle; + public float prevThrottle; + /** Per seat and rack: view yaw and pitch of the occupant relative to the hull. */ + public float[] seatYaw = new float[0]; + public float[] prevSeatYaw = new float[0]; + public float[] seatPitch = new float[0]; + public float[] prevSeatPitch = new float[0]; + + void resize(AircraftDefinition def) { + rotatingParts = new float[def.rotatingParts.size()]; + prevRotatingParts = new float[def.rotatingParts.size()]; + int seats = def.seatAndRackCount(); + seatYaw = new float[seats]; + prevSeatYaw = new float[seats]; + seatPitch = new float[seats]; + prevSeatPitch = new float[seats]; + for (int i = def.seatCount(); i < seats; i++) { + RackDefinition rack = def.rack(i); + seatYaw[i] = prevSeatYaw[i] = rack.yaw(); + seatPitch[i] = prevSeatPitch[i] = rack.pitch(); + } + } + + void tick(AircraftEntity vehicle, AircraftDefinition def) { + AircraftEntity.ControlInput input = vehicle.input(); + float visibleThrottle = vehicle.visibleThrottle(); + float reverse = vehicle.visibleReverse(); + prevThrottle = throttle; + throttle = visibleThrottle; + + tickWheels(def, input, visibleThrottle, reverse); + tickTracks(def, input, visibleThrottle, reverse); + tickRotatingParts(def, vehicle.getControllingPassenger() != null); + tickCrewAim(vehicle, def); + } + + private void tickWheels(AircraftDefinition def, AircraftEntity.ControlInput input, double throttle, float reverse) { + prevWheelRoll = wheelRoll; + prevSteer = steer; + if (def.enableBack && reverse > 0.01F && throttle <= 0.0) { + throttle = -reverse * 15.0F; + } + if (input.left && !input.right) { + steer = Math.min(steer + 0.1F, 1.0F); + } else if (input.right && !input.left) { + steer = Math.max(steer - 0.1F, -1.0F); + } else { + steer *= 0.9F; + } + wheelRoll += (float) (throttle * def.partWheelRot); + if (wheelRoll >= 360.0F) { + wheelRoll -= 360.0F; + prevWheelRoll -= 360.0F; + } else if (wheelRoll < 0.0F) { + wheelRoll += 360.0F; + prevWheelRoll += 360.0F; + } + } + + private void tickTracks(AircraftDefinition def, AircraftEntity.ControlInput input, double throttle, float reverse) { + System.arraycopy(roller, 0, prevRoller, 0, 2); + System.arraycopy(track, 0, prevTrack, 0, 2); + double pivot = def.pivotTurnThrottle <= 0.0F ? 1.0 : def.pivotTurnThrottle * 0.1F; + boolean left = input.left; + boolean right = input.right; + int dir = 1; + if (def.enableBack && reverse > 0.0F && throttle <= 0.0) { + throttle = -reverse * 5.0F; + // Reversing swaps which track leads a turn. + if (left != right) { + boolean t = left; + left = right; + right = t; + dir = -1; + } + } + if (left && !right) { + double t = 0.2 * dir; + trackSpeed[0] += (float) t; + trackSpeed[1] -= (float) (pivot * t); + } else if (right && !left) { + double t = 0.2 * dir; + trackSpeed[0] -= (float) (pivot * t); + trackSpeed[1] += (float) t; + } else { + float t = (float) Mth.clamp(throttle, -0.2, 0.2); + trackSpeed[0] += t; + trackSpeed[1] += t; + } + + for (int i = 0; i < 2; i++) { + trackSpeed[i] = Mth.clamp(trackSpeed[i], -0.72F, 0.72F); + roller[i] += trackSpeed[i] * def.trackRollerRot; + if (roller[i] >= 360.0F) { + roller[i] -= 360.0F; + prevRoller[i] -= 360.0F; + } else if (roller[i] < 0.0F) { + roller[i] += 360.0F; + prevRoller[i] += 360.0F; + } + track[i] -= trackSpeed[i]; + while (track[i] >= 1.0F) { + track[i]--; + prevTrack[i]--; + } + while (track[i] < 0.0F) { + track[i]++; + } + while (prevTrack[i] < 0.0F) { + prevTrack[i]++; + } + trackSpeed[i] *= 0.75F; + } + } + + private void tickRotatingParts(AircraftDefinition def, boolean piloted) { + for (int i = 0; i < rotatingParts.length && i < def.rotatingParts.size(); i++) { + RotatingPart part = def.rotatingParts.get(i); + prevRotatingParts[i] = rotatingParts[i]; + if (part.always() || piloted) { + float r = rotatingParts[i] + part.speed(); + if (r < 0.0F) { + r += 360.0F; + prevRotatingParts[i] += 360.0F; + } else if (r >= 360.0F) { + r -= 360.0F; + prevRotatingParts[i] -= 360.0F; + } + rotatingParts[i] = r; + } + } + } + + private void tickCrewAim(AircraftEntity vehicle, AircraftDefinition def) { + float hullYaw = vehicle.getYRot(); + float hullPitch = vehicle.getXRot(); + float hullRoll = vehicle.roll(); + for (int i = 0; i < seatYaw.length && i < def.seatCount(); i++) { + prevSeatYaw[i] = seatYaw[i]; + prevSeatPitch[i] = seatPitch[i]; + Entity crew = vehicle.occupant(i); + if (crew == null) { + continue; + } + float yaw = Mth.wrapDegrees(crew.getYRot() - hullYaw); + double rad = Math.toRadians(yaw); + // The hull tilt along the crew member's view direction, as in the original tank code. + float tilt = (float) (hullPitch * Math.cos(rad) + hullRoll * Math.sin(rad)); + float pitch = crew.getXRot() - tilt; + // Keep the previous value on the same side of the seam so interpolation never spins. + prevSeatYaw[i] = yaw + Mth.wrapDegrees(prevSeatYaw[i] - yaw); + seatYaw[i] = yaw; + seatPitch[i] = pitch; + } + } + + public float seatYaw(int seat, float partialTicks) { + if (seat < 0 || seat >= seatYaw.length) { + return 0.0F; + } + return prevSeatYaw[seat] + (seatYaw[seat] - prevSeatYaw[seat]) * partialTicks; + } + + public float seatPitch(int seat, float partialTicks) { + if (seat < 0 || seat >= seatPitch.length) { + return 0.0F; + } + return prevSeatPitch[seat] + (seatPitch[seat] - prevSeatPitch[seat]) * partialTicks; + } +} From 2fd1712387b8856f5b76d55d15598a90ed54dde4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:44 +0800 Subject: [PATCH 210/314] Add ClientAccess to the shared 1.21.1 sources --- .../mcheli/entity/ClientAccess.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java new file mode 100644 index 0000000..bf89ea5 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/ClientAccess.java @@ -0,0 +1,50 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.network.PackDataPacket; +import com.jnhprojects.mcheli.network.PackListPacket; +import net.minecraft.world.entity.Entity; +import org.jspecify.annotations.Nullable; + +import java.util.function.Supplier; + +/** + * The few client facts common code needs, such as who the local player is. The client installs + * the real supplier at startup; a dedicated server keeps the empty default and never loads a + * client class through here. + */ +public final class ClientAccess { + private static Supplier<@Nullable Entity> localPlayer = () -> null; + private static PackReceiver packs = new PackReceiver() { + @Override + public void onPackList(PackListPacket packet) { + } + + @Override + public void onPackData(PackDataPacket packet) { + } + }; + + private ClientAccess() { + } + + public static void install(Supplier<@Nullable Entity> player, PackReceiver receiver) { + localPlayer = player; + packs = receiver; + } + + public static @Nullable Entity localPlayer() { + return localPlayer.get(); + } + + /** Where the server's content pack packets go. Called on the client's main thread. */ + public static PackReceiver packs() { + return packs; + } + + /** The client half of the content pack sync. */ + public interface PackReceiver { + void onPackList(PackListPacket packet); + + void onPackData(PackDataPacket packet); + } +} From 0b319b4a7a3d38e393fc3ace8b1a91adc81bb51c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:44 +0800 Subject: [PATCH 211/314] Add EngineSoundSource to the shared 1.21.1 sources --- .../mcheli/entity/EngineSoundSource.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java new file mode 100644 index 0000000..6b25a38 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/EngineSoundSource.java @@ -0,0 +1,13 @@ +package com.jnhprojects.mcheli.entity; + +/** + * A vehicle with an engine loop. The client sound player polls these values every tick; the + * definition's {@code SoundVolume} and {@code SoundPitch} are applied on top. + */ +public interface EngineSoundSource { + /** Loudness before the definition's multiplier. Zero silences the loop. */ + float engineVolume(); + + /** Pitch before the definition's multiplier. */ + float enginePitch(); +} From 71e92eeb4a8767df230d2ea476cf4b33e9ced0f3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:45 +0800 Subject: [PATCH 212/314] Add GroundVehicleEntity to the shared 1.21.1 sources --- .../mcheli/entity/GroundVehicleEntity.java | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java new file mode 100644 index 0000000..3029014 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/GroundVehicleEntity.java @@ -0,0 +1,130 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.VehicleDefinition; +import com.jnhprojects.mcheli.content.aircraft.WeaponMount; +import com.jnhprojects.mcheli.weapon.WeaponSet; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * Static guns and launchers, ported from {@code MCH_EntityVehicle}. + * + *

The mount itself does not turn; the gunner looks around and the {@code AddPart} tree turns + * with the view. {@code CanMove} lets W/S push the mount and A/D turn it, for the few that set it. + */ +public class GroundVehicleEntity extends AircraftEntity implements EngineSoundSource { + private static final double MIN_SPEED = 0.07; + private double currentSpeed = MIN_SPEED; + + public GroundVehicleEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected @Nullable VehicleDefinition lookup(String name) { + return MCHeli.content().vehicle(name); + } + + @Override + public @Nullable VehicleDefinition definition() { + return (VehicleDefinition) super.definition(); + } + + /** + * {@code MCH_EntityVehicle.useCurrentWeapon}: the mount stays put while its {@code AddPart} + * tree turns with the gunner's view, so a weapon fires where the gunner looks. Only weapons + * with both yaw limits set aim like turrets instead. The original checked the first + * {@code AddWeapon} line of that weapon, and so does this. + */ + @Override + public boolean aimsWithUser(WeaponSet set) { + WeaponMount mount = set.mounts().getFirst(); + return mount.minYaw() == 0.0F || mount.maxYaw() == 0.0F; + } + + @Override + protected void simulate() { + VehicleDefinition def = definition(); + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + + if (getControllingPassenger() == null) { + throttle = Math.max(throttle - 0.00125, 0.0); + } else if (def.canMove) { + double x = 0.0; + double z = 0.0; + double yaw = Math.toRadians(getYRot()); + if (input.throttleUp) { + x += Math.sin(yaw); + z += Math.cos(yaw); + } + if (input.throttleDown) { + x -= Math.sin(yaw); + z -= Math.cos(yaw); + } + if (input.left && !input.right) { + setYRot(getYRot() - 0.5F); + } else if (input.right && !input.left) { + setYRot(getYRot() + 0.5F); + } + double d = Math.sqrt(x * x + z * z); + if (d > 1.0E-6) { + mx -= x / d * 0.03; + mz += z / d * 0.03; + } + } + if (isInWater()) { + setXRot(getXRot() * 0.9F); + } + + double prevMotion = Math.sqrt(mx * mx + mz * mz); + double depth = def.floats ? waterDepth() : 0.0; + if (depth == 0.0) { + my += isInWater() ? def.gravityInWater : def.gravity; + } else if (depth < 1.0) { + my -= 1.0E-4; + my += 0.007 * throttle; + } else { + if (my < 0.0) { + my /= 2.0; + } + my += 0.007; + } + + double motion = Math.sqrt(mx * mx + mz * mz); + float limit = def.speed; + if (motion > limit) { + mx *= limit / motion; + mz *= limit / motion; + motion = limit; + } + if (motion > prevMotion && currentSpeed < limit) { + currentSpeed = Math.min(limit, currentSpeed + (limit - currentSpeed) / 35.0); + } else { + currentSpeed = Math.max(MIN_SPEED, currentSpeed - (currentSpeed - MIN_SPEED) / 35.0); + } + if (onGround()) { + mx *= 0.5; + mz *= 0.5; + } + + move(MoverType.SELF, new Vec3(mx, my, mz)); + setDeltaMovement(mx * 0.99, my * 0.95, mz * 0.99); + } + + @Override + public float engineVolume() { + return visibleThrottle() * 2.0F; + } + + @Override + public float enginePitch() { + return visibleThrottle() * 0.5F; + } +} From fad9f74bc9bbaf16458a54b26a4fb606502fc412 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:45 +0800 Subject: [PATCH 213/314] Add HelicopterEntity to the shared 1.21.1 sources --- .../mcheli/entity/HelicopterEntity.java | 318 ++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java new file mode 100644 index 0000000..b140713 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/HelicopterEntity.java @@ -0,0 +1,318 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A helicopter, ported from {@code MCH_EntityHeli}. + * + *

The flight model is the original one: throttle produces lift that weakens as the aircraft + * tilts, and pitching the nose down converts throttle into forward speed through a cubic curve. + * Hover mode holds full throttle, levels the aircraft and lets W/A/S/D slide it around instead. + * + *

Two deliberate differences from 1.7.10: + *

    + *
  • The original ran roll damping once per rendered frame, so it depended on frame rate. It + * now runs once per tick with constants tuned to match the feel at 60 FPS.
  • + *
  • Blade folding, weapons, fuel and damage effects are not ported yet.
  • + *
+ */ +public class HelicopterEntity extends AircraftEntity implements EngineSoundSource { + /** + * Rotation per mouse count. The original used {@code 0.06 * partialTicks} with the partial tick + * low-pass filtered to roughly 0.5. + */ + private static final float LOOK_SCALE = 0.03F; + /** Vanilla turns the view 0.15 degrees per mouse count, so this recovers the raw count. */ + private static final float MOUSE_COUNTS_PER_DEGREE = 1.0F / 0.15F; + private static final float ROLL_DAMPING = 0.06F; + private static final float ROLL_DAMPING_HOVER = 0.1F; + private static final float ROLL_RATE = 1.8F; + private static final double MIN_SPEED = 0.07; + + private double currentSpeed = MIN_SPEED; + private float rotorAngle; + public float rotorAngleO; + + public HelicopterEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected @Nullable HelicopterDefinition lookup(String name) { + return MCHeli.content().helicopter(name); + } + + @Override + public @Nullable HelicopterDefinition definition() { + return (HelicopterDefinition) super.definition(); + } + + @Override + public boolean steersWithMouse() { + return true; + } + + @Override + public float engineVolume() { + HelicopterDefinition def = definition(); + return def != null && def.throttleUpDown <= 0.0F ? 0.0F : visibleThrottle() * 2.0F; + } + + @Override + public float enginePitch() { + return 0.2F + visibleThrottle() * 0.2F; + } + + /** Hovering or slow helicopters drop their gear sooner than ones passing over at speed. */ + @Override + protected float autoLowerGearThrottle() { + HelicopterDefinition def = definition(); + double dx = getX() - xo; + double dy = getY() - yo; + double dz = getZ() - zo; + float slow = def != null ? def.speed / 3.5F : 0.0F; + return dx * dx + dy * dy + dz * dz <= slow ? 0.8F : 0.3F; + } + + public float rotorAngle(float partialTicks) { + float prev = rotorAngleO; + if (rotorAngle - prev < -180.0F) { + prev -= 360.0F; + } else if (prev - rotorAngle < -180.0F) { + prev += 360.0F; + } + return prev + (rotorAngle - prev) * partialTicks; + } + + /** Pilot toggles hover mode. Only allowed while fairly level and not in gunner mode, as in the original. */ + public void toggleHovering() { + if (isHoveringMode()) { + setHovering(false); + } else if (!isGunnerMode() && isLevelEnough()) { + setHovering(true); + } + } + + private boolean isLevelEnough() { + return Math.abs(Mth.wrapDegrees(roll())) < 40.0F && Math.abs(Mth.wrapDegrees(getXRot())) < 40.0F; + } + + /** The original also needed the blades unfolded; folding is not ported yet. */ + @Override + public boolean canSwitchGunnerMode() { + return super.canSwitchGunnerMode() && isLevelEnough(); + } + + @Override + public void applyPilotLook(float dYaw, float dPitch) { + HelicopterDefinition def = definition(); + if (def == null) { + return; + } + float x = dYaw * MOUSE_COUNTS_PER_DEGREE; + float y = dPitch * MOUSE_COUNTS_PER_DEGREE; + + float yaw = 0.0F; + float pitch = 0.0F; + float rollDelta = 0.0F; + if (!isNearGround(2)) { + yaw = clamp(x, 40.0F * def.mobilityYaw) * LOOK_SCALE; + } + if (!isHovering()) { + pitch = clamp(y, 40.0F * def.mobilityPitch) * LOOK_SCALE; + rollDelta = clamp(x, 40.0F * def.mobilityRoll) * LOOK_SCALE; + } + if (yaw == 0.0F && pitch == 0.0F && rollDelta == 0.0F) { + return; + } + + float[] r = AircraftRotation.applyLocal(getYRot(), getXRot(), roll, yaw, pitch, rollDelta); + float newPitch = r[1]; + float newRoll = r[2]; + if (def.limitRotation) { + newPitch = Mth.clamp(newPitch, def.minRotationPitch, def.maxRotationPitch); + newRoll = Mth.clamp(newRoll, def.minRotationRoll, def.maxRotationRoll); + } + // Keep yaw continuous so the pilot's own yaw never jumps across the +-180 seam. + setYRot(getYRot() + Mth.wrapDegrees(r[0] - getYRot())); + setXRot(newPitch); + roll = newRoll; + } + + private static float clamp(float v, float limit) { + return Mth.clamp(v, -limit, limit); + } + + @Override + protected void simulate() { + HelicopterDefinition def = definition(); + boolean piloted = getControllingPassenger() != null; + + updateThrottle(def, piloted); + updateAttitude(def); + updateMotion(def); + } + + /** {@code onUpdate_Control} and the hover/non-hover branches. */ + private void updateThrottle(HelicopterDefinition def, boolean piloted) { + if (piloted) { + Vec3 motion = getDeltaMovement(); + if (!isHovering()) { + if (input.throttleUp) { + throttle = Math.min(1.0, throttle + 0.02 * def.throttleUpDown); + } else if (input.throttleDown) { + throttle = Math.max(0.0, throttle - (1.0 / 70.0) * def.throttleUpDown); + } + motion = strafe(motion, false, input.left, input.right, 0.02 * def.speed); + } else { + throttle = Math.min(1.0, throttle + 1.0 / 30.0); + motion = strafe(motion, input.throttleUp, input.left, input.right, 0.01 * def.speed); + if (input.throttleDown) { + motion = push(motion, getYRot() - 180.0F, 0.01 * def.speed); + } + } + setDeltaMovement(motion); + } else { + throttle = Math.max(0.0, throttle - 0.00125); + if (isHoveringMode() && !level().isClientSide()) { + setHovering(false); + } + } + } + + private Vec3 strafe(Vec3 motion, boolean forward, boolean left, boolean right, double amount) { + if (forward) { + motion = push(motion, getYRot(), amount); + } + if (left && !right) { + motion = push(motion, getYRot() - 90.0F, amount); + } else if (right && !left) { + motion = push(motion, getYRot() + 90.0F, amount); + } + return motion; + } + + /** Adds {@code amount} of horizontal motion towards {@code yaw}, Minecraft style (0 = south). */ + private static Vec3 push(Vec3 motion, float yaw, double amount) { + double rad = Math.toRadians(yaw); + return motion.add(-Math.sin(rad) * amount, 0.0, Math.cos(rad) * amount); + } + + /** Per-tick version of {@code onUpdateAngles}: roll levels out, A/D bank in the air. */ + private void updateAttitude(HelicopterDefinition def) { + float damping = 1.0F - (isHovering() ? ROLL_DAMPING_HOVER : ROLL_DAMPING); + if (Math.abs(roll) > 0.1F && Math.abs(roll) < 65.0F) { + roll *= damping; + } + if (getControllingPassenger() != null && !isNearGround(3)) { + if (input.left && !input.right) { + roll -= ROLL_RATE; + } else if (input.right && !input.left) { + roll += ROLL_RATE; + } + } + if (isHovering() && Math.abs(getXRot()) < 70.0F) { + setXRot(getXRot() * 0.95F); + } + if (def.limitRotation) { + roll = Mth.clamp(roll, def.minRotationRoll, def.maxRotationRoll); + } + } + + /** {@code onUpdate_Server}: gravity, lift, forward thrust, speed limit and ground friction. */ + private void updateMotion(HelicopterDefinition def) { + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + double prevMotion = Math.sqrt(mx * mx + mz * mz); + float groundPitch = def.onGroundPitch; + + if (!isHovering()) { + double depth = def.floats ? waterDepth() : 0.0; + if (depth == 0.0) { + my += isInWater() ? def.gravityInWater : def.gravity; + double yawRad = Math.toRadians(getYRot()); + float pitch = getXRot(); + if (isNearGround(3)) { + pitch -= groundPitch; + } + double thrust = 0.1 * currentSpeed * (pitch * pitch * pitch / 30000.0F) * throttle; + mx += Math.sin(yawRad) * -thrust; + mz += Math.cos(yawRad) * thrust; + + double tilt = (Math.abs(getXRot()) + Math.abs(roll)) * 0.6; + double lift = tilt <= 50.0 ? 1.0 - tilt / 50.0 : 0.0; + my += (lift * 0.025 + 0.03) * throttle; + } else { + levelOut(groundPitch); + if (depth < 1.0) { + my -= 1.0E-4; + my += 0.007 * throttle; + } else { + if (my < 0.0) { + my *= 0.7; + } + my += 0.007; + } + } + } else { + if (random.nextInt(50) == 0) { + mx += (random.nextDouble() - 0.5) / 30.0; + } + if (random.nextInt(50) == 0) { + my += (random.nextDouble() - 0.5) / 50.0; + } + if (random.nextInt(50) == 0) { + mz += (random.nextDouble() - 0.5) / 30.0; + } + } + + double motion = Math.sqrt(mx * mx + mz * mz); + double limit = def.speed; + if (motion > limit) { + mx *= limit / motion; + mz *= limit / motion; + motion = limit; + } + if (motion > prevMotion && currentSpeed < limit) { + currentSpeed = Math.min(limit, currentSpeed + (limit - currentSpeed) / 35.0); + } else { + currentSpeed = Math.max(MIN_SPEED, currentSpeed - (currentSpeed - MIN_SPEED) / 35.0); + } + + if (onGround()) { + mx *= 0.5; + mz *= 0.5; + levelOut(groundPitch); + } + + move(MoverType.SELF, new Vec3(mx, my, mz)); + setDeltaMovement(mx * 0.99, my * 0.95, mz * 0.99); + } + + private void levelOut(float groundPitch) { + if (Math.abs(getXRot()) < 40.0F) { + setXRot((getXRot() - groundPitch) * 0.9F + groundPitch); + } + if (Math.abs(roll) < 40.0F) { + roll *= 0.9F; + } + } + + @Override + protected void tickAnimation() { + HelicopterDefinition def = definition(); + rotorAngleO = rotorAngle; + float spool = 1.0F - visibleThrottle(); + rotorAngle = (rotorAngle + (1.0F - spool * spool * spool) * def.rotorSpeed) % 360.0F; + } +} From 9ac5db0aaf16c73c21fcfe14af97306863845a57 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:45 +0800 Subject: [PATCH 214/314] Add PartState to the shared 1.21.1 sources --- .../jnhprojects/mcheli/entity/PartState.java | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/PartState.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/PartState.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/PartState.java new file mode 100644 index 0000000..54755d5 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/PartState.java @@ -0,0 +1,122 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.ModSounds; + +/** + * Open/closed state of a group of moving parts (canopy, landing gear, hatches, wings, VTOL nozzles). + * Ported from {@code MCH_Parts}. + * + *

The target state is one bit of a synched integer, so only the server changes it and every + * side sees the same value. The angle runs from 0 to {@link #max} at {@link #speed} degrees per tick + * and is advanced locally on every side, which keeps the animation smooth without sending angles. + */ +public final class PartState { + private final AircraftEntity owner; + private final int bit; + private final float max; + private final float speed; + private final Sounds sounds; + + private float angle; + private float prevAngle; + + PartState(AircraftEntity owner, int bit, float speed, Sounds sounds) { + this.owner = owner; + this.bit = bit; + this.max = 90.0F; + this.speed = speed; + this.sounds = sounds; + } + + /** Target state: {@code true} means open, folded, retracted or tilted, depending on the part. */ + public boolean status() { + return (owner.partBits() & (1 << bit)) != 0; + } + + /** Fully back in the default position. */ + public boolean isOff() { + return !status() && angle <= 0.02F; + } + + /** Fully in the switched position. */ + public boolean isOn() { + return status() && angle >= max - 0.02F; + } + + public float angle() { + return angle; + } + + public float angle(float partialTicks) { + return prevAngle + (angle - prevAngle) * partialTicks; + } + + /** Progress from 0 to 1. */ + public float factor() { + return angle / max; + } + + public float factor(float partialTicks) { + return angle(partialTicks) / max; + } + + /** Changes the target state. Server only; clients follow through synched data. */ + void set(boolean on, boolean playSound) { + if (owner.level().isClientSide() || status() == on) { + return; + } + owner.setPartBit(bit, on); + if (playSound) { + play(sounds.switching); + play(on ? sounds.startOn : sounds.startOff); + } + tick(); + } + + /** Jumps straight to {@code on} without animating, used for parts that start deployed. */ + void force(boolean on) { + if (!owner.level().isClientSide()) { + owner.setPartBit(bit, on); + } + angle = prevAngle = on ? max : 0.0F; + } + + void tick() { + prevAngle = angle; + if (status()) { + if (angle < max) { + angle = Math.min(angle + speed, max); + if (angle >= max) { + play(sounds.endOn); + } + } + } else if (angle > 0.0F) { + angle = Math.max(angle - speed, 0.0F); + if (angle <= 0.0F) { + play(sounds.endOff); + } + } + } + + private void play(Cue cue) { + if (cue != null && !owner.level().isClientSide()) { + ModSounds.playAt(owner, cue.sound(), 1.0F, cue.pitch()); + } + } + + /** One sound from {@code sounds.json} with its pitch. */ + record Cue(String sound, float pitch) { + } + + /** What plays when the part starts and finishes moving each way; {@code null} entries stay silent. */ + record Sounds(Cue startOn, Cue endOn, Cue startOff, Cue endOff, Cue switching) { + private static final Cue CLICK = new Cue("plane_cc", 1.0F); + private static final Cue CLICK_LOW = new Cue("plane_cc", 0.5F); + + static final Sounds HATCH = new Sounds(null, CLICK, null, CLICK, new Cue("plane_cv", 0.5F)); + static final Sounds CANOPY = new Sounds(null, CLICK, null, CLICK, null); + static final Sounds LANDING_GEAR = new Sounds(CLICK_LOW, CLICK_LOW, CLICK_LOW, CLICK_LOW, new Cue("plane_cv", 0.75F)); + static final Sounds NOZZLE = new Sounds(CLICK_LOW, CLICK_LOW, CLICK_LOW, CLICK_LOW, new Cue("plane_cv", 0.5F)); + static final Sounds WING = new Sounds(CLICK_LOW, CLICK_LOW, CLICK_LOW, CLICK_LOW, null); + } +} From 87f641f1ab8d7d9016fc94b42567545415f1a573 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:46 +0800 Subject: [PATCH 215/314] Add PlaneEntity to the shared 1.21.1 sources --- .../mcheli/entity/PlaneEntity.java | 516 ++++++++++++++++++ 1 file changed, 516 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java new file mode 100644 index 0000000..b80171b --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/PlaneEntity.java @@ -0,0 +1,516 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.PlaneDefinition; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * A fixed wing aircraft, ported from {@code MCP_EntityPlane}. + * + *

Throttle pushes the plane along its nose, tilted ten degrees up, and the vertical part of + * that push is what keeps it in the air: below full throttle it sinks. VTOL capable planes tilt + * their nozzles (and tilt rotors) towards straight down, which turns the push into lift. + * + *

The original applied its attitude rules once per rendered frame with the partial tick + * filtered to about 0.5. Here they run three times per tick with that value, which is what a + * 60 FPS client did, so the handling is the same without depending on the frame rate. + */ +public class PlaneEntity extends AircraftEntity implements EngineSoundSource { + private static final float LOOK_SCALE = 0.03F; + private static final float MOUSE_COUNTS_PER_DEGREE = 1.0F / 0.15F; + private static final int FRAMES_PER_TICK = 3; + private static final float FRAME_PARTIAL_TICK = 0.5F; + private static final double MIN_SPEED = 0.07; + + private @Nullable PartState nozzle; + private @Nullable PartState wing; + + private double currentSpeed = MIN_SPEED; + private float keyRoll; + private float rotorAngle; + public float rotorAngleO; + private float soundLevel; + + public PlaneEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected @Nullable PlaneDefinition lookup(String name) { + return MCHeli.content().plane(name); + } + + @Override + public @Nullable PlaneDefinition definition() { + return (PlaneDefinition) super.definition(); + } + + @Override + protected void onDefinitionChanged(AircraftDefinition definition) { + super.onDefinitionChanged(definition); + PlaneDefinition def = (PlaneDefinition) definition; + nozzle = def.hasNozzleState() ? addPart(PART_NOZZLE, 1.5F, PartState.Sounds.NOZZLE) : null; + wing = def.wings.isEmpty() ? null : addPart(PART_WING, 2.5F, PartState.Sounds.WING); + if (nozzle != null && def.defaultVtol && !nozzle.status()) { + nozzle.force(true); + } + } + + @Override + public boolean steersWithMouse() { + return true; + } + + public @Nullable PartState nozzle() { + return nozzle; + } + + public @Nullable PartState wing() { + return wing; + } + + public float nozzleAngle() { + return nozzle != null ? nozzle.angle() : 0.0F; + } + + @Override + public int vtolMode() { + if (nozzle == null) { + return 0; + } + if (!nozzle.status()) { + return nozzle.angle() <= 0.005F ? 0 : 1; + } + return nozzle.angle() >= 89.995F ? 2 : 1; + } + + public float rotorAngle(float partialTicks) { + return rotorAngleO + (rotorAngle - rotorAngleO) * partialTicks; + } + + // Pilot actions, server side + + public boolean canSwitchVtol() { + PlaneDefinition def = definition(); + if (def == null || !def.vtol || nozzle == null || modeSwitchCooldown > 0 || vtolMode() == 1) { + return false; + } + return Math.abs(roll) <= 30.0F && !(onGround() && def.defaultVtol); + } + + public void toggleVtol() { + if (canSwitchVtol()) { + modeSwitchCooldown = 20; + setVtol(!nozzle.status()); + } + } + + private void setVtol(boolean on) { + PlaneDefinition def = definition(); + if (nozzle == null || def == null || (def.defaultVtol && onGround() && !on)) { + return; + } + nozzle.set(on, true); + } + + public boolean canFoldWing() { + PlaneDefinition def = definition(); + if (wing == null || def == null || modeSwitchCooldown > 0) { + return false; + } + if (def.variableSweepWing) { + // Sweep back only when fast and well clear of the ground, spread again only when slow. + if (!onGround() && !hasBlockBelow(3, 20, true)) { + if (throttle < 0.7F) { + return false; + } + } else if (throttle > 0.1F) { + return false; + } + } else { + if (!onGround() && !hasBlockBelow(3, 3, true)) { + return false; + } + if (throttle > 0.01F) { + return false; + } + } + return wing.isOff(); + } + + public boolean canUnfoldWing() { + return wing != null && modeSwitchCooldown <= 0 && wing.isOn(); + } + + @Override + public void toggleFold() { + PlaneDefinition def = definition(); + if (def == null) { + return; + } + if (!def.hatches.isEmpty()) { + super.toggleFold(); + } else if (canFoldWing()) { + wing.set(true, true); + modeSwitchCooldown = 20; + } else if (canUnfoldWing()) { + wing.set(false, true); + modeSwitchCooldown = 20; + } + } + + /** Folded wings ground the plane; swept wings only below 20% throttle. */ + private boolean canUseWing(PlaneDefinition def) { + if (wing == null) { + return true; + } + if (def.variableSweepWing) { + return throttle >= 0.2 || wing.isOff(); + } + return wing.isOff(); + } + + @Override + protected float autoLowerGearThrottle() { + return 0.7F; + } + + @Override + protected void updatePartsServer(AircraftDefinition definition) { + super.updatePartsServer(definition); + PlaneDefinition def = (PlaneDefinition) definition; + // Swept wings spread out again when the plane slows down or gets close to the ground. + if (wing != null && def.variableSweepWing && wing.isOn() && throttle >= 0.2F + && (throttle < 0.5 || hasBlockBelow(1, 10, true))) { + wing.set(false, true); + } + if (nozzle != null && def.defaultVtol && onGround() && vtolMode() == 0) { + nozzle.set(true, true); + } + } + + // Mouse + + @Override + public void applyPilotLook(float dYaw, float dPitch) { + PlaneDefinition def = definition(); + if (def == null || ticksAlive() < 30 || isPassenger() || hasBlockBelow(3, 2, true)) { + return; + } + float x = dYaw * MOUSE_COUNTS_PER_DEGREE; + float y = dPitch * MOUSE_COUNTS_PER_DEGREE; + boolean vtol = vtolMode() > 0; + float yawFactor = (vtol ? def.vtolYaw : 1.0F) * 0.8F; + float pitchFactor = (vtol ? def.vtolPitch : 1.0F) * 0.8F; + float rollInput = vtolMode() == 0 ? x * 0.5F : x; + + float yaw = clamp(x, 40.0F * def.mobilityYaw) * yawFactor * LOOK_SCALE; + float pitch = clamp(y, 40.0F * def.mobilityPitch) * pitchFactor * LOOK_SCALE; + float rollDelta = clamp(rollInput, 40.0F * def.mobilityRoll) * yawFactor * LOOK_SCALE; + if (yaw == 0.0F && pitch == 0.0F && rollDelta == 0.0F) { + return; + } + float[] r = AircraftRotation.applyLocal(getYRot(), getXRot(), roll, yaw, pitch, rollDelta); + float newPitch = r[1]; + float newRoll = r[2]; + if (def.limitRotation) { + newPitch = Mth.clamp(newPitch, def.minRotationPitch, def.maxRotationPitch); + newRoll = Mth.clamp(newRoll, def.minRotationRoll, def.maxRotationRoll); + } + setYRot(getYRot() + Mth.wrapDegrees(r[0] - getYRot())); + setXRot(newPitch); + roll = newRoll; + } + + private static float clamp(float v, float limit) { + return Mth.clamp(v, -limit, limit); + } + + // Simulation + + @Override + protected void simulate() { + PlaneDefinition def = definition(); + boolean piloted = getControllingPassenger() != null; + if (def.targetDrone && !piloted) { + input.throttleUp = true; + } + updateThrottle(def, piloted); + if (piloted) { + for (int i = 0; i < FRAMES_PER_TICK; i++) { + updateAttitude(def, FRAME_PARTIAL_TICK); + } + } + updateMotion(def); + } + + /** + * Gunner mode needs a plane in level flight with throttle up and nothing solid within five + * blocks below, so the autopilot has room to circle. + */ + @Override + public boolean canSwitchGunnerMode() { + if (!super.canSwitchGunnerMode()) { + return false; + } + float r = Math.abs(Mth.wrapDegrees(roll())); + float p = Math.abs(Mth.wrapDegrees(getXRot())); + return r <= 40.0F && p <= 40.0F && visibleThrottle() > 0.6F && !hasBlockBelow(3, 5, false); + } + + /** {@code onUpdate_Control} and {@code onUpdate_ControlNotHovering}. */ + private void updateThrottle(PlaneDefinition def, boolean piloted) { + reverse *= 0.8F; + boolean canFly = piloted && isCanopyClosed() && canUseWing(def); + if (canFly && isGunnerMode()) { + // The autopilot holds the throttle the pilot left it at. + return; + } + if (canFly || def.targetDrone) { + float upDown = def.throttleUpDown; + boolean turning = input.left != input.right; + boolean up = input.throttleUp; + if (turning && throttle < def.pivotTurnThrottle && !up && !input.throttleDown) { + up = true; + upDown *= 2.0F; + } + if (up) { + if (def.enableBack && reverse > 0.0F) { + reverse -= 0.01F * upDown; + } else { + reverse = 0.0F; + throttle = Math.min(throttle + 0.01 * upDown, 1.0); + } + } else if (input.throttleDown) { + if (throttle > 0.0) { + throttle -= 0.01 * upDown; + } else { + throttle = 0.0; + if (def.enableBack) { + reverse = Math.min(reverse + 0.0025F * upDown, 0.6F); + } + } + } + } else if (throttle > 0.0) { + throttle -= 0.0025 * def.throttleUpDown; + } + throttle = Math.max(throttle, 0.0); + } + + /** One frame of {@code onUpdateAngles}. In gunner mode the autopilot levels out and circles by {@code AutoPilotRot}. */ + private void updateAttitude(PlaneDefinition def, float pt) { + boolean gunner = isGunnerMode(); + if (gunner) { + setXRot(getXRot() * 0.95F); + setYRot(getYRot() + def.autoPilotRot * 0.2F); + if (Math.abs(roll) > 20.0F) { + roll *= 0.95F; + } + } + boolean flying = !hasBlockBelow(3, 3, true); + if (!flying || input.freeLook || gunner || (def.floats && waterDepth() > 0.0)) { + float yawRate = 1.0F; + if (!flying) { + yawRate = def.mobilityYawOnGround; + if (!def.canRotateOnGround && isOnSolidGround()) { + yawRate = 0.0F; + } + } + if (input.left && !input.right) { + setYRot(getYRot() - 0.6F * yawRate * pt); + } else if (input.right && !input.left) { + setYRot(getYRot() + 0.6F * yawRate * pt); + } + } else { + // A/D bank the plane; in VTOL mode the keys do nothing in the air. + float rate = vtolMode() != 0 ? 0.0F : 0.2F; + if (input.left && !input.right) { + keyRoll -= rate * pt; + } else if (input.right && !input.left) { + keyRoll += rate * pt; + } + roll += keyRoll * 0.5F * def.mobilityRoll; + } + keyRoll *= 1.0F - 0.1F * pt; + if (!flying && Math.abs(getXRot()) < 40.0F) { + applyOnGroundPitch(0.97F); + } + if (nozzleAngle() > 0.001F) { + setXRot(getXRot() * (1.0F - 0.03F * pt)); + roll *= 1.0F - 0.1F * pt; + } + roll = AircraftRotation.wrap(roll); + if (def.limitRotation) { + setXRot(Mth.clamp(getXRot(), def.minRotationPitch, def.maxRotationPitch)); + roll = Mth.clamp(roll, def.minRotationRoll, def.maxRotationRoll); + } + } + + private float maxSpeed(PlaneDefinition def) { + float extra = 0.0F; + if (def.variableSweepWing) { + if (wing != null) { + extra = (def.sweepWingSpeed - def.speed) * wing.factor(); + } else if (hatch != null) { + extra = (def.sweepWingSpeed - def.speed) * hatch.factor(); + } + } + return def.speed + extra; + } + + /** {@code onUpdate_Server}. */ + private void updateMotion(PlaneDefinition def) { + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + double prevMotion = Math.sqrt(mx * mx + mz * mz); + double depth = def.floats ? waterDepth() : 0.0; + // The gunner mode autopilot holds its altitude. + boolean levelOff = isGunnerMode(); + + if (depth == 0.0) { + if (def.targetDrone) { + levelOff = targetDroneAutopilot(def) || levelOff; + } + if (!levelOff) { + my += 0.04 + (isInWater() ? def.gravityInWater : def.gravity); + my += -0.047 * (1.0 - throttle); + } else { + my *= 0.8; + } + } else { + setXRot(getXRot() * 0.8F); + if (Math.abs(roll) < 40.0F) { + roll *= 0.9F; + } + if (depth < 1.0) { + my -= 1.0E-4; + my += 0.007 * throttle; + } else { + if (my < 0.0) { + my /= 2.0; + } + my += 0.007; + } + } + + double push = throttle / 10.0; + float nozzleAngle = nozzleAngle(); + Vec3 v; + if (nozzleAngle > 0.001F) { + setXRot(getXRot() * 0.95F); + v = direction(getYRot(), getXRot() - nozzleAngle); + if (nozzleAngle >= 90.0F) { + v = new Vec3(v.x * 0.8, v.y, v.z * 0.8); + } + } else { + v = direction(getYRot(), getXRot() - 10.0F); + } + if (!levelOff) { + my += v.y * push / (nozzleAngle <= 0.01F ? 2.0 : 8.0); + } + + boolean canMove = def.canMoveOnGround || !isOnSolidGround(); + if (canMove) { + if (def.enableBack && reverse > 0.0F) { + mx -= v.x * reverse; + mz -= v.z * reverse; + } else { + mx += v.x * push; + mz += v.z * push; + } + } + + double motion = Math.sqrt(mx * mx + mz * mz); + float limit = maxSpeed(def); + if (motion > limit) { + mx *= limit / motion; + mz *= limit / motion; + motion = limit; + } + if (motion > prevMotion && currentSpeed < limit) { + currentSpeed = Math.min(limit, currentSpeed + (limit - currentSpeed) / 35.0); + } else { + currentSpeed = Math.max(MIN_SPEED, currentSpeed - (currentSpeed - MIN_SPEED) / 35.0); + } + + if (onGround() || hasBlockBelow(1, 2, true)) { + mx *= def.motionFactor; + mz *= def.motionFactor; + if (Math.abs(getXRot()) < 40.0F) { + applyOnGroundPitch(0.8F); + } + } + + move(MoverType.SELF, new Vec3(mx, my, mz)); + setDeltaMovement(mx * def.motionFactor, my * 0.95, mz * def.motionFactor); + } + + /** Target drones circle at a safe height on their own. Returns whether to hold altitude. */ + private boolean targetDroneAutopilot(PlaneDefinition def) { + if (hasBlockBelow(3, 40, true)) { + if (!hasBlockBelow(3, 5, true)) { + setYRot(getYRot() + def.autoPilotRot * 2.0F); + if (getXRot() > -20.0F) { + setXRot(getXRot() - 0.5F); + } + } + return false; + } + setYRot(getYRot() + def.autoPilotRot); + setXRot(getXRot() * 0.95F); + if (!level().isClientSide() && canRaiseLandingGear() && modeSwitchCooldown <= 0) { + toggleLandingGear(); + } + return true; + } + + /** {@code MCH_Lib.Rot2Vec3}: unit vector along a yaw and pitch, Minecraft conventions. */ + private static Vec3 direction(float yaw, float pitch) { + double y = Math.toRadians(yaw); + double p = Math.toRadians(pitch); + return new Vec3(-Math.sin(y) * Math.cos(p), -Math.sin(p), Math.cos(y) * Math.cos(p)); + } + + // Animation and sound + + @Override + protected void tickAnimation() { + PlaneDefinition def = definition(); + float throttle = visibleThrottle(); + rotorAngleO = rotorAngle; + rotorAngle += throttle * def.rotorSpeed; + if (rotorAngle > 360.0F) { + rotorAngle -= 360.0F; + rotorAngleO -= 360.0F; + } else if (rotorAngle < 0.0F) { + rotorAngle += 360.0F; + rotorAngleO += 360.0F; + } + + float target = throttle; + if (getControllingPassenger() != null && (canopy == null || canopy.angle() < 1.0F)) { + target += 0.1F; + } + soundLevel = soundLevel < target ? Math.min(soundLevel + 0.02F, target) : Math.max(soundLevel - 0.02F, target); + } + + @Override + public float engineVolume() { + PlaneDefinition def = definition(); + return def != null && def.throttleUpDown <= 0.0F ? 0.0F : soundLevel * 0.7F; + } + + @Override + public float enginePitch() { + return 0.6F + visibleThrottle() * 0.4F; + } +} From 25d3419ba7019f88283aed50579c3422347b67fb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:46 +0800 Subject: [PATCH 216/314] Add TankEntity to the shared 1.21.1 sources --- .../jnhprojects/mcheli/entity/TankEntity.java | 267 ++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java new file mode 100644 index 0000000..e729c7f --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/TankEntity.java @@ -0,0 +1,267 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.TankDefinition; +import com.jnhprojects.mcheli.physics.GroundImpact; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * Tanks and cars, ported from {@code MCH_EntityTank}. + * + *

The drive model is the plane's minus lift: throttle pushes along the hull, A/D turn it, + * S brakes and then reverses when {@code EnableBack} is set, and Space is a hand brake. The hull + * does not follow the mouse. Instead a {@link TerrainFollower} reads the terrain under the + * wheels and the hull eases towards that pitch and roll, while the crew look around freely and + * the turret follows their view. + */ +public class TankEntity extends AircraftEntity implements EngineSoundSource { + private static final int FRAMES_PER_TICK = 3; + private static final float FRAME_PARTIAL_TICK = 0.5F; + private static final double MIN_SPEED = 0.07; + + private final TerrainFollower terrain = new TerrainFollower(this); + private double currentSpeed = MIN_SPEED; + /** + * Squared horizontal distance covered by last tick's move. The original read + * {@code posX - prevPosX} here, but vanilla resets {@code xo} before {@link #simulate()} runs, + * which left vehicles with {@code PivotTurnThrottle} unable to steer. + */ + private double lastStepSq; + private float soundLevel; + private float turnSound; + + public TankEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected @Nullable TankDefinition lookup(String name) { + return MCHeli.content().tank(name); + } + + @Override + public @Nullable TankDefinition definition() { + return (TankDefinition) super.definition(); + } + + @Override + protected void onDefinitionChanged(AircraftDefinition definition) { + super.onDefinitionChanged(definition); + TankDefinition def = (TankDefinition) definition; + terrain.create(def.groundWheels, new Vector3(0.0, -0.35, def.weightedCenterZ)); + } + + /** Walls and long drops hurt tanks and cars, scaled by speed and height. */ + @Override + protected boolean takesGroundImpacts() { + return true; + } + + /** Tanks climb and tilt on slopes all the time; only rolling over damages them. */ + @Override + protected float crashTilt() { + return 91.0F; + } + + @Override + protected void simulate() { + TankDefinition def = definition(); + boolean piloted = getControllingPassenger() != null; + updateThrottle(def, piloted); + if (piloted) { + for (int i = 0; i < FRAMES_PER_TICK; i++) { + updateAttitude(def, FRAME_PARTIAL_TICK); + } + if (def.limitRotation) { + setXRot(Mth.clamp(getXRot(), -90.0F, 90.0F)); + roll = Mth.clamp(roll, -90.0F, 90.0F); + } + } + updateMotion(def, piloted); + } + + /** {@code onUpdate_Control} and {@code onUpdate_ControlSub}. */ + private void updateThrottle(TankDefinition def, boolean piloted) { + reverse *= 0.8F; + float upDown = def.throttleUpDown; + if (input.brake) { + reverse *= 0.5F; + throttle = Math.max(throttle - 0.02 * upDown, 0.0); + } + if ((piloted && isCanopyClosed()) || def.targetDrone) { + if (input.throttleUp || (def.targetDrone && !piloted)) { + if (def.enableBack && reverse > 0.0F) { + reverse -= 0.01F * upDown; + } else { + reverse = 0.0F; + throttle = Math.min(throttle + 0.01 * upDown, 1.0); + } + } else if (input.throttleDown) { + if (throttle > 0.0) { + throttle -= 0.01 * upDown; + } else { + throttle = 0.0; + if (def.enableBack) { + reverse = Math.min(reverse + 0.0025F * upDown, 0.6F); + } + } + } + } else if (throttle > 0.0) { + throttle -= 0.0025 * upDown; + } + throttle = Math.max(throttle, 0.0); + } + + /** One frame of {@code onUpdateAngles}: ease onto the terrain, steer with A/D. */ + private void updateAttitude(TankDefinition def, float pt) { + setXRot(getXRot() + (terrain.targetPitch - getXRot()) * pt); + roll += (terrain.targetRoll - roll) * pt; + + boolean flying = !hasBlockBelow(3, 3, true); + if (flying && !(def.floats && waterDepth() > 0.0)) { + return; + } + float yawRate = 1.0F; + if (!flying) { + yawRate = def.mobilityYawOnGround; + if (!def.canRotateOnGround && isOnSolidGround()) { + yawRate = 0.0F; + } + } + float pivot = def.pivotTurnThrottle; + double dist = lastStepSq; + // With PivotTurnThrottle set the vehicle has to be rolling before it can turn, like a car. + if (pivot <= 0.0F || throttle >= pivot || reverse >= pivot / 10.0F || dist > reverse * 0.01) { + float speedScale = pivot <= 0.0F ? 1.0F : (float) Math.sqrt(Math.min(dist, 1.0)); + float direction = !input.throttleUp && input.throttleDown && throttle < pivot + 0.05 ? -1.0F : 1.0F; + float step = 0.6F * yawRate * pt * direction * speedScale; + if (input.left && !input.right) { + setYRot(getYRot() - step); + } else if (input.right && !input.left) { + setYRot(getYRot() + step); + } + } + } + + /** {@code onUpdate_Server}. */ + private void updateMotion(TankDefinition def, boolean piloted) { + Vec3 m = getDeltaMovement(); + double mx = m.x; + double my = m.y; + double mz = m.z; + double prevMotion = Math.sqrt(mx * mx + mz * mz); + double depth = def.floats ? waterDepth() : 0.0; + + if (depth == 0.0) { + my += 0.04 + (isInWater() ? def.gravityInWater : def.gravity); + my += -0.047 * (1.0 - throttle); + } else if (depth < 1.0) { + my -= 1.0E-4; + my += 0.007 * throttle; + } else { + if (my < 0.0) { + my /= 2.0; + } + my += 0.007; + } + + double push = throttle / 10.0; + double yawRad = Math.toRadians(getYRot()); + double pitchRad = Math.toRadians(getXRot() - 10.0F); + double vx = -Math.sin(yawRad) * Math.cos(pitchRad); + double vy = -Math.sin(pitchRad); + double vz = Math.cos(yawRad) * Math.cos(pitchRad); + my += vy * push / 8.0; + + if (def.canMoveOnGround || !isOnSolidGround()) { + if (def.enableBack && reverse > 0.0F) { + mx -= vx * reverse; + mz -= vz * reverse; + } else { + mx += vx * push; + mz += vz * push; + } + } + + double motion = Math.sqrt(mx * mx + mz * mz); + float limit = def.speed; + if (motion > limit) { + mx *= limit / motion; + mz *= limit / motion; + motion = limit; + } + if (motion > prevMotion && currentSpeed < limit) { + currentSpeed = Math.min(limit, currentSpeed + (limit - currentSpeed) / 35.0); + } else { + currentSpeed = Math.max(MIN_SPEED, currentSpeed - (currentSpeed - MIN_SPEED) / 35.0); + } + + if (onGround() || hasBlockBelow(1, 2, true)) { + mx *= def.motionFactor; + mz *= def.motionFactor; + } + + if (!terrain.isEmpty()) { + Vec3 slide = terrain.move(mx, mz); + mx += slide.x; + mz += slide.z; + if (!piloted) { + // Nobody is easing the hull in per frame, so take the attitude straight away. + setXRot(terrain.targetPitch); + roll = terrain.targetRoll; + } + } + + double startX = getX(); + double startZ = getZ(); + move(MoverType.SELF, new Vec3(mx, my, mz)); + double stepX = getX() - startX; + double stepZ = getZ() - startZ; + lastStepSq = stepX * stepX + stepZ * stepZ; + setDeltaMovement(mx * def.motionFactor, my * 0.95, mz * def.motionFactor); + } + + /** The original's {@code applyOnGroundPitch} is a no-op for tanks; the terrain decides. */ + @Override + protected void applyOnGroundPitch(float factor) { + } + + @Override + protected float autoLowerGearThrottle() { + return 0.7F; + } + + @Override + protected void tickAnimation() { + float target = visibleThrottle(); + if (getControllingPassenger() != null && (canopy == null || canopy.angle() < 1.0F)) { + target += 0.1F; + } + // Turning on the spot still revs the engine. + if (!input.left && !input.right && !input.throttleDown) { + turnSound *= 0.8F; + } else { + turnSound = Math.min(turnSound + 0.1F, 0.75F); + } + target = Math.max(target, turnSound); + soundLevel = soundLevel < target ? Math.min(soundLevel + 0.02F, target) : Math.max(soundLevel - 0.02F, target); + } + + @Override + public float engineVolume() { + TankDefinition def = definition(); + return def != null && def.throttleUpDown <= 0.0F ? 0.0F : soundLevel * 0.7F; + } + + @Override + public float enginePitch() { + return Math.max(0.5F + visibleThrottle() * 0.5F, 0.5F + turnSound * 0.5F); + } +} From bd6543fd153d409565ad1c2b2fd510363cea7fb8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:47 +0800 Subject: [PATCH 217/314] Add TerrainFollower to the shared 1.21.1 sources --- .../mcheli/entity/TerrainFollower.java | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java new file mode 100644 index 0000000..92e8e59 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/TerrainFollower.java @@ -0,0 +1,225 @@ +package com.jnhprojects.mcheli.entity; + +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.physics.AircraftRotation; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; + +import java.util.List; + +/** + * Works out the pitch and roll of a ground vehicle from where its wheels touch the terrain. + * Ported from {@code MCH_WheelManager}. + * + *

The original spawned an invisible entity per wheel and let each one fall onto the ground + * under its mount point. Here every wheel is a one block collision box moved with vanilla's + * collision code, which gives the same result without extra entities to sync. Each left/right + * pair spans a plane with the centre of gravity; the weighted average of their normals is the + * ground normal the hull leans towards. + */ +final class TerrainFollower { + private static final double STEP_HEIGHT = 1.5; + private static final double MAX_DRIFT = 2.0; + private static final double SNAP_DISTANCE = 8.0; + + private final AircraftEntity vehicle; + private Probe[] probes = new Probe[0]; + private Vector3 centre = Vector3.ZERO; + private double wheelBase = 1.0; + float targetPitch; + float targetRoll; + + TerrainFollower(AircraftEntity vehicle) { + this.vehicle = vehicle; + } + + /** + * @param wheels positive X wheel positions, front to back; the mirrored wheel is added for each + * @param centre centre of gravity in hull space + */ + void create(List wheels, Vector3 centre) { + this.centre = centre; + probes = new Probe[wheels.size() * 2]; + double minZ = Double.MAX_VALUE; + double maxZ = -Double.MAX_VALUE; + for (int i = 0; i < probes.length; i++) { + Vector3 w = wheels.get(i / 2); + Vector3 local = new Vector3(i % 2 == 0 ? w.x() : -w.x(), w.y(), w.z()); + Probe probe = new Probe(local, local.z() >= centre.z()); + Vec3 at = vehicle.toWorld(local); + probe.setPos(at.x, at.y + 1.0, at.z); + probes[i] = probe; + minZ = Math.min(minZ, local.z()); + maxZ = Math.max(maxZ, local.z()); + } + wheelBase = Math.max(maxZ - minZ, 1.0E-3); + } + + boolean isEmpty() { + return probes.length == 0; + } + + /** + * Drags the wheels along with the hull, lets them settle on the terrain and derives the target + * attitude from them. + * + * @return extra horizontal motion from sliding down steep slopes + */ + Vec3 move(double mx, double mz) { + for (Probe probe : probes) { + Vec3 target = vehicle.toWorld(probe.local); + if (target.distanceToSqr(probe.x, probe.y, probe.z) > SNAP_DISTANCE * SNAP_DISTANCE) { + // Teleported, or created before the vehicle was placed: start over under the hull. + probe.setPos(target.x, target.y + 1.0, target.z); + } + double dx = target.x - probe.x + mx; + double dy = (target.y - probe.y) * 0.15; + double dz = target.z - probe.z + mz; + probe.move(dx, dy, dz); + probe.move(0.0, -0.1, 0.0); + } + + int pairs = probes.length / 2; + // If neither half touches the ground, the outermost pair of that half carries the hull, + // which makes the vehicle tip over a crest instead of floating level. + int rear = pairs - 1; + for (int i = 0; i < pairs; i++) { + if (!probes[i * 2].front && (probes[i * 2].onGround || probes[i * 2 + 1].onGround)) { + rear = -1; + break; + } + } + if (rear >= 0) { + probes[rear * 2].onGround = true; + probes[rear * 2 + 1].onGround = true; + } + int front = 0; + for (int i = pairs - 1; i >= 0; i--) { + if (probes[i * 2].front && (probes[i * 2].onGround || probes[i * 2 + 1].onGround)) { + front = -1; + break; + } + } + if (front >= 0 && pairs > 0) { + probes[front * 2].onGround = true; + probes[front * 2 + 1].onGround = true; + } + + Vec3 wc = vehicle.toWorld(centre).subtract(vehicle.position()); + double cx = vehicle.getX() + wc.x; + double cy = vehicle.getY() + centre.y(); + double cz = vehicle.getZ() + wc.z; + Vec3 normal = Vec3.ZERO; + for (int i = 0; i < pairs; i++) { + Probe w1 = probes[i * 2]; + Probe w2 = probes[i * 2 + 1]; + Vec3 v1 = new Vec3(w1.x - cx, w1.y - cy, w1.z - cz); + Vec3 v2 = new Vec3(w2.x - cx, w2.y - cy, w2.z - cz); + Vec3 n = (w1.local.z() >= 0.0 ? v2.cross(v1) : v1.cross(v2)).normalize(); + double weight = w1.onGround || w2.onGround ? Math.abs(w1.local.z() / wheelBase) : 0.0; + normal = normal.add(n.scale(weight)); + } + normal = normal.normalize(); + + Vec3 slide = Vec3.ZERO; + if (normal.y > 0.01 && normal.y < 0.7) { + slide = new Vec3(normal.x / 50.0, 0.0, normal.z / 50.0); + } + + // Into the hull's frame, then read pitch and roll off the normal. + double yaw = Math.toRadians(vehicle.getYRot()); + double c = Math.cos(yaw); + double s = Math.sin(yaw); + double lx = normal.x * c + normal.z * s; + double lz = normal.z * c - normal.x * s; + float pitch = (float) (90.0 - Math.toDegrees(Math.atan2(normal.y, lz))); + float roll = (float) -(90.0 - Math.toDegrees(Math.atan2(normal.y, lx))); + if (normal.lengthSqr() < 1.0E-6) { + pitch = vehicle.getXRot(); + roll = vehicle.roll; + } + + var def = vehicle.definition(); + float pitchStep = def != null ? def.onGroundPitchFactor : 0.0F; + float rollStep = def != null ? def.onGroundRollFactor : 0.0F; + targetPitch = Mth.clamp(pitch, vehicle.getXRot() - pitchStep, vehicle.getXRot() + pitchStep); + targetRoll = Mth.clamp(roll, vehicle.roll - rollStep, vehicle.roll + rollStep); + + // Wheels that fell behind (stuck on a ledge) are pulled back under the hull. + for (Probe probe : probes) { + Vec3 v = vehicle.position().add(new Vec3(AircraftRotation.toWorld(vehicle.getYRot(), targetPitch, targetRoll, + probe.local.x(), probe.local.y(), probe.local.z()))); + boolean pulled = false; + double x = probe.x; + double z = probe.z; + if (x > v.x + MAX_DRIFT || x < v.x - MAX_DRIFT) { + x = Mth.clamp(x, v.x - MAX_DRIFT, v.x + MAX_DRIFT); + pulled = true; + } + if (z > v.z + MAX_DRIFT || z < v.z - MAX_DRIFT) { + z = Mth.clamp(z, v.z - MAX_DRIFT, v.z + MAX_DRIFT); + pulled = true; + } + if (pulled) { + probe.setPos(x, v.y + STEP_HEIGHT / 2.0, z); + } + } + return slide; + } + + /** One wheel: a one block box that falls onto the terrain and climbs steps up to 1.5 high. */ + private final class Probe { + final Vector3 local; + /** In front of the centre of gravity. */ + final boolean front; + double x; + double y; + double z; + boolean onGround; + + Probe(Vector3 local, boolean front) { + this.local = local; + this.front = front; + } + + void setPos(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + private AABB box() { + return new AABB(x - 0.5, y, z - 0.5, x + 0.5, y + 1.0, z + 0.5); + } + + void move(double dx, double dy, double dz) { + AABB box = box(); + Vec3 wanted = new Vec3(dx, dy, dz); + Vec3 moved = collide(wanted, box); + boolean blockedSideways = moved.x != dx || moved.z != dz; + boolean wasGrounded = onGround || (moved.y != dy && dy < 0.0); + if (blockedSideways && wasGrounded) { + // Same step-up rule as the original wheel entity: climb, move, settle back down. + Vec3 up = collide(new Vec3(0.0, STEP_HEIGHT, 0.0), box); + AABB raised = box.move(up); + Vec3 across = collide(new Vec3(dx, 0.0, dz), raised); + AABB shifted = raised.move(across); + Vec3 down = collide(new Vec3(0.0, -STEP_HEIGHT, 0.0), shifted); + Vec3 stepped = up.add(across).add(down); + if (stepped.horizontalDistanceSqr() > moved.horizontalDistanceSqr()) { + moved = stepped; + } + } + x += moved.x; + y += moved.y; + z += moved.z; + onGround = dy < 0.0 && moved.y != dy; + } + + private Vec3 collide(Vec3 movement, AABB box) { + return Entity.collideBoundingBox(vehicle, movement, box, vehicle.level(), List.of()); + } + } +} From f6d4f31bec73a5c86ca70f8dcd3c4e80ec5a2809 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:47 +0800 Subject: [PATCH 218/314] Port the A-10 to 1.21.1's damage, save and rotation API --- .../mcheli/entity/projectile/A10Entity.java | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java new file mode 100644 index 0000000..926c678 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/A10Entity.java @@ -0,0 +1,168 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.util.Mth; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import org.jspecify.annotations.Nullable; + +/** + * The A-10 that answers a close air support call, {@code MCH_EntityA10}. It dives in for a second, + * strafes the target with forty GAU-8 rounds and climbs away. + */ +public class A10Entity extends Entity { + private static final EntityDataAccessor DATA_WEAPON = SynchedEntityData.defineId(A10Entity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_DIRECTION = SynchedEntityData.defineId(A10Entity.class, EntityDataSerializers.INT); + private static final int LIFETIME = 70; + + private int despawnCount; + private int shotCount; + public int power = 32; + public int explosionPower = 1; + public float acceleration = 4.0F; + private @Nullable Entity shooter; + private @Nullable Entity user; + + public A10Entity(EntityType type, Level level) { + super(type, level); + noPhysics = true; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_WEAPON, ""); + builder.define(DATA_DIRECTION, 0); + } + + /** + * @param direction 0 heading south (+Z), then clockwise in steps of 90 degrees + */ + public void setup(String weapon, int direction, double mx, double mz, @Nullable Entity shooter, @Nullable Entity user) { + entityData.set(DATA_WEAPON, weapon); + entityData.set(DATA_DIRECTION, direction); + setYRot(90 * direction); + yRotO = getYRot(); + setDeltaMovement(mx, 0.0, mz); + this.shooter = shooter; + this.user = user; + } + + public int direction() { + return entityData.get(DATA_DIRECTION); + } + + /** Hidden during the first second, while it is still out of sight. */ + public boolean isVisible() { + return despawnCount > 20; + } + + @Override + public void tick() { + super.tick(); + despawnCount++; + if (!level().isClientSide()) { + if (despawnCount > LIFETIME) { + discard(); + return; + } + if (shotCount < 40) { + for (int i = 0; i < 2; i++) { + fireGau8(shotCount); + shotCount++; + } + if (shotCount == 38) { + ModSounds.playAt(this, "gau-8_snd", 150.0F, 1.0F); + } + } + } + if (despawnCount <= 20) { + setDeltaMovement(getDeltaMovement().x, -0.3, getDeltaMovement().z); + } else { + setPos(getX() + getDeltaMovement().x, getY() + getDeltaMovement().y, getZ() + getDeltaMovement().z); + setDeltaMovement(getDeltaMovement().add(0.0, 0.02, 0.0)); + } + } + + private void fireGau8(int count) { + int dir = direction(); + double x = getX(); + double z = getZ(); + double tx = random.nextDouble() - 0.5; + double ty = -2.6; + double tz = random.nextDouble() - 0.5; + switch (dir) { + case 0 -> { + tz += 10.0; + z += count * 0.6; + } + case 1 -> { + tx -= 10.0; + x -= count * 0.6; + } + case 2 -> { + tz -= 10.0; + z -= count * 0.6; + } + default -> { + tx += 10.0; + x += count * 0.6; + } + } + BulletEntity round = ModEntities.BULLET.get().create(level()); + if (round == null) { + return; + } + round.launch(x, getY(), z, tx, ty, tz, 90 * dir, 30.0F, acceleration); + round.setWeapon(entityData.get(DATA_WEAPON)); + round.explosionPower = shotCount % 4 == 0 ? explosionPower : 0; + round.power = power; + round.setOwners(shooter, user); + level().addFreshEntity(round); + } + + @Override + public boolean hurt(DamageSource source, float damage) { + return false; + } + + @Override + public boolean isPickable() { + return false; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + return distance < 512.0 * 512.0; + } + + @Override + protected void readAdditionalSaveData(CompoundTag tag) { + discard(); + } + + @Override + protected void addAdditionalSaveData(CompoundTag tag) { + } + + @Override + public boolean shouldBeSaved() { + return false; + } + + /** The yaw for this frame, as 26.x's {@code Entity#getYRot(float)} gives it. */ + public float getYRot(float partialTicks) { + return partialTicks == 1.0F ? getYRot() : Mth.rotLerp(partialTicks, yRotO, getYRot()); + } + + /** The pitch for this frame, as 26.x's {@code Entity#getXRot(float)} gives it. */ + public float getXRot(float partialTicks) { + return partialTicks == 1.0F ? getXRot() : Mth.lerp(partialTicks, xRotO, getXRot()); + } +} From 517d71622384514b332e92f1e8f097c3b22f89a5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:48 +0800 Subject: [PATCH 219/314] Add AAMissileEntity to the shared 1.21.1 sources --- .../entity/projectile/AAMissileEntity.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java new file mode 100644 index 0000000..a9d4152 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/AAMissileEntity.java @@ -0,0 +1,50 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; + +/** An air to air missile homing on its locked target, {@code MCH_EntityAAMissile}. */ +public class AAMissileEntity extends ProjectileEntity { + private static final double MAX_DISTANCE_SQ = 3422500.0; + + public AAMissileEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (age() > 4 && w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 7.0F * w.smokeSize * 0.5F); + } + if (level().isClientSide() || w == null || isRemoved()) { + return; + } + Entity t = target(); + if (user() == null || t == null || t.isRemoved()) { + discard(); + return; + } + double d = distanceToSqr(t); + if (d > MAX_DISTANCE_SQ) { + discard(); + } else if (age() > w.rigidityTime) { + // The original compared the squared distance with the plain fuse distance. + if (w.proximityFuseDist >= 0.1F && d < w.proximityFuseDist) { + setPos((t.getX() + getX()) / 2.0, (t.getY() + getY()) / 2.0, (t.getZ() + getZ()) / 2.0); + impact(new Impact(position(), t, null, Direction.UP), 1.0F); + } else { + guideTo(t.getX(), t.getY(), t.getZ()); + } + } + } + + @Override + public String defaultModel() { + return "aamissile"; + } +} From 7f366d78e019a9e4cde72f687fb4db6bdaf106a1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:48 +0800 Subject: [PATCH 220/314] Add ASMissileEntity to the shared 1.21.1 sources --- .../entity/projectile/ASMissileEntity.java | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java new file mode 100644 index 0000000..e5f46c9 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ASMissileEntity.java @@ -0,0 +1,91 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** + * An air to surface missile flying to the block the gunner aimed at, {@code MCH_EntityASMissile}. + * Powered ones climb for half a second first; gliding bombs only steer sideways. + */ +public class ASMissileEntity extends ProjectileEntity { + public double targetX; + public double targetY; + public double targetZ; + + public ASMissileEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public float gravity() { + return isBomblet() ? -0.03F : super.gravity(); + } + + @Override + public float gravityInWater() { + return isBomblet() ? -0.03F : super.gravityInWater(); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke && !isBomblet()) { + trail(w.trajectoryParticle, 3, 10.0F * w.smokeSize * 0.5F); + } + if (w != null && !level().isClientSide() && !isBomblet() && !isRemoved()) { + BlockPos aim = BlockPos.containing(targetX, targetY, targetZ); + if (!level().getBlockState(aim).isAir()) { + double dist = Math.sqrt(distanceToSqr(targetX, targetY, targetZ)); + if (dist < w.proximityFuseDist) { + if (w.bomblet > 0) { + for (int i = 0; i < w.bomblet; i++) { + sprinkleBomblet(); + } + } else { + impact(new Impact(position(), null, blockPosition(), Direction.UP), 1.0F); + } + discard(); + } else if (gravity() == 0.0F) { + double up = age() < 10 ? 20.0 : 0.0; + aimAlong(targetX - getX(), targetY + up - getY(), targetZ - getZ()); + } else { + double dx = targetX - getX(); + double dy = (targetY - getY()) * 0.3; + double dz = targetZ - getZ(); + double d = Math.sqrt(dx * dx + dy * dy + dz * dz); + Vec3 m = getDeltaMovement(); + setDeltaMovement(dx * acceleration / d, m.y, dz * acceleration / d); + } + } + } + faceMotion(); + tickBomblets(); + } + + @Override + protected void sprinkleBomblet() { + WeaponDefinition w = weapon(); + Vec3 m = getDeltaMovement(); + ASMissileEntity child = spawnChild(ModEntities.AS_MISSILE.get(), m.x, m.y, m.z, random.nextInt(360), 0.0F); + if (child == null || w == null) { + return; + } + float spread = w.bombletDiff; + child.setDeltaMovement(m.x * 0.5 + (random.nextFloat() - 0.5F) * spread, + m.y * 0.5 / 2.0 + (random.nextFloat() - 0.5F) * spread / 2.0F, + m.z * 0.5 + (random.nextFloat() - 0.5F) * spread); + child.setBomblet(); + level().addFreshEntity(child); + } + + @Override + public String defaultModel() { + return "asmissile"; + } +} From 745e943f30a810f7b158c2d00db6b470bef71e7b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:48 +0800 Subject: [PATCH 221/314] Add ATMissileEntity to the shared 1.21.1 sources --- .../entity/projectile/ATMissileEntity.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java new file mode 100644 index 0000000..d7031ec --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ATMissileEntity.java @@ -0,0 +1,84 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; + +/** + * An anti tank missile, {@code MCH_EntityATMissile}. In the default mode it flies straight at the + * locked target; in top attack mode it climbs 150 blocks above the shooter and dives. + */ +public class ATMissileEntity extends ProjectileEntity { + private static final double MAX_DISTANCE_SQ = 2250000.0; + + /** 0 direct, 1 top attack. */ + public int guidanceType; + + public ATMissileEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke && tickCount >= w.trajectoryParticleStartTick) { + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } + if (!level().isClientSide() && !isRemoved()) { + Entity t = target(); + if (user() != null && t != null && !t.isRemoved() && w != null) { + steer(w, t); + } else { + discard(); + } + } + faceMotion(); + } + + private void steer(WeaponDefinition w, Entity t) { + double x = t.getX() - getX(); + double y = t.getY() - getY(); + double z = t.getZ() - getZ(); + double d = x * x + y * y + z * z; + if (d > MAX_DISTANCE_SQ) { + discard(); + return; + } + if (w.proximityFuseDist >= 0.1F && d < w.proximityFuseDist) { + impact(new Impact(position(), null, blockPosition(), Direction.UP), 1.0F); + return; + } + int rigidity = w.rigidityTime; + if (age() <= rigidity) { + return; + } + float af = age() < rigidity + w.trajectoryParticleStartTick ? 0.5F : 1.0F; + if (guidanceType == 1) { + Entity shooter = user(); + if (age() <= rigidity + 20) { + guideTo(t.getX(), shooter.getY() + 150.0, t.getZ()); + } else if (age() <= rigidity + 30) { + guideTo(t.getX(), shooter.getY(), t.getZ()); + } else { + if (age() == rigidity + 35) { + power = (int) (power * 1.2F); + if (explosionPower > 0) { + explosionPower++; + } + } + guideTo(t.getX(), t.getY(), t.getZ()); + } + } else { + d = Math.sqrt(d); + setDeltaMovement(x * acceleration / d * af, y * acceleration / d * af, z * acceleration / d * af); + } + } + + @Override + public String defaultModel() { + return "asmissile"; + } +} From d7434609f4da7fe254f4c035da1aeaf5c82a3cd7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:49 +0800 Subject: [PATCH 222/314] Add BombEntity to the shared 1.21.1 sources --- .../mcheli/entity/projectile/BombEntity.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java new file mode 100644 index 0000000..c758d75 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/BombEntity.java @@ -0,0 +1,68 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** A free fall bomb, {@code MCH_EntityBomb}. Cluster bombs split into bomblets. */ +public class BombEntity extends ProjectileEntity { + public BombEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (!level().isClientSide() && w != null && !isRemoved()) { + Vec3 m = getDeltaMovement(); + m = new Vec3(m.x * 0.999, m.y, m.z * 0.999); + if (isInWater()) { + m = m.scale(w.velocityInWater); + } + setDeltaMovement(m); + float fuse = w.proximityFuseDist; + if (fuse > 0.1F && age() % 10 == 0) { + for (Entity entity : level().getEntities(this, getBoundingBox().inflate(fuse), this::canHit)) { + if (entity instanceof LivingEntity) { + impact(new Impact(position(), null, blockPosition(), Direction.UP), 1.0F); + break; + } + } + } + } + tickBomblets(); + } + + @Override + protected void sprinkleBomblet() { + WeaponDefinition w = weapon(); + Vec3 m = getDeltaMovement(); + BombEntity child = spawnChild(ModEntities.BOMB.get(), m.x, m.y, m.z, random.nextInt(360), 0.0F); + if (child == null || w == null) { + return; + } + float spread = w.bombletDiff; + child.setDeltaMovement(m.x + (random.nextFloat() - 0.5F) * spread, + m.y / 2.0 + (random.nextFloat() - 0.5F) * spread / 2.0F, + m.z + (random.nextFloat() - 0.5F) * spread); + child.setBomblet(); + level().addFreshEntity(child); + } + + /** A cluster bomb that has not split yet is not drawn when it splits right away. */ + public boolean isVisible() { + WeaponDefinition w = weapon(); + return w == null || isBomblet() || w.bomblet <= 0 || w.bombletSTime > 0; + } + + @Override + public String defaultModel() { + return "bomb"; + } +} From 6a154f7e083a72c0f2d0edf03e3416ee7947de96 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:49 +0800 Subject: [PATCH 223/314] Add BulletEntity to the shared 1.21.1 sources --- .../entity/projectile/BulletEntity.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java new file mode 100644 index 0000000..b2b418a --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/BulletEntity.java @@ -0,0 +1,45 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; + +/** A gun round, {@code MCH_EntityBullet}. High explosive rounds may carry a proximity fuse. */ +public class BulletEntity extends ProjectileEntity { + public BulletEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected boolean boostsAcceleration() { + return true; + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (isRemoved() || level().isClientSide() || age() <= 1 || w == null || explosionPower <= 0) { + return; + } + float fuse = w.proximityFuseDist; + if (fuse > 0.1F) { + fuse++; + float range = fuse + Math.abs(w.acceleration); + for (Entity entity : level().getEntities(this, getBoundingBox().inflate(range), this::canHit)) { + if (entity.distanceToSqr(this) < fuse * fuse) { + setPos((entity.getX() + getX()) / 2.0, (entity.getY() + getY()) / 2.0, (entity.getZ() + getZ()) / 2.0); + impact(new Impact(position(), null, blockPosition(), Direction.UP), 1.0F); + break; + } + } + } + } + + @Override + public String defaultModel() { + return "bullet"; + } +} From 3bdabddf2b14a36f8b41f4a60f833dacf7d480a3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:50 +0800 Subject: [PATCH 224/314] Read build height the 1.21.1 way --- .../projectile/DispensedItemEntity.java | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java new file mode 100644 index 0000000..2fe1f43 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/DispensedItemEntity.java @@ -0,0 +1,121 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.weapon.LegacyItems; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseFireBlock; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; + +/** + * A canister that uses an item on every block around where it lands, {@code MCH_EntityDispensedItem}: + * water bombers put out fires, crop dusters spread bone meal or seeds, and so on. + */ +public class DispensedItemEntity extends ProjectileEntity { + public DispensedItemEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 7.0F * w.smokeSize); + } + if (!level().isClientSide() && w != null) { + Vec3 m = getDeltaMovement(); + if (acceleration < 1.0E-4) { + m = new Vec3(m.x * 0.999, m.y, m.z * 0.999); + } + if (isInWater()) { + m = m.scale(w.velocityInWater); + } + setDeltaMovement(m); + } + tickBomblets(); + } + + @Override + protected void impact(Impact hit, float damageFactor) { + if (!(level() instanceof ServerLevel server)) { + return; + } + WeaponDefinition w = weapon(); + Item item = w != null ? LegacyItems.item(w.dispenseItem, w.dispenseDamage) : null; + if (w != null && item != null && user() instanceof ServerPlayer player && !player.isRemoved()) { + BlockPos center = hit.block() != null ? hit.block() : blockPosition(); + int range = w.dispenseRange - 1; + for (int x = -range; x <= range; x++) { + for (int y = -range; y <= range; y++) { + for (int z = -range; z <= range; z++) { + int dist = x * x + y * y + z * z; + if (dist <= range * range && (dist <= 0.5 * range * range || random.nextInt(2) == 0)) { + useOn(server, center.offset(x, y, z), item); + } + } + } + } + } + discard(); + } + + private void useOn(ServerLevel server, BlockPos pos, Item item) { + if (pos.getY() < server.getMinBuildHeight() || pos.getY() >= server.getMaxBuildHeight() - 1) { + return; + } + BlockState state = server.getBlockState(pos); + if (state.isAir()) { + return; + } + if (item == Items.WATER_BUCKET) { + // A water drop douses fire and quenches lava instead of placing water. + FluidState fluid = server.getFluidState(pos); + if (state.getBlock() instanceof BaseFireBlock) { + server.removeBlock(pos, false); + } else if (fluid.is(FluidTags.LAVA)) { + server.setBlockAndUpdate(pos, fluid.isSource() ? Blocks.OBSIDIAN.defaultBlockState() : Blocks.COBBLESTONE.defaultBlockState()); + } + return; + } + // Used from above with nobody holding it, the way a dispenser block would. + BlockHitResult hit = new BlockHitResult(Vec3.atCenterOf(pos).add(0.0, 0.5, 0.0), Direction.UP, pos, false); + item.useOn(new UseOnContext(server, null, InteractionHand.MAIN_HAND, new ItemStack(item), hit)); + } + + @Override + protected void sprinkleBomblet() { + WeaponDefinition w = weapon(); + Vec3 m = getDeltaMovement(); + DispensedItemEntity child = spawnChild(ModEntities.DISPENSED_ITEM.get(), m.x, m.y, m.z, random.nextInt(360), 0.0F); + if (child == null || w == null) { + return; + } + float spread = w.bombletDiff; + child.setDeltaMovement(m.x + (random.nextFloat() - 0.5F) * spread, + m.y / 2.0 + (random.nextFloat() - 0.5F) * spread / 2.0F, + m.z + (random.nextFloat() - 0.5F) * spread); + child.setBomblet(); + level().addFreshEntity(child); + } + + @Override + public String defaultModel() { + return "bomb"; + } +} From 0780250b40941f0387463106e86b1a8440f5ba23 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:50 +0800 Subject: [PATCH 225/314] Port flares to 1.21.1's damage, save and placement API --- .../mcheli/entity/projectile/FlareEntity.java | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java new file mode 100644 index 0000000..c8a9905 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/FlareEntity.java @@ -0,0 +1,159 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; + +/** + * A burning flare, or with a fuse a smoke grenade that bursts into a cloud. Port of + * {@code MCH_EntityFlare}. Flares only look the part: decoying happens in the dispenser. + */ +public class FlareEntity extends Entity { + private static final EntityDataAccessor DATA_FUSE = SynchedEntityData.defineId(FlareEntity.class, EntityDataSerializers.INT); + + private double gravity = -0.013; + private double airResistance = 0.992; + private float size = 6.0F; + + public FlareEntity(EntityType type, Level level) { + super(type, level); + noPhysics = true; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_FUSE, 0); + } + + /** + * @param fuse ticks until a smoke grenade bursts, 0 for a flare + * @param slow the long hanging type 4 flare + */ + public void launch(double x, double y, double z, double mx, double my, double mz, float size, int fuse, boolean slow) { + moveTo(x, y, z, random.nextFloat() * 360.0F, random.nextFloat() * 360.0F); + setDeltaMovement(mx, my, mz); + this.size = size; + entityData.set(DATA_FUSE, fuse); + if (slow) { + gravity *= 0.6; + airResistance = 0.995; + } + } + + private int fuse() { + return entityData.get(DATA_FUSE); + } + + /** A smoke grenade rather than a burning flare. */ + public boolean isSmoke() { + return fuse() > 0; + } + + @Override + public void tick() { + if (fuse() > 0 && tickCount >= fuse()) { + // Removal bursts the grenade on the client. + discard(); + return; + } + boolean server = !level().isClientSide(); + if (server && (!level().isLoaded(blockPosition()) || tickCount > 300)) { + discard(); + return; + } + super.tick(); + Vec3 m = getDeltaMovement(); + if (server) { + HitResult hit = level().clip(new ClipContext(position(), position().add(m), ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, this)); + if (hit.getType() != HitResult.Type.MISS) { + discard(); + return; + } + } else { + trail(); + } + setPos(getX() + m.x, getY() + m.y, getZ() + m.z); + setDeltaMovement(m.x * airResistance, m.y + gravity, m.z * airResistance); + if (server && (isInWater() || onGround())) { + discard(); + } + } + + private void trail() { + double x = (getX() - xo) / 2.0; + double y = (getY() - yo) / 2.0; + double z = (getZ() - zo) / 2.0; + for (int i = 0; i < 2; i++) { + if (size < 5.0F && random.nextInt(2) == 0) { + continue; + } + level().addAlwaysVisibleParticle(fuse() > 0 ? ParticleTypes.CLOUD : ParticleTypes.LARGE_SMOKE, true, + xo + x * i, yo + y * i, zo + z * i, 0.0, 0.0, 0.0); + if (fuse() == 0) { + level().addAlwaysVisibleParticle(ParticleTypes.FLAME, true, xo + x * i, yo + y * i, zo + z * i, 0.0, 0.0, 0.0); + } + } + } + + /** A smoke grenade bursting into a cloud that hangs around for ten seconds or more. */ + private void burst() { + if (!level().isClientSide()) { + return; + } + for (int i = 0; i < 20; i++) { + level().addAlwaysVisibleParticle(ParticleTypes.CAMPFIRE_SIGNAL_SMOKE, true, + getX() + (random.nextDouble() - 0.5) * 10.0, + getY() + (random.nextDouble() - 0.5) * 10.0, + getZ() + (random.nextDouble() - 0.5) * 10.0, + (random.nextDouble() - 0.5) * 0.045, 0.005, (random.nextDouble() - 0.5) * 0.045); + } + } + + @Override + public void remove(RemovalReason reason) { + if (level().isClientSide() && fuse() > 0) { + burst(); + entityData.set(DATA_FUSE, 0); + } + super.remove(reason); + } + + @Override + public boolean hurt(DamageSource source, float damage) { + return false; + } + + @Override + public boolean isInvulnerable() { + return true; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + double d = getBoundingBox().getSize() * 4.0 * 64.0; + return distance < d * d; + } + + @Override + protected void readAdditionalSaveData(CompoundTag tag) { + discard(); + } + + @Override + protected void addAdditionalSaveData(CompoundTag tag) { + } + + @Override + public boolean shouldBeSaved() { + return false; + } +} From 5ffe6348c912e58ce880542cdcd414b6c00e26fa Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:50 +0800 Subject: [PATCH 226/314] Port marker rockets to 1.21.1's particles and damage API --- .../entity/projectile/MarkerRocketEntity.java | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java new file mode 100644 index 0000000..7ccac6f --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/MarkerRocketEntity.java @@ -0,0 +1,138 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.DustParticleOptions; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.phys.Vec3; + +/** + * A marking rocket, {@code MCH_EntityMarkerRocket}. Where it lands under open sky it pops red + * smoke, and five seconds later a stick of bombs falls on the mark. + */ +public class MarkerRocketEntity extends ProjectileEntity { + private static final EntityDataAccessor DATA_STATUS = SynchedEntityData.defineId(MarkerRocketEntity.class, EntityDataSerializers.BYTE); + + /** 0 inert, 1 flying, 2 marking. */ + private int countDown; + + public MarkerRocketEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + super.defineSynchedData(builder); + builder.define(DATA_STATUS, (byte) 0); + } + + public void setStatus(int status) { + if (!level().isClientSide()) { + entityData.set(DATA_STATUS, (byte) status); + } + } + + public int status() { + return entityData.get(DATA_STATUS); + } + + @Override + public void tick() { + int status = status(); + if (level().isClientSide()) { + if (weapon() == null) { + super.tick(); + } + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke && status != 0) { + if (status == 1) { + super.tick(); + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } else { + float gb = random.nextFloat() * 0.3F; + int rgb = (int) ((random.nextFloat() * 0.2F + 0.8F) * 255) << 16 | (int) (gb * 255) << 8 | (int) (gb * 255); + for (int i = 0; i < 5; i++) { + level().addAlwaysVisibleParticle(new DustParticleOptions(Vec3.fromRGB24(rgb).toVector3f(), 4.0F), true, getX(), getY(), getZ(), + (random.nextFloat() - 0.5F) * 0.7F, 0.3F + random.nextFloat() * 0.3F, (random.nextFloat() - 0.5F) * 0.7F); + } + } + } + } else if (status == 0 || isInWater()) { + discard(); + } else if (status == 1) { + super.tick(); + } else if (countDown > 0) { + countDown--; + if (countDown == 40) { + dropBombs(); + } + } else { + discard(); + } + } + + private void dropBombs() { + int count = 6 + random.nextInt(2); + for (int i = 0; i < count; i++) { + BombEntity bomb = ModEntities.BOMB.get().create(level()); + if (bomb == null) { + continue; + } + bomb.launch(getX() + (random.nextFloat() - 0.5F) * 15.0F, 260.0F + random.nextFloat() * 10.0F + i * 30, + getZ() + (random.nextFloat() - 0.5F) * 15.0F, 0.0, -0.5, 0.0, 0.0F, 90.0F, 4.0); + bomb.setWeapon(weaponName()); + bomb.explosionPower = 3 + random.nextInt(2); + bomb.explosionPowerInWater = 0; + bomb.power = 30; + bomb.piercing = 0; + bomb.setOwners(shooter, user); + level().addFreshEntity(bomb); + } + } + + @Override + protected void impact(Impact hit, float damageFactor) { + if (level().isClientSide() || hit.entity() != null || hit.block() == null) { + return; + } + BlockPos mark = hit.block().relative(hit.side()); + if (!level().getBlockState(mark).isAir()) { + discard(); + return; + } + level().setBlockAndUpdate(mark, Blocks.FIRE.defaultBlockState()); + int solid = 0; + for (int y = mark.getY() + 1; y < level().getMaxBuildHeight() - 1; y++) { + if (!level().getBlockState(new BlockPos(mark.getX(), y, mark.getZ())).isAir() && ++solid >= 5) { + break; + } + } + if (solid < 5) { + setStatus(2); + setPos(mark.getX() + 0.5, mark.getY() + 1.1, mark.getZ() + 0.5); + xo = getX(); + yo = getY(); + zo = getZ(); + countDown = 100; + } else { + discard(); + } + } + + @Override + public boolean hurt(DamageSource source, float damage) { + return false; + } + + @Override + public String defaultModel() { + return "rocket"; + } +} From 6a6ba406d4a6c5dc4dec99a886ad0c36bf423e38 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:51 +0800 Subject: [PATCH 227/314] Port projectiles to 1.21.1's damage, save and spawn API --- .../entity/projectile/ProjectileEntity.java | 764 ++++++++++++++++++ 1 file changed, 764 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java new file mode 100644 index 0000000..3ffc508 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/ProjectileEntity.java @@ -0,0 +1,764 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.weapon.Combat; +import com.jnhprojects.mcheli.weapon.Explosions; +import com.jnhprojects.mcheli.weapon.ModDamageTypes; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.particles.BlockParticleOption; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Mth; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.damagesource.DamageTypes; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Everything a weapon fires: rounds, rockets, missiles, bombs and torpedoes. A port of + * {@code MCH_EntityBaseBullet}. + * + *

Projectiles fly on both sides, the client only for looks; hits, fuses and guidance are decided + * on the server. They are never saved: like the original, one found in a loaded chunk vanishes. + */ +public abstract class ProjectileEntity extends Entity { + private static final EntityDataAccessor DATA_WEAPON = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_TARGET = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_BOMBLET = SynchedEntityData.defineId(ProjectileEntity.class, EntityDataSerializers.BOOLEAN); + + /** Beyond 5820 blocks from whoever fired it, a projectile is dropped. */ + private static final double MAX_RANGE_SQ = 3.38724E7; + + protected @Nullable Entity shooter; + protected @Nullable Entity user; + protected @Nullable Entity target; + private @Nullable WeaponDefinition weapon; + + public int explosionPower; + public int explosionPowerInWater; + public int power = 1; + public int piercing; + public int delayFuse; + public int sprinkleTime; + public double acceleration = 1.0; + public double accelerationFactor = 1.0; + private int age; + + public Vec3 prevMotion = Vec3.ZERO; + /** Position two ticks ago, used to spread trail particles over the distance covered. */ + protected double xo2; + protected double yo2; + protected double zo2; + + protected ProjectileEntity(EntityType type, Level level) { + super(type, level); + noPhysics = true; + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_WEAPON, ""); + builder.define(DATA_TARGET, 0); + builder.define(DATA_BOMBLET, false); + } + + /** + * The second constructor of the original: place the projectile and point it along + * {@code (mx, my, mz)} at {@code acceleration} blocks per tick, capped at 3.9. + */ + public void launch(double x, double y, double z, double mx, double my, double mz, float yaw, float pitch, double acceleration) { + moveTo(x, y, z, yaw, pitch); + xo2 = x; + yo2 = y; + zo2 = z; + double a = Math.min(acceleration, 3.9); + double d = Math.sqrt(mx * mx + my * my + mz * mz); + Vec3 motion = d > 0.0 ? new Vec3(mx * a / d, my * a / d, mz * a / d) : Vec3.ZERO; + setDeltaMovement(motion); + prevMotion = motion; + this.acceleration = a; + } + + // Weapon + + public void setWeapon(String name) { + WeaponDefinition def = MCHeli.content().weapon(name); + if (def == null) { + return; + } + weapon = def; + if (!level().isClientSide()) { + entityData.set(DATA_WEAPON, name); + } + if (def.bomblet > 0) { + sprinkleTime = def.bombletSTime; + } + piercing = def.piercing; + if (boostsAcceleration() && def.acceleration > 4.0F) { + accelerationFactor = def.acceleration / 4.0F; + } + } + + /** Rounds and rockets faster than 4 blocks a tick move several steps per tick instead. */ + protected boolean boostsAcceleration() { + return false; + } + + public @Nullable WeaponDefinition weapon() { + return weapon; + } + + /** Model drawn when the weapon names none, one of the original's {@code MCH_DefaultBulletModels}. */ + public abstract String defaultModel(); + + /** The model to draw: {@code ModelBomblet} for bomblets, else {@code ModelBullet}, else the default. */ + public String model() { + if (weapon == null) { + return defaultModel(); + } + String name = isBomblet() ? weapon.bombletModel : weapon.bulletModel; + return name.isEmpty() ? defaultModel() : name; + } + + public String weaponName() { + return entityData.get(DATA_WEAPON); + } + + /** {@code setParameterFromWeapon}: power and charges from the weapon file, and who to credit. */ + public void setParameters(WeaponDefinition def, @Nullable Entity shooter, @Nullable Entity user) { + explosionPower = def.explosion; + explosionPowerInWater = def.explosionInWater; + power = def.power; + piercing = def.piercing; + this.shooter = shooter; + this.user = user; + } + + /** The same, inherited from a parent projectile, for bomblets. */ + public void setParameters(ProjectileEntity parent) { + explosionPower = parent.explosionPower; + explosionPowerInWater = parent.explosionPowerInWater; + power = parent.power; + piercing = parent.piercing; + shooter = parent.shooter; + user = parent.user; + } + + public void setOwners(@Nullable Entity shooter, @Nullable Entity user) { + this.shooter = shooter; + this.user = user; + } + + public @Nullable Entity user() { + return user; + } + + public @Nullable Entity shooter() { + return shooter; + } + + public boolean isBomblet() { + return entityData.get(DATA_BOMBLET); + } + + public void setBomblet() { + sprinkleTime = 0; + entityData.set(DATA_BOMBLET, true); + } + + public void setTarget(@Nullable Entity entity) { + target = entity; + if (!level().isClientSide()) { + entityData.set(DATA_TARGET, entity != null ? entity.getId() : 0); + } + } + + public @Nullable Entity target() { + return target; + } + + /** A flare seduced this missile: it loses its target and is gone. */ + public void decoy() { + target = null; + discard(); + } + + public int age() { + return age; + } + + public float gravity() { + return weapon != null ? weapon.gravity : 0.0F; + } + + public float gravityInWater() { + return weapon != null ? weapon.gravityInWater : 0.0F; + } + + // Tick + + @Override + public void tick() { + if (level().isClientSide() && age == 0) { + int targetId = entityData.get(DATA_TARGET); + if (targetId > 0) { + target = level().getEntity(targetId); + } + } + Vec3 motion = getDeltaMovement(); + if (!motion.equals(prevMotion)) { + faceMotion(); + } + prevMotion = motion; + age++; + xo2 = xo; + yo2 = yo; + zo2 = zo; + super.tick(); + + if (weapon == null) { + if (age >= 2) { + discard(); + return; + } + setWeapon(weaponName()); + if (weapon == null) { + return; + } + } + + if (!level().isClientSide()) { + if (getY() <= level().getMaxBuildHeight() - 1 && !level().isLoaded(blockPosition())) { + if (weapon.delayFuse <= 0) { + discard(); + return; + } + if (delayFuse == 0) { + delayFuse = weapon.delayFuse; + } + } + if (delayFuse > 0) { + delayFuse--; + if (delayFuse == 0) { + onTimeout(); + discard(); + return; + } + } + if (!isValid()) { + discard(); + return; + } + if (weapon.timeFuse > 0 && age > weapon.timeFuse) { + onTimeout(); + discard(); + return; + } + if (weapon.explosionAltitude > 0 && isGroundWithin(weapon.explosionAltitude)) { + impact(new Impact(position(), null, null, Direction.DOWN), 1.0F); + } + } + + motion = getDeltaMovement(); + setDeltaMovement(motion.x, motion.y + (isInWater() ? gravityInWater() : gravity()), motion.z); + if (!isRemoved()) { + checkCollisions(); + } + motion = getDeltaMovement().scale(accelerationFactor); + double nx = getX() + motion.x; + double ny = getY() + motion.y; + double nz = getZ() + motion.z; + if (level().isClientSide()) { + splash(nx, ny, nz); + } + if (isInWater()) { + Vec3 m = getDeltaMovement(); + level().addParticle(ParticleTypes.BUBBLE, nx - m.x * 0.25, ny - m.y * 0.25, nz - m.z * 0.25, m.x, m.y, m.z); + } + setPos(nx, ny, nz); + } + + /** Turns the nose along the flight path, the way every projectile class did after moving. */ + protected void faceMotion() { + Vec3 m = getDeltaMovement(); + setYRot((float) Math.toDegrees(Math.atan2(m.z, m.x)) - 90.0F); + setXRot(-(float) Math.toDegrees(Math.atan2(m.y, Math.sqrt(m.x * m.x + m.z * m.z)))); + } + + /** {@code setMotion}: flies straight along {@code (dx, dy, dz)} at full speed. */ + protected void aimAlong(double dx, double dy, double dz) { + double d = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (d > 0.0) { + setDeltaMovement(dx * acceleration / d, dy * acceleration / d, dz * acceleration / d); + } + } + + /** {@code guidanceToTarget}: turns a seventh of the way towards the target each tick. */ + protected void guideTo(double tx, double ty, double tz) { + double dx = tx - getX(); + double dy = ty - getY(); + double dz = tz - getZ(); + double d = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (d <= 0.0) { + return; + } + Vec3 m = getDeltaMovement(); + setDeltaMovement((m.x * 6.0 + dx * acceleration / d) / 7.0, + (m.y * 6.0 + dy * acceleration / d) / 7.0, + (m.z * 6.0 + dz * acceleration / d) / 7.0); + faceMotion(); + } + + /** {@code checkValid}: whoever fired it still exists and is not too far away. */ + private boolean isValid() { + if (shooter == null && user == null) { + return false; + } + if (user != null && user.isRemoved()) { + return false; + } + Entity origin = user != null ? user : shooter; + double dx = getX() - origin.getX(); + double dz = getZ() - origin.getZ(); + return dx * dx + dz * dz < MAX_RANGE_SQ; + } + + private boolean isGroundWithin(int depth) { + BlockPos.MutableBlockPos pos = blockPosition().mutable(); + for (int i = 0; i < depth; i++) { + pos.move(Direction.DOWN); + if (!level().getBlockState(pos).isAir()) { + return true; + } + } + return false; + } + + /** Spawns {@code count} trail particles spread over the last two ticks of flight. */ + protected void trail(String name, int count, float size) { + if (!level().isClientSide() || name.isEmpty() || count < 1 || count > 50) { + return; + } + ParticleOptions particle = trailParticle(name); + double x = (getX() - xo) / count; + double y = (getY() - yo) / count; + double z = (getZ() - zo) / count; + double x2 = (xo - xo2) / count; + double y2 = (yo - yo2) / count; + double z2 = (zo - zo2) / count; + for (int i = 0; i < count; i++) { + double px = (xo + x * i + xo2 + x2 * i) / 2.0; + double py = (yo + y * i + yo2 + y2 * i) / 2.0; + double pz = (zo + z * i + zo2 + z2 * i) / 2.0; + level().addAlwaysVisibleParticle(particle, true, px, py, pz, 0.0, 0.0, 0.0); + } + } + + /** + * The original's own smoke particle ({@code explode}) was a large white puff; the cloud is the + * closest vanilla one. Other names are 1.7.10 particle names. + */ + private static ParticleOptions trailParticle(String name) { + return switch (name) { + case "explode", "cloud" -> ParticleTypes.CLOUD; + case "flame" -> ParticleTypes.FLAME; + case "largesmoke" -> ParticleTypes.LARGE_SMOKE; + case "heart" -> ParticleTypes.HEART; + case "fireworksspark" -> ParticleTypes.FIREWORK; + default -> ParticleTypes.SMOKE; + }; + } + + private void splash(double nx, double ny, double nz) { + if (weapon == null || weapon.power <= 0) { + return; + } + boolean wasDry = !level().getFluidState(BlockPos.containing(xo + 0.5, yo + 0.5, zo + 0.5)).is(FluidTags.WATER); + if (wasDry && level().getFluidState(BlockPos.containing(nx + 0.5, ny + 0.5, nz + 0.5)).is(FluidTags.WATER)) { + float pwr = Math.min(weapon.power, 20); + int n = random.nextInt(1 + (int) pwr / 3) + (int) pwr / 2 + 1; + for (int i = 0; i < n; i++) { + level().addParticle(ParticleTypes.SPLASH, nx, ny + 0.5, nz, + pwr * 0.03 * (random.nextDouble() - 0.5) * 0.3, + pwr * 0.03 * (random.nextDouble() * 0.5 + 0.5) * 1.8, + pwr * 0.03 * (random.nextDouble() - 0.5) * 0.3); + } + } + } + + /** {@code onUpdateTimeout}: the fuse ran out in mid air. */ + protected void onTimeout() { + if (!(level() instanceof ServerLevel server) || weapon == null) { + return; + } + if (isInWater()) { + if (explosionPowerInWater > 0) { + explode(server, getX(), getY(), getZ(), explosionPowerInWater, explosionPowerInWater, true); + } + } else if (explosionPower > 0) { + explode(server, getX(), getY(), getZ(), explosionPower, weapon.explosionBlock, false); + } else if (explosionPower < 0) { + Explosions.playSound(level(), getX(), getY(), getZ()); + } + } + + /** {@code onUpdateBomblet}: once the sprinkle timer runs out the carrier splits up. */ + protected void tickBomblets() { + if (!level().isClientSide() && sprinkleTime > 0 && !isRemoved() && weapon != null) { + sprinkleTime--; + if (sprinkleTime == 0) { + for (int i = 0; i < weapon.bomblet; i++) { + sprinkleBomblet(); + } + discard(); + } + } + } + + protected void sprinkleBomblet() { + } + + /** Creates a copy of this projectile's kind for a bomblet, flying from here. */ + protected @Nullable T spawnChild(EntityType type, double mx, double my, double mz, float yaw, float pitch) { + T child = type.create(level()); + if (child == null) { + return null; + } + child.launch(getX(), getY(), getZ(), mx, my, mz, yaw, pitch, acceleration); + child.setWeapon(weaponName()); + child.setParameters(this); + return child; + } + + // Collision + + /** Where something was hit. {@code entity} is set for entity hits, {@code block} for block hits. */ + public record Impact(Vec3 location, @Nullable Entity entity, @Nullable BlockPos block, Direction side) { + } + + /** {@code onUpdateCollided}: the swept ray of this tick against blocks, then entities. */ + protected void checkCollisions() { + if (weapon == null) { + return; + } + Vec3 motion = getDeltaMovement().scale(accelerationFactor); + Vec3 from = position(); + Vec3 to = from.add(motion); + BlockHitResult blockHit = null; + for (int i = 0; i < 5; i++) { + blockHit = level().clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, this)); + if (blockHit.getType() != HitResult.Type.BLOCK) { + blockHit = null; + break; + } + BlockState state = level().getBlockState(blockHit.getBlockPos()); + if (user == null || level().isClientSide() || !Combat.isBulletBreakable(state)) { + break; + } + level().destroyBlock(blockHit.getBlockPos(), true, user); + } + + if (weapon.delayFuse > 0) { + if (blockHit != null) { + bounce(blockHit.getDirection()); + if (delayFuse == 0) { + delayFuse = weapon.delayFuse; + } + } + return; + } + + if (blockHit != null) { + to = blockHit.getLocation(); + } + Impact entityHit = findEntity(from, to, motion); + if (entityHit != null) { + // A vehicle applies the factor of the box struck itself, when it takes the damage. + impact(entityHit, 1.0F); + } else if (blockHit != null) { + impact(new Impact(blockHit.getLocation(), null, blockHit.getBlockPos(), blockHit.getDirection()), 1.0F); + } + } + + private @Nullable Impact findEntity(Vec3 from, Vec3 to, Vec3 motion) { + AABB area = getBoundingBox().expandTowards(motion).inflate(21.0); + Entity best = null; + Vec3 bestAt = null; + double bestDistance = 0.0; + List clipped = new ArrayList<>(); + for (Entity candidate : level().getEntities(this, area, this::canHit)) { + Optional at; + if (candidate instanceof AircraftEntity vehicle) { + at = vehicle.clipHitBoxes(from, to, 0.3); + clipped.add(vehicle); + } else { + at = candidate.getBoundingBox().inflate(0.3).clip(from, to); + } + if (at.isPresent()) { + double d = from.distanceTo(at.get()); + if (best == null || d < bestDistance) { + best = candidate; + bestAt = at.get(); + bestDistance = d; + } + } + } + // Only the vehicle actually hit keeps the factor of the box the ray struck. + for (AircraftEntity vehicle : clipped) { + if (vehicle != best) { + vehicle.consumeHitBoxFactor(); + } + } + return best != null ? new Impact(bestAt, best, null, Direction.UP) : null; + } + + /** {@code canBeCollidedEntity}: never the shooter, its crew, or rounds fired by the same side. */ + public boolean canHit(Entity entity) { + if (!entity.isPickable() || entity == user || entity == shooter) { + return false; + } + if (entity instanceof ProjectileEntity other) { + if (level().isClientSide()) { + return false; + } + if (other.shooter != null && other.shooter == shooter || other.user != null && other.user == user) { + return false; + } + } + return !(shooter instanceof AircraftEntity vehicle) || !vehicle.hasPassenger(entity); + } + + /** {@code boundBullet}: a delayed fuse round bounces off what it hits. */ + private void bounce(Direction side) { + Vec3 m = getDeltaMovement(); + float bound = weapon != null ? weapon.bound : 0.0F; + switch (side) { + case DOWN -> { + if (m.y > 0.0) { + setDeltaMovement(m.x, -m.y * bound, m.z); + } + } + case UP -> { + if (m.y < 0.0) { + setDeltaMovement(m.x, -m.y * bound, m.z); + } + } + case NORTH -> { + if (m.z > 0.0) { + setDeltaMovement(m.x, m.y, -m.z * bound); + } else { + setPos(getX(), getY(), getZ() + m.z); + } + } + case SOUTH -> { + if (m.z < 0.0) { + setDeltaMovement(m.x, m.y, -m.z * bound); + } else { + setPos(getX(), getY(), getZ() + m.z); + } + } + case WEST -> { + if (m.x > 0.0) { + setDeltaMovement(-m.x * bound, m.y, m.z); + } else { + setPos(getX() + m.x, getY(), getZ()); + } + } + case EAST -> { + if (m.x < 0.0) { + setDeltaMovement(-m.x * bound, m.y, m.z); + } else { + setPos(getX() + m.x, getY(), getZ()); + } + } + } + } + + /** {@code onImpact}: damage what was hit, then explode or pierce through. */ + protected void impact(Impact hit, float damageFactor) { + if (!(level() instanceof ServerLevel server)) { + if (weapon != null && (weapon.explosion == 0 || weapon.modeNum >= 2) && hit.block() != null) { + debris(hit); + } + return; + } + if (weapon == null) { + discard(); + return; + } + if (hit.entity() != null) { + hitEntity(hit.entity(), damageFactor); + piercing = 0; + } + float expPower = explosionPower * damageFactor; + float expPowerInWater = explosionPowerInWater * damageFactor; + Vec3 at = hit.location(); + if (piercing > 0) { + piercing--; + if (expPower > 0.0F) { + explode(server, at.x, at.y, at.z, 1.0F, 1.0F, false); + } + return; + } + if (expPowerInWater == 0.0F) { + if (weapon.fae) { + explodeFae(server, expPower); + } else if (expPower > 0.0F) { + explode(server, at.x, at.y, at.z, expPower, weapon.explosionBlock, false); + } else if (expPower < 0.0F) { + Explosions.playSound(level(), getX(), getY(), getZ()); + } + } else if (hit.entity() != null) { + if (isInWater()) { + explode(server, at.x, at.y, at.z, expPowerInWater, expPowerInWater, true); + } else { + explode(server, at.x, at.y, at.z, expPower, weapon.explosionBlock, false); + } + } else if (isInWater() || hit.block() != null && isWaterAround(hit.block())) { + BlockPos b = hit.block() != null ? hit.block() : blockPosition(); + explode(server, b.getX(), b.getY(), b.getZ(), expPowerInWater, expPowerInWater, true); + } else if (expPower > 0.0F) { + explode(server, at.x, at.y, at.z, expPower, weapon.explosionBlock, false); + } else if (expPower < 0.0F) { + Explosions.playSound(level(), getX(), getY(), getZ()); + } + discard(); + } + + /** {@code MCH_Lib.isBlockInWater}: water on any side of the block. */ + private boolean isWaterAround(BlockPos pos) { + for (Direction d : Direction.values()) { + if (level().getFluidState(pos.relative(d)).is(FluidTags.WATER)) { + return true; + } + } + return level().getFluidState(pos).is(FluidTags.WATER); + } + + /** Client side chips flying off the block a round struck. */ + private void debris(Impact hit) { + if (weapon == null || hit.block() == null) { + return; + } + BlockState state = level().getBlockState(hit.block()); + if (state.isAir()) { + return; + } + float p = weapon.power; + Vec3 m = getDeltaMovement(); + var particle = new BlockParticleOption(ParticleTypes.BLOCK, state); + for (int i = 0; i < p / 3.0F; i++) { + level().addParticle(particle, + hit.location().x + (random.nextFloat() - 0.5) * p / 10.0, + hit.location().y + 0.1, + hit.location().z + (random.nextFloat() - 0.5) * p / 10.0, + -m.x * p / 2.0, p / 2.0F, -m.z * p / 2.0); + } + } + + /** {@code onImpactEntity}. */ + protected void hitEntity(Entity entity, float damageFactor) { + if (!entity.isRemoved()) { + DamageSource source = ModDamageTypes.bullet(level(), this, user); + Combat.hurt(entity, source, power * damageFactor, weapon); + } + confirmHit(); + } + + /** {@code notifyHitBullet}: the shooter hears that a round connected. */ + protected void confirmHit() { + if (user instanceof ServerPlayer player) { + ModSounds.playTo(player, "hit", 1.0F, 1.0F); + } + } + + protected void explode(ServerLevel server, double x, double y, double z, float size, float blockSize, boolean inWater) { + if (weapon == null) { + return; + } + boolean destroyBlocks = !isBomblet() || random.nextInt(3) == 0; + Explosions.Params p = new Explosions.Params(this, user, size, blockSize, weapon.flaming, destroyBlocks, 0, inWater, weapon); + if (Explosions.explode(server, x, y, z, p)) { + confirmHit(); + } + } + + /** Fuel air explosives burn whatever they catch. */ + private void explodeFae(ServerLevel server, float size) { + Explosions.Params p = new Explosions.Params(this, user, size, weapon.explosionBlock, weapon.flaming, true, 15, false, weapon); + if (Explosions.explode(server, getX(), getY(), getZ(), p)) { + confirmHit(); + } + } + + // Entity plumbing + + @Override + public boolean hurt(DamageSource source, float damage) { + // Something thrown at a projectile sets it off, as in the original. + if (damage > 0.0F && source.is(DamageTypes.THROWN) && !isRemoved()) { + impact(new Impact(position().add(0.5, 0.5, 0.5), null, blockPosition(), Direction.UP), 1.0F); + return true; + } + return false; + } + + @Override + public boolean isPickable() { + return true; + } + + @Override + public boolean shouldRenderAtSqrDistance(double distance) { + double d = getBoundingBox().getSize() * 4.0 * 64.0; + return distance < d * d; + } + + @Override + protected void readAdditionalSaveData(CompoundTag tag) { + discard(); + } + + @Override + protected void addAdditionalSaveData(CompoundTag tag) { + tag.putString("WeaponName", weaponName()); + } + + @Override + public boolean shouldBeSaved() { + return false; + } + + /** The yaw for this frame, as 26.x's {@code Entity#getYRot(float)} gives it. */ + public float getYRot(float partialTicks) { + return partialTicks == 1.0F ? getYRot() : Mth.rotLerp(partialTicks, yRotO, getYRot()); + } + + /** The pitch for this frame, as 26.x's {@code Entity#getXRot(float)} gives it. */ + public float getXRot(float partialTicks) { + return partialTicks == 1.0F ? getXRot() : Mth.lerp(partialTicks, xRotO, getXRot()); + } +} From c53bdbc6c11b9bcc99068e98221907be4c202b8c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:51 +0800 Subject: [PATCH 228/314] Add RocketEntity to the shared 1.21.1 sources --- .../entity/projectile/RocketEntity.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java new file mode 100644 index 0000000..c39fa6f --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/RocketEntity.java @@ -0,0 +1,49 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** An unguided rocket, {@code MCH_EntityRocket}, which may burst into bomblets. */ +public class RocketEntity extends ProjectileEntity { + public RocketEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + protected boolean boostsAcceleration() { + return !isBomblet(); + } + + @Override + public void tick() { + super.tick(); + tickBomblets(); + WeaponDefinition w = weapon(); + if (!isBomblet() && w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } + } + + @Override + protected void sprinkleBomblet() { + WeaponDefinition w = weapon(); + Vec3 m = getDeltaMovement(); + RocketEntity child = spawnChild(ModEntities.ROCKET.get(), m.x, m.y, m.z, getYRot(), getXRot()); + if (child == null || w == null) { + return; + } + float spread = w.bombletDiff; + child.setDeltaMovement(child.getDeltaMovement().add( + (random.nextFloat() - 0.5) * spread, (random.nextFloat() - 0.5) * spread, (random.nextFloat() - 0.5) * spread)); + child.setBomblet(); + level().addFreshEntity(child); + } + + @Override + public String defaultModel() { + return "rocket"; + } +} From 04bd64315bfa03dd2973a997003ca6cd52f7db1c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:52 +0800 Subject: [PATCH 229/314] Add TorpedoEntity to the shared 1.21.1 sources --- .../entity/projectile/TorpedoEntity.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java new file mode 100644 index 0000000..a89f7c0 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/TorpedoEntity.java @@ -0,0 +1,56 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** + * A torpedo, {@code MCH_EntityTorpedo}. Once in the water it settles on its running speed and + * either holds course or, when guided, runs to the point the gunner aimed at. + */ +public class TorpedoEntity extends ProjectileEntity { + public double targetX; + public double targetY; + public double targetZ; + public double accelerationInWater = 2.0; + + public TorpedoEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + boolean guided = w != null && w.guidedTorpedo; + if (!level().isClientSide() && isInWater()) { + if (acceleration < accelerationInWater) { + acceleration += 0.1; + } else if (acceleration > accelerationInWater + 0.2F) { + acceleration -= 0.1; + } + if (guided) { + aimAlong(targetX - getX(), targetY - getY(), targetZ - getZ()); + } else { + Vec3 m = getDeltaMovement(); + aimAlong(m.x, m.y * 0.8F, m.z); + } + } + if (isInWater()) { + Vec3 m = getDeltaMovement(); + setYRot((float) Math.toDegrees(Math.atan2(m.z, m.x)) - 90.0F); + if (guided) { + setXRot(-(float) Math.toDegrees(Math.atan2(m.y, Math.sqrt(m.x * m.x + m.z * m.z)))); + } + if (w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } + } + } + + @Override + public String defaultModel() { + return "torpedo"; + } +} From 8a6e448f58fa31b988be78eef488bd3587ab9f03 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:52 +0800 Subject: [PATCH 230/314] Add TvMissileEntity to the shared 1.21.1 sources --- .../entity/projectile/TvMissileEntity.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java new file mode 100644 index 0000000..a06fef1 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/entity/projectile/TvMissileEntity.java @@ -0,0 +1,55 @@ +package com.jnhprojects.mcheli.entity.projectile; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +/** + * A wire guided missile flown by the gunner's view, {@code MCH_EntityTvMissile}. It steers while + * it is the vehicle's active TV missile; once another is fired it carries on straight. + */ +public class TvMissileEntity extends ProjectileEntity { + private static final double MAX_DISTANCE_SQ = 1440000.0; + + public TvMissileEntity(EntityType type, Level level) { + super(type, level); + } + + @Override + public void tick() { + super.tick(); + WeaponDefinition w = weapon(); + if (w != null && !w.disableSmoke) { + trail(w.trajectoryParticle, 3, 5.0F * w.smokeSize * 0.5F); + } + Entity aimer = user(); + if (aimer != null) { + if (distanceToSqr(aimer) > MAX_DISTANCE_SQ) { + discard(); + } + if (!level().isClientSide() && !isRemoved()) { + steer(aimer); + } + } else if (!level().isClientSide()) { + discard(); + } + } + + private void steer(Entity aimer) { + if (aimer.isRemoved() || !(shooter() instanceof AircraftEntity vehicle) || vehicle.tvMissile() != this) { + return; + } + Vec3 dir = Vec3.directionFromRotation(aimer.getXRot(), aimer.getYRot()); + aimAlong(dir.x, dir.y, dir.z); + setYRot(aimer.getYRot()); + setXRot(aimer.getXRot()); + } + + @Override + public String defaultModel() { + return "asmissile"; + } +} From 2e5f4c00b36930f4878d7bd0602acd3aecf54eb9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:52 +0800 Subject: [PATCH 231/314] Use 1.21.1's item use result and custom model data icons --- .../jnhprojects/mcheli/item/AircraftItem.java | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java new file mode 100644 index 0000000..f7258aa --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/AircraftItem.java @@ -0,0 +1,126 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.ServerConfig; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import com.jnhprojects.mcheli.registry.ModEntities; +import com.jnhprojects.mcheli.registry.ModItems; +import net.minecraft.core.component.DataComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.stats.Stats; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.CustomModelData; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.gameevent.GameEvent; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * Places a vehicle, the same way a boat is placed. + * + *

There is one item per vehicle kind ({@code mcheli:helicopter}, {@code mcheli:plane} and so + * on). Which vehicle a stack places is stored in its {@link ModDataComponents#VEHICLE} component, + * and its look comes from the {@code item_model} component pointing at the model the import wrote + * for that vehicle. Items are registered once at startup, so this is what lets definitions be + * added or reloaded while the game runs. + */ +public class AircraftItem extends Item { + private final String kind; + + public AircraftItem(String kind, Properties properties) { + super(properties); + this.kind = kind; + } + + /** The vehicle kind this item holds, such as {@code helicopter}. */ + public String kind() { + return kind; + } + + /** + * A stack that places {@code def}. Vehicles from the import show the item model the import + * wrote for them, picked by the kind model's {@code custom_model_data} overrides (1.21.1 has no + * {@code item_model} component); item models only load with the resources, so pack vehicles keep + * their kind's own model. + */ + public static ItemStack stackOf(AircraftDefinition def) { + ItemStack stack = new ItemStack(ModItems.forKind(def.kind())); + stack.set(ModDataComponents.VEHICLE.get(), def.name()); + if (def.pack().equals(ContentSource.ORIGINAL)) { + MCHeli.legacyContent().iconIndex(def.name()) + .ifPresent(index -> stack.set(DataComponents.CUSTOM_MODEL_DATA, new CustomModelData(index))); + } + return stack; + } + + /** The definition name a stack places, or {@code null} if it is not a vehicle item or has none. */ + public static @Nullable String vehicleOf(ItemStack stack) { + return stack.getItem() instanceof AircraftItem item ? item.definitionName(stack) : null; + } + + protected @Nullable String definitionName(ItemStack stack) { + return stack.get(ModDataComponents.VEHICLE.get()); + } + + @Override + public Component getName(ItemStack stack) { + String name = definitionName(stack); + if (name == null) { + return super.getName(stack); + } + AircraftDefinition def = MCHeli.content().any(name); + // Vehicles added at runtime may bring no lang entry; their DisplayName stands in. + return Component.translatableWithFallback("item.mcheli." + name, def != null ? def.displayName : name); + } + + @Override + public InteractionResultHolder use(Level level, Player player, InteractionHand hand) { + ItemStack stack = player.getItemInHand(hand); + HitResult hit = getPlayerPOVHitResult(level, player, ClipContext.Fluid.ANY); + if (!(hit instanceof BlockHitResult blockHit) || hit.getType() != HitResult.Type.BLOCK) { + return InteractionResultHolder.pass(stack); + } + + String name = definitionName(stack); + AircraftDefinition def = name != null ? MCHeli.content().any(name) : null; + if (def == null) { + if (!level.isClientSide()) { + player.displayClientMessage(Component.translatable("message.mcheli.vehicle.unknown", String.valueOf(name)), true); + } + return InteractionResultHolder.fail(stack); + } + if (!level.isClientSide() && ServerConfig.isDisabled(def.name())) { + player.displayClientMessage(Component.translatable("message.mcheli.vehicle.disabled", stack.getHoverName()), true); + return InteractionResultHolder.fail(stack); + } + + AircraftEntity vehicle = ModEntities.typeFor(def).create(level); + if (vehicle == null) { + return InteractionResultHolder.fail(stack); + } + Vec3 at = blockHit.getLocation(); + vehicle.setType(def); + vehicle.moveTo(at.x, at.y, at.z, player.getYRot(), 0.0F); + if (!level.noCollision(vehicle, vehicle.getBoundingBox())) { + return InteractionResultHolder.fail(stack); + } + + if (!level.isClientSide()) { + level.addFreshEntity(vehicle); + level.gameEvent(player, GameEvent.ENTITY_PLACE, at); + stack.consume(1, player); + } + player.awardStat(Stats.ITEM_USED.get(this)); + return InteractionResultHolder.sidedSuccess(stack, level.isClientSide()); + } +} From ffc536c1cb49c7a3ae90a10127025a2f8c644d27 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:53 +0800 Subject: [PATCH 232/314] Convert 1.0.0 items from 1.21.1's inventory tick --- .../mcheli/item/LegacyAircraftItem.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java new file mode 100644 index 0000000..14ca564 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/LegacyAircraftItem.java @@ -0,0 +1,60 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.registry.ModDataComponents; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import org.jspecify.annotations.Nullable; + +/** + * A vehicle item as 1.0.0 saved it: one item per vehicle, such as {@code mcheli:ah-64}. + * + *

These are still registered for the vehicles imported at startup, so that stacks in 1.0.0 + * worlds load instead of vanishing. They work as before, are not listed anywhere, and turn into + * the generic item of their kind ({@link AircraftItem#stackOf}) as soon as they sit in a player's + * inventory. Stacks in chests convert once a player takes them out. + */ +public final class LegacyAircraftItem extends AircraftItem { + private final String definitionName; + + public LegacyAircraftItem(String kind, String definitionName, Properties properties) { + super(kind, properties); + this.definitionName = definitionName; + } + + @Override + protected @Nullable String definitionName(ItemStack stack) { + return definitionName; + } + + @Override + public void inventoryTick(ItemStack stack, Level level, Entity owner, int slot, boolean selected) { + if (level.isClientSide() || !(owner instanceof Player player)) { + return; + } + AircraftDefinition def = MCHeli.content().any(definitionName); + if (def == null) { + return; + } + Inventory inventory = player.getInventory(); + for (int i = 0; i < inventory.getContainerSize(); i++) { + if (inventory.getItem(i) == stack) { + inventory.setItem(i, convert(stack, def)); + return; + } + } + } + + /** The generic stack for {@code def}, keeping count, custom name and anything else set on the old one. */ + static ItemStack convert(ItemStack legacy, AircraftDefinition def) { + ItemStack converted = AircraftItem.stackOf(def); + converted.applyComponents(legacy.getComponentsPatch()); + converted.set(ModDataComponents.VEHICLE.get(), def.name()); + converted.setCount(legacy.getCount()); + return converted; + } +} From 28041e163f02318bb1b683d130a94c0c8c1f32e6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:53 +0800 Subject: [PATCH 233/314] List the vehicle stack for 1.21.1's ingredient display --- .../mcheli/item/VehicleIngredient.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java new file mode 100644 index 0000000..364c383 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/item/VehicleIngredient.java @@ -0,0 +1,65 @@ +package com.jnhprojects.mcheli.item; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.registry.ModItems; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +import java.util.function.Function; +import java.util.stream.Stream; + +/** + * Matches one vehicle, such as an AH-6 in the recipe that turns it into an AH-6X. Every vehicle of + * a kind shares an item, so a plain item ingredient would accept any of them. In JSON: + *

{"type": "mcheli:vehicle", "vehicle": "ah-6"}
+ * with the type also under {@code neoforge:ingredient_type} and {@code fabric:type}, since each + * loader names custom ingredients its own way. + * + *

This is the loader-independent half; each loader module wraps it in its own ingredient class. + */ +public final class VehicleIngredient { + public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "vehicle"); + + private VehicleIngredient() { + } + + /** The JSON fields, given a way to build the loader's ingredient from a vehicle name. */ + public static MapCodec codec(Function create, Function vehicle) { + return Codec.STRING.fieldOf("vehicle").xmap(create, vehicle); + } + + public static StreamCodec streamCodec(Function create, Function vehicle) { + return ByteBufCodecs.STRING_UTF8.map(create, vehicle).cast(); + } + + /** The items a vehicle can be: the generic item of each kind. */ + @SuppressWarnings("deprecation") + public static HolderSet kindItems() { + return HolderSet.direct(Item::builtInRegistryHolder, + ModItems.HELICOPTER.get(), ModItems.PLANE.get(), ModItems.TANK.get(), ModItems.VEHICLE.get()); + } + + public static Stream> items() { + return kindItems().stream(); + } + + public static boolean matches(String vehicle, @Nullable ItemStack stack) { + return stack != null && vehicle.equals(AircraftItem.vehicleOf(stack)); + } + + /** What recipe books and JEI show: the vehicle itself, or nothing while it is not loaded. */ + public static Stream displayStacks(String vehicle) { + AircraftDefinition def = MCHeli.content().any(vehicle); + return def != null ? Stream.of(AircraftItem.stackOf(def)) : Stream.empty(); + } +} From ae62f1c89c171af9e3a652d23285312ce9ad60e2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:54 +0800 Subject: [PATCH 234/314] Board through interactAt on 1.21.1 --- .../mcheli/network/BoardVehiclePacket.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java new file mode 100644 index 0000000..0f7ccf7 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/BoardVehiclePacket.java @@ -0,0 +1,58 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.phys.Vec3; + +/** + * Client to server: the player right-clicked a vehicle's hull outside its main hit box. The + * server repeats the reach check against the hull boxes, then treats it as an ordinary click. + * + * @param location where on the hull the click landed, in world coordinates + */ +public record BoardVehiclePacket(int entityId, Vec3 location) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("board_vehicle"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(BoardVehiclePacket::encode, BoardVehiclePacket::decode); + + /** The same slack vanilla allows on top of the player's reach for a click on an entity. */ + private static final double REACH_SLACK = 3.0; + + public static void encode(BoardVehiclePacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeDouble(msg.location.x); + buf.writeDouble(msg.location.y); + buf.writeDouble(msg.location.z); + } + + public static BoardVehiclePacket decode(FriendlyByteBuf buf) { + return new BoardVehiclePacket(buf.readVarInt(), new Vec3(buf.readDouble(), buf.readDouble(), buf.readDouble())); + } + + public static void handle(BoardVehiclePacket msg, ServerPlayer sender) { + if (sender == null || !(sender.level().getEntity(msg.entityId) instanceof AircraftEntity vehicle) || vehicle.isRemoved()) { + return; + } + Vec3 eye = sender.getEyePosition(); + double reach = sender.entityInteractionRange() + REACH_SLACK; + // The click must land on the hull, and the hull must be within reach. + if (eye.distanceToSqr(msg.location) > reach * reach) { + return; + } + Vec3 through = eye.add(msg.location.subtract(eye).scale(1.05)); + boolean onHull = vehicle.clipHitBoxes(eye, through, 0.5).isPresent(); + vehicle.consumeHitBoxFactor(); + if (onHull) { + vehicle.interactAt(sender, msg.location.subtract(vehicle.position()), InteractionHand.MAIN_HAND); + } + } + + @Override + public Type type() { + return TYPE; + } +} From a6968f5757a7dea46649686a74f63e83552382e7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:54 +0800 Subject: [PATCH 235/314] Add CrewActionPacket to the shared 1.21.1 sources --- .../mcheli/network/CrewActionPacket.java | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java new file mode 100644 index 0000000..8e7136f --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/CrewActionPacket.java @@ -0,0 +1,142 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.weapon.Resupply; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * A one-off request from a crew member: lower the gear, fold the wings, change seats and so on. + * + *

The client only asks; the server checks that the sender sits in that vehicle, in a seat + * allowed to do it, and then applies the same conditions the original checked before switching. + * The result reaches every client through synched data. + */ +public record CrewActionPacket(int entityId, Action action) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("crew_action"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(CrewActionPacket::encode, CrewActionPacket::decode); + + public enum Action { + /** Raise or lower the landing gear. Pilot only. */ + LANDING_GEAR, + /** Fold or unfold hatches, wings or swept wings. Pilot only. */ + FOLD, + /** Tilt the nozzles for vertical flight. Pilot only. */ + VTOL, + /** Eject from seat zero or one. */ + EJECT, + /** Move to the next free crew seat. */ + SWITCH_SEAT, + /** Load nearby cargo onto the racks, or drive onto a carrier's rack. Pilot only. */ + RACK_LOAD, + /** Drop the last cargo, or leave the rack this vehicle rides on. Pilot only. */ + RACK_UNLOAD, + /** Select the next weapon this seat may use. */ + NEXT_WEAPON, + /** Select the previous weapon this seat may use. */ + PREVIOUS_WEAPON, + /** Cycle the fire mode of the selected weapon. */ + WEAPON_MODE, + /** Release flares. Pilot and first crew seat. */ + FLARE, + /** Pay the selected weapon's {@code Item} cost from the inventory for one resupply. */ + RESUPPLY, + /** Hand the controls to the autopilot and man the weapons, or take them back. Pilot only. */ + GUNNER_MODE + } + + public static void encode(CrewActionPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeVarInt(msg.action.ordinal()); + } + + public static CrewActionPacket decode(FriendlyByteBuf buf) { + int entityId = buf.readVarInt(); + int ordinal = buf.readVarInt(); + Action[] actions = Action.values(); + if (ordinal < 0 || ordinal >= actions.length) { + throw new IllegalArgumentException("Unknown crew action " + ordinal); + } + return new CrewActionPacket(entityId, actions[ordinal]); + } + + public static void handle(CrewActionPacket msg, ServerPlayer sender) { + if (sender == null || !(sender.getVehicle() instanceof AircraftEntity vehicle) || vehicle.getId() != msg.entityId) { + return; + } + boolean pilot = vehicle.getControllingPassenger() == sender; + switch (msg.action) { + case LANDING_GEAR -> { + if (pilot) { + vehicle.toggleLandingGear(); + } + } + case FOLD -> { + if (pilot) { + vehicle.toggleFold(); + } + } + case VTOL -> { + if (pilot && vehicle instanceof PlaneEntity plane) { + plane.toggleVtol(); + } + } + case GUNNER_MODE -> { + if (pilot) { + vehicle.setGunnerMode(!vehicle.isGunnerMode()); + } + } + case EJECT -> vehicle.eject(sender); + case SWITCH_SEAT -> vehicle.switchSeat(sender); + case RACK_LOAD -> { + if (pilot) { + if (!vehicle.boardNearbyRack()) { + vehicle.loadNearbyCargo(); + } + } + } + case RACK_UNLOAD -> { + if (pilot) { + if (vehicle.isPassenger()) { + vehicle.leaveRack(); + } else { + vehicle.unloadCargo(); + } + } + } + case NEXT_WEAPON, PREVIOUS_WEAPON -> { + VehicleWeapons weapons = vehicle.weapons(); + int seat = vehicle.seatOf(sender); + if (weapons != null && !vehicle.isDestroyed()) { + int next = weapons.next(seat, msg.action == Action.NEXT_WEAPON ? 1 : -1); + if (next >= 0) { + weapons.switchTo(sender, seat, next); + } + } + } + case WEAPON_MODE -> { + VehicleWeapons weapons = vehicle.weapons(); + if (weapons != null) { + weapons.switchMode(vehicle.seatOf(sender)); + } + } + case FLARE -> { + VehicleWeapons weapons = vehicle.weapons(); + if (weapons != null && vehicle.seatOf(sender) <= 1) { + weapons.flares.use(); + } + } + case RESUPPLY -> Resupply.fromInventory(sender, vehicle); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 2dbb1bc0570282b40a8c7d818e4a77ce976a4cd2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:54 +0800 Subject: [PATCH 236/314] Add ExplosionEffectPacket to the shared 1.21.1 sources --- .../mcheli/network/ExplosionEffectPacket.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java new file mode 100644 index 0000000..279b151 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/ExplosionEffectPacket.java @@ -0,0 +1,54 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.ClientAccess; +import com.jnhprojects.mcheli.weapon.ExplosionEffect; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.world.entity.Entity; + +/** + * Server to nearby players: draw an MCHeli explosion here. The original's + * {@code MCH_PacketEffectExplosion}, plus a flag for the bigger fireball of a crashing vehicle. + */ +public record ExplosionEffectPacket(double x, double y, double z, float size, boolean inWater, boolean crash) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("explosion_effect"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(ExplosionEffectPacket::encode, ExplosionEffectPacket::decode); + + public ExplosionEffectPacket(double x, double y, double z, float size, boolean inWater) { + this(x, y, z, size, inWater, false); + } + + public static void encode(ExplosionEffectPacket msg, FriendlyByteBuf buf) { + buf.writeDouble(msg.x); + buf.writeDouble(msg.y); + buf.writeDouble(msg.z); + buf.writeFloat(msg.size); + buf.writeBoolean(msg.inWater); + buf.writeBoolean(msg.crash); + } + + public static ExplosionEffectPacket decode(FriendlyByteBuf buf) { + return new ExplosionEffectPacket(buf.readDouble(), buf.readDouble(), buf.readDouble(), buf.readFloat(), buf.readBoolean(), + buf.readBoolean()); + } + + public static void handle(ExplosionEffectPacket msg) { + Entity player = ClientAccess.localPlayer(); + if (player == null || !Float.isFinite(msg.size)) { + return; + } + float size = Math.min(msg.size, 50.0F); + if (msg.crash) { + ExplosionEffect.crash(player.level(), msg.x, msg.y, msg.z, Math.min(size, ExplosionEffect.MAX_CRASH_SIZE)); + } else { + ExplosionEffect.play(player.level(), msg.x, msg.y, msg.z, size, msg.inWater); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 49aa0b6b6599ea035794c8b96c2598bc660c45e8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:55 +0800 Subject: [PATCH 237/314] Add FireWeaponPacket to the shared 1.21.1 sources --- .../mcheli/network/FireWeaponPacket.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java new file mode 100644 index 0000000..0056dcc --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/FireWeaponPacket.java @@ -0,0 +1,54 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.weapon.ServerFiring; +import com.jnhprojects.mcheli.weapon.Shot; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * Client to server: the crew member's selected weapon fired on their side, so fire it for real. + * The client already checked readiness, ammunition and lock; the server checks again with its own + * timers and only trusts the two option values, a locked entity and a fire mode. + * + * @param weapon the weapon the client fired, ignored unless it is still the seat's selection + */ +public record FireWeaponPacket(int entityId, int weapon, int option1, int option2) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("fire_weapon"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(FireWeaponPacket::encode, FireWeaponPacket::decode); + + public static void encode(FireWeaponPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeVarInt(msg.weapon); + buf.writeVarInt(msg.option1); + buf.writeVarInt(msg.option2); + } + + public static FireWeaponPacket decode(FriendlyByteBuf buf) { + return new FireWeaponPacket(buf.readVarInt(), buf.readVarInt(), buf.readVarInt(), buf.readVarInt()); + } + + public static void handle(FireWeaponPacket msg, ServerPlayer sender) { + if (sender == null || !(sender.getVehicle() instanceof AircraftEntity vehicle) || vehicle.getId() != msg.entityId) { + return; + } + VehicleWeapons weapons = vehicle.weapons(); + int seat = vehicle.seatOf(sender); + if (weapons == null || weapons.selected(seat) != msg.weapon || weapons.operatorSeat(msg.weapon) != seat) { + return; + } + Shot shot = new Shot(vehicle, sender); + shot.option1 = msg.option1; + shot.option2 = msg.option2; + weapons.use(sender, seat, shot, ServerFiring.INSTANCE); + } + + @Override + public Type type() { + return TYPE; + } +} From 639e57d447c54fc3627b906a50c3908c332a160f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:55 +0800 Subject: [PATCH 238/314] Add LockWarningPacket to the shared 1.21.1 sources --- .../mcheli/network/LockWarningPacket.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java new file mode 100644 index 0000000..97d2cbf --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/LockWarningPacket.java @@ -0,0 +1,44 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; + +/** + * Client to server: this player is locking on to an entity. If it is a vehicle with flares, or a + * crew member of one, its crew hears the warning. The original's {@code MCH_PacketNotifyLock}. + */ +public record LockWarningPacket(int targetId) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("lock_warning"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(LockWarningPacket::encode, LockWarningPacket::decode); + + public static void encode(LockWarningPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.targetId); + } + + public static LockWarningPacket decode(FriendlyByteBuf buf) { + return new LockWarningPacket(buf.readVarInt()); + } + + public static void handle(LockWarningPacket msg, ServerPlayer sender) { + if (sender == null || !(sender.getVehicle() instanceof AircraftEntity)) { + return; + } + Entity target = sender.level().getEntity(msg.targetId); + if (target != null && target.distanceToSqr(sender) < 400.0 * 400.0) { + AircraftEntity vehicle = target instanceof AircraftEntity a ? a : target.getVehicle() instanceof AircraftEntity a ? a : null; + if (vehicle != null) { + vehicle.warnLocked(); + } + } + } + + @Override + public Type type() { + return TYPE; + } +} From 3367636c14fcf29a7f0eb7ed9de024c5ec95599a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:56 +0800 Subject: [PATCH 239/314] Add ModNetwork to the shared 1.21.1 sources --- .../mcheli/network/ModNetwork.java | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java new file mode 100644 index 0000000..de59563 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/ModNetwork.java @@ -0,0 +1,81 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; + +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * Our packets. Each one is a custom payload with its own id and the same encoding on every loader, + * so a client on one loader can talk to a server on another. The loader module registers the + * lists below with its own networking API; handlers always run on the main thread. + */ +public final class ModNetwork { + /** Bump whenever a packet layout changes. Clients and servers must match exactly. */ + public static final int PROTOCOL_VERSION = 6; + + /** Client to server. The handler gets the sending player. */ + public static final List> SERVERBOUND = List.of( + new Serverbound<>(PilotStatePacket.TYPE, PilotStatePacket.STREAM_CODEC, PilotStatePacket::handle), + new Serverbound<>(CrewActionPacket.TYPE, CrewActionPacket.STREAM_CODEC, CrewActionPacket::handle), + new Serverbound<>(FireWeaponPacket.TYPE, FireWeaponPacket.STREAM_CODEC, FireWeaponPacket::handle), + new Serverbound<>(BoardVehiclePacket.TYPE, BoardVehiclePacket.STREAM_CODEC, BoardVehiclePacket::handle), + new Serverbound<>(LockWarningPacket.TYPE, LockWarningPacket.STREAM_CODEC, LockWarningPacket::handle), + new Serverbound<>(PackRequestPacket.TYPE, PackRequestPacket.STREAM_CODEC, PackRequestPacket::handle)); + + /** Server to client. Handlers touch client classes, so they only ever run on a client. */ + public static final List> CLIENTBOUND = List.of( + new Clientbound<>(WeaponStatePacket.TYPE, WeaponStatePacket.STREAM_CODEC, WeaponStatePacket::handle), + new Clientbound<>(ExplosionEffectPacket.TYPE, ExplosionEffectPacket.STREAM_CODEC, ExplosionEffectPacket::handle), + new Clientbound<>(PackListPacket.TYPE, PackListPacket.STREAM_CODEC, PackListPacket::handle), + new Clientbound<>(PackDataPacket.TYPE, PackDataPacket.STREAM_CODEC, PackDataPacket::handle)); + + private ModNetwork() { + } + + static CustomPacketPayload.Type type(String name) { + return new CustomPacketPayload.Type<>(ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, name)); + } + + public static void sendToServer(CustomPacketPayload message) { + Platform.get().sendToServer(message); + } + + public static void sendToPlayer(ServerPlayer player, CustomPacketPayload message) { + Platform.get().sendToPlayer(player, message); + } + + public static void sendToAll(MinecraftServer server, CustomPacketPayload message) { + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + sendToPlayer(player, message); + } + } + + /** Sends to every player in {@code level} within {@code range} blocks of the point. */ + public static void sendNear(ServerLevel level, double x, double y, double z, double range, CustomPacketPayload message) { + for (ServerPlayer player : level.players()) { + if (player.distanceToSqr(x, y, z) < range * range) { + sendToPlayer(player, message); + } + } + } + + public record Serverbound(CustomPacketPayload.Type type, + StreamCodec codec, + BiConsumer handler) { + } + + public record Clientbound(CustomPacketPayload.Type type, + StreamCodec codec, + Consumer handler) { + } +} From aca8c2a3182c466020c9f16242824b6237157084 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:56 +0800 Subject: [PATCH 240/314] Add PackDataPacket to the shared 1.21.1 sources --- .../mcheli/network/PackDataPacket.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java new file mode 100644 index 0000000..89c1644 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackDataPacket.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.ClientAccess; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; + +/** + * Server to player: one chunk of a pack's zip. Chunks of a pack arrive in order, so + * {@code offset} only lets the client check that nothing went missing. + * + * @param total length of the whole zip, as in the {@link PackListPacket} + */ +public record PackDataPacket(String hash, int offset, int total, byte[] data) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_data"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackDataPacket::encode, PackDataPacket::decode); + + /** Well under the 1 MiB limit of a clientbound custom payload. */ + public static final int CHUNK_SIZE = 128 * 1024; + + public static void encode(PackDataPacket msg, FriendlyByteBuf buf) { + buf.writeUtf(msg.hash, 64); + buf.writeVarInt(msg.offset); + buf.writeVarInt(msg.total); + buf.writeByteArray(msg.data); + } + + public static PackDataPacket decode(FriendlyByteBuf buf) { + return new PackDataPacket(buf.readUtf(64), buf.readVarInt(), buf.readVarInt(), buf.readByteArray(CHUNK_SIZE)); + } + + public static void handle(PackDataPacket msg) { + ClientAccess.packs().onPackData(msg); + } + + @Override + public Type type() { + return TYPE; + } +} From 3a9d98074ffa4bee9223ab16e9d5ce6fb8b2e093 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:56 +0800 Subject: [PATCH 241/314] Add PackListPacket to the shared 1.21.1 sources --- .../mcheli/network/PackListPacket.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java new file mode 100644 index 0000000..ad7f0c8 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackListPacket.java @@ -0,0 +1,61 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.ClientAccess; +import io.netty.handler.codec.DecoderException; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; + +import java.util.ArrayList; +import java.util.List; + +/** + * Server to player: the content packs the server uses, in load order. Sent on join and after + * {@code /mcheli reload}. The client requests the packs it has not cached ({@link PackRequestPacket}) + * and then rebuilds its content from exactly this list, so both sides fly the same definitions. + * The imported 1.0.3 content is never listed: every player imports their own copy. + */ +public record PackListPacket(List packs) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_list"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackListPacket::encode, PackListPacket::decode); + + public static final int MAX_PACKS = 256; + + /** + * @param size length of the pack's zip in bytes, which is what {@link PackDataPacket} sends + */ + public record Entry(String id, String version, String hash, int size) { + } + + public static void encode(PackListPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.packs.size()); + for (Entry entry : msg.packs) { + buf.writeUtf(entry.id, 64); + buf.writeUtf(entry.version, 32); + buf.writeUtf(entry.hash, 64); + buf.writeVarInt(entry.size); + } + } + + public static PackListPacket decode(FriendlyByteBuf buf) { + int count = buf.readVarInt(); + if (count < 0 || count > MAX_PACKS) { + throw new DecoderException("Too many content packs: " + count); + } + List packs = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + packs.add(new Entry(buf.readUtf(64), buf.readUtf(32), buf.readUtf(64), buf.readVarInt())); + } + return new PackListPacket(List.copyOf(packs)); + } + + public static void handle(PackListPacket msg) { + ClientAccess.packs().onPackList(msg); + } + + @Override + public Type type() { + return TYPE; + } +} From 7f8d8a061ccb594ca5579c62034d9ec1ecf52c65 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:57 +0800 Subject: [PATCH 242/314] Add PackRequestPacket to the shared 1.21.1 sources --- .../mcheli/network/PackRequestPacket.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java new file mode 100644 index 0000000..50f10f7 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackRequestPacket.java @@ -0,0 +1,50 @@ +package com.jnhprojects.mcheli.network; + +import io.netty.handler.codec.DecoderException; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +import java.util.ArrayList; +import java.util.List; + +/** + * Player to server: send me these packs, by content hash. The server only answers for hashes in + * the list it sent, so a client cannot ask for anything else. + */ +public record PackRequestPacket(List hashes) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pack_request"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PackRequestPacket::encode, PackRequestPacket::decode); + + public static void encode(PackRequestPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.hashes.size()); + for (String hash : msg.hashes) { + buf.writeUtf(hash, 64); + } + } + + public static PackRequestPacket decode(FriendlyByteBuf buf) { + int count = buf.readVarInt(); + if (count < 0 || count > PackListPacket.MAX_PACKS) { + throw new DecoderException("Too many content packs requested: " + count); + } + List hashes = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + hashes.add(buf.readUtf(64)); + } + return new PackRequestPacket(List.copyOf(hashes)); + } + + public static void handle(PackRequestPacket msg, ServerPlayer sender) { + if (sender != null) { + PackSync.request(sender, msg.hashes); + } + } + + @Override + public Type type() { + return TYPE; + } +} From 48ff55cf60df45b04e35fe375b9759f2625fe515 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:57 +0800 Subject: [PATCH 243/314] Check the singleplayer owner and permissions the 1.21.1 way --- .../jnhprojects/mcheli/network/PackSync.java | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackSync.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackSync.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackSync.java new file mode 100644 index 0000000..2a12ac2 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PackSync.java @@ -0,0 +1,156 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.pack.ContentPack; +import net.minecraft.commands.Commands; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Deque; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * The server half of the content pack sync. + *

+ *  player joins          ── PackListPacket(id, version, hash, size per pack) ──▶ client
+ *                        ◀── PackRequestPacket(hashes it has not cached) ────────
+ *  every tick            ── PackDataPacket(chunk) ──▶  until each pack is sent
+ * 
+ * Each player gets {@link #CHUNKS_PER_TICK} chunks a tick (about 5 MB a second), one pack after + * the other, so a download never crowds out the game's own packets. A pack's bytes are read when + * its turn comes and dropped once sent. Everything here runs on the server thread. + */ +public final class PackSync { + private static final int CHUNKS_PER_TICK = 2; + + private static final Map> TRANSFERS = new HashMap<>(); + + private PackSync() { + } + + /** Sends a joining player the pack list, and operators any pack problems. */ + public static void onLogin(ServerPlayer player) { + ModNetwork.sendToPlayer(player, list()); + if (seesProblems(player)) { + MCHeli.packs().reportProblems(player::sendSystemMessage); + } + } + + public static void onLogout(ServerPlayer player) { + TRANSFERS.remove(player.getUUID()); + } + + /** The packs the server uses now, as the client should load them. */ + public static PackListPacket list() { + List entries = new ArrayList<>(); + for (ContentPack pack : MCHeli.packs().active()) { + try { + entries.add(new PackListPacket.Entry(pack.id(), pack.manifest().version(), pack.hash(), (int) pack.archiveSize())); + } catch (IOException e) { + MCHeli.LOGGER.error("Content pack {} cannot be sent to players", pack, e); + } + } + return new PackListPacket(List.copyOf(entries)); + } + + /** After a reload: drops transfers in progress and sends every player the new list. */ + public static void sendListToAll(MinecraftServer server) { + TRANSFERS.clear(); + ModNetwork.sendToAll(server, list()); + } + + /** Operators, and the owner of a single player or LAN world, are told what is wrong with the packs. */ + public static boolean seesProblems(ServerPlayer player) { + MinecraftServer server = player.level().getServer(); + return server.isSingleplayerOwner(player.getGameProfile()) + || player.createCommandSourceStack().hasPermission(Commands.LEVEL_GAMEMASTERS); + } + + static void request(ServerPlayer player, List hashes) { + Deque queue = TRANSFERS.computeIfAbsent(player.getUUID(), id -> new ArrayDeque<>()); + for (String hash : hashes) { + ContentPack pack = find(hash); + if (pack == null) { + MCHeli.LOGGER.debug("{} asked for content pack {}, which the server does not use", player.getScoreboardName(), hash); + continue; + } + if (queue.stream().noneMatch(t -> t.pack == pack)) { + queue.add(new Transfer(pack)); + } + } + } + + private static @Nullable ContentPack find(String hash) { + for (ContentPack pack : MCHeli.packs().active()) { + if (pack.hash().equals(hash)) { + return pack; + } + } + return null; + } + + public static void pump(MinecraftServer server) { + Iterator>> it = TRANSFERS.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry> entry = it.next(); + ServerPlayer player = server.getPlayerList().getPlayer(entry.getKey()); + Deque queue = entry.getValue(); + if (player == null || queue.isEmpty()) { + it.remove(); + continue; + } + for (int sent = 0; sent < CHUNKS_PER_TICK && !queue.isEmpty(); sent++) { + Transfer transfer = queue.peek(); + PackDataPacket chunk = transfer.next(); + if (chunk == null) { + queue.poll(); + continue; + } + ModNetwork.sendToPlayer(player, chunk); + if (transfer.done()) { + queue.poll(); + } + } + } + } + + /** One pack on its way to one player. */ + private static final class Transfer { + private final ContentPack pack; + private byte @Nullable [] data; + private int offset; + + Transfer(ContentPack pack) { + this.pack = pack; + } + + /** The next chunk, or {@code null} when the pack could not be read. */ + @Nullable PackDataPacket next() { + if (data == null) { + try { + data = pack.archive(); + } catch (IOException e) { + MCHeli.LOGGER.error("Could not read content pack {} to send it", pack, e); + return null; + } + } + int end = Math.min(offset + PackDataPacket.CHUNK_SIZE, data.length); + PackDataPacket chunk = new PackDataPacket(pack.hash(), offset, data.length, Arrays.copyOfRange(data, offset, end)); + offset = end; + return chunk; + } + + boolean done() { + return data != null && offset >= data.length; + } + } +} From a039b00eb0e80dbf5342c41c471cf06401032b16 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:58 +0800 Subject: [PATCH 244/314] Add PilotStatePacket to the shared 1.21.1 sources --- .../mcheli/network/PilotStatePacket.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java new file mode 100644 index 0000000..6426f06 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/PilotStatePacket.java @@ -0,0 +1,53 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * Sent by the pilot every tick. Position, yaw and pitch already travel through the vanilla + * vehicle move packet; this carries the parts vanilla does not know about. + * + * @param keys the pilot's key state as packed by {@link AircraftEntity.ControlInput#write()}, so + * other players see wheels steer and tracks turn + */ +public record PilotStatePacket(int entityId, float throttle, float reverse, float roll, boolean hovering, byte keys) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("pilot_state"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(PilotStatePacket::encode, PilotStatePacket::decode); + + public static void encode(PilotStatePacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeFloat(msg.throttle); + buf.writeFloat(msg.reverse); + buf.writeFloat(msg.roll); + buf.writeBoolean(msg.hovering); + buf.writeByte(msg.keys); + } + + public static PilotStatePacket decode(FriendlyByteBuf buf) { + return new PilotStatePacket(buf.readVarInt(), buf.readFloat(), buf.readFloat(), buf.readFloat(), buf.readBoolean(), buf.readByte()); + } + + public static void handle(PilotStatePacket msg, ServerPlayer sender) { + if (sender == null) { + return; + } + // Only the player actually flying this aircraft may drive it. + if (sender.getVehicle() instanceof AircraftEntity aircraft + && aircraft.getId() == msg.entityId + && aircraft.getControllingPassenger() == sender + && Float.isFinite(msg.throttle) + && Float.isFinite(msg.reverse) + && Float.isFinite(msg.roll)) { + aircraft.acceptPilotState(msg.throttle, msg.reverse, msg.roll, msg.hovering, msg.keys); + } + } + + @Override + public Type type() { + return TYPE; + } +} From a89eaf12777349f95de8f9dad95b632e0e74e7fb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:58 +0800 Subject: [PATCH 245/314] Add WeaponStatePacket to the shared 1.21.1 sources --- .../mcheli/network/WeaponStatePacket.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java new file mode 100644 index 0000000..292f643 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/network/WeaponStatePacket.java @@ -0,0 +1,45 @@ +package com.jnhprojects.mcheli.network; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.ClientAccess; +import com.jnhprojects.mcheli.weapon.VehicleWeapons; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.world.entity.Entity; + +/** + * Server to crew: the magazine and reserve of one weapon after it fired, reloaded or was + * resupplied. The original's {@code MCH_PacketNotifyAmmoNum}. + */ +public record WeaponStatePacket(int entityId, int weapon, int ammo, int rest) implements CustomPacketPayload { + public static final Type TYPE = ModNetwork.type("weapon_state"); + public static final StreamCodec STREAM_CODEC = StreamCodec.ofMember(WeaponStatePacket::encode, WeaponStatePacket::decode); + + public static void encode(WeaponStatePacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.entityId); + buf.writeVarInt(msg.weapon); + buf.writeVarInt(msg.ammo); + buf.writeVarInt(msg.rest); + } + + public static WeaponStatePacket decode(FriendlyByteBuf buf) { + return new WeaponStatePacket(buf.readVarInt(), buf.readVarInt(), buf.readVarInt(), buf.readVarInt()); + } + + public static void handle(WeaponStatePacket msg) { + Entity player = ClientAccess.localPlayer(); + if (player != null && player.level().getEntity(msg.entityId) instanceof AircraftEntity vehicle) { + VehicleWeapons weapons = vehicle.weapons(); + if (weapons != null) { + weapons.applyAmmo(msg.weapon, msg.ammo, msg.rest); + } + } + } + + @Override + public Type type() { + return TYPE; + } +} From facc8171dc6bf0553f2260a103204b23abd13ba8 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:58 +0800 Subject: [PATCH 246/314] Read the glow colour as a plain int on 1.21.1 --- .../mcheli/particle/GlowParticleOptions.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java new file mode 100644 index 0000000..22f7370 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/particle/GlowParticleOptions.java @@ -0,0 +1,57 @@ +package com.jnhprojects.mcheli.particle; + +import com.jnhprojects.mcheli.registry.ModParticles; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.particles.ParticleType; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.util.ExtraCodecs; + +/** + * A soft ball of light blended on top of the scene: the flash of an explosion, a burning wreck, an + * ember. Spawned on the client only, but the codecs let {@code /particle mcheli:glow} show it too. + * + * @param color RGB of the light + * @param size radius in blocks at its largest + * @param lifetime ticks until it has faded out + * @param flicker whether it wavers like a fire rather than fading smoothly like a flash + */ +public record GlowParticleOptions(int color, float size, int lifetime, boolean flicker) implements ParticleOptions { + /** White hot at the centre of a blast. */ + public static final int FLASH = 0xFFE2B0; + /** Burning fuel and the fire inside the smoke. */ + public static final int FIRE = 0xFF8A2E; + /** Embers and the deeper red at the edge of a fireball. */ + public static final int EMBER = 0xFF5A1E; + + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(i -> i.group( + Codec.INT.fieldOf("color").forGetter(GlowParticleOptions::color), + ExtraCodecs.POSITIVE_FLOAT.fieldOf("size").forGetter(GlowParticleOptions::size), + ExtraCodecs.POSITIVE_INT.fieldOf("lifetime").forGetter(GlowParticleOptions::lifetime), + Codec.BOOL.optionalFieldOf("flicker", false).forGetter(GlowParticleOptions::flicker)) + .apply(i, GlowParticleOptions::new)); + + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.INT, GlowParticleOptions::color, + ByteBufCodecs.FLOAT, GlowParticleOptions::size, + ByteBufCodecs.VAR_INT, GlowParticleOptions::lifetime, + ByteBufCodecs.BOOL, GlowParticleOptions::flicker, + GlowParticleOptions::new); + + public static GlowParticleOptions flash(int color, float size, int lifetime) { + return new GlowParticleOptions(color, size, lifetime, false); + } + + public static GlowParticleOptions fire(int color, float size, int lifetime) { + return new GlowParticleOptions(color, size, lifetime, true); + } + + @Override + public ParticleType getType() { + return ModParticles.GLOW.get(); + } +} From 0de112a0306c4c864cc73fcc113ce83d189a5db3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:59 +0800 Subject: [PATCH 247/314] Add Platform to the shared 1.21.1 sources --- .../jnhprojects/mcheli/platform/Platform.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/platform/Platform.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/platform/Platform.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/platform/Platform.java new file mode 100644 index 0000000..e951f49 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/platform/Platform.java @@ -0,0 +1,66 @@ +package com.jnhprojects.mcheli.platform; + +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; + +import java.nio.file.Path; +import java.util.Objects; + +/** + * What the shared code needs from the mod loader. Each loader module installs its implementation + * first thing during mod construction, before anything else of ours runs. + * + *

Kept small on purpose: registration and events go the other way, with the loader module + * calling into the shared code (see {@code Registration} and the {@code *Hooks} classes). + */ +public interface Platform { + + static Platform get() { + Platform platform = Holder.instance; + if (platform == null) { + throw new IllegalStateException("No mod loader platform installed yet"); + } + return platform; + } + + static void install(Platform platform) { + if (Holder.instance != null) { + throw new IllegalStateException("Platform already installed: " + Holder.instance.loaderName()); + } + Holder.instance = Objects.requireNonNull(platform); + } + + /** {@code forge}, {@code neoforge} or {@code fabric}, for logs and {@code /mcheli status}. */ + String loaderName(); + + /** The game directory, which holds the {@code mcheli} folder. */ + Path gameDir(); + + /** The config directory, which holds the client config. */ + Path configDir(); + + /** Our version as the loader read it from the mod metadata. */ + String modVersion(); + + /** Whether this is a physical client, as opposed to a dedicated server. */ + boolean isClient(); + + void sendToServer(CustomPacketPayload payload); + + void sendToPlayer(ServerPlayer player, CustomPacketPayload payload); + + /** + * A builder for one of our creative tabs, placed after {@code after}. Vanilla's builder is not + * public on every loader and only Forge and NeoForge can order tabs. + */ + CreativeModeTab.Builder creativeTab(ResourceKey after); + + final class Holder { + private static volatile Platform instance; + + private Holder() { + } + } +} From 8c5c3ac4107ee2cbcd009f036072ef06c30f855f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:13:59 +0800 Subject: [PATCH 248/314] Add Registered to the shared 1.21.1 sources --- .../mcheli/platform/Registered.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/platform/Registered.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/platform/Registered.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/platform/Registered.java new file mode 100644 index 0000000..e90f274 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/platform/Registered.java @@ -0,0 +1,35 @@ +package com.jnhprojects.mcheli.platform; + +import net.minecraft.resources.ResourceLocation; + +import java.util.function.Supplier; + +/** + * A registry entry the shared code declares and the loader module registers, like Forge's + * {@code RegistryObject}. {@link #get()} works once the loader has registered it. + */ +public final class Registered implements Supplier { + private final ResourceLocation id; + private volatile T value; + + Registered(ResourceLocation id) { + this.id = id; + } + + public ResourceLocation id() { + return id; + } + + @Override + public T get() { + T v = value; + if (v == null) { + throw new IllegalStateException("Registry entry " + id + " is not registered yet"); + } + return v; + } + + void bind(T value) { + this.value = value; + } +} From 7fff8ab6c9dd31666c04cc48a8805c03b31eeff3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:00 +0800 Subject: [PATCH 249/314] Add Registration to the shared 1.21.1 sources --- .../mcheli/platform/Registration.java | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/platform/Registration.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/platform/Registration.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/platform/Registration.java new file mode 100644 index 0000000..67d899a --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/platform/Registration.java @@ -0,0 +1,80 @@ +package com.jnhprojects.mcheli.platform; + +import com.jnhprojects.mcheli.MCHeli; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.resources.ResourceKey; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * Every vanilla registry entry the mod adds. The shared registry classes declare their entries + * here during mod construction; the loader module then registers them its own way: Forge and + * NeoForge from their register event, Fabric straight away. + */ +public final class Registration { + private static final List> PENDING = new ArrayList<>(); + private static final Set>> REGISTRIES = new LinkedHashSet<>(); + + private Registration() { + } + + /** + * Declares an entry. + * + * @param factory makes the value; it gets the entry's key for {@code Item.Properties#setId} + * and the like + */ + public static synchronized Registered register(ResourceKey> registry, String name, + Function, T> factory) { + ResourceLocation id = ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, name); + Registered entry = new Registered<>(id); + PENDING.add(new Pending<>(registry, entry, factory)); + REGISTRIES.add(registry); + return entry; + } + + /** The registries with entries of ours, in the order they were first used. */ + public static synchronized List>> registries() { + return List.copyOf(REGISTRIES); + } + + /** + * A key from {@link #registries()} with its element type filled in, for the loader's generic + * register calls. The type is whatever the caller needs; entries are only ever handed to the + * registry they were declared for. + */ + @SuppressWarnings("unchecked") + public static ResourceKey> typed(ResourceKey> registry) { + return (ResourceKey>) registry; + } + + /** + * Creates our entries for one registry, in declaration order, and hands each to the loader. + * Called once per registry. + */ + public static synchronized void registerAll(ResourceKey> registry, BiConsumer sink) { + for (Pending pending : PENDING) { + if (pending.registry.equals(registry)) { + @SuppressWarnings("unchecked") + Pending ours = (Pending) pending; + ours.create(sink); + } + } + } + + private record Pending(ResourceKey> registry, Registered entry, + Function, T> factory) { + @SuppressWarnings("unchecked") + void create(BiConsumer sink) { + T value = factory.apply(ResourceKey.create((ResourceKey>) registry, entry.id())); + entry.bind(value); + sink.accept(entry.id(), value); + } + } +} From 29f93b73ae441ae01139347864247fb8bb83b583 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:00 +0800 Subject: [PATCH 250/314] Add ModCreativeTabs to the shared 1.21.1 sources --- .../mcheli/registry/ModCreativeTabs.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java new file mode 100644 index 0000000..8b24af0 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModCreativeTabs.java @@ -0,0 +1,61 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.item.CreativeModeTabs; +import net.minecraft.world.item.ItemStack; + +import java.util.List; + +/** + * One tab per vehicle kind, listing a stack for each loaded definition of that kind. The list is + * read from the content registry whenever a tab is built, so reloaded content shows up. + */ +public final class ModCreativeTabs { + public static final Registered HELICOPTERS = register("helicopters", "helicopter", "ah-64", + CreativeModeTabs.SPAWN_EGGS); + public static final Registered PLANES = register("planes", "plane", "f-35b", key(HELICOPTERS)); + public static final Registered TANKS = register("tanks", "tank", "m1a2", key(PLANES)); + public static final Registered VEHICLES = register("vehicles", "vehicle", "mk15", key(TANKS)); + + private ModCreativeTabs() { + } + + private static ResourceKey key(Registered tab) { + return ResourceKey.create(Registries.CREATIVE_MODE_TAB, tab.id()); + } + + private static Registered register(String name, String kind, String icon, ResourceKey after) { + return Registration.register(Registries.CREATIVE_MODE_TAB, name, key -> Platform.get().creativeTab(after) + .title(Component.translatable("itemGroup.mcheli." + name)) + .icon(() -> { + AircraftDefinition def = MCHeli.content().any(icon); + return def != null ? AircraftItem.stackOf(def) : new ItemStack(ModItems.forKind(kind)); + }) + .displayItems((params, output) -> { + for (AircraftDefinition def : MCHeli.content().all()) { + if (def.kind().equals(kind)) { + output.accept(AircraftItem.stackOf(def)); + } + } + }) + .build()); + } + + /** Our tabs, for rebuilding their contents after a content reload. */ + public static List all() { + return List.of(HELICOPTERS.get(), PLANES.get(), TANKS.get(), VEHICLES.get()); + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 94bbe8e9216e650774a0b694eb6995153805b043 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:01 +0800 Subject: [PATCH 251/314] Add ModDataComponents to the shared 1.21.1 sources --- .../mcheli/registry/ModDataComponents.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java new file mode 100644 index 0000000..08b564e --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModDataComponents.java @@ -0,0 +1,27 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import com.mojang.serialization.Codec; +import net.minecraft.core.component.DataComponentType; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.codec.ByteBufCodecs; + +public final class ModDataComponents { + /** + * The definition a vehicle item places, such as {@code ah-64}. Saved with the stack and sent + * to clients; the item itself only says which kind of vehicle it is. + */ + public static final Registered> VEHICLE = Registration.register(Registries.DATA_COMPONENT_TYPE, "vehicle", + key -> DataComponentType.builder() + .persistent(Codec.STRING) + .networkSynchronized(ByteBufCodecs.STRING_UTF8) + .build()); + + private ModDataComponents() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From dd567c45a351ecfc85d5abdebc80166e248f4afc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:01 +0800 Subject: [PATCH 252/314] Build entity types from a name on 1.21.1 --- .../mcheli/registry/ModEntities.java | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java new file mode 100644 index 0000000..19441e8 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModEntities.java @@ -0,0 +1,108 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.HelicopterDefinition; +import com.jnhprojects.mcheli.content.aircraft.PlaneDefinition; +import com.jnhprojects.mcheli.content.aircraft.TankDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.GroundVehicleEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import com.jnhprojects.mcheli.entity.projectile.A10Entity; +import com.jnhprojects.mcheli.entity.projectile.AAMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ASMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ATMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.BombEntity; +import com.jnhprojects.mcheli.entity.projectile.BulletEntity; +import com.jnhprojects.mcheli.entity.projectile.DispensedItemEntity; +import com.jnhprojects.mcheli.entity.projectile.FlareEntity; +import com.jnhprojects.mcheli.entity.projectile.MarkerRocketEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.entity.projectile.RocketEntity; +import com.jnhprojects.mcheli.entity.projectile.TorpedoEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MobCategory; + +/** + * One entity type per vehicle kind. The concrete vehicle is stored on the entity, the same approach + * the original took, so adding content never changes the entity registry. + */ +public final class ModEntities { + public static final Registered> HELICOPTER = register("helicopter", HelicopterEntity::new); + public static final Registered> PLANE = register("plane", PlaneEntity::new); + public static final Registered> TANK = register("tank", TankEntity::new); + public static final Registered> VEHICLE = register("vehicle", GroundVehicleEntity::new); + + public static final Registered> BULLET = projectile("bullet", BulletEntity::new); + public static final Registered> ROCKET = projectile("rocket", RocketEntity::new); + public static final Registered> BOMB = projectile("bomb", BombEntity::new); + public static final Registered> AA_MISSILE = projectile("aa_missile", AAMissileEntity::new); + public static final Registered> AS_MISSILE = projectile("as_missile", ASMissileEntity::new); + public static final Registered> AT_MISSILE = projectile("at_missile", ATMissileEntity::new); + public static final Registered> TV_MISSILE = projectile("tv_missile", TvMissileEntity::new); + public static final Registered> TORPEDO = projectile("torpedo", TorpedoEntity::new); + public static final Registered> MARKER_ROCKET = projectile("marker_rocket", MarkerRocketEntity::new); + public static final Registered> DISPENSED_ITEM = projectile("dispensed_item", DispensedItemEntity::new); + public static final Registered> FLARE = Registration.register(Registries.ENTITY_TYPE, "flare", key -> EntityType.Builder + .of(FlareEntity::new, MobCategory.MISC) + .sized(1.0F, 1.0F) + .clientTrackingRange(16) + .updateInterval(2) + .fireImmune() + .noSave() + .build(key.location().toString())); + public static final Registered> A10 = Registration.register(Registries.ENTITY_TYPE, "a10", key -> EntityType.Builder + .of(A10Entity::new, MobCategory.MISC) + .sized(5.0F, 3.0F) + .clientTrackingRange(32) + .updateInterval(1) + .fireImmune() + .noSave() + .build(key.location().toString())); + + private ModEntities() { + } + + /** Projectiles are a metre cube like the original's, and never saved. */ + private static Registered> projectile(String name, EntityType.EntityFactory factory) { + return Registration.register(Registries.ENTITY_TYPE, name, key -> EntityType.Builder.of(factory, MobCategory.MISC) + .sized(1.0F, 1.0F) + .clientTrackingRange(16) + .updateInterval(1) + .fireImmune() + .noSave() + .build(key.location().toString())); + } + + private static Registered> register(String name, EntityType.EntityFactory factory) { + return Registration.register(Registries.ENTITY_TYPE, name, key -> EntityType.Builder.of(factory, MobCategory.MISC) + .sized(2.0F, 0.7F) + .clientTrackingRange(16) + .updateInterval(1) + .fireImmune() + .build(key.location().toString())); + } + + /** The entity type that hosts a definition of this kind. */ + public static EntityType typeFor(AircraftDefinition def) { + if (def instanceof HelicopterDefinition) { + return HELICOPTER.get(); + } + if (def instanceof PlaneDefinition) { + return PLANE.get(); + } + if (def instanceof TankDefinition) { + return TANK.get(); + } + return VEHICLE.get(); + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From 7b6726d5def7741f4087f40b9b6170a7c753a4f9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:01 +0800 Subject: [PATCH 253/314] Drop the item id on the properties, which 1.21.1 does not have --- .../jnhprojects/mcheli/registry/ModItems.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java new file mode 100644 index 0000000..7eec878 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModItems.java @@ -0,0 +1,61 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.content.ContentRegistry; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.pack.ContentSource; +import com.jnhprojects.mcheli.item.AircraftItem; +import com.jnhprojects.mcheli.item.LegacyAircraftItem; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.item.Item; + +import java.util.Map; + +/** + * One item per vehicle kind; the stack says which vehicle (see {@link AircraftItem}). The 1.0.0 + * items, one per vehicle, are kept for the content present at startup so old worlds keep them. + */ +public final class ModItems { + public static final Registered HELICOPTER = kind("helicopter"); + public static final Registered PLANE = kind("plane"); + public static final Registered TANK = kind("tank"); + public static final Registered VEHICLE = kind("vehicle"); + + private static final Map> BY_KIND = + Map.of("helicopter", HELICOPTER, "plane", PLANE, "tank", TANK, "vehicle", VEHICLE); + + private ModItems() { + } + + private static Registered kind(String kind) { + return Registration.register(Registries.ITEM, kind, key -> new AircraftItem(kind, new Item.Properties().stacksTo(1))); + } + + /** + * Declares the entries, with a 1.0.0 item for each original vehicle in {@code content}; the + * loader module registers them. + */ + public static void init(ContentRegistry content) { + for (AircraftDefinition def : content.all()) { + String name = def.name(); + // Only the imported content existed in 1.0.0; pack vehicles never had their own item. + if (!def.pack().equals(ContentSource.ORIGINAL) || BY_KIND.containsKey(name)) { + // 1.0.0 could not have had such an item either: it would have clashed the same way. + continue; + } + String kind = def.kind(); + Registration.register(Registries.ITEM, name, key -> new LegacyAircraftItem(kind, name, + new Item.Properties().stacksTo(1))); + } + } + + /** The generic item of a vehicle kind ({@code helicopter}, {@code plane}, {@code tank} or {@code vehicle}). */ + public static Item forKind(String kind) { + Registered item = BY_KIND.get(kind); + if (item == null) { + throw new IllegalArgumentException("Unknown vehicle kind " + kind); + } + return item.get(); + } +} From 4c1705f6443e9bf918828287fe72aa056b3ac808 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:02 +0800 Subject: [PATCH 254/314] Add ModParticles to the shared 1.21.1 sources --- .../mcheli/registry/ModParticles.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java new file mode 100644 index 0000000..ce9a1da --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/registry/ModParticles.java @@ -0,0 +1,33 @@ +package com.jnhprojects.mcheli.registry; + +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import com.jnhprojects.mcheli.platform.Registered; +import com.jnhprojects.mcheli.platform.Registration; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.particles.ParticleType; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; + +public final class ModParticles { + /** Ignores the particle limit: an explosion that loses its flash to the cap looks broken. */ + public static final Registered> GLOW = Registration.register(Registries.PARTICLE_TYPE, "glow", + key -> new ParticleType<>(true) { + @Override + public MapCodec codec() { + return GlowParticleOptions.CODEC; + } + + @Override + public StreamCodec streamCodec() { + return GlowParticleOptions.STREAM_CODEC; + } + }); + + private ModParticles() { + } + + /** Declares the entries; the loader module registers them. */ + public static void init() { + } +} From f96f0dff7ec63b4c435ed054b747ff6f110158ab Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:02 +0800 Subject: [PATCH 255/314] Deal weapon damage through 1.21.1's hurt --- .../com/jnhprojects/mcheli/weapon/Combat.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java new file mode 100644 index 0000000..ca7a9c4 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Combat.java @@ -0,0 +1,73 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.GroundVehicleEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.BlockTags; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import org.jspecify.annotations.Nullable; + +/** Damage rules shared by bullets and explosions. */ +public final class Combat { + private Combat() { + } + + /** The {@code DamageFactor} kind a target belongs to, or {@code null} for anything else. */ + public static @Nullable String kind(Entity entity) { + return switch (entity) { + case Player p -> "player"; + case HelicopterEntity h -> "helicopter"; + case PlaneEntity p -> "plane"; + case TankEntity t -> "tank"; + case GroundVehicleEntity v -> "vehicle"; + default -> null; + }; + } + + public static float factor(@Nullable WeaponDefinition def, Entity target) { + String kind = kind(target); + return def != null && kind != null ? def.damageFactor(kind) : 1.0F; + } + + /** + * Deals weapon damage. The original scaled every living target's invulnerability window by + * {@code HurtResistantTime}, zero by default, so each round of a burst lands. + */ + public static boolean hurt(Entity target, DamageSource source, float damage, @Nullable WeaponDefinition def) { + if (!(target.level() instanceof ServerLevel)) { + return false; + } + if (target instanceof LivingEntity living) { + living.invulnerableTime = 0; + } + return target.hurt(source, damage * factor(def, target)); + } + + /** The convention tag every loader fills, so modded panes count too. */ + private static final TagKey GLASS_PANES = TagKey.create(Registries.BLOCK, ResourceLocation.fromNamespaceAndPath("c", "glass_panes")); + + /** + * {@code BulletBreakableBlocks} with its default list: panes, grass, flowers, vines, wheat, + * sugar cane and lily pads, which a round shoots through. + */ + public static boolean isBulletBreakable(BlockState state) { + return state.is(GLASS_PANES) + || state.is(BlockTags.FLOWERS) + || state.is(Blocks.SHORT_GRASS) || state.is(Blocks.TALL_GRASS) + || state.is(Blocks.FERN) || state.is(Blocks.LARGE_FERN) + || state.is(Blocks.VINE) || state.is(Blocks.WHEAT) + || state.is(Blocks.SUGAR_CANE) || state.is(Blocks.LILY_PAD); + } +} From 615454a437dff3ed853670f6c9d422f070ab45b1 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:03 +0800 Subject: [PATCH 256/314] Add ExplosionEffect to the shared 1.21.1 sources --- .../mcheli/weapon/ExplosionEffect.java | 232 ++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java new file mode 100644 index 0000000..4f4b0dc --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/ExplosionEffect.java @@ -0,0 +1,232 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.particle.GlowParticleOptions; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.BlockParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.HashSet; +import java.util.Set; + +/** + * The look of an MCHeli explosion, ported from {@code MCH_Explosion.effectExplosion} and + * {@code effectExplosionInWater}: a fireball, ground dust thrown up from the crater, and columns + * and rings of smoke that hang around for seconds. It runs on each client that receives the + * explosion; the burning debris arcs are real flares spawned by the server. + */ +public final class ExplosionEffect { + /** The largest crash fireball drawn; beyond this the particle count gets out of hand. */ + public static final float MAX_CRASH_SIZE = 12.0F; + /** The widest explosion flash, in blocks; past this it washes out the whole screen. */ + private static final float MAX_FLASH_RADIUS = 30.0F; + + private ExplosionEffect() { + } + + /** + * Not in the original: a crashing vehicle goes up in a far bigger show than its fuel explosion + * alone makes. The same shell of smoke as a missile hit in mid-air, a cluster of fireballs + * around it, a plume of smoke that keeps climbing for seconds and burning fuel thrown about. + * The fire glows for several seconds afterwards, which at night lights up the smoke above it. + */ + public static void crash(Level level, double x, double y, double z, float size) { + play(level, x, y, z, size, false); + RandomSource random = level.getRandom(); + level.addAlwaysVisibleParticle(GlowParticleOptions.flash(GlowParticleOptions.FLASH, Math.min(size * 3.0F, MAX_FLASH_RADIUS), 20), + true, x, y + size * 0.3, z, 0.0, 0.0, 0.0); + int fireballs = Math.max(3, (int) (size / 2.0F)); + for (int i = 0; i < fireballs; i++) { + double r = size * 0.5 * random.nextDouble(); + double a = random.nextDouble() * Math.PI * 2.0; + double fx = x + Math.cos(a) * r; + double fy = y + random.nextDouble() * size * 0.4; + double fz = z + Math.sin(a) * r; + level.addAlwaysVisibleParticle(ParticleTypes.EXPLOSION_EMITTER, true, fx, fy, fz, 1.0, 0.0, 0.0); + level.addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.FIRE, size * (0.4F + random.nextFloat() * 0.3F), + 80 + random.nextInt(60)), true, fx, fy, fz, 0.0, 0.01, 0.0); + } + for (int i = 0; i < size * 4; i++) { + level.addAlwaysVisibleParticle(ParticleTypes.CAMPFIRE_SIGNAL_SMOKE, true, + x + (random.nextDouble() - 0.5) * size * 0.6, y + random.nextDouble() * 2.0, z + (random.nextDouble() - 0.5) * size * 0.6, + (random.nextDouble() - 0.5) * 0.02, 0.05 + random.nextDouble() * 0.1, (random.nextDouble() - 0.5) * 0.02); + } + for (int i = 0; i < size * 10; i++) { + double a = random.nextDouble() * Math.PI * 2.0; + double speed = (0.2 + random.nextDouble() * 0.4) * size / 6.0; + double up = random.nextDouble() * speed; + level.addAlwaysVisibleParticle(random.nextInt(4) == 0 ? ParticleTypes.LAVA : ParticleTypes.FLAME, true, x, y + 0.5, z, + Math.cos(a) * speed, up, Math.sin(a) * speed); + if (i % 4 == 0) { + level.addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.EMBER, 0.5F + random.nextFloat() * 0.4F, + 20 + random.nextInt(20)), true, x, y + 0.5, z, Math.cos(a) * speed, up, Math.sin(a) * speed); + } + } + } + + /** + * Not in the original: the light of the blast itself, a white hot flash inside a wider red + * glow, and a few embers thrown out. Faint by day; at night it lights up the smoke. + */ + private static void flash(Level level, double x, double y, double z, float size) { + RandomSource random = level.getRandom(); + float radius = Math.min(2.0F + size * 1.5F, MAX_FLASH_RADIUS); + level.addAlwaysVisibleParticle(GlowParticleOptions.flash(GlowParticleOptions.FLASH, radius, 8 + (int) size), true, x, y, z, 0.0, 0.0, 0.0); + level.addAlwaysVisibleParticle(GlowParticleOptions.flash(GlowParticleOptions.EMBER, Math.min(radius * 1.6F, MAX_FLASH_RADIUS), + 14 + (int) (size * 2.0F)), true, x, y, z, 0.0, 0.0, 0.0); + int embers = Math.min((int) (size * 2.0F), 24); + for (int i = 0; i < embers; i++) { + double a = random.nextDouble() * Math.PI * 2.0; + double speed = 0.2 + random.nextDouble() * 0.3 * Math.min(size, 4.0F); + level.addAlwaysVisibleParticle(GlowParticleOptions.fire(GlowParticleOptions.EMBER, 0.3F + random.nextFloat() * 0.3F, + 15 + random.nextInt(15)), true, x, y, z, Math.cos(a) * speed, 0.1 + random.nextDouble() * speed, Math.sin(a) * speed); + } + } + + public static void play(Level level, double x, double y, double z, float size, boolean inWater) { + if (size <= 0.0F) { + return; + } + if (inWater) { + splash(level, x, y, z, size); + return; + } + RandomSource random = level.getRandom(); + level.addAlwaysVisibleParticle(size >= 2.0F ? ParticleTypes.EXPLOSION_EMITTER : ParticleTypes.EXPLOSION, true, x, y, z, 1.0, 0.0, 0.0); + flash(level, x, y, z, size); + + int count = 0; + int puffEvery = (int) Math.max(size, 4.0F); + for (BlockPos pos : affectedBlocks(level, x, y, z, size)) { + count++; + double bx = pos.getX() + random.nextFloat(); + double by = pos.getY() + random.nextFloat(); + double bz = pos.getZ() + random.nextFloat(); + double mx = bx - x; + double my = by - y; + double mz = bz - z; + double d = Math.sqrt(mx * mx + my * my + mz * mz); + if (d == 0.0) { + continue; + } + double push = 0.5 / (d / size + 0.1) * (random.nextFloat() * random.nextFloat() + 0.3F) * 0.5; + mx = mx / d * push; + my = my / d * push; + mz = mz / d * push; + double px = (bx + x) / 2.0; + double py = (by + y) / 2.0; + double pz = (bz + z) / 2.0; + double angle = Math.toRadians(random.nextInt(360)); + + if (count % 4 == 0) { + // Dirt and stone thrown up from the ground under the blast. + BlockState ground = level.getBlockState(BlockPos.containing(px + 0.5, py - 0.5, pz + 0.5)); + if (!ground.isAir()) { + float force = Math.min(size / 3.0F, 2.0F) * (0.5F + random.nextFloat() * 0.5F); + BlockParticleOption dust = new BlockParticleOption(ParticleTypes.BLOCK, ground); + for (int i = 0; i < 3; i++) { + level.addAlwaysVisibleParticle(dust, true, px, py + 1.0, pz, + Math.sin(angle) * force * 0.5, 0.5 + my / 5.0 * force, Math.cos(angle) * force * 0.5); + } + } + } + if (size <= 1.0F || count % puffEvery == 0) { + // Smoke either shoots up as a column or rolls out along the ground. + if (random.nextBoolean()) { + my *= 3.0; + mx *= 0.1; + mz *= 0.1; + } else { + my *= 0.2; + mx *= 3.0; + mz *= 3.0; + } + level.addAlwaysVisibleParticle(ParticleTypes.CAMPFIRE_COSY_SMOKE, true, px, py, pz, mx * 0.3, my * 0.3, mz * 0.3); + for (int i = 0; i < 2; i++) { + level.addAlwaysVisibleParticle(ParticleTypes.LARGE_SMOKE, true, px, py, pz, mx, my, mz); + } + if (random.nextInt(3) == 0) { + level.addAlwaysVisibleParticle(ParticleTypes.FLAME, true, px, py, pz, mx * 0.5, my * 0.5, mz * 0.5); + } + } + } + } + + /** + * The blocks the original's effect ray marched through: sixteen by sixteen rays from the + * centre, each running until block resistance used up its strength. + */ + @SuppressWarnings("deprecation") // The plain block resistance is what the original used here. + private static Set affectedBlocks(Level level, double x, double y, double z, float size) { + Set out = new HashSet<>(); + RandomSource random = level.getRandom(); + BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); + for (int i = 0; i < 16; i++) { + for (int j = 0; j < 16; j++) { + for (int k = 0; k < 16; k++) { + if (i != 0 && i != 15 && j != 0 && j != 15 && k != 0 && k != 15) { + continue; + } + double dx = i / 15.0F * 2.0F - 1.0F; + double dy = j / 15.0F * 2.0F - 1.0F; + double dz = k / 15.0F * 2.0F - 1.0F; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + dx /= len; + dy /= len; + dz /= len; + float strength = size * (0.7F + random.nextFloat() * 0.6F); + double cx = x; + double cy = y; + double cz = z; + for (float step = 0.3F; strength > 0.0F; strength -= step * 0.75F) { + cursor.set(Mth.floor(cx), Mth.floor(cy), Mth.floor(cz)); + BlockState state = level.getBlockState(cursor); + if (!state.isAir()) { + strength -= (state.getBlock().getExplosionResistance() + 0.3F) * step; + } + if (strength > 0.0F) { + out.add(cursor.immutable()); + } + cx += dx * step; + cy += dy * step; + cz += dz * step; + } + } + } + } + return out; + } + + /** {@code effectExplosionInWater}: a dome of spray from the water around the charge. */ + private static void splash(Level level, double x, double y, double z, float size) { + RandomSource random = level.getRandom(); + int range = Math.max((int) (size + 0.5), 1); + int ex = (int) (x + 0.5); + int ey = (int) (y + 0.5); + int ez = (int) (z + 0.5); + BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); + for (int dy = -range; dy <= range; dy++) { + for (int dx = -range; dx <= range; dx++) { + for (int dz = -range; dz <= range; dz++) { + if (dx * dx + dy * dy + dz * dz >= range * range) { + continue; + } + cursor.set(ex + dx, ey + dy, ez + dz); + if (!level.getFluidState(cursor).is(FluidTags.WATER)) { + continue; + } + for (int n = random.nextInt(2); n > 0; n--) { + double up = 1.0 - Math.sqrt(dx * dx + dz * dz) / range + random.nextFloat() * 0.4 * range * 0.4; + level.addAlwaysVisibleParticle(ParticleTypes.SPLASH, true, cursor.getX(), cursor.getY() + 1.0, cursor.getZ(), + (double) dx / range * (random.nextFloat() - 0.2), up, (double) dz / range * (random.nextFloat() - 0.2)); + level.addAlwaysVisibleParticle(ParticleTypes.BUBBLE, true, cursor.getX(), cursor.getY(), cursor.getZ(), 0.0, up * 0.5, 0.0); + } + } + } + } + } +} From 43f3ecbaf8314f8b89f8a2f7e8971c8cf28ef7f3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:03 +0800 Subject: [PATCH 257/314] Read explosion exposure the 1.21.1 way --- .../jnhprojects/mcheli/weapon/Explosions.java | 215 ++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java new file mode 100644 index 0000000..0ac55ea --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Explosions.java @@ -0,0 +1,215 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.ServerConfig; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.projectile.FlareEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.network.ExplosionEffectPacket; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.ExperienceOrb; +import net.minecraft.world.entity.item.FallingBlockEntity; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Explosion; +import net.minecraft.world.level.ExplosionDamageCalculator; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.Optional; + +/** + * Port of {@code MCH_Explosion}. + * + *

The original explosion had two sizes: one for blocks and one, doubled, for entities, with its + * own damage curve and the weapon's per-target {@code DamageFactor}. Entity damage is computed + * here as it was; the blocks, particles and sound go through a vanilla explosion of the block size, + * so mob griefing rules, protection mods and the client effects all behave as players expect. + */ +public final class Explosions { + /** Players this far away still see the smoke of an explosion. */ + private static final double EFFECT_RANGE = 256.0; + /** Share of the blast knockback an empty vehicle takes; they are far heavier than a mob. */ + private static final double VEHICLE_PUSH = 0.25; + + private Explosions() { + } + + /** What an explosion request needs besides its position and size. */ + public record Params(@Nullable Entity exploder, @Nullable Entity cause, float size, float blockSize, + boolean flaming, boolean destroyBlocks, int fireSeconds, boolean inWater, + @Nullable WeaponDefinition weapon) { + } + + /** + * @return whether any entity took damage, which the shooter hears as a hit confirmation + */ + public static boolean explode(ServerLevel level, double x, double y, double z, Params p) { + Vec3 center = new Vec3(x, y, z); + boolean hit = damageEntities(level, center, p); + + boolean breakBlocks = p.destroyBlocks && p.blockSize > 0.0F && ServerConfig.explosionsBreakBlocks(); + float radius = breakBlocks ? p.blockSize : Math.max(p.size, 0.5F); + level.explode(p.exploder, explosionSource(level, p), new Calculator(p.inWater, level), + x, y, z, radius, p.flaming && breakBlocks && ServerConfig.explosionsStartFires(), + breakBlocks ? Level.ExplosionInteraction.MOB : Level.ExplosionInteraction.NONE); + ModNetwork.sendNear(level, x, y, z, EFFECT_RANGE, new ExplosionEffectPacket(x, y, z, p.size, p.inWater)); + if (!p.inWater) { + throwDebris(level, x, y, z, p.size); + } + return hit; + } + + /** + * The look of a vehicle going up when it crashes: no damage or blocks of its own (the crash + * explosion does those), but the {@link ExplosionEffect#crash big fireball} on every nearby + * client and twice the burning debris. Drawn a little above the impact so its smoke spreads + * into the air the way a missile hit's does, rather than straight into the ground. + */ + public static void crashFireball(ServerLevel level, double x, double y, double z, float size) { + float visual = Math.clamp(size * 2.0F, 6.0F, ExplosionEffect.MAX_CRASH_SIZE); + double lift = visual * 0.25; + ModNetwork.sendNear(level, x, y + lift, z, EFFECT_RANGE, new ExplosionEffectPacket(x, y + lift, z, visual, false, true)); + throwDebris(level, x, y, z, visual, 24); + } + + private static void throwDebris(ServerLevel level, double x, double y, double z, float size) { + throwDebris(level, x, y, z, size, 12); + } + + /** + * Big blasts throw burning debris that arcs away trailing smoke, which the original made of + * flares. One piece per point of size, up to {@code maxPieces}. + */ + private static void throwDebris(ServerLevel level, double x, double y, double z, float size, int maxPieces) { + if (size < 4.0F) { + return; + } + int pieces = Math.min((int) size * maxPieces / 12, maxPieces); + for (int i = 0; i < pieces; i++) { + double angle = Math.toRadians(level.getRandom().nextInt(360)); + double speed = Math.min(size / 12.0F, 0.6) * (0.5F + level.getRandom().nextFloat() * 0.5F); + FlareEntity debris = ModEntities.FLARE.get().create(level); + if (debris != null) { + double spread = size / 2.0; + debris.launch(x + (level.getRandom().nextDouble() - 0.5) * spread, y + 2.0, z + (level.getRandom().nextDouble() - 0.5) * spread, + Math.sin(angle) * speed, (1.0 + level.getRandom().nextFloat() * 0.2) * speed, Math.cos(angle) * speed, 2.0F, 0, false); + level.addFreshEntity(debris); + } + } + } + + private static DamageSource explosionSource(ServerLevel level, Params p) { + return level.damageSources().explosion(p.exploder, p.cause); + } + + /** {@code doExplosionA} for entities: twice the block size, the 1.7.10 damage curve and knockback. */ + private static boolean damageEntities(ServerLevel level, Vec3 center, Params p) { + float size = p.size * 2.0F; + if (size <= 0.0F) { + return false; + } + AABB area = new AABB(center, center).inflate(size + 1.0); + DamageSource source = explosionSource(level, p); + boolean hit = false; + for (Entity entity : level.getEntities(p.exploder, area)) { + double d7 = Math.sqrt(entity.distanceToSqr(center)) / size; + if (d7 > 1.0) { + continue; + } + double dx = entity.getX() - center.x; + double dy = entity.getEyeY() - center.y; + double dz = entity.getZ() - center.z; + double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (dist == 0.0) { + continue; + } + dx /= dist; + dy /= dist; + dz /= dist; + double exposure = Explosion.getSeenPercent(center, entity); + double impact = (1.0 - d7) * exposure; + float damage = (int) ((impact * impact + impact) / 2.0 * 8.0 * size + 1.0); + if (damage > 0.0F && !(entity instanceof ItemEntity) && !(entity instanceof ExperienceOrb) + && !(entity instanceof FallingBlockEntity)) { + // Our own rounds caught in the blast do not count as hitting something. + if (!(entity instanceof ProjectileEntity bullet) || bullet.user() != p.cause) { + hit = true; + } + } + Combat.hurt(entity, source, damage, p.weapon); + if (entity instanceof AircraftEntity vehicle) { + // Not in the original: a vehicle someone is driving is simulated on their client, + // so a push here only snaps it back a step. An empty one still rocks a little. + if (vehicle.getControllingPassenger() == null) { + entity.push(dx * impact * VEHICLE_PUSH * 0.4, dy * impact * VEHICLE_PUSH * 0.1, dz * impact * VEHICLE_PUSH * 0.4); + } + } else if (!(entity instanceof ProjectileEntity)) { + entity.push(dx * impact * 0.4, dy * impact * 0.1, dz * impact * 0.4); + } + if (damage > 0.0F && p.fireSeconds > 0) { + double fire = 1.0 - dist / size; + if (fire > 0.0) { + entity.igniteForSeconds((float) (fire * p.fireSeconds)); + } + } + } + return hit; + } + + /** + * Vanilla explosion settings for the block pass: no entity damage or knockback (done above), + * and under water the blocks crumble the way the original's water charges made them. + */ + private static final class Calculator extends ExplosionDamageCalculator { + private final boolean inWater; + private final Level level; + + Calculator(boolean inWater, Level level) { + this.inWater = inWater; + this.level = level; + } + + // Vanilla's own calculator reads the block's resistance the same deprecated way. + @SuppressWarnings("deprecation") + @Override + public Optional getBlockExplosionResistance(Explosion explosion, BlockGetter getter, BlockPos pos, + BlockState block, FluidState fluid) { + if (!inWater) { + return super.getBlockExplosionResistance(explosion, getter, pos, block, fluid); + } + if (block.isAir()) { + return Optional.empty(); + } + float resistance = block.getBlock().getExplosionResistance(); + return Optional.of(resistance * (level.getRandom().nextFloat() * 0.2F + 0.2F)); + } + + @Override + public boolean shouldDamageEntity(Explosion explosion, Entity entity) { + return false; + } + + @Override + public float getKnockbackMultiplier(Entity entity) { + return 0.0F; + } + } + + /** {@code playExplosionSound}: the bang of a round that has no explosive charge. */ + public static void playSound(Level level, double x, double y, double z) { + float pitch = (1.0F + (level.getRandom().nextFloat() - level.getRandom().nextFloat()) * 0.2F) * 0.7F; + level.playSound(null, x, y, z, SoundEvents.GENERIC_EXPLODE.value(), SoundSource.BLOCKS, 4.0F, pitch); + } +} From 938fe7c25c0156b8e8b5bb3082c286e0118f2f4f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:03 +0800 Subject: [PATCH 258/314] Create flares the 1.21.1 way --- .../mcheli/weapon/FlareDispenser.java | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java new file mode 100644 index 0000000..ff583c8 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/FlareDispenser.java @@ -0,0 +1,194 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.projectile.FlareEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.block.LevelEvent; +import net.minecraft.world.phys.Vec3; + +/** + * Flares and smoke dischargers, ported from {@code MCH_Flare} and the flare half of + * {@code MCH_MissileDetector}. While a salvo burns, every missile homing on the vehicle within 60 + * blocks is decoyed. + * + *

The ten {@code FlareType} values pick a pattern: 1 random scatter, 2 and 3 to the sides and + * rear, 4 and 5 trailing, 10 a ring of smoke grenades. Unknown types fall back to type 1. + */ +public final class FlareDispenser { + private static final int DECOY_RANGE = 60; + + /** {@code num, interval, tickWait, tickEnable, numFlareMax} per type. */ + private static final int[][] TYPES = new int[11][]; + + static { + TYPES[1] = new int[]{1, 3, 200, 100, 16}; + TYPES[2] = new int[]{3, 5, 300, 200, 16}; + TYPES[3] = new int[]{2, 3, 200, 100, 16}; + TYPES[4] = new int[]{1, 3, 200, 100, 16}; + TYPES[5] = new int[]{2, 3, 200, 100, 16}; + TYPES[10] = new int[]{8, 1, 250, 60, 1}; + for (int i : new int[]{0, 6, 7, 8, 9}) { + TYPES[i] = TYPES[1]; + } + } + + private final AircraftEntity vehicle; + private final AircraftDefinition def; + private final RandomSource random = RandomSource.create(); + private int tick; + private int fired; + private int type; + private int typeIndex; + + FlareDispenser(AircraftEntity vehicle, AircraftDefinition def) { + this.vehicle = vehicle; + this.def = def; + } + + public boolean isFitted() { + return !def.flareTypes.isEmpty(); + } + + /** Waiting out the reload of the last salvo. */ + public boolean isPreparing() { + return tick != 0; + } + + /** The salvo is still burning, so incoming missiles are being decoyed. */ + public boolean isActive() { + int[] p = params(type); + return tick != 0 && tick > p[2] - p[3]; + } + + public int ticksLeft() { + return tick; + } + + public int cooldown() { + return params(type)[2]; + } + + public int currentType() { + return def.flareTypes.isEmpty() ? 0 : def.flareTypes.get(typeIndex % def.flareTypes.size()); + } + + public void nextType() { + if (!def.flareTypes.isEmpty()) { + typeIndex = (typeIndex + 1) % def.flareTypes.size(); + } + } + + /** {@code useFlare}: starts a salvo of the current type if the last one has finished. */ + public boolean use() { + if (!isFitted() || tick != 0 || vehicle.isDestroyed()) { + return false; + } + type = currentType(); + fired = 0; + // The client waited 50 ticks longer before showing the dispenser as ready again. + tick = params(type)[2] + (vehicle.level().isClientSide() ? 50 : 0); + return true; + } + + private static int[] params(int type) { + return type >= 0 && type < TYPES.length && TYPES[type] != null ? TYPES[type] : TYPES[1]; + } + + void tick() { + if (type <= 0 || vehicle.isRemoved()) { + return; + } + if (tick > 0) { + tick--; + } + int[] p = params(type); + if (!vehicle.level().isClientSide()) { + if (tick > 0 && tick % p[1] == 0 && fired < p[4]) { + Vec3 at = vehicle.toWorld(def.flarePosition); + spawnSalvo(new Vec3(at.x + vehicle.xo - vehicle.getX(), at.y + vehicle.yo - vehicle.getY(), at.z + vehicle.zo - vehicle.getZ())); + } + if (isActive()) { + decoyMissiles(); + } + } + } + + private void spawnSalvo(Vec3 at) { + fired++; + int[] p = params(type); + Vec3 m = vehicle.getDeltaMovement(); + double x = at.x - m.x * 2.0; + double y = at.y - m.y * 2.0 - 1.0; + double z = at.z - m.z * 2.0; + vehicle.level().levelEvent(LevelEvent.SOUND_DISPENSER_PROJECTILE_LAUNCH, net.minecraft.core.BlockPos.containing(x, y, z), 0); + for (int i = 0; i < p[0]; i++) { + double tx = 0.0; + double ty = m.y; + double tz = 0.0; + int fuse = 0; + double r = vehicle.getYRot(); + double fy = y; + switch (type) { + case 1 -> { + tx = Mth.sin(random.nextFloat() * 360.0F); + tz = Mth.cos(random.nextFloat() * 360.0F); + } + case 2, 3 -> { + if (i == 0) { + r += 90.0; + } else if (i == 1) { + r -= 90.0; + } else if (i == 2) { + r += 180.0; + } + r = Math.toRadians(r); + tx = -Math.sin(r) + (random.nextFloat() - 0.5) * 0.6; + tz = Math.cos(r) + (random.nextFloat() - 0.5) * 0.6; + } + case 4 -> { + r = Math.toRadians(r); + tx = -Math.sin(r) + (random.nextFloat() - 0.5) * 1.3; + tz = Math.cos(r) + (random.nextFloat() - 0.5) * 1.3; + } + case 5 -> { + r = Math.toRadians(r); + tx = (-Math.sin(r) + (random.nextFloat() - 0.5) * 0.9) * 0.3; + tz = (Math.cos(r) + (random.nextFloat() - 0.5) * 0.9) * 0.3; + } + default -> { + } + } + tx += m.x; + ty += m.y / 2.0; + tz += m.z; + if (type == 10) { + r += 360.0 / p[0] / 2.0 + i * (360.0 / p[0]); + r = Math.toRadians(r); + tx = -Math.sin(r) * 2.0; + tz = Math.cos(r) * 2.0; + ty = 0.7; + fy += 2.0; + fuse = 10; + } + FlareEntity flare = ModEntities.FLARE.get().create(vehicle.level()); + if (flare != null) { + flare.launch(x, fy, z, tx * 0.5, ty * 0.5, tz * 0.5, 6.0F, fuse, type == 4); + vehicle.level().addFreshEntity(flare); + } + } + } + + /** {@code destroyMissile}: missiles homing on this vehicle or its crew lose their target. */ + private void decoyMissiles() { + for (ProjectileEntity missile : vehicle.level().getEntitiesOfClass(ProjectileEntity.class, + vehicle.getBoundingBox().inflate(DECOY_RANGE), p -> p.target() != null)) { + if (missile.target() == vehicle || vehicle.hasPassenger(missile.target())) { + missile.decoy(); + } + } + } +} From 305576c358c4007cb04de0f7a9da95a825bde96c Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:04 +0800 Subject: [PATCH 259/314] Add Guidance to the shared 1.21.1 sources --- .../jnhprojects/mcheli/weapon/Guidance.java | 220 ++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java new file mode 100644 index 0000000..b8142e9 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Guidance.java @@ -0,0 +1,220 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.function.Predicate; + +/** + * Lock-on for seeking missiles, ported from {@code MCH_WeaponGuidanceSystem}. It runs on the + * client of the crew member aiming; the locked entity travels to the server with the fire request. + * + *

A target is acquired inside a cone around the aim, then the lock count climbs while it stays + * in the cone. Stealth shrinks both the range and the cone and stretches the time to lock. + */ +public final class Guidance { + public final boolean canLockInAir; + public final boolean canLockOnGround; + public final boolean canLockInWater = false; + public final boolean ridableOnly; + public final double lockRange = 200.0; + public final int lockAngle = 5; + private final int lockCountMax; + private final @Nullable Predicate filter; + + private @Nullable Entity target; + private @Nullable Entity lastLocked; + private int lockCount; + private int prevLockCount; + private int continueLockCount; + private int soundCount; + + public Guidance(boolean air, boolean ground, boolean ridableOnly, int lockTime, @Nullable Predicate filter) { + this.canLockInAir = air; + this.canLockOnGround = ground; + this.ridableOnly = ridableOnly; + this.lockCountMax = Math.max(lockTime, 1); + this.filter = filter; + } + + public int lockCount() { + return lockCount; + } + + public int lockCountMax() { + float stealth = stealth(target); + return (int) (lockCountMax + lockCountMax * stealth); + } + + public @Nullable Entity target() { + return lockCount > 0 && target != null && !target.isRemoved() ? target : null; + } + + public @Nullable Entity lastLocked() { + return lastLocked; + } + + public boolean isLocked() { + return lockCount == lockCountMax() && lastLocked != null; + } + + /** Called every client tick: a lock decays unless {@link #lock} kept it alive this tick. */ + public void tick() { + if (lockCount != prevLockCount) { + prevLockCount = lockCount; + } else { + lockCount = 0; + prevLockCount = 0; + } + } + + /** + * Advances the lock for one tick of aiming from {@code user}'s view. + * + * @param eye where the view is seen from. The original used the player's position, which on a + * 1.7.10 client was eye height; gunner sights and launcher cameras sit higher still, + * and a 5 degree cone measured from the wrong point misses targets close by. + * @return true once the lock is complete; {@link #lastLocked()} is the target to send + */ + public boolean lock(Entity user, Vec3 eye) { + if (lockCount == 0) { + acquire(user, eye); + return false; + } + if (target == null || target.isRemoved() || !inLockableState(target)) { + clear(); + return false; + } + double range = lockRange * (1.0F - stealth(target)); + if (user.distanceToSqr(target) >= range * range) { + clear(); + return false; + } + soundCount = (soundCount + 1) % 15; + if (inCone(eye, user.getYRot(), user.getXRot(), target, lockAngle)) { + if (lockCount < lockCountMax()) { + lockCount++; + } + } else if (continueLockCount > 0) { + continueLockCount--; + if (continueLockCount <= 0 && lockCount > 0) { + lockCount--; + } + } else { + continueLockCount = 0; + lockCount--; + } + if (lockCount >= lockCountMax()) { + if (continueLockCount <= 0) { + continueLockCount = Math.min(lockCountMax() / 3, 20); + } + lastLocked = target; + prevLockCount = lockCount - 1; + return true; + } + return false; + } + + /** True on the tick the target should hear that it is being tracked. */ + public boolean shouldNotifyTarget() { + return lockCount > 0 && soundCount == 1; + } + + private void acquire(Entity user, Vec3 eye) { + Entity best = null; + double bestDistance = lockRange * lockRange * 2.0; + for (Entity entity : user.level().getEntities(user, user.getBoundingBox().inflate(lockRange), this::canLock)) { + double d = entity.distanceToSqr(user); + float stealth = 1.0F - stealth(entity); + double range = lockRange * stealth; + float angle = lockAngle * (stealth / 2.0F + 0.5F); + if (d < range * range && d < bestDistance && inCone(eye, user.getYRot(), user.getXRot(), entity, angle)) { + Vec3 to = new Vec3(entity.getX(), entity.getY() + entity.getBbHeight() / 2.0F, entity.getZ()); + HitResult hit = user.level().clip(new ClipContext(eye, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, user)); + if (hit.getType() == HitResult.Type.MISS) { + best = entity; + bestDistance = d; + } + } + } + target = best; + if (best != null) { + lockCount++; + } + } + + public void clear() { + target = null; + lockCount = 0; + continueLockCount = 0; + soundCount = 0; + } + + private boolean canLock(Entity entity) { + if (ridableOnly && entity instanceof Player && entity.getVehicle() == null) { + return false; + } + if (!(entity instanceof LivingEntity) && !(entity instanceof AircraftEntity)) { + return false; + } + if (filter != null && !filter.test(entity)) { + return false; + } + return inLockableState(entity); + } + + private boolean inLockableState(Entity entity) { + if (!canLockInWater && entity.isInWater()) { + return false; + } + boolean onGround = isOnGround(entity); + return onGround ? canLockOnGround : canLockInAir; + } + + /** On the ground, or less than twelve blocks above something solid. */ + public static boolean isOnGround(Entity entity) { + if (entity.onGround()) { + return true; + } + BlockPos.MutableBlockPos pos = BlockPos.containing(entity.getX() + 0.5, entity.getY() + 0.5, entity.getZ() + 0.5).mutable(); + for (int i = 0; i < 12; i++) { + if (!entity.level().getBlockState(pos).isAir() && !entity.level().getFluidState(pos).is(FluidTags.WATER)) { + return true; + } + pos.move(0, -1, 0); + } + return false; + } + + public static float stealth(@Nullable Entity entity) { + if (entity instanceof AircraftEntity vehicle) { + return vehicle.stealth(); + } + return entity != null && entity.getVehicle() instanceof AircraftEntity vehicle ? vehicle.stealth() : 0.0F; + } + + /** {@code inLockRange}: whether {@code target} is within {@code angle} degrees of the aim. */ + public static boolean inCone(Entity from, float yaw, float pitch, Entity target, float angle) { + return inCone(from.position(), yaw, pitch, target, angle); + } + + public static boolean inCone(Vec3 from, float yaw, float pitch, Entity target, float angle) { + double dx = target.getX() - from.x; + double dy = target.getY() + target.getBbHeight() / 2.0F - from.y; + double dz = target.getZ() - from.z; + float targetYaw = (float) Math.toDegrees(Math.atan2(dz, dx)); + float diffYaw = Mth.positiveModulo(targetYaw - yaw - 90.0F, 360.0F); + float targetPitch = -(float) Math.toDegrees(Math.atan2(dy, Math.sqrt(dx * dx + dz * dz))); + float diffPitch = targetPitch - pitch; + return (diffYaw < angle || diffYaw > 360.0F - angle) && Math.abs(diffPitch) < angle; + } +} From fb8198f2034443a7fbc52864a68ce7defb5de7d4 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:04 +0800 Subject: [PATCH 260/314] Look up items the 1.21.1 way --- .../mcheli/weapon/LegacyItems.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java new file mode 100644 index 0000000..fe7b760 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/LegacyItems.java @@ -0,0 +1,41 @@ +package com.jnhprojects.mcheli.weapon; + +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; +import org.jspecify.annotations.Nullable; + +/** + * Maps the 1.7.10 item names and damage values in the content files to today's items. Names + * that still exist resolve directly; the few that were renamed or flattened are listed here. + */ +public final class LegacyItems { + private static final String[] DYES = { + "ink_sac", "red_dye", "green_dye", "cocoa_beans", "lapis_lazuli", "purple_dye", "cyan_dye", "light_gray_dye", + "gray_dye", "pink_dye", "lime_dye", "yellow_dye", "light_blue_dye", "magenta_dye", "orange_dye", "bone_meal"}; + + private LegacyItems() { + } + + public static @Nullable Item item(String name, int damage) { + if (name.isEmpty()) { + return null; + } + String id = switch (name) { + case "dye" -> DYES[Math.floorMod(damage, DYES.length)]; + case "boat" -> "oak_boat"; + case "reeds" -> "sugar_cane"; + case "wool" -> "white_wool"; + case "log" -> "oak_log"; + case "planks" -> "oak_planks"; + default -> name.startsWith("minecraft:") ? name.substring("minecraft:".length()) : name; + }; + ResourceLocation key = ResourceLocation.tryParse(id.contains(":") ? id : "minecraft:" + id); + if (key == null) { + return null; + } + Item item = BuiltInRegistries.ITEM.get(key); + return item == Items.AIR ? null : item; + } +} From b267c83bc6057ebfe56e9ce101f90abdec6100cc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:04 +0800 Subject: [PATCH 261/314] Add ModDamageTypes to the shared 1.21.1 sources --- .../mcheli/weapon/ModDamageTypes.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java new file mode 100644 index 0000000..73d53c7 --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/ModDamageTypes.java @@ -0,0 +1,28 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.MCHeli; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.damagesource.DamageType; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import org.jspecify.annotations.Nullable; + +/** + * Damage types defined in {@code data/mcheli/damage_type}. Bullets and missiles use their own type + * so death messages name the gun instead of calling it a thrown snowball, which is what the + * original's {@code causeThrownDamage} amounted to. + */ +public final class ModDamageTypes { + public static final ResourceKey BULLET = + ResourceKey.create(Registries.DAMAGE_TYPE, ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "bullet")); + + private ModDamageTypes() { + } + + public static DamageSource bullet(Level level, Entity projectile, @Nullable Entity owner) { + return new DamageSource(level.registryAccess().lookupOrThrow(Registries.DAMAGE_TYPE).getOrThrow(BULLET), projectile, owner); + } +} From 158d7ce7850bd0ea6e8f4ea437708cf5c53c0072 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:05 +0800 Subject: [PATCH 262/314] Add Resupply to the shared 1.21.1 sources --- .../jnhprojects/mcheli/weapon/Resupply.java | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java new file mode 100644 index 0000000..44df1ac --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Resupply.java @@ -0,0 +1,77 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.content.weapon.RoundItem; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; + +/** + * Resupplying a weapon from the crew member's inventory, the {@code supplyAmmo} path of the + * original. There the vehicle's inventory screen did it; until that screen exists a key does. + * One supply costs the weapon's {@code Item} lines and adds {@code SuppliedNum} rounds. + */ +public final class Resupply { + private Resupply() { + } + + public static boolean fromInventory(ServerPlayer player, AircraftEntity vehicle) { + VehicleWeapons weapons = vehicle.weapons(); + int seat = vehicle.seatOf(player); + WeaponSet set = weapons != null ? weapons.current(seat) : null; + if (set == null || vehicle.isDestroyed()) { + return false; + } + WeaponDefinition w = set.definition(); + if (set.ammo() + set.restAmmo() >= set.maxAmmo()) { + return false; + } + if (!player.getAbilities().instabuild) { + if (!canPay(player.getInventory(), w)) { + return false; + } + pay(player.getInventory(), w); + } + set.supply(); + if (set.ammo() <= 0) { + set.reloadMagazine(); + } + return true; + } + + private static boolean canPay(Inventory inventory, WeaponDefinition w) { + for (RoundItem cost : w.roundItems) { + Item item = LegacyItems.item(cost.item(), cost.damage()); + if (item == null || inventory.countItem(item) < cost.count()) { + return false; + } + } + return true; + } + + private static void pay(Inventory inventory, WeaponDefinition w) { + for (RoundItem cost : w.roundItems) { + Item item = LegacyItems.item(cost.item(), cost.damage()); + int left = cost.count(); + for (int i = 0; i < inventory.getContainerSize() && left > 0; i++) { + ItemStack stack = inventory.getItem(i); + if (!stack.is(item)) { + continue; + } + if (item == Items.WATER_BUCKET || item == Items.LAVA_BUCKET) { + // Buckets give their bucket back, one at a time. + inventory.setItem(i, new ItemStack(Items.BUCKET)); + left--; + } else { + int take = Math.min(left, stack.getCount()); + stack.shrink(take); + left -= take; + } + } + } + inventory.setChanged(); + } +} From c4cfa70ca0c089452751714a5d4c01c4469c3d2b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:05 +0800 Subject: [PATCH 263/314] Create projectiles the 1.21.1 way --- .../mcheli/weapon/ServerFiring.java | 456 ++++++++++++++++++ 1 file changed, 456 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java new file mode 100644 index 0000000..5a536ba --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/ServerFiring.java @@ -0,0 +1,456 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.ModSounds; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.entity.GroundVehicleEntity; +import com.jnhprojects.mcheli.entity.HelicopterEntity; +import com.jnhprojects.mcheli.entity.PlaneEntity; +import com.jnhprojects.mcheli.entity.TankEntity; +import com.jnhprojects.mcheli.entity.projectile.A10Entity; +import com.jnhprojects.mcheli.entity.projectile.AAMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ASMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.ATMissileEntity; +import com.jnhprojects.mcheli.entity.projectile.BombEntity; +import com.jnhprojects.mcheli.entity.projectile.BulletEntity; +import com.jnhprojects.mcheli.entity.projectile.DispensedItemEntity; +import com.jnhprojects.mcheli.entity.projectile.MarkerRocketEntity; +import com.jnhprojects.mcheli.entity.projectile.ProjectileEntity; +import com.jnhprojects.mcheli.entity.projectile.RocketEntity; +import com.jnhprojects.mcheli.entity.projectile.TorpedoEntity; +import com.jnhprojects.mcheli.entity.projectile.TvMissileEntity; +import com.jnhprojects.mcheli.registry.ModEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.effect.MobEffects; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.monster.Enemy; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +/** + * What each weapon type does when the server fires it: the {@code shot} methods of the + * {@code MCH_Weapon*} classes. Every method returns whether a round was used. + */ +public final class ServerFiring implements WeaponSet.Trigger { + public static final ServerFiring INSTANCE = new ServerFiring(); + private static final RandomSource RANDOM = RandomSource.create(); + + private ServerFiring() { + } + + @Override + public boolean fire(WeaponSet set, int mount, Shot shot) { + if (!(shot.shooter.level() instanceof ServerLevel level)) { + return false; + } + WeaponDefinition w = set.definition(); + return switch (w.type) { + case MACHINE_GUN_1, MACHINE_GUN_2 -> gun(level, set, shot); + case ROCKET -> rocket(level, set, shot); + case BOMB -> bomb(level, set, shot); + case AA_MISSILE -> aaMissile(level, set, mount, shot); + case AS_MISSILE -> asMissile(level, set, shot); + case AT_MISSILE -> atMissile(level, set, mount, shot); + case TV_MISSILE -> tvMissile(level, set, mount, shot); + case TORPEDO -> torpedo(level, set, shot); + case CAS -> cas(level, set, shot); + case MARKER_ROCKET -> markerRocket(level, set, shot); + case DISPENSER -> dispenser(level, set, shot); + case TARGETING_POD -> targetingPod(level, set, shot); + case DUMMY, SMOKE -> false; + case null -> false; + }; + } + + private static @Nullable T spawn(ServerLevel level, EntityType type, WeaponSet set, Shot shot, + Vec3 dir, float yaw, float pitch, double acceleration) { + T e = type.create(level); + if (e == null) { + return null; + } + e.launch(shot.x, shot.y, shot.z, dir.x, dir.y, dir.z, yaw, pitch, acceleration); + e.setWeapon(set.name()); + e.setParameters(set.definition(), shot.shooter, shot.user); + return e; + } + + private static Vec3 forward(float yaw, float pitch) { + return Vec3.directionFromRotation(pitch, yaw); + } + + private static void sound(WeaponSet set, Entity at) { + sound(set, at, set.definition().sound); + } + + private static void sound(WeaponSet set, Entity at, String name) { + if (set.soundAllowed) { + WeaponDefinition w = set.definition(); + float rnd = w.soundPitchRandom; + ModSounds.playAt(at, name, w.soundVolume, w.soundPitch * (1.0F - rnd) + RANDOM.nextFloat() * rnd); + } + } + + // Guns, rockets, bombs + + private static boolean gun(ServerLevel level, WeaponSet set, Shot shot) { + WeaponDefinition w = set.definition(); + BulletEntity e = spawn(level, ModEntities.BULLET.get(), set, shot, forward(shot.yaw, shot.pitch), shot.yaw, shot.pitch, w.acceleration); + if (e == null) { + return false; + } + if (w.type == WeaponType.MACHINE_GUN_2 && w.modeNum >= 2) { + // Mode 0 fires ball ammunition, which only bangs; mode 1 is high explosive. + e.explosionPower = shot.option1 == 0 ? -w.explosion : w.explosion; + } + Vec3 m = e.getDeltaMovement(); + e.setPos(e.getX() + m.x * 0.5, e.getY() + m.y * 0.5, e.getZ() + m.z * 0.5); + level.addFreshEntity(e); + sound(set, shot.shooter); + return true; + } + + private static boolean rocket(ServerLevel level, WeaponSet set, Shot shot) { + sound(set, shot.shooter); + RocketEntity e = spawn(level, ModEntities.ROCKET.get(), set, shot, forward(shot.yaw, shot.pitch), shot.yaw, shot.pitch, + set.definition().acceleration); + if (e == null) { + return false; + } + if (shot.option1 == 0 && set.definition().modeNum > 1) { + e.piercing = 0; + } + level.addFreshEntity(e); + return true; + } + + private static boolean bomb(ServerLevel level, WeaponSet set, Shot shot) { + WeaponDefinition w = set.definition(); + if (w.destruct) { + // A drone's self destruct charge. + if (shot.shooter instanceof HelicopterEntity drone && drone.definition() != null && drone.definition().uav) { + Explosions.explode(level, drone.getX(), drone.getY(), drone.getZ(), + new Explosions.Params(null, shot.user, w.explosion, w.explosionBlock, w.flaming, true, 0, false, null)); + sound(set, shot.shooter); + drone.ejectPassengers(); + drone.discard(); + } + return true; + } + sound(set, shot.shooter); + Vec3 motion = shot.shooter.getDeltaMovement(); + BombEntity e = spawn(level, ModEntities.BOMB.get(), set, shot, motion, shot.shooter.getYRot(), 0.0F, w.acceleration); + if (e == null) { + return false; + } + e.setDeltaMovement(motion); + level.addFreshEntity(e); + return true; + } + + // Missiles + + private static float fixedYaw(WeaponSet set, int mount) { + return set.mounts().get(mount).yaw(); + } + + private static float fixedPitch(WeaponSet set, int mount) { + return set.mounts().get(mount).pitch(); + } + + private static boolean aaMissile(ServerLevel level, WeaponSet set, int mount, Shot shot) { + Entity target = level.getEntity(shot.option1); + if (target == null || target.isRemoved()) { + return false; + } + sound(set, shot.shooter); + float yaw = shot.shooter.getYRot() + fixedYaw(set, mount); + float pitch = shot.shooter.getXRot() + fixedPitch(set, mount); + AAMissileEntity e = spawn(level, ModEntities.AA_MISSILE.get(), set, shot, forward(yaw, pitch), yaw, pitch, set.definition().acceleration); + if (e == null) { + return false; + } + e.setTarget(target); + level.addFreshEntity(e); + return true; + } + + /** Ray from the shooter along the aimer's view, as the original's AS missile and CAS sights. */ + private static @Nullable BlockHitResult sight(ServerLevel level, Shot shot, double eyeHeight, double range) { + Entity aimer = shot.aimer(); + Vec3 from = shot.shooter.position().add(0.0, eyeHeight, 0.0); + Vec3 to = from.add(forward(aimer.getYRot(), aimer.getXRot()).scale(range)); + BlockHitResult hit = level.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, shot.shooter)); + return hit.getType() == HitResult.Type.BLOCK ? hit : null; + } + + private static boolean nextToWater(ServerLevel level, BlockPos pos) { + if (level.getFluidState(pos).is(FluidTags.WATER)) { + return true; + } + for (Direction d : Direction.values()) { + if (level.getFluidState(pos.relative(d)).is(FluidTags.WATER)) { + return true; + } + } + return false; + } + + private static boolean asMissile(ServerLevel level, WeaponSet set, Shot shot) { + BlockHitResult hit = sight(level, shot, 1.62, 250.0); + if (hit == null || nextToWater(level, hit.getBlockPos())) { + return false; + } + Entity aimer = shot.aimer(); + ASMissileEntity e = spawn(level, ModEntities.AS_MISSILE.get(), set, shot, forward(aimer.getYRot(), aimer.getXRot()), + aimer.getYRot(), aimer.getXRot(), set.definition().acceleration); + if (e == null) { + return false; + } + e.targetX = hit.getLocation().x; + e.targetY = hit.getLocation().y; + e.targetZ = hit.getLocation().z; + level.addFreshEntity(e); + sound(set, shot.shooter); + return true; + } + + private static boolean atMissile(ServerLevel level, WeaponSet set, int mount, Shot shot) { + Entity target = level.getEntity(shot.option1); + if (target == null || target.isRemoved()) { + return false; + } + float yaw = shot.aimer().getYRot() + fixedYaw(set, mount); + float pitch = shot.shooter.getXRot() + fixedPitch(set, mount); + ATMissileEntity e = spawn(level, ModEntities.AT_MISSILE.get(), set, shot, forward(yaw, pitch), yaw, pitch, set.definition().acceleration); + if (e == null) { + return false; + } + e.setTarget(target); + e.guidanceType = shot.option2; + level.addFreshEntity(e); + sound(set, shot.shooter); + return true; + } + + private static boolean tvMissile(ServerLevel level, WeaponSet set, int mount, Shot shot) { + float yaw = shot.aimer().getYRot() + fixedYaw(set, mount); + float pitch = shot.aimer().getXRot() + fixedPitch(set, mount); + boolean tvGuided = shot.option1 == 0; + float acceleration = set.definition().acceleration * (tvGuided ? 1.0F : 1.5F); + TvMissileEntity e = spawn(level, ModEntities.TV_MISSILE.get(), set, shot, forward(yaw, pitch), yaw, pitch, acceleration); + if (e == null) { + return false; + } + level.addFreshEntity(e); + if (shot.shooter instanceof AircraftEntity vehicle) { + vehicle.setTvMissile(e); + } + sound(set, shot.shooter); + return true; + } + + private static boolean torpedo(ServerLevel level, WeaponSet set, Shot shot) { + WeaponDefinition w = set.definition(); + Vec3 vehicleMotion = shot.shooter.getDeltaMovement(); + float yaw = shot.yaw; + float pitch = shot.pitch; + Vec3 target = null; + if (w.guidedTorpedo) { + Entity aimer = shot.aimer(); + yaw = aimer.getYRot(); + pitch = aimer.getXRot(); + Vec3 from = aimer.position(); + Vec3 to = from.add(forward(yaw, pitch).scale(150.0)); + BlockHitResult hit = level.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, shot.shooter)); + if (hit.getType() != HitResult.Type.BLOCK || !nextToWater(level, hit.getBlockPos())) { + return false; + } + target = hit.getLocation(); + } + Vec3 m = forward(yaw, pitch).scale(w.acceleration).add(vehicleMotion); + double speed = m.length(); + TorpedoEntity e = spawn(level, ModEntities.TORPEDO.get(), set, shot, m, yaw, 0.0F, speed); + if (e == null) { + return false; + } + e.setDeltaMovement(m); + e.accelerationInWater = w.accelerationInWater; + if (target != null) { + e.targetX = target.x; + e.targetY = target.y; + e.targetZ = target.z; + } + level.addFreshEntity(e); + sound(set, shot.shooter); + return true; + } + + // Support weapons + + private static boolean markerRocket(ServerLevel level, WeaponSet set, Shot shot) { + sound(set, shot.shooter); + MarkerRocketEntity e = spawn(level, ModEntities.MARKER_ROCKET.get(), set, shot, forward(shot.yaw, shot.pitch), shot.yaw, shot.pitch, + set.definition().acceleration); + if (e == null) { + return false; + } + e.setStatus(1); + level.addFreshEntity(e); + return true; + } + + private static boolean dispenser(ServerLevel level, WeaponSet set, Shot shot) { + sound(set, shot.shooter); + DispensedItemEntity e = spawn(level, ModEntities.DISPENSED_ITEM.get(), set, shot, forward(shot.yaw, shot.pitch), shot.yaw, shot.pitch, + set.definition().acceleration); + if (e == null) { + return false; + } + Vec3 m = shot.shooter.getDeltaMovement().add(e.getDeltaMovement().scale(0.5)); + e.setDeltaMovement(m); + e.setPos(e.getX() + m.x * 0.5, e.getY() + m.y * 0.5, e.getZ() + m.z * 0.5); + level.addFreshEntity(e); + return true; + } + + /** + * Spots everything of the weapon's {@code Target} kinds inside its cone, which then glows for + * {@code MarkTime}. Marking a block for the team needs the team manager of a later phase. + */ + private static boolean targetingPod(ServerLevel level, WeaponSet set, Shot shot) { + WeaponDefinition w = set.definition(); + Entity aimer = shot.aimer(); + if ((w.target & WeaponDefinition.TARGET_BLOCK) != 0) { + sound(set, aimer, "ng"); + return true; + } + boolean spotted = false; + double range = Math.max(w.length, 1.0F); + for (LivingEntity entity : level.getEntitiesOfClass(LivingEntity.class, aimer.getBoundingBox().inflate(range))) { + if (entity == aimer || shot.shooter.hasPassenger(entity) || aimer.distanceToSqr(entity) > range * range + || !matchesTarget(entity, w.target) || !Guidance.inCone(aimer, aimer.getYRot(), aimer.getXRot(), entity, w.angle)) { + continue; + } + entity.addEffect(new MobEffectInstance(MobEffects.GLOWING, Math.max(w.markTime, 20))); + spotted = true; + } + sound(set, shot.shooter, spotted ? w.sound : "ng"); + return true; + } + + private static boolean matchesTarget(LivingEntity entity, int bits) { + Entity vehicle = entity.getVehicle(); + if (vehicle instanceof PlaneEntity) { + return (bits & WeaponDefinition.TARGET_PLANES) != 0; + } + if (vehicle instanceof HelicopterEntity) { + return (bits & WeaponDefinition.TARGET_HELICOPTERS) != 0; + } + if (vehicle instanceof TankEntity || vehicle instanceof GroundVehicleEntity) { + return (bits & WeaponDefinition.TARGET_GROUND_VEHICLES) != 0; + } + if (entity instanceof Player) { + return (bits & WeaponDefinition.TARGET_PLAYERS) != 0; + } + if (entity instanceof Enemy) { + return (bits & WeaponDefinition.TARGET_MONSTERS) != 0; + } + return (bits & WeaponDefinition.TARGET_OTHERS) != 0; + } + + // Close air support + + private static boolean cas(ServerLevel level, WeaponSet set, Shot shot) { + BlockHitResult hit = sight(level, shot, 2.0, 150.0); + if (hit == null) { + return false; + } + Vec3 at = hit.getLocation(); + set.casX = at.x; + set.casY = at.y; + set.casZ = at.z; + int direction = (int) Mth.positiveModulo(shot.aimer().getYRot() + 45.0F, 360.0F) / 90; + direction += RANDOM.nextBoolean() ? -1 : 1; + set.casDirection = Math.floorMod(direction, 4); + set.casUser = shot.user; + set.casShooter = shot.shooter; + set.casStartTick = 50; + set.casCount = 0; + if (shot.user instanceof ServerPlayer player) { + ModSounds.playTo(player, set.definition().sound, 1.0F, 1.0F); + } + return true; + } + + /** {@code MCH_WeaponCAS.update}: three passes, 45 ticks apart, the later ones offset to the sides. */ + static void tick(AircraftEntity vehicle, WeaponSet set) { + if (set.casCount >= 3 || set.countWait == 0 || set.ticksSinceShot(0) != set.casStartTick + || !(vehicle.level() instanceof ServerLevel level)) { + return; + } + double x = 0.0; + double z = 0.0; + if (set.casCount >= 1) { + double sign = set.casCount == 1 ? 1.0 : -1.0; + if (set.casDirection % 2 == 0) { + x = RANDOM.nextDouble() * 10.0 * sign; + z = (RANDOM.nextDouble() - 0.5) * 10.0; + } else { + z = RANDOM.nextDouble() * 10.0 * sign; + x = (RANDOM.nextDouble() - 0.5) * 10.0; + } + } + spawnA10(level, set, set.casX + x, set.casY + 20.0, set.casZ + z); + set.casStartTick = set.ticksSinceShot(0) + 45; + set.casCount++; + } + + private static void spawnA10(ServerLevel level, WeaponSet set, double x, double y, double z) { + double mx = 0.0; + double mz = 0.0; + switch (set.casDirection) { + case 0 -> { + mz = 3.0; + z -= 90.0; + } + case 1 -> { + mx = -3.0; + x += 90.0; + } + case 2 -> { + mz = -3.0; + z += 90.0; + } + default -> { + mx = 3.0; + x -= 90.0; + } + } + A10Entity a10 = ModEntities.A10.get().create(level); + if (a10 == null) { + return; + } + a10.setPos(x, y, z); + WeaponDefinition w = set.definition(); + a10.setup(set.name(), set.casDirection, mx, mz, set.casShooter, set.casUser); + a10.power = w.power; + a10.explosionPower = w.explosion; + a10.acceleration = w.acceleration; + level.addFreshEntity(a10); + ModSounds.playAt(level, x, y, z, "a-10_snd", 150.0F, 1.0F); + } +} From 75bd7287e55bbbaaacd4812d4f648371ba970ad5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:06 +0800 Subject: [PATCH 264/314] Add Shot to the shared 1.21.1 sources --- .../com/jnhprojects/mcheli/weapon/Shot.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java new file mode 100644 index 0000000..6a9f53c --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/Shot.java @@ -0,0 +1,40 @@ +package com.jnhprojects.mcheli.weapon; + +import net.minecraft.world.entity.Entity; +import org.jspecify.annotations.Nullable; + +/** + * One trigger pull, the port of {@code MCH_WeaponParam}. The weapon set fills in the aim and the + * muzzle position; the client fills in the two option values (a locked target, a fire mode) and + * the server receives them with the fire request. + */ +public final class Shot { + /** The vehicle firing, or the player for hand held weapons. */ + public final Entity shooter; + /** Who pulled the trigger. Damage and kills are credited to this entity. */ + public final @Nullable Entity user; + public double x; + public double y; + public double z; + public float yaw; + public float pitch; + public float roll; + public int option1; + public int option2; + public boolean infiniteAmmo; + /** Set by the weapon set when the magazine ran dry with this shot. */ + public boolean reload; + + public Shot(Entity shooter, @Nullable Entity user) { + this.shooter = shooter; + this.user = user; + this.x = shooter.getX(); + this.y = shooter.getY(); + this.z = shooter.getZ(); + } + + /** The entity whose view steers guided weapons: the user if there is one, else the shooter. */ + public Entity aimer() { + return user != null ? user : shooter; + } +} From 13c4d4205e4afd90d69c8ea6f8146fc936027ce5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:06 +0800 Subject: [PATCH 265/314] Build dust and item particles the 1.21.1 way --- .../mcheli/weapon/VehicleWeapons.java | 596 ++++++++++++++++++ 1 file changed, 596 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java new file mode 100644 index 0000000..a5dc19b --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/VehicleWeapons.java @@ -0,0 +1,596 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.content.Vector3; +import com.jnhprojects.mcheli.content.aircraft.AircraftDefinition; +import com.jnhprojects.mcheli.content.aircraft.WeaponGroup; +import com.jnhprojects.mcheli.content.aircraft.WeaponMount; +import com.jnhprojects.mcheli.content.aircraft.part.WeaponBayPart; +import com.jnhprojects.mcheli.content.weapon.MuzzleFlash; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import com.jnhprojects.mcheli.entity.AircraftEntity; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.network.WeaponStatePacket; +import net.minecraft.core.particles.DustParticleOptions; +import net.minecraft.core.particles.ItemParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.joml.Vector3f; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * The armament of one vehicle: its weapon sets, which one each seat has selected, the flares, and + * the effects every client derives from the synced firing state. Ported from the weapon handling + * in {@code MCH_EntityAircraft} ({@code createWeapon}, {@code switchWeapon}, + * {@code useCurrentWeapon}, {@code updateWeapons} and {@code updateWeaponsRotation}). + */ +public final class VehicleWeapons { + private final AircraftEntity vehicle; + private final AircraftDefinition def; + private final boolean clientSide; + private final List sets = new ArrayList<>(); + private final int[] selected; + private final @Nullable Guidance[] guidance; + private final RandomSource random = RandomSource.create(); + + /** One bit per mount, set while that mount shows muzzle effects. Synced by the vehicle. */ + private int useStat; + private int prevUseStat; + + private final float[] bayAngle; + private final float[] prevBayAngle; + + private final int[] sentAmmo; + private final int[] sentRest; + + public final FlareDispenser flares; + + // Recoil that kicks the whole hull, from Recoil in the weapon file. + private int recoilCount; + private float recoilValue; + private float recoilYaw; + + public VehicleWeapons(AircraftEntity vehicle, AircraftDefinition def) { + this.vehicle = vehicle; + this.def = def; + this.clientSide = vehicle.level().isClientSide(); + for (WeaponGroup group : def.weaponGroups) { + WeaponDefinition weapon = MCHeli.content().weapon(group.weaponType()); + if (weapon != null && !group.mounts().isEmpty()) { + sets.add(new WeaponSet(weapon, group.mounts(), clientSide)); + } + } + selected = new int[def.seatCount()]; + Arrays.fill(selected, -1); + guidance = new Guidance[sets.size()]; + for (int i = 0; i < sets.size(); i++) { + WeaponDefinition w = sets.get(i).definition(); + if (w.type == WeaponType.AA_MISSILE || w.type == WeaponType.AT_MISSILE) { + boolean aa = w.type == WeaponType.AA_MISSILE; + guidance[i] = new Guidance(aa, !aa, w.ridableOnly, w.lockTime, this::canLock); + } + } + bayAngle = new float[def.weaponBays.size()]; + prevBayAngle = new float[def.weaponBays.size()]; + sentAmmo = new int[sets.size()]; + sentRest = new int[sets.size()]; + Arrays.fill(sentAmmo, -1); + flares = new FlareDispenser(vehicle, def); + } + + public List sets() { + return sets; + } + + public @Nullable WeaponSet set(int id) { + return id >= 0 && id < sets.size() ? sets.get(id) : null; + } + + public int indexOf(String weapon) { + for (int i = 0; i < sets.size(); i++) { + if (sets.get(i).name().equalsIgnoreCase(weapon)) { + return i; + } + } + return -1; + } + + public @Nullable WeaponSet byName(String weapon) { + return set(indexOf(weapon)); + } + + public @Nullable Guidance guidance(int id) { + return id >= 0 && id < guidance.length ? guidance[id] : null; + } + + /** A vehicle does not lock its own crew or cargo. */ + private boolean canLock(Entity entity) { + return entity != vehicle && !vehicle.hasPassenger(entity); + } + + // Selection + + public int selected(int seat) { + return seat >= 0 && seat < selected.length ? selected[seat] : -1; + } + + public @Nullable WeaponSet current(int seat) { + return set(selected(seat)); + } + + /** The seat that operates a weapon: its own seat, or the pilot while that seat has no player. */ + public int operatorSeat(int id) { + WeaponSet set = set(id); + if (set == null) { + return -1; + } + WeaponMount mount = set.firstMount(); + if (mount.usableByPilot() && !(vehicle.occupant(mount.seatIndex()) instanceof Player)) { + return 0; + } + return mount.seatIndex(); + } + + /** {@code getNextWeaponID}: the next weapon {@code seat} may use, stepping forwards or back. */ + public int next(int seat, int step) { + if (sets.isEmpty() || seat < 0) { + return -1; + } + int id = selected(seat); + for (int i = 0; i < sets.size(); i++) { + id = step >= 0 ? (id + 1) % sets.size() : id > 0 ? id - 1 : sets.size() - 1; + WeaponMount mount = sets.get(id).firstMount(); + int owner = mount.seatIndex(); + if (owner == seat || seat == 0 && mount.usableByPilot() && !(vehicle.occupant(owner) instanceof Player)) { + return id; + } + } + return -1; + } + + /** {@code initCurrentWeapon}: a player took a seat. Server only. */ + public void onSeated(Entity entity, int seat) { + if (seat < 0 || seat >= selected.length) { + return; + } + selected[seat] = -1; + if (entity instanceof Player) { + switchTo(entity, seat, next(seat, 1)); + } else { + vehicle.publishWeaponSelection(); + } + } + + public void onLeft(int seat) { + if (seat >= 0 && seat < selected.length) { + selected[seat] = -1; + vehicle.publishWeaponSelection(); + } + } + + /** {@code switchWeapon}. Server only; the selection reaches clients through synced data. */ + public void switchTo(Entity user, int seat, int id) { + if (seat < 0 || seat >= selected.length || sets.isEmpty()) { + return; + } + WeaponSet before = current(seat); + if (before != null) { + before.reload(); + } + selected[seat] = Math.min(id, sets.size() - 1); + WeaponSet now = current(seat); + if (now != null) { + now.onSwitchWeapon(vehicle.hasInfiniteAmmo(user)); + } + vehicle.publishWeaponSelection(); + } + + public String encodeSelection() { + StringBuilder b = new StringBuilder(); + for (int i = 0; i < selected.length; i++) { + if (i > 0) { + b.append(','); + } + b.append(selected[i]); + } + return b.toString(); + } + + /** Applies the server's selection on a client, running the switch delay where it changed. */ + public void decodeSelection(String encoded, @Nullable Entity localPlayer) { + String[] parts = encoded.isEmpty() ? new String[0] : encoded.split(","); + for (int i = 0; i < selected.length; i++) { + int id; + try { + id = i < parts.length ? Integer.parseInt(parts[i]) : -1; + } catch (NumberFormatException e) { + id = -1; + } + if (selected[i] != id) { + selected[i] = id; + WeaponSet set = set(id); + Entity occupant = vehicle.occupant(i); + if (set != null && occupant != null && occupant == localPlayer) { + set.onSwitchWeapon(vehicle.hasInfiniteAmmo(occupant)); + Guidance g = guidance(id); + if (g != null) { + g.clear(); + } + } + } + } + } + + // Firing + + /** + * {@code useCurrentWeapon}: fires the selected weapon of {@code seat} if it is ready. The + * trigger decides what the shot does on this side. + */ + public boolean use(Entity user, int seat, Shot shot, WeaponSet.Trigger trigger) { + int id = selected(seat); + WeaponSet set = set(id); + if (set == null || !set.canUse() || vehicle.isDestroyed()) { + return false; + } + shot.infiniteAmmo = vehicle.hasInfiniteAmmo(user); + int mount = set.currentMount(); + // As the original did, face the vehicle where the user looks for the length of the shot. + boolean followUser = vehicle.aimsWithUser(set); + float hullYaw = vehicle.getYRot(); + float hullPitch = vehicle.getXRot(); + if (followUser) { + vehicle.setYRot(user.getYRot()); + vehicle.setXRot(user.getXRot()); + } + boolean fired; + try { + fired = set.use(shot, (s, m, sh) -> { + Vec3 muzzle = muzzleOffset(s, m, 1.0F); + sh.x += muzzle.x; + sh.y += muzzle.y; + sh.z += muzzle.z; + return trigger.fire(s, m, sh); + }); + } finally { + if (followUser) { + vehicle.setYRot(hullYaw); + vehicle.setXRot(hullPitch); + } + } + if (!fired) { + return false; + } + String group = set.definition().group; + if (!group.isEmpty()) { + for (WeaponSet other : sets) { + if (other != set && group.equals(other.definition().group)) { + other.waitForGroup(shot.reload); + } + } + } + if (!clientSide) { + int bit = bitOf(id, mount); + if (bit < 32) { + useStat |= 1 << bit; + } + } + return true; + } + + /** World offset of a mount from the vehicle origin; turret mounts turn with the turret. */ + public Vec3 muzzleOffset(WeaponSet set, int mount, float partialTicks) { + WeaponMount m = set.mounts().get(mount); + Vector3 local = m.turret() ? vehicle.onTurret(def, m.position(), partialTicks) : m.position(); + Vector3f v = vehicle.toWorldOffset(local); + return new Vec3(v.x, v.y, v.z); + } + + private int bitOf(int id, int mount) { + int shift = 0; + for (int i = 0; i < id; i++) { + shift += sets.get(i).mounts().size(); + } + return shift + mount; + } + + /** Whether a mount is cooling down after a shot; missile parts are hidden meanwhile. */ + public boolean isMountUsed(int id, int mount) { + int bit = bitOf(id, mount); + return bit < 32 && (useStat & 1 << bit) != 0; + } + + public int useStat() { + return useStat; + } + + public void setUseStat(int stat) { + useStat = stat; + } + + public void switchMode(int seat) { + WeaponSet set = current(seat); + if (set != null) { + set.switchMode(); + } + } + + // Ticking + + /** {@code updateWeapons} and {@code updateWeaponsRotation}, once per tick on both sides. */ + public void tick() { + if (sets.isEmpty()) { + flares.tick(); + return; + } + int previous = prevUseStat; + if (!clientSide) { + useStat |= usedStat(); + vehicle.publishWeaponUse(useStat); + } else { + useStat = vehicle.syncedWeaponUse(); + } + prevUseStat = useStat; + + float yaw = Mth.wrapDegrees(vehicle.getYRot()); + float pitch = Mth.wrapDegrees(vehicle.getXRot()); + int bit = 0; + for (int id = 0; id < sets.size(); id++) { + WeaponSet set = sets.get(id); + boolean longDelay = set.isLongDelay(); + boolean anyUsed = false; + for (int mount = 0; mount < set.mounts().size(); mount++) { + boolean wasUsed = bit < 32 && (previous & 1 << bit) != 0; + boolean used = bit < 32 && (useStat & 1 << bit) != 0; + if (longDelay && wasUsed && used) { + used = false; + } + anyUsed |= used; + if (!wasUsed && used && set.definition().recoil > 0.0F) { + recoilCount = 30; + recoilValue = set.definition().recoil; + recoilYaw = set.rotationYaw; + } + if (clientSide && used) { + muzzleEffects(set, mount, yaw); + } + set.tickRecoil(mount, used); + bit++; + } + set.tick(anyUsed); + if (!clientSide) { + ServerFiring.tick(vehicle, set); + } + updateRotation(set); + } + if (!clientSide) { + useStat = 0; + syncAmmo(); + } else { + for (Guidance g : guidance) { + if (g != null) { + g.tick(); + } + } + } + if (recoilCount > 0) { + recoilCount--; + } + tickBays(); + if (clientSide) { + smoke(); + } + flares.tick(); + } + + private int usedStat() { + int stat = 0; + int bit = 0; + for (WeaponSet set : sets) { + for (int mount = 0; mount < set.mounts().size() && bit < 32; mount++) { + stat |= set.isUsed(mount) ? 1 << bit : 0; + bit++; + } + } + return stat; + } + + /** Turret aim of one set, following whoever operates it, within the mount's traverse limits. */ + private void updateRotation(WeaponSet set) { + if (vehicle.isDestroyed()) { + return; + } + WeaponMount mount = set.firstMount(); + int seat = mount.seatIndex(); + Entity operator = vehicle.occupant(seat); + if (mount.usableByPilot() && !(operator instanceof Player)) { + operator = vehicle.occupant(0); + } + float hullYaw = Mth.wrapDegrees(vehicle.getYRot()); + float hullPitch = Mth.wrapDegrees(vehicle.getXRot()); + float turretYaw = vehicle.turretYaw(); + if (operator instanceof Player) { + if ((int) mount.minYaw() != 0 || (int) mount.maxYaw() != 0) { + float ty = mount.turret() ? turretYaw : 0.0F; + float ey = Mth.wrapDegrees(operator.getYRot() - hullYaw - mount.defaultYaw() - ty); + if (Math.abs((int) mount.minYaw()) < 360 && Math.abs((int) mount.maxYaw()) < 360) { + float targetYaw = Mth.clamp(ey, mount.minYaw(), mount.maxYaw()); + float wy = set.rotationYaw - mount.defaultYaw() - ty; + if (targetYaw < wy) { + wy = wy - targetYaw > 15.0F ? wy - 15.0F : targetYaw; + } else if (targetYaw > wy) { + wy = targetYaw - wy > 15.0F ? wy + 15.0F : targetYaw; + } + set.rotationYaw = wy + mount.defaultYaw() + ty; + } else { + set.rotationYaw = ey + ty; + } + } + float ep = Mth.wrapDegrees(operator.getXRot() - hullPitch); + set.rotationPitch = Mth.clamp(ep, mount.minPitch(), mount.maxPitch()); + set.rotationTurretYaw = 0.0F; + } else { + set.rotationTurretYaw = turretYaw; + } + } + + /** {@code updateRecoil}: the hull kick of a big gun, applied on the simulating side. */ + public float @Nullable [] recoilKick() { + if (recoilCount < 12) { + return null; + } + float rel = (float) Math.toRadians(recoilYaw - vehicle.roll()); + float kick = Mth.cos((float) Math.toRadians(recoilCount * 6)) * recoilValue; + return new float[]{kick * Mth.cos(rel), kick * Mth.sin(rel)}; + } + + /** {@code updateWeaponBay}: bays swing open while a seated crew member has their weapon selected. */ + private void tickBays() { + for (int i = 0; i < bayAngle.length; i++) { + WeaponBayPart bay = def.weaponBays.get(i); + boolean open = false; + for (String weapon : bay.weapons()) { + int id = indexOf(weapon); + for (int seat = 0; seat < selected.length && id >= 0; seat++) { + if (selected[seat] == id && vehicle.occupant(seat) != null) { + open = true; + } + } + } + prevBayAngle[i] = bayAngle[i]; + bayAngle[i] = Mth.clamp(bayAngle[i] + (open ? 3.0F : -3.0F), 0.0F, 90.0F); + } + } + + public float bayAngle(int bay, float partialTicks) { + return bay < bayAngle.length ? Mth.lerp(partialTicks, prevBayAngle[bay], bayAngle[bay]) : 0.0F; + } + + // Effects, client only + + /** {@code spawnParticleMuzzleFlash} plus the spent case of guns with {@code SetCartridge}. */ + private void muzzleEffects(WeaponSet set, int mount, float hullYaw) { + Level level = vehicle.level(); + WeaponDefinition w = set.definition(); + Vec3 offset = muzzleOffset(set, mount, 1.0F); + double px = vehicle.getX() + offset.x; + double py = vehicle.getY() + offset.y; + double pz = vehicle.getZ() + offset.z; + Vec3 dir = Vec3.directionFromRotation(vehicle.getXRot() + set.rotationPitch, hullYaw + set.rotationYaw); + for (MuzzleFlash mf : w.muzzleSmokes) { + double x = px + dir.x * mf.distance(); + double y = py + dir.y * mf.distance(); + double z = pz + dir.z * mf.distance(); + for (int i = 0; i < mf.count(); i++) { + double r = mf.range(); + level.addParticle(mf.size() >= 2.0F ? ParticleTypes.LARGE_SMOKE : ParticleTypes.SMOKE, + x + (random.nextDouble() - 0.5) * r, y + (random.nextDouble() - 0.5) * r, z + (random.nextDouble() - 0.5) * r, + (random.nextDouble() - 0.5) * 0.05, 0.02, (random.nextDouble() - 0.5) * 0.05); + } + } + for (MuzzleFlash mf : w.muzzleFlashes) { + int rgb = mf.color().argb() & 0xFFFFFF; + level.addParticle(new DustParticleOptions(Vec3.fromRGB24(rgb).toVector3f(), Mth.clamp(mf.size() * 4.0F, 0.5F, 4.0F)), + px + dir.x * mf.distance(), py + dir.y * mf.distance(), pz + dir.z * mf.distance(), 0.0, 0.0, 0.0); + } + if (w.cartridge != null) { + Vec3 motion = vehicle.getDeltaMovement(); + Vec3 side = Vec3.directionFromRotation(0.0F, hullYaw + set.rotationYaw + w.cartridge.yaw() + 90.0F) + .scale(0.1 + w.cartridge.acceleration()); + level.addParticle(new ItemParticleOption(ParticleTypes.ITEM, new ItemStack(Items.GOLD_NUGGET)), + px, py, pz, motion.x + side.x, motion.y + 0.1, motion.z + side.z); + } + } + + /** {@code onUpdate_ParticleSmoke}: smoke generators trail while the pilot has them selected. */ + private void smoke() { + WeaponSet set = current(0); + if (set == null || set.definition().type != WeaponType.SMOKE || vehicle.visibleThrottle() <= 0.1F) { + return; + } + WeaponDefinition w = set.definition(); + Vec3 back = Vec3.directionFromRotation(vehicle.getXRot(), vehicle.getYRot()).reverse(); + int rgb = w.color.argb() & 0xFFFFFF; + for (int mount = 0; mount < set.mounts().size(); mount++) { + if (random.nextFloat() > vehicle.visibleThrottle() * 1.5F) { + continue; + } + Vec3 offset = muzzleOffset(set, mount, 1.0F); + for (int i = 0; i < w.smokeNum; i++) { + vehicle.level().addParticle(new DustParticleOptions(Vec3.fromRGB24(rgb).toVector3f(), Mth.clamp(w.smokeSize * 2.0F, 0.5F, 4.0F)), + vehicle.getX() + offset.x + back.x, vehicle.getY() + offset.y + back.y, vehicle.getZ() + offset.z + back.z, + back.x * w.acceleration + (random.nextDouble() - 0.5) * 0.2, + back.y * w.acceleration + (random.nextDouble() - 0.5) * 0.2, + back.z * w.acceleration + (random.nextDouble() - 0.5) * 0.2); + } + } + } + + // Ammunition + + /** Sends magazine and reserve counts to the crew whenever they change. Server only. */ + private void syncAmmo() { + for (int id = 0; id < sets.size(); id++) { + WeaponSet set = sets.get(id); + if (set.ammo() != sentAmmo[id] || set.restAmmo() != sentRest[id]) { + sentAmmo[id] = set.ammo(); + sentRest[id] = set.restAmmo(); + WeaponStatePacket packet = new WeaponStatePacket(vehicle.getId(), id, set.ammo(), set.restAmmo()); + for (Entity passenger : vehicle.getPassengers()) { + if (passenger instanceof ServerPlayer player) { + ModNetwork.sendToPlayer(player, packet); + } + } + } + } + } + + /** Forces a full resend, for a player who just boarded. */ + public void resendAmmo() { + Arrays.fill(sentAmmo, -1); + } + + public void applyAmmo(int id, int ammo, int rest) { + WeaponSet set = set(id); + if (set != null) { + set.setAmmo(ammo); + set.setRestAmmo(rest); + } + } + + /** {@code reloadAllWeapon}: tops up every magazine from its reserve. */ + public void reloadAll() { + for (WeaponSet set : sets) { + set.reloadMagazine(); + } + } + + public int[] saveAmmo() { + int[] out = new int[sets.size() * 2]; + for (int i = 0; i < sets.size(); i++) { + out[i * 2] = sets.get(i).ammo(); + out[i * 2 + 1] = sets.get(i).restAmmo(); + } + return out; + } + + public void loadAmmo(int[] saved) { + for (int i = 0; i < sets.size() && i * 2 + 1 < saved.length; i++) { + sets.get(i).setAmmo(Math.min(saved[i * 2], sets.get(i).magazineSize())); + sets.get(i).setRestAmmo(saved[i * 2 + 1]); + } + } +} From 01aaa8d71e4a4ebe079dd9e3b9f606e5693f5e0d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:07 +0800 Subject: [PATCH 266/314] Add WeaponSet to the shared 1.21.1 sources --- .../jnhprojects/mcheli/weapon/WeaponSet.java | 446 ++++++++++++++++++ 1 file changed, 446 insertions(+) create mode 100644 minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java diff --git a/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java new file mode 100644 index 0000000..b358a4c --- /dev/null +++ b/minecraft-1.21.1/src/main/java/com/jnhprojects/mcheli/weapon/WeaponSet.java @@ -0,0 +1,446 @@ +package com.jnhprojects.mcheli.weapon; + +import com.jnhprojects.mcheli.content.aircraft.WeaponMount; +import com.jnhprojects.mcheli.content.weapon.WeaponDefinition; +import com.jnhprojects.mcheli.content.weapon.WeaponType; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import org.jspecify.annotations.Nullable; + +import java.util.List; + +/** + * One selectable weapon of a vehicle: every mount that fires the same weapon, with the magazine, + * reload, heat and recoil they share. A port of {@code MCH_WeaponSet} together with the parts of + * {@code MCH_WeaponBase} and {@code MCH_WeaponCreator} that are not specific to a weapon type. + * + *

Both sides keep one. The server's is authoritative for ammunition and decides what is fired; + * the client's runs with the slightly longer intervals the original used there, so the client never + * asks to fire faster than the server accepts. + */ +public final class WeaponSet { + private static final int SWITCH_WAIT = 15; + + /** Fires one mount. Returns whether the shot happened, which consumes a round. */ + @FunctionalInterface + public interface Trigger { + boolean fire(WeaponSet set, int mount, Shot shot); + } + + private final RandomSource random = RandomSource.create(); + private final WeaponDefinition def; + private final List mounts; + private final boolean clientSide; + + public final int interval; + public final int heatCount; + private final int numMode; + + private int currentMount; + private int mode; + private int ammo; + private int restAmmo; + public int currentHeat; + private int cooldownSpeed = 1; + public int countWait; + public int countReloadWait; + private final int[] lastUsedCount; + /** Ticks since each mount last fired, {@code MCH_WeaponBase.tick}. */ + final int[] ticks; + public int soundWait; + /** Whether the shot being fired may play its sound, see {@code SoundDelay}. */ + public boolean soundAllowed = true; + + /** Aim relative to the hull, following the crew member who operates the weapon. */ + public float rotationYaw; + public float rotationPitch; + public float prevRotationYaw; + public float prevRotationPitch; + public final float defaultRotationYaw; + /** How far behind the pilot's view a turret weapon lags, for drawing. */ + public float rotationTurretYaw; + + public float barrelSpeed; + public float barrel; + public float prevBarrel; + private final Recoil[] recoil; + + // Close air support run, server only. See ServerFiring. + @Nullable Entity casUser; + @Nullable Entity casShooter; + double casX; + double casY; + double casZ; + int casDirection; + int casStartTick; + int casCount = 3; + + public WeaponSet(WeaponDefinition def, List mounts, boolean clientSide) { + this.def = def; + this.mounts = List.copyOf(mounts); + this.clientSide = clientSide; + this.lastUsedCount = new int[mounts.size()]; + this.ticks = new int[mounts.size()]; + this.recoil = new Recoil[mounts.size()]; + for (int i = 0; i < recoil.length; i++) { + recoil[i] = new Recoil(def.recoilBufCount, def.recoilBufCountSpeed); + } + this.defaultRotationYaw = mounts.isEmpty() ? 0.0F : mounts.getFirst().defaultYaw(); + this.rotationYaw = defaultRotationYaw; + this.prevRotationYaw = defaultRotationYaw; + + int wait = def.delay; + if (def.type == WeaponType.CAS && wait > -250) { + wait = -250; + } + int heat = def.heatCount; + if (def.maxHeatCount > 0 && heat < 2) { + heat = 2; + } + if (clientSide) { + wait = clientInterval(wait); + heat++; + } + this.interval = wait; + this.heatCount = heat; + this.numMode = def.type == WeaponType.MACHINE_GUN_2 && def.explosion == 0 ? 0 : def.modeNum; + } + + /** The original gave the client longer intervals so its requests never outran the server. */ + private static int clientInterval(int wait) { + if (wait < 4) { + return wait + 1; + } else if (wait < 7) { + return wait + 2; + } else if (wait < 10) { + return wait + 3; + } else if (wait < 20) { + return wait + 6; + } + wait += 10; + return wait >= 40 ? -wait : wait; + } + + public WeaponDefinition definition() { + return def; + } + + public String name() { + return def.name(); + } + + public List mounts() { + return mounts; + } + + public WeaponMount firstMount() { + return mounts.getFirst(); + } + + public int currentMount() { + return currentMount; + } + + // Ammunition + + public int ammo() { + return ammo; + } + + public int magazineSize() { + return def.round; + } + + public int restAmmo() { + return restAmmo; + } + + public int maxAmmo() { + return def.maxAmmo; + } + + public void setAmmo(int n) { + ammo = n; + } + + /** Everything not in the magazine, capped so magazine plus reserve never exceed {@code MaxAmmo}. */ + public void setRestAmmo(int n) { + restAmmo = Math.min(n, def.maxAmmo - ammo); + } + + /** {@code supplyRestAllAmmo}: one resupply adds {@code SuppliedNum} rounds. */ + public boolean supply() { + if (restAmmo + ammo < def.maxAmmo) { + setRestAmmo(restAmmo + ammo + def.suppliedNum); + return true; + } + return false; + } + + public boolean isInPreparation() { + return countWait < 0 || countReloadWait > 0; + } + + public boolean isReloading() { + return countReloadWait > 0; + } + + public boolean canUse() { + return countWait == 0; + } + + public boolean isLongDelay() { + return def.delay > 4; + } + + public boolean isOverheated() { + return def.maxHeatCount > 0 && currentHeat >= def.maxHeatCount; + } + + public void reload() { + if (magazineSize() > 0 && ammo < magazineSize() && def.reloadTime > 0) { + countReloadWait = def.reloadTime; + if (clientSide) { + ammo = 0; + } else { + countReloadWait = Math.max(countReloadWait - 20, 1); + } + } + } + + public void reloadMagazine() { + int needed = magazineSize() - ammo; + if (needed > 0) { + needed = Math.min(needed, restAmmo); + ammo += needed; + setRestAmmo(restAmmo - needed); + } + } + + // Modes + + public int mode() { + return def.fixMode > 0 ? def.fixMode : mode; + } + + public boolean switchMode() { + if (def.fixMode > 0) { + return false; + } + int before = mode(); + mode = numMode > 0 ? (mode() + 1) % numMode : 0; + if (before == mode()) { + return false; + } + waitAfterSwitch(SWITCH_WAIT); + return true; + } + + /** {@code getName}: the display name with the mode suffix the weapon class added. */ + public String displayName() { + String suffix = switch (def.type) { + case MACHINE_GUN_2 -> mode() == 0 ? "" : " [HE]"; + case ROCKET -> mode() == 0 ? "" : " [HEIAP]"; + case AT_MISSILE -> mode() == 1 ? " [TA]" : ""; + case TV_MISSILE -> switch (mode()) { + case 0 -> " [TV]"; + case 2 -> " [TA]"; + default -> ""; + }; + case null, default -> ""; + }; + return def.displayName + suffix; + } + + public void onSwitchWeapon(boolean infiniteAmmo) { + waitAfterSwitch(clientSide ? SWITCH_WAIT + 10 : SWITCH_WAIT); + currentMount = 0; + if (infiniteAmmo) { + ammo = magazineSize(); + } + } + + private void waitAfterSwitch(int wait) { + if (countWait >= -wait) { + countWait = countWait > wait ? -countWait : -wait; + } + } + + /** Another weapon of the same {@code Group} fired; this one waits and reloads with it. */ + public void waitForGroup(boolean reload) { + countWait = interval; + countReloadWait = 0; + if (reload && magazineSize() > 0 && def.reloadTime > 0) { + countReloadWait = clientSide ? def.reloadTime : Math.max(def.reloadTime - 20, 1); + } + } + + // Firing + + /** + * Fires the next mount if the set is ready. The aim is the shooter's rotation plus this set's + * turret aim and the mount's fixed offset, scattered by {@code Accuracy}. + */ + public boolean use(Shot shot, Trigger trigger) { + if (magazineSize() > 0 && ammo <= 0 || isOverheated()) { + return false; + } + WeaponMount mount = mounts.get(currentMount); + soundAllowed = soundWait == 0; + float yaw = shot.shooter.getYRot() + rotationYaw + mount.yaw(); + float pitch = shot.shooter.getXRot() + rotationPitch + mount.pitch(); + if (def.accuracy > 0.0F) { + yaw += (random.nextFloat() - 0.5F) * def.accuracy; + pitch += (random.nextFloat() - 0.5F) * def.accuracy; + } + shot.yaw = Mth.wrapDegrees(yaw); + shot.pitch = Mth.wrapDegrees(pitch); + if (!trigger.fire(this, currentMount, shot)) { + return false; + } + ticks[currentMount] = 0; + + if (def.maxHeatCount > 0) { + cooldownSpeed = 1; + currentHeat += heatCount; + if (currentHeat >= def.maxHeatCount) { + currentHeat += 30; + } + } + if (def.soundDelay > 0 && soundWait == 0) { + soundWait = def.soundDelay; + } + lastUsedCount[currentMount] = Math.abs(interval); + if (isCooldownReloadTime() && def.reloadTime - 10 > lastUsedCount[currentMount]) { + lastUsedCount[currentMount] = def.reloadTime - 10; + } + currentMount = (currentMount + 1) % mounts.size(); + countWait = interval; + countReloadWait = 0; + if (ammo > 0) { + ammo--; + } + if (ammo <= 0) { + if (shot.infiniteAmmo && restAmmo < magazineSize()) { + setRestAmmo(magazineSize()); + } + reload(); + shot.reload = true; + } + return true; + } + + /** Missiles keep their mount empty for the whole reload, which hides the missile part meanwhile. */ + private boolean isCooldownReloadTime() { + return def.type == WeaponType.AA_MISSILE || def.type == WeaponType.AS_MISSILE || def.type == WeaponType.AT_MISSILE; + } + + /** Whether {@code mount} fired recently enough to show muzzle effects or keep a missile hidden. */ + public boolean isUsed(int mount) { + if (mount >= lastUsedCount.length) { + return false; + } + int count = lastUsedCount[mount]; + return interval >= 4 && count > interval / 2 || count >= 4; + } + + public int ticksSinceShot(int mount) { + return ticks[mount]; + } + + // Ticking + + /** {@code MCH_WeaponSet.update}, once per tick on both sides. */ + public void tick(boolean used) { + if (countReloadWait > 0) { + countReloadWait--; + if (countReloadWait == 0) { + reloadMagazine(); + } + } + for (int i = 0; i < lastUsedCount.length; i++) { + if (lastUsedCount[i] > 0) { + if (lastUsedCount[i] != 4 || currentMount == 0 && canUse() && (ammo > 0 || def.maxAmmo <= 0)) { + lastUsedCount[i]--; + } + } + } + if (currentHeat > 0) { + if (currentHeat < def.maxHeatCount) { + cooldownSpeed++; + } + currentHeat = Math.max(currentHeat - (cooldownSpeed / 20 + 1), 0); + } + if (countWait > 0) { + countWait--; + } else if (countWait < 0) { + countWait++; + } + prevRotationYaw = rotationYaw; + prevRotationPitch = rotationPitch; + if (countWait != 0) { + for (int i = 0; i < ticks.length; i++) { + ticks[i]++; + } + } + if (soundWait > 0) { + soundWait--; + } + + if (used && barrelSpeed < 75.0F) { + barrelSpeed = Math.min(barrelSpeed + 25 + random.nextInt(3), 74.0F); + } + prevBarrel = barrel; + barrel += barrelSpeed; + if (barrel >= 360.0F) { + barrel -= 360.0F; + prevBarrel -= 360.0F; + } + if (barrelSpeed > 0.0F) { + barrelSpeed = Math.max(barrelSpeed - 1.48F, 0.0F); + } + } + + /** {@code updateWeapon}: the recoil kick of one mount. */ + public void tickRecoil(int mount, boolean used) { + Recoil r = recoil[mount]; + r.prev = r.value; + if (used && r.count <= 0) { + r.count = r.max; + } + if (r.count > 0) { + if (r.max <= 1) { + r.value = 1.0F; + } else if (r.max == 2) { + r.value = r.count == 2 ? 1.0F : 0.6F; + } else { + if (r.count > r.max / 2) { + r.count -= r.speed; + } + r.value = Mth.sin((float) r.count / r.max * Mth.PI); + } + r.count--; + } else { + r.value = 0.0F; + } + } + + public float recoil(int mount, float partialTicks) { + Recoil r = recoil[Math.min(mount, recoil.length - 1)]; + return Mth.lerp(partialTicks, r.prev, r.value); + } + + private static final class Recoil { + final int max; + final int speed; + int count; + float value; + float prev; + + Recoil(int max, int speed) { + this.max = max; + this.speed = speed; + } + } +} From cc1d73306a9f9e7b734dc0a165c2e264da422806 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:07 +0800 Subject: [PATCH 267/314] Add assets/mcheli/particles/glow.json to the shared 1.21.1 resources --- .../src/main/resources/assets/mcheli/particles/glow.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/assets/mcheli/particles/glow.json diff --git a/minecraft-1.21.1/src/main/resources/assets/mcheli/particles/glow.json b/minecraft-1.21.1/src/main/resources/assets/mcheli/particles/glow.json new file mode 100644 index 0000000..296e539 --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/assets/mcheli/particles/glow.json @@ -0,0 +1,5 @@ +{ + "textures": [ + "mcheli:glow" + ] +} From 1d1738881e0bf8ea410e6f2a07c689bb7ac964ab Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:07 +0800 Subject: [PATCH 268/314] Add assets/mcheli/textures/particle/glow.png to the shared 1.21.1 resources --- .../assets/mcheli/textures/particle/glow.png | Bin 0 -> 705 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 minecraft-1.21.1/src/main/resources/assets/mcheli/textures/particle/glow.png diff --git a/minecraft-1.21.1/src/main/resources/assets/mcheli/textures/particle/glow.png b/minecraft-1.21.1/src/main/resources/assets/mcheli/textures/particle/glow.png new file mode 100644 index 0000000000000000000000000000000000000000..339b97292b3a86cec21a5a2c2d4e9159da1241c6 GIT binary patch literal 705 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=hEVFm-skIEGZjoxNt=CF&@0?BmDQ z$G05=nLjo)u{~0d=sc02G=rmQ;>?7sgruZLOlR&iZR9TC);p3q<+=OyiMy`WR_B%6 ze!zcpW%0hxt6uJ`UOHv3s-)z$2S*IQ{M+2aBkl2V^1+p-UiNMId~?@~iHS`VvGKGtbJ(?@PMX`Q_JU)-AEGyIw3c zQrRtbha-<|yVkj!cRxOeDwmZ>DLa>nOo)xwXnye(C@s6DY4^dsUB4m0R;efKDR26HaPb4r4<;3I zGNE&B-ud`Y+f9cz$|Q5W??V51i?`b=Utv|8`!Zyb_`%f=Ny@jd)@AxOp)i%w?!a<|&&Tjr8oN`JQToU~(`->fg1 zZdP9Nqf5}cIn2IwE7PsJ)h7Sugyi*n%i6MQ@`K~}e`S@^nPlVK#j7xFV z+_j8uPnwhRR~zlgnR{cs;OihI&w3VnZD^oOJGP2v)|RI;bPnXz1l3;M6Sw&D^QW!V zDOSc=cfEdS|A;M20|xneiH#;17nkTB-(PMJ>hSJ9`!Q!Lt-~I+X1}kkT&Mak7@_E? z^^gDD5jm{g_J>^m^K5+4zM_`ByS7X6x8|EK^8>{;-q`=xrFCc5-2VzEHJ@mS{q(#h zwlHh)?U((R_Hk6-{(1h>N~aR1kZ_?NOSWy;`Ple&>FE<27@gj_D0q6`GXA*nq@huc lV^{v7@AFerZkX0Ht_;k6d;0MSU0~8=@O1TaS?83{1OQn@Sb6{e literal 0 HcmV?d00001 From 472c9fe99c0223bd68e8f07ab29423b45607d241 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:08 +0800 Subject: [PATCH 269/314] Add data/mcheli/damage_type/bullet.json to the shared 1.21.1 resources --- .../src/main/resources/data/mcheli/damage_type/bullet.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/data/mcheli/damage_type/bullet.json diff --git a/minecraft-1.21.1/src/main/resources/data/mcheli/damage_type/bullet.json b/minecraft-1.21.1/src/main/resources/data/mcheli/damage_type/bullet.json new file mode 100644 index 0000000..92ee676 --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/data/mcheli/damage_type/bullet.json @@ -0,0 +1,5 @@ +{ + "exhaustion": 0.1, + "message_id": "mcheli.bullet", + "scaling": "when_caused_by_living_non_player" +} From 593840627f2f9487ae61a6befa26b189dfdbb02f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:08 +0800 Subject: [PATCH 270/314] Add data/minecraft/tags/damage_type/is_projectile.json to the shared 1.21.1 resources --- .../data/minecraft/tags/damage_type/is_projectile.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json diff --git a/minecraft-1.21.1/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json b/minecraft-1.21.1/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json new file mode 100644 index 0000000..1042db9 --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json @@ -0,0 +1,6 @@ +{ + "replace": false, + "values": [ + "mcheli:bullet" + ] +} From d8c2db9e6004040d8652c6e728a0c4f950d07556 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:09 +0800 Subject: [PATCH 271/314] Add mcheli_logo.png to the shared 1.21.1 resources --- .../src/main/resources/mcheli_logo.png | Bin 0 -> 23745 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 minecraft-1.21.1/src/main/resources/mcheli_logo.png diff --git a/minecraft-1.21.1/src/main/resources/mcheli_logo.png b/minecraft-1.21.1/src/main/resources/mcheli_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..95c72198d76d40c8c513965b3c40c83712ee54cf GIT binary patch literal 23745 zcmYhi2|SeF7e9VyVeAH3vJQ%}WZ#z=Nfe=wO16}(vV_PoGbpr(RV-1m9k^W3+#H05LyW&;3lo-#Xe4geT* z2?H$5(8uLlh3?RM+^G{rwjoZ7>A@j(O+%YqnPXv(3ohVbbBWfxnA*g|DmnsC@J^w*!<}JV&wSul!dC2 zLt!s!f3yeeO~1PMF_^Jlz;>iuWCky}UX7-%PPN%Be-|7!waf1JPBX3xr#?ui9P13> z$qlu={F6Yvx3fAWU%aDK(?Y+HWfE`}YVg{{-?_QA-zRNN3C{!luNx30``K#p+_uQ> z>eQRnxLpl*lQh}^f|_$(xZUaW2}b|Ugt*Vii7|GD(eD?ATQeAdss?DCCf2+dn4{E>X`G>og9cU(oP_I^ZE-5r} zL$cd%@N9*d{n6Y1edqq}=#faPjf=N8_kVc0X><2V!``Y`1a;L;aie~C55EJA$;rD_ z&kdaGyb~*_^bJN`_<+I-f+g-_Pm+@Jz-2=K$@pazHV!pBj|MXu-z1?~3@E&s_Dxw+ z92;*-DiOx~iCAY8D2jwb{qhA0gY6RcXeH;m*Y%8rguRIWcl*o#Mkx&k+pBibQwTD2 zr-B54I&-0d<*7H!Z78CWv&IEz#3sjbIULO(;?6VZ78M??L$=jkO%I);xltZaQ~pnt zUn*JiI(<_XU$ZyjothE`?mI_|32hq;*zWfvr}Z&_@7Cn&*L=n+0eWAeoYi9C^)o3_ z&{zt$RQ0t4w8XW%tUD%-Fq~j3*Cz)%Deq+9U!cK_ojAHUn>amG+~Yn>Lcc!vsj(Wi z3`aWI51cXt-lc*06>9ogPn$e7Wl9##ZlwHvNVzV{82#kuvNgfPGQiIaHAJmXH=kOTnIgcebhPTT{t> z=YUFFVKHVIW*yI;KDQAw(>&1}pc;aBPk7rj*1(FU4H>|)QW~HRG!AIh9cv0!txf;) zIjeH+pT(%MBx8c&g+k%EmvE(DiP5-wMs|oVnVtNCi*G__Ylz+&YIY7t<-odau`1l_ z&#!M72OStVbHv`jRk(=#)x0eE>Xdh)r(FZv0fKLwvvfUbgVDcxe!=+1S+iQE`Ex(s zy}f>M>w{v5h@B=C6f(1oZBH3>oGB4Lu3@0{RPdSBt+SH?e;#H=LpI{#qB#XBTSvMu zMZ%-yxZo1N?n1^RO84&t3*W&8y@MI4%I5-FPSY+0x|AZK9B#YZi#%6vhejHL*QB&Z zzKe?^@g-`t_6-4Rzlu(^ZQrj?O8?3f#l`GogmJb0Wi*Z;J@{6=k`4_%*0Juy*%e>( z+UwzdGkfiyurI94uGg53Va9KU$^3;{wsvm+upRrgJ-q&UB`AL;-ybr+O#jmpgLM*d&o7 zQm`mNp%d&;-<1_0L)~wRr#xRW^$wTq3;6g`BqO!k`;r~&yAL@6*aAI_6bc?|bQNg+PJr@S`nVCBJZTpXKMrzl$@%#R00=F#gE<4gmOB5-}{2myYcpwac+^dih z%FCr#w{X3_H_d~oqod2iA;To=hd(a*3lglvM7PTqh@@y(iM$v`O%x{06_yYm(u%?z zep2Ct{nR|TvFVc^tg5G#XWMpuwEuOvUATna%2pX7S`3}$g0?)5=3#-~443%;lK?Mp zPGJ!mBMJ9bav)oZXyv6BO&*5yqTI;FTw~e56gg5#%Kiv7^thnA7TdS*=2=*eu(L;K zNz9CUnax7(zvTOwzb*g3jku#*jYyUPF{zx5B5*^{nJ8+5=dP`@pZr}w{V_WFKU=+) zJ9<>!ePU*l4W)f{mX zBPIJWAm{iu{{sXiJ?-V|Gq~K@0%#+?ea*TOmCNSzmC2l;3hm~bp6o!f%ynz`-qd}{T^UA=TGd&kP%M^Nu z%JrVMt)9aQ#zA-IWvMXR9w0L)7+i@!aA zq+#bnvoL;lDfV@_D|51xTH#;f(4nL^H+lKgu2lc!czAB;YV&290S2!~yEL$yxRpOz z@)qHw^`Y~-k;~~H@81UW*WY0OrRgbKK62F|+wq!6PHoSUDl?ipj{TtY$r|JY>bSmS zG1^&wx(1q}=)IUf(^`*8k|j9brbxg@(4SOCSG-b(MDfZUmo}n(JjFW{^)cYt@uGdZ zhzb28jw2D}P9f3#g%i9V${e$$K;i>ow-;*-kxvgw4}LHj;>ve4L<^e|oNOy_&wXMU}S&)`0s{Ce!Q!GLeN zDd*GG!i&{X-s08|%dVXMu8{SYc$@`wlFM7TylQsy-3ipC0=WfdE5S15A+V8 z?7Hg`S3l?>U!-T4Up)TGOXs;9F>y#!t?+=!V(!pYA#ZsyxYy3Za+s-KmvsKy#h6QM zN6gASZ_1s;VI)b@$2*XsxhZT%aLfSieZbln7R3x35UjE$lANx5%BngZ)Ho)1DEaC8 zd)me|l1o=Pq{Qz$xW#0?fpFJ!z`hCUw>5uy}R73dwF z+Q`0|)t_DW{QPi&s`<`YpP9%QwkqztZ1E|%C05Ry$FLW1^?Dn^r_=9MnQdqeXpnr4 z=S9Sv+cGph@Q0EG>Bs|D+qh8M_I;p$(oJ06Ma!|9dC>gMbaixU{N;Gy%%1B+A>yNp zEQkefx4`^Jj?%_Ir;`S!cdZ9I4GJGxo5EgAWFNEb&bq7nm*2+6Qu5k00qf99;(2+W z3N&wi(2oY_L<~R%VoIZl$Vd@n6ebS9HF!Ze-*20dT=?s;{8nYPN5iTGv95pnv+>V= zyvJUVDvTfaZ3 zwsA7D|Nec^%nL4b)Qj3*b>?xw4UD!d35V-oK7>5PQga*tzbITe68En0a>IE1xzij0 z=5DcW^L_`E)*7~NPjB@uylJ-Ypj-|2iSs+7#HoYt*S=IS?G(B(fLNaz9P3u9r~9zf zPnL%@eY&61(qn>%y?dC8Nfx(!{h=_kE*g1gVW|C6yJq6eD9!mF5k!>}@<*0V=};Yy zYP^v?oHuQA<<_U$X6WlIehFrFcL2u3)O+;$&%E``FA33+(-7Y64U-5z(Qwi-H@ECK zEb4|SOQCU;r=;~Y7*RzhbaU9pJ%O{eQe!uOP1Vfb%HN~|tLeJ{%-&|8zgF9hd zv{VYCaW9x~q2Gv!>a^I*#}0D+5O&U$FyUy+5|6<*IqqJUMY*v?%CUnC*o~UKv*r+l zw|h~x_{EfSEqFoWXGs6=4~`j&d{GySNZ;YO)15Gv55B8GYqOo@4xUpxyOa6V(L?UD z;w#ntaw`>CoZa-Yp>EAfIi8ATDz*#oD*K#`2b#kk11C+kj8ZLb-s*Ap{8*%@IrO?m zQ&3EMIpuKa$a;$QG(+*X{`FKruj2TJV(tC*44l#SR(PEMm$Qsx};E zcSNRzpGDv7Wj`wUElnse%wqN+oDFCrU+aUw2ZJ_1DPgy!bMrTLk~^ZVBy&oMP)xaY zJF{#4cgNo~(|}vk5!=8ucYLKR{_{Dv>@x zOF=x{YR**sX>iIqQ~al)ii;KdUuk1%rE)!q{n}KZx;URq`8TeMY9t4xaVEH>6-pb~ zuK^$61wIb~#cUbkn z-(4YISY7*y|B+cTxul0{f44`HUFNU9z0!C*+pzy#0Qdax`{%j>L0Mc>v7=fA#1E>g zM>Gz%+&-K&@>Oyq6jp*Rbx_ES#>F$?w9nbYHPK|xVM`9JUcIvqA06x0Ef6XHrkMAf z%iw4^N%mUsKXaYK^umY&hHZBchX1yRS$gP6Up(W6b40PknUZ}^M=RyexQDy6oqxEJ zCyw^onf+pXT>YeU9e0w*fih(w?c|vCMPE-pN}YPChlFtVN39n@3qFM&r|jhv=PVo_ zo-VX8QsJNBw|4bGeQ)!&T2%XObKYCvT3K7}`t5@;%dZdtuik&|*S>n^{fDv($8k}( ziYR3$bpkawGKz@$ropJ%pnte^wx`g~)=(H0A9>PfvNgs#V>p9%=8(&oi z87H}!imoreHzc@RBnbBh6SXGwlASb!wWQkb>`f~QOq$>PUH0na`hrq_Qq0EOyMaU} z125~XUJ~=m+XZ;Xs&f}-I;ZCQUxf+?qUntoM$3Kc%zCV&W1E(KMy$8*C|0q- z+6lMFKb4o_En)3*nt3WW<-4WI3w~?&)A=wME1ddAdSi{5rp7$)!Cr&IiSeFbgRui4 zWH_j7okJM@Np{vuUMZ?OKgIc=*+jaH*>tiEmlzp|F)GD)Mr95+6#VS2K4S3rV2C?f zIJ233-J0bbQsM6Si!Tb%B9X_U4L#vr4~V(`E>cBNWw-eAJ&<+>x?k&2J_tE`4?N)S zSCKZj!T;N=uHbp7!FSN{v$^J`avS@TSD;}2-LK;idS zfXVgn^f|l9qWrtxKCk4Re|Yool}xjyFQn*bE*4#(6KwF3U$`1cQuD=1^fGLmWY*CI z8^4NY1xnLN5@+o?oEar}q2cGN^elk2<`q z3*{hR{|;|Yfy#%v*RGyW5`TUyn;PCL(aC|9WL3 z`gL^K5Pa~}8P2|p?iE8TYVG=tr2)P&pIaqISr(G9#2utJ;jIO^pYG28Mcy-W8Gp_L z^|+l#>|(DpA|K_mhM|u^mJ6}YQzu|N)0%hnr=T6GrH3dHdeZ zSc&`*r5c>+<8Ut(L%~fd?PFJT=w9vC+h6druns2Lq3u*q`&vz^_r6K~_*AOd2fWj$2E{S_mH7N2;qD$jtk4h#|LYi1NtG zGR!(iZSeITE)PkT%@ljDo|}HFitCf8#C^H!tzb1Qg`T4VWku3Wp&mF+Z*yW@ppxDCQOXg`8oSIPV;g~ zKgT?eV>Z_u_z*ADcc0lu@%f?WqShik+c!;=%lKeV;gUyAB<4z4pR>r5CZ1yg=Ma4O zNT3EV(bh;$C)`OTeimBHFz;?Nz1&8T@)k2>}>f=v6~QOCV$*9Kaj({6IH&N9!Lhz;W^ z?=h6R+g@zh;$+EePUdj^6SIMDHU(aNRT}-(N3d^i;Mif;%+OrOC~Hj_;@N$;X}c8Et#(&BX+mb zcQ%4k!j|J?*PN-}jZ#*7cLS-TTeBYwD7{|?vDp#MdqO|bzWlM`>dAf5Q26GiCR$gE z9Euq!y&q~UiJZf=|8CE}Or1;Ngf_%q zFP$*xE(KCAe0-vfOBXPm&C@WpK}e-65Y#&2BNXZ_xtg#9fOYRIL%EA;JtMuuTmmN zL|?M7>v8xZ3lYNt-(9cWzSVXo=(Tq(;m|bY;$<`E@IMhocGrH~cgE+u@)g_k!RIo% zYLB+>F~<{{)%PwW@7=fe>Yv=5ooI2&>Pe_S?l4M!Zpm1EGexglz8HNlvIGFd$}tk2+>H@1j#L_mCAY^(L4m^j?#WHRXiy zgZFodk94#^;YIR66ljLHk8^wloviGj`{x^Xhf(7fOu$tiS|)ZWqhrmqZaY5NOHmcC z7g95=Qq>s3Yi`YB%GhRl4Ar^7|H?4;T0$f>_|>$;(RS>_Sucix`ud*{_3bZ$p52J9 zKJsp6j$KK__)hM4HKnfLVOyvOdVCF@F@uylB>>X>fy>tf5$D&t-S-1mZD$X?6ag8( z=ntk0Z0&3*&OOtV-{oqKDtLXx6uqQP%5M+W;dzq7sO|~-y6|rGz0sd31o9`3qOq4A zv&}P3Ic^Hmm0S@tR?Z_wSCO#arg!-POct~%j6Z^tTwg~}ls4v`lo-p6aRTha=Ig)e z3Z`l?=9CgZez%p-^b>n&E$MHNGK00{(e>i27j2Z>mL~1J#fq__TW@xy1~wbU8V6_* zTG%t!QZurzXN25)oS}R34#95M`Od4;0jdVZ0K}n?X<(c=QjLpv+ay9UW=41U$F3wl zg&E9NeKuF5Fx^|5wer9hjE;5C{;V@k+lmS3!Ksz}0Z6dVA&88u!>)AN-?Tl)-Y8^4?kub=Ox>i;%gSk?SOq zK{~4EoGn4P9YY@C4?b6+zz3Wp(bqxcHCUn;KzDf@0G4z0{lzm*Y(;@e;Abr*s4Yxq zgOQjtD#4$xR_{njI2jo@O-Zyr(GbH`uuxJPx%AmX!{GhGsnD(3=s?=vu9vvAYiyZ2 zspWeOm8tbJJC6O6qb+mZu*k`RMG3wu@l7w>OqnFd)m4OiSz0gj!N|whwI{*sPv5ep z<;x-x;R?CEwE)PErQ`;4-BDr(qM-y<;KT3>+xxWmp~#~IrtY&;)QVDWb_ zv2JHt&s>?hns3S#Att?sm2LOUXY|r)wO*#y?^d-G26k9ht<2FM%O72dH?Ca^Fvg6R zT~QWh0U(hJJaLCXb9Q8o|0tAkedML-ak*i3K>9Y=#ePRAS)zA?!3&;jGIXfvUdtwK zo;ze+?Zg#vvA)fN7XI(_^D8J)MQ4U=_GLWTvE_e>I=r$T|$uYD|N zBd7n`0N15jia}dY;iZ6?TXOu(6|uM*0G+V!b!2m9^ijy8KPqh-z8u9q`iAvlyey*j zmk(l6{C?S1QGZ-3+sG0t+bge_t(Hi*1pjIuUOro%AlFVyr;Pf{=2_K$xE$O6ep1%+ zDn*>1J5u=r@ide^1b`<1Wb)uBzojawR}BAHF!BTg3nX)7`hq?Rc7HEVoxSf7+}9Fo z_))#8M6EzDAzbhib?Q;QI<#yfmcqU(6G}L%3k%fuhSK}br0ivw7WRE#w7oTGZ2M{R z?9Oy0oz|YiZRi3pKhS(r_IL>9ITSH4xb2N5~5pVOL$Sw1a`t*YZ zuRkBE=ORrrNC@9rQ`0v+6@SiqxL)yD{pcHVX-bfNO|T>+@xf9!(Pwz$!PKSPbDgi8kaT6Biu^u%F=)RldQC*cD;$r}CVMgq z-NXoHUcCB=z<4=#EaxihUB>kwW650!~H?6NQZYL!s1}t0~K!PtaxX`<>wn( zwhCdiSFs=e&3$O4v*qRWCzrGXbYD#8ikur{4&;^NnNL0(1H1Tu+C*W3vst5H_rfJv ziI-t9msiwGkI0`%!AAG?-K8=i&oxnw5{a-V0FXjYo4O9cB7fXO93*c4%DyRZ_`w(- z5}6{4QYHR94M3q|3tnK-6b^JYx9rpcmv`ONRg0$yM79(2V@AnXiRt>AYE#EXPay0m zrF6N~q^*DXOP^&x)lu~9KOQ&$jj+UY_j^P!FckL|cbteI8k5d}@qdk?-{r)vs1URP zc?-7Oylh7=U!DrSA+V!evbs>WsnSRL+U}T3wNIH=x~)v;%-%h+@FTu#cT!1V5bk=| zqkcl)lqDIocEd3#3OE#~P=I;`aAe>sq9E8dAao2=kBPtYTk|+JjI%dfehc}8yXC8Y zEI?`Biu&y1VEiH^6X`Sx9BpU)yCuHF3g(01JFh=i%{5hIYhm4K_oxqZsVYm(a?SLL zn_0t$^Qj+Q84jA(%~2cAjoM3A2{{p3IZ7#DcOWUC9z zG|c3)J~c&nE%jC#%h}`Qmsg$@c&;@hILM9Jcl4z?@1a-Z>MePau@Q{g=eO5%ho~>{ zs>?5GtBX8*QeZGc$yOMt4gL>cK0?I@9{?^ApdeR~GXS}rR^JPDB_bZ+X>;0(;X5Xi zlV5Dy6?0?Y=v$f7I(g4+f5!G!b7cD%%LoexHE{?NHf~hi`T3>7f_AvJ?n+KcJwt|tc;PBc6w*~UjJ%*s>P2AAr7H$n zUeQdKwPp8ya;Dtj>cJ|~HO&E@*S}RC&X<4BX1_Pju0f6^T=F>UQ!v`^R9H5qEUbP- zpR;zGw`zXqSKvnZN6)3G!(~^0IhOq^wkzBC66=-P-PcuX7Unc+y1Q9_G8Ri8?tahr zhkjkodBD7ORdwPelOH&t#!>hQ5UE{f+CPAC+RIR&?&-)yJh}bhZmxoN%5yJ$c@0Cl zEI#||)u}A0_yoo$4S6B{csA21k<49gw2of2K@es1Vb#{GiC{uya&y8#D|?HoNA6?F zech|qg1f)#QL;I=igQfG@yWMYYH6!&Wxiwb>z#U(Zja0epVm3)qB^5~0&qs3gum-?@zhqw2=hX` z64O*qx2NB)Z5AYwDGFf}H42B>{CJ;uY`!o0{Q zDt?)Lb0uge$+^fXRsjLL|8KNlafb>2rxzVy-AmvHl5_QNu+Gp0g|+oa;?QmYAf3f| zZ~-s8wId23VW9{vfaHuq8Qpa`Zk-iuMEvHGs$H3R7!zUlwvIae+a(Oz#YMr2D6=2H z4Tytq8aG&8cZTE6>_vmpQ+sEh=+n93xHh(SyOPLTeONW8>mr{=3(fltZ<+|QBo}bwIar22&R+KB=R(DojMAA{+tjbg9 zS?{Zft+!=?J$`dAXu?LpA0zdy=H6#EZAgV#r@fMif~z8OZEmNqB&+jid%@PE3%jdZ zG3FZmZz6*_=CiXnDAKf>%q7BRmY+u`k-$E&mG-H$!3%HFVoBORW>d>|h!=q}*W z%s$bKBp$SilMgtH)=dkPUTRk$KQe7RJi+JxAjD)i1)@<^U&{g)MgX6hBG9hCb_BZK z5eDiIpN9fBhw>bt+&eSFoe2o543et&fjh;QIf@NCtl++s|D(oKdOWIQaK8`c_qZlEYOd@Uf$GQ_|n zyNgRA{{tauVKHNVFUy>_s2e&I<<@_~pTH`K-HKo4LjiZ1D>Wy{HT= zA9t7lKu$bC0DlKJKyKON1@qfuJV1e-1jZ)`wtfoDd+*KmctTjnEfRP#Z`wHq8e{uj z7#D(Suq?=Nr!3j*=C#CgzIqHMVA0-z~(%1T;z_KdG_`U>Ce7wxt_bij_zV zAj^V=J8+WNo(Nbrgb8(SJ7+Rdzclp`keH~IsugMa;YNN^`j&5gB(+E#*RM05jv(o? zmv$mZjyfQ18m0}h(^=lW2XSqD#3Qoc&JIPL5qx^Dhg}lQlW4UV(#l&}ccox4^+(@$ zR2dpnaw4? zpAY**m=y5I67pZL&vcJ-6{(vg6Rc74kQ5C`a1 zxc2}0`}>TDr8^4cRn1Bq_h9eKl;$85Z{NvK4pVfc4Oz?MaF9vg%C|dN%^uI%IVFWa zH~SvxHxsIRuN?mDF91WhgU`Ev*j~}Q3bI#En0z{UE$Z#j$O4e)Njz|tT9j?n+Y1i> zo}%iuCz+?>PDXREkW_j~BbhY;F(wv`)DeQmkbg#kqps}hi(e`ml zjjvRY5ICa588=E#^tOU??NAhv!t=Dm&3LmD*J|LnR(4>0aT-lN2zqHwAW@0M7l!+! z1u)5hVD>tK_=V}H`s7od^>-{+#DCVM1^xYKlVSWlOR{vzxxhZwCyI-AuIP>2UOXW@sFQv&*dp)Aaa|}_K=7v>J`5v5 zRAm6bh-(A1mMDl*QUbV|9~Xeo7-Ad-X6e%VaYGM=%ir?>iM3c1(PGaS=E9uD27aj( z0o=ILA@zl)4`*M@WajtPGrUEB`6wcKnL6ktPAUx#i`-9!~>kDRYrmYG_vMY9%N3`Z<6R?-s&pHOK{ynv` z)YdiEU5%Q3S8LU~#$B9`O6tm*vggh@i1;{{GU^p*r*u}w<_*T+kL67dm95qjBCC4Qd z!)Hqw$*0~N>&PyD_R*6*b+OF{ZGu=6Aw`Sj8{Jg%MCK@`? zyt&>#yt&?9v$_6{wz;0xy~e)!D05q+#7%FrfuYUSGO4dE(fM#~GGZAY;#`9*o}A^* z+GE|R6EPJTj9+MpXctdDsfNn!Td(s9bi9}X1H)QN&Tz01wPR$ERo96GXbC4k3<*FG z(@@aa1dNyl9?3#c1y}-WS&$CMnE5iNU?!N4r zMs4Vwd--RM8TeFd8vvq3l{pyCFjw`8S01lw`{3EYe=%_E#aEKeN6+6HI_up*I%SSR z!7~=Fc-N~?9fMdt!PPZvD8k&ydVgABX!wM-Xu{ffxVBPf!q%7FKM^a8%Ew!qF?j66 z=;E743(XsohJbhx4%Bv70Sk-80v{Xbk41v{QxMRXg>aImEacbKVbE0eXVfYHB4vR; z69_{JT?ez`u=)KRAPA68yksM?vVr1RCoZBDHWb^4`~A|%JRtTt!eylufaa^c>2g5k zT5s(v{hp+j@7=o=)8kHDa`8zTgdwK7PF{%xQS#s+nsi%km^2Rj?8W@-X65bXp-^Yo8 zzyv6q|JBD`h9%A;Fmj;qI+%a~9hi_6DC8sF29;%RSRk?v*l$|oq@E07r;2_lf{tUN z&fZf`fVs>Sa$FJsX85Em zqj}-yv5!DBcG6!wNXP{W1DR~Za(jWEB4_;3SYl-+e^Qe0V8U)9sLZN|6P>)8=_25W z#zGS(xSarZV+D=(xq;w-I#|{K?~&vmdOmD(2G9wDovTovwkQZb0xksI_rkFf$G5SG z40C`)hRuDxCZyr7cna#sOm zk^6H(i+z_JNOZ~qr4Af_5tx&!Fti@1q4owyHvcNed!YflKES1eMr}tpS!l28E+nmon}Zb5T%xlPpvo!8qDNBAP;z-`}ZgTIDcON7o!JS$NL5dkhdvbCE+ zU^GGU52Yl_G;N+MX;U`Lw%<5h`raBzZuw9AWvvRb-DT?lG92@Y{Vgb5ja?r=m*@~* ze3ZGDJe{h9)p``A@{tR?2;u^R(F;3_1*PLl9&KO0@oS=%TR(Mt*%~>9--4k{n-D-_ zS)ZlfUJ|I9VMAe}WPQM#0Ws(I~}zE;WU1>k2;Ssa^mS>P6S48L(_co25EzZ?;f zMHOe}2Mk@Jko+aEEf2Gqn`X`0b3g~cKB8uX<)>YeUzhQsA%Uv`u861WAJi@I9~?kT zeB$+U@fQ&54u^OydndMo@L4ckf_!wG;Q{-K06#uSm%jfYc72p*nR_I_!K}M(V>KYg z$bC}_=H~`L@~QfuyLhQ?A0fZJvQ-7(7QzoVqntti70z$5qQwsyw@W0`8t(WRRJO(cEak6P2ijfT^bH18rT7? z3w|gDMNFZ2;lf{Y!s4wy7lDwy4SQqw7DLZhwYeqvNQ1Q6$TOhBO^$_KM>GHpc5?Cv zJQA=de?fgTKgfp$!1jhe&R(S9F4PctV2e;ciWt z9GBiAYwlQRHDH8f*Y1F7SK^dEDUH3`$R$Qc7LA0dSJQ%P9xwicxZpJVMf%hv_SJtP z$d}AoUSK^!;oQ<8Ts=FBB&|Xi#5xoFjF2F2kBZG@G->sjqKuQyya}CuFV?SzHTda1 zk0h~yvI;@ax>7F!XcUACC_FmGqVDA>QRUiuAw|c_!uq3ZLY7X_tE#g!!>v^nbt_K> zznkZu7jCoXv&2Ns#~_#NGYO~Vi$%=k3lj3G@15@wHNEcsI4|g9OHYi>q~~P(q-Wo- zf7!LskReTizCo#rzCmDHNazL6mWh?JfE8ruzfY`X*XNL-SALxcU449ieLbCT_LZJ& zZ-D%MnMZq)v+R2r683ZRy|dHyPUUluqoRCUI4Wxy>y)QC>Xcxh1|b-q2-DE0iY z9FN&Sibp+Lw*dH#Bq0F-?t%gvJS0AdIoIF0Bl@(WDf=k>%k|FMjkC=iW z0^~5j#QxXEeFO;HfD=RBF(q2T!$F!3J$zn$@y@1KN)Q#<77Poat0sC5f}b!V7Jh;z z^_UaT&LrpR&Pjaz>%rcTDUHuyXKfhdoh}&>Mz?0KIk24DwZ3gYn}lAM2FE zja|1-eqD~gml8LY%BI1`{{kcT_OtbIy^p<@NW5oXWk)?bVIuo5^JeM9XZtr*KSHHmQBB!mpo#@WbqVn}eeTRevP;?w`j z;^l_~7ey`h0@}54`a;v(jjR(){?Xe;qLLZ^Veo?!oc^)f#-dW!h(Gjh|G|%7qzWO)Qv7KoLE?+P_ zy4@-jaAmpYFyCy?@8&*Bi(yuSV`U4sEqK{n&m~-c^;vA!mGIx53jqX9%N<2^S^6>q z+V8lF*#~168aF1Y{OOh4!`w^q+ElHM8sA~;JZvNKKYzD&PU4)mbPGGsP5OF3_(Jk& z1e?VMg%r+=qdmS2<=J6bBif+5&J&`Q8W9ln&3Hto}yNpBXI?sMsXl9O;RM%V%$t_^q z)E~>dv6L|BS9AHF+uJR{ue;;)<+>2UWmK^#ep6!b{z6mGQX!3vQoZxk~BU=UyNUNkSUp_jiznx$H9) z${&ePS_SdWWMv!*Sijh(KtutloC}bunLs3zJ^lBEHERZFf)zxCptxa2WPdIHnB__IYAe zXl}qU2wL7c|M3UlJ4CRka}!;9|7Y@L1rWKY!CtAG$aLA^<;lSipG(Q3u4KZ0nBPFLJhFK=MPP< z3CWl!#Z$P5{yhP2loN5|Pk(mpfq$U_Cir|B2%U7es0k*sXEbA(r<6;dsT*vz`1fvq7;J&Efca!>BMot2?fN+a zw4e1U*BeuO!`BD1wi0CM!NPmeh9Nr=4_V3HtiTMSgiesSz#o0qxT{jiPwx;tp-o2? zF`(cQDBFjetp3TtB#I%=AO$WTibYOga0&0e;^44aU5jJ)>hu2C^|gsh%&3LZd8T)l zRqCHngNm?AgBP|a@3*vIE@Qit1hd7yx! z06CKdGuyNKi<3!I64QG_M;7o+I0XMqgerXanR!_*-%3@lO<&nZPG0!Sre;IFZg|J} zgk4jE$}ZY(1a5GD6jN13a0)n~Z+{*9JcGOiv^x-fpfOaO>|;P2bS3s>W5l4da^k3u zb_`tXoiU7ROSEFU#ZqX6>X$TPVSyVXSP}dzy5Ayl=lpBDX^GDX*3=vIO|%=mN>_h4 zpP{c5)#K(V9H5qZhe9daZZY5S-KAjxFyEp0)10LNy7PpKNjpOO9V*UU2)%Tb#D|PB zj^yn-Fdt653!1yCEO3Tm*0IO{=2T`oBeLtbT+{mb$=H0_txwK_YbYov-^*;LHG41& z3K*Th52}+ovzMN+7pul7(E%(0Qp)GofaL-#TZ}m*onsviV+Do>N>jPXxvy?(qeD&< zaWjU>R*LaW)@ zQl$!>*`ch{#=K|=A;m8NF%Wd0L7r6QMt=(>7RJC}EI(nmB&HK;f|yLUWh&r@zx+d7 zjkE4x4#Zfy62(PhnzzNKr@!uJp~o#sRO*Hn+Ds#cX&+{irU&D9bLQwl(-jN!bb-AS z&lq8M7`~1ELCm;HzR271aA_y*L~o{j+e~I-m|@8C3Lk(N@(~pzpc5C6QETwiniwNY zEVC%H3_921S*ohD%@%tK9!J%77REvk!DOt3sno9ojh#&yE82>!+&4?*wkZry=(V|f zRV##eK!hB7R>Y$6YfzZm8OH|>9rON9Yw}{8M^8J5Wy7#?(uc)?mlJA z2ph;?x&W*rFA_zN+kG%YrEw(u6~CE~8OH30-?y8$obSBt`K>3EnXm;pRVsA^O65k! zZmwD#W86PPx0l$HeEdcOV8p=JbX45DzsqZW=$Ne(fz$s8l+f0)OghNFjikFcKQq!* zgecPNn-0ezbTMc0kmXNtb$zYc=56m1B^yTKFFXn=$AyZpumZL9gH6~cxqHyb&Eqw| z)c{f2S5!FsE*)r;Bry?<1;~<^=(EQ`q%b?n1#zAal95nJnUPC7kFpRla$M?jiFat4 z)pZ*ALS_MtcGg*;y-SvXDVZMJV0Z?nQ=!1+NTF!ZP4x#^z=tg+5$kIqA2~g z_P|$kMb<}NDnD1Q|2crK!t63ZJJ~9y)=ZDLHf09rrI)wcD;LCU-ZvDe0bfA2u zw6!ZH5_7oY-RY5I31`nFtbMo56v6&nXnPJ-6ZyD7J!5(I^)ez(zw?R~aEWFEC5>}n zoT1$vAv9iggyn@VikN&A#sOzhHk1@Ph-@bXM-zR_&`&v!Ko8PHvLRWS^on-Bxj_$R zHqX@7rL2aj)kn-z-0?vZU4V#MGwp-6{n7>c)P#f=zA>O2MNDdm?BPJ92{8HZuON3A z)15`Hq5d~sZPRDgr$tKog2y6E>*<>!^sV~6JOX{eIgs{KWoMke$%iE*ZZs7bq)HzO zPcnn_iT8j#^s2P)L+Y~Gvirk{S> zF46X%2gAX%X~FqPQ0hmO>uP`q+fUW!S8RR?R6q!Z6tSycMXB4J#qLqLv33`xG6^)J z;E}q!y5-P;`aJZohKZJ}i`Jq2k#1$++?kCqND?W^NcMAr{>cnu4JhajIE@v}wkKqa z72UnbV3A>@g%T=E36{?YMu29Q+caS0)HX^l>C^eTJj2$IDQjzhcSHfJ=NS|y5Y&0 zQm;SE-18_ZQ*J>LiAMIw!-t~EMBo|{7+kk5>h@Tax-tFkP6O?p<}aeMpnBtBgb>Va zVkK2sN&Pgyuc54%7>>A2~3E`${{`bX=@)j0%U}`ISsLm2=sG#(ItY+Mfa< z?tK0%y{Rh@MpTDI_j3qg8{#Ayc_vu>?`E|Eol9fu6~%_lK%xPFJ{2mO=Rd?pNFj&ALTOe)uT+ z?9X@4@!Kn=cu$MA>5c307>-CB0K9NZrZ3!lW@xgg&o81)Bx|0L>v=Au-E2qRw&uXH zlZ8^ek}NgtfqI=c!|7N6kOOkVw6p6B4RH|Q>s-VHl|}e`b~}vie_sf?ufcAreVFa2 z7@dy2_&pM_>-C40>eWqx^%vzP>;9dxE?F9xO?gw$%~vg+L)hDOrX?n>*6;1stNS!! zOrg0Wu@)Gk@xvm1a>HqIXlj07s)gSffeK9-|d|vO!#CTYe}#_z$iya zqD5JFk?c|Kt>n7(o>+bPpI10P<4y}ZYY-BM ze{1fGuIX*i&V$Bg+R7|_VNwONZ*@m1JpsFIzW$q1Z?pLKa*F~9UoGl}bqbt$c5lCE zL3LHP`#zuSw&a)9FR>EmPR(?l%*k09!C0SrZbhlzI)*({N*mjwOE~IRwzF735Q;5I zLM9PY+Qw{)*5>~|C0utrRsa9Lwv4Wk6`6%Dvb$N`8=+73u5gWzz4y5GCW-7-wnEA( z*Ua9tOE$R(ab0yUF86o({?6n4dB*#^&+DA$^Zj}~pD({UI2-g62iE6a9wPEu-AwhP z&G(q>e!ML*?9fUYlh%j_gZMLQqW6b=B-@9rWF8N#83@u3USk?$(57~v1=-R4Z`a4$ zloA9q03qgOsOXr{w0e1E^01~*iS)F3X~g)h96rmO@Vndl?8NEU_jY5sd)M#lP{Q+Q zW8|2bBDkkN%E4lu1u)y`B-{uT+CHY>hSF#{_uBN4Y( zeTkKPyErwzDOn}n5-PNA#oJrC-MFiw`Z1lcKRx?KCBD1)QCkx8ayZri^5l#BU`;}?=o zdC4Pi&tAm9tyiYEh)kP_T^mY=7)cs>$l_HHACf4&s`3OQk>gPZma5|8rMz2bHKNBM zZiU2tVa`HIvt%w7yeT~&}nn3P-CcgNK(gvtK$P1)QnieB6 z1)p~9`EI@|CvI6w1A<6$x;DSY319E{5Bz})PF+z;f;$2U4$svk9g7ua7u1fC-(do(T9U~R z4`Du$7Y}(M=BjY2t`i05v1RlYR-}kl{Hixi#0^R&3tAaU?x?F|v;v&WnGj$U1$5-a zYVd5vDge8rO84K46br_vuE&b-ZpFu%to$I!Fp7`U<{{)evR!^+G>5?oY)UxXEMTDZ zB8vdr4~JHgU#LOW!v(RkZZSxe@b!rGZgDsjyfRYgv`%9;1WWl9`CIkU`(qVI|8s;6 z9@Z7Wm_^lcN9HOXB_f^jSesVC6|xIVBzcEJIT8OlIX!P z$$@j-TP!#qQ123vHzYiVPt5YY3P^ZaDs9{)U!?!=`-^SpQgGW95Ow52ayHg-w|Ill z?-V5G_gA^&_7nXDMcVy)yt2waJ&>KplB_0cK02`dd= z?*53iSy2d)IZwtL#0VLx(2DfuAzdenz@o>bn-5ivgjXNxb+l$*PC>qrM^pxM_6D$RlAu*}RgJf0_h*8X zKs=|v#5!C)`tDB6kVpK_vKR|8PFFujy~R-GuYyWF==eRAwm(TLUQaA#k=CSFo|mAn#Yt8;i={EK>@#-=wQiRon_*H>p>;Kl_-3s~ZI z@T(tuBt$juD{0%uItd)<`yJg7n>0Y4|1tZudK=nH@Q#|O_S}5_Zr1ISoGAuP#?Ouk z*!vfn8rol0<`Ig9Fz_oh{@p9_l8+?Mj%0&j4V@CjcRj+|?L{7R?8ePk$w}GpDU9JKt_f{_u5H(xPT4Ziz3_-m=Vhr0p@T>G)(K6cM4p!y#qdn(W^6W(t;@}WZv zMBcwWfiFzTJJz%XIC1B@QDDtS4jL@!6fT>3ny5OqnCtwbf70rDO9c!nV{b;C+Mam{ zl-I}IS2WItI$Ycf^unFyqb?!iRF>~^e|{r$A{Jd0J53;|dEShxJmUVKF6i0(o7w#( zeh^9(M0QPr67s3ahq|WOV|fsWUZ-pe*T}fMX+6&Acs>?!b98-k&gwyW(T~0Nz_1>d zVFDfl#zGTN7qI~pAxGk(>3S;Vk8Acd&9Ivp>`4fBfAuK5t_lrrn2~1o#^J4k-G+YA-09X*TdE@*IL8vlv? za&^^{{M`fPD%a|K@Brk?cQ3``D!U5k`n36B#{du%MbU9wsUgjdsAsA9MmLx_8J{)2 ztEhc_yTMKA@nd?;d((QMX{3AR?1I~gj{;bnnm+r_7K z(zgfFF6?rr0@^g1(30V(?NIHE-iOZf%8|c%1{q@`ZF++lN2q-0F2Awg?#iYfnCTYx zLAlP0ginh*0ZYpa%_qo{Y(J+2*V^lLE7kOQ{vCN@1 zqsk=zZszN(4O%k4M<7}t>0p%J&3BcyAn6D9`#t)jb}FoB=jE~1UgquM$Z{%)_U z)Wr-sd*)N~bWcoRj;9oQO{LAo|8Vm- zDbw=yz5)9uz>@h4wn;h~A4qju`ntS3&w6R;?b`>Uva)kth_33YG!)lZzR!;Xz_LSoceQ=wcSo2t+X2Z^3r#dle8%!2^HH6uTpc8{5Pd^*^nKFaE1<_H6o@~ zFs@67UJaHXab@5|QFwaAvQ)kdeNq)-1~dC4MaewL%TY;Mcx?EQJpY<8flgI~=@BQu zN~=nK&FlU35o%Y&IL-uCiHgM@PcH(#Z<+E&C!POl&GmiCHplXemNjfe=Sr9hZ&c)- zb|79h$Rh3KIssV_YZ@;vtFOu6k%j-Q_hsgT$YpAV^)^oXqns&>X&n>u=@us3dDoVszgzbr=dE~?>9iz&jRw%OBX+~9xnB`3vg}7r ze|JZTfOURU#L9_-sS6z@TI!zak9+`3%d4~eCO?gT-rxQ{u%yOXBh=sXCr_@F`dz%+ z>l|xsq?f{*vz{(Pjs#rcYL#@M(0C^j#}53K7;dP1x9xY_0?tgIWsM2GYm7#qF81xD zzeL-GRySb!2=de*H(ZkVc?W7$5nLjcBdMRJ7TYu;iD{#vuMcgq$!i+%wE!O_wN@x5 z)~cwLG_LZWWq+enONllwbI9%HP`6fFdZehp)=@ueZ&Zl|fNV3%E$gE6jnO)uM_1(w z7fCyJl_o;8_7hIxe7tk)%n4@M9ZCkfd%KciRpN>MTNGJTs!-$3OEzEu7bSkP#RzIf ztKGT*IkUut#X3LzyxuX%&p_#XFLJDpacj;Z)T+8-==O<6J}D{YTx)o=fk$ZWOMsQ4 z*YVBv%kPUPU4JzlyI+^fxgWF5HiSqzFMX}G4+mIJ$R9&JMDPWjY$ws28LGsw@#P0a zNzk)qd#ewOM}|^XU1>UxCf3`il0Q6IVfqv~&h{-a4Xi+d@{4mL1Gk^c+4kIYE-ch+ z%DZWJP$40DtNzuUHjH90?jV35kQL>@)k7!Eo4`Wp9yz8{VssC)ab6R=KUDc|1+?}D zTBZA$J?r^LPlsYP_V`VLv^#q6aDo*Sxa{1k<1XUvDH5AgNV>rURmx7;b_PfEAIONG z7tP$8k`B}8_TL{!bDJK&jMj9f!cnI)zA@ETg%vCIfm5Bh!cX61!G8iIdx_HBTb2X* zuBKpP{N*0m{mY-Pr1>UG)LG^{rt*SM_#R3gd?wkNW(t&kz|punVDLA$jK%-dBycbN zx3s29CyhF56Lj}4v{N<<0a?r|_I;8(8ID8~>IA58ltj)drx5)Qd>7Ig!h$&Zft&6aseGw2)*or#;OV4(YudxP1tQm2>tsQP`aCLm;9j2 zCmYiJ&-F#*ivU79uBZKJpI+igFwLrbnRQVFABE+soY4SAcEl9QYc_6VZ#uf;@~;vy zscQH*#58%OJ@keGcyyQ!RCba>nc5ucO)zpUm>u_aL)4`o?rm`*D%W$0xfhd{Mh~e# z%Ky8A%a7?#l0O?O_|z{LB5R(F?VDv{hyA1B7p4b0mj_A&r=B|lFZrOt)IqNlBD<AZ!Fi@ep8VX+FM_69k6sC%CUcc!-rIU+_D_RG3T++&thk> z4ua$7Cv|jV5BJKn>hm72#-wuA0MpolL+te}s2U6e}?{kGb){YJn*7OOZ&YCcUGpCAo&p=9#r@x-KgsFT0Ig{!o3-kq*2NhCg?;@- zH%7RK`5wfY!M*Aif8p+O@7}Zysul$SQa7QXUVYSt-v*9n6;%;>%pG2F2bz7PM4;VynSwu88H_0t8&ovDA}^(6aKk6rH%PBSGE>L~ry` z#x+*2XcG50;-%bBV_wkz1a9k;@ z*j|6+ww)vg15{|^8;oVF9*%BOl*sLF)|Y{kaboT{zxa`9>q%&@d(%xG>Un&}Wcp+q zjODBzo{J@ul!mPv4|oz=(`#N6+8+u6A)VAxU0%qBeKM6eP+$hMM&!p*&m=-ox8Rwb zL2z|J-r4jd?hi6aRz77Lew>Mc=0CV#z?@&6z&PZHlHpTkp}pMWy2`Ywsu3{C53_TdFoW* zNkKO7SztoG{B^9)fEwzhAe)D*mJ(#6!CHjZe4TEE#7YPB>9mA-j5h7`##x*>_+4#B zNk!k;T^#mK$#jJyQM$_a>_F{d<`oU?VGcf8cRf9yW;GoA>C4T6kB+!HxPHTi=4Uco zBGeNb4}KV*oz%IWIfd0>ac_64XWRL^ZLbv)+Tm5D9u$6ZPiL87aaz0sR`GRwS$3`= z9!|ndj?2BDRIXzc6OGqT&e)uggHoN~h{0Y8^U4wG#%3N7Ha1WUdzy!fYq7k{ZAyme zHN)NNF;#N!@3b>=TKz2zl<#YH$$S=ezrL-Ns94_^*zxUyZ{r3p2zzLTyY*mxEk`bH z98NL8^xx&(kU%5EYp)}#JL3A?3g%->EvLrk|JLo~61=|p98vqTu2j0jQ*O?;(@(2K z?U~xR&RIhd1w1U7A(%?;`fT=_&&W+{+!iY|($ZGWvb@|x%Poy+<~}l)Cl$9Ob*9x% zNH+_duL&EL%Sy8Umh+g3`K~R!N81jCjUVtBQ8&vVLZ1I8EU%R^P9(0B8YB!^_AbsQ z2NEBO@qrA0s&F;#y8PYdu+F;!pT>UkpccH9gyCl=} zubU#lkDll2m&Ww>U#`{<-R-~?(|nvw`p?0?!CG5Xxy-@B?r(>bG&B=Z7zA`fb^SgC ze)p1ax6;SrduJ0C?~*Lj8wPFOKU60o-?2ek@&nDkCJyL)00N(G7Zee`u^?D|d)ep9 zLYoRya@o6JMj`N9o6|_+wM~Lo*fXw3lbo3)0{5De=;+I0f|6`)ZbO*yL{|SQ7?Evz z#n$9dTz;El5{GlsH=2rme%{uSJt5?o@n8ux8APsJn~^{L3!Zg?suDsLEGyiRkPSf& zy6u!CS%rV-O+64^K_Yse3un=hcg4dAuOl5R>&>?G`9Ny%ef*I}vZC4dOxO zGTquFsIHFm|9b6SWrrX42|5_lREoze8e>D&&opKY z3|_X_^aheC*h%i3VsS@Fu0qLc=Qx*y_|}H@gqA`MY+z!!>gXW_e<;>E>uh($;;X|W z7J26Oo3OZKzW1zPIun_4lE2^G!kH50Fz?FTOZ;J`Bjs|}xI{{)GzT_d3aFwI+Ma6p z41%z0aW7X~I`C9FC*NZDLEqJ>L8Fq|+<6bjRJ2!yo_iKgjO%bzcs#`G$bY8Mc09(X zRJK+FgUB_*K1pn9C53@vs!JZI3!&LHzBF2iona9())UF_K4lQ_6%|Ey-sy{06;Y^$ z1{ia2RiGXUe@+9k-s|AjR507#GEI|=rR* z1(pR5W2d|r4mh@#EDQ=^Ymsso#Gb`fpOG}z{IA~O<+qTH&u8@>An$(`V&ArQmvSTS z{aB+_6#ObpLE|L^$JwSCi>47K!y-cSAwl84T*4Hy(9CfVBIli;tIDx}_UT*UR>qHK zFB4+}3j^6aiamQ5?<1nnUr4Dv0Vnp7N;pKNGVBmt9c$l~?X%Mxtqy_C6c93gSr%}9 zy77(7FT~)SQft!NhHsy~ir+*tE$tMf%v%6cT#%A7JdWDOuy0nqJ{+>j21QaQ*wC&1 z-r`}*gGytVJX2fmbf~akgK5oBZUDmgq58u33<={MC!b^wpZ>W{HRJuE;5~X2c?T|E zQ8;@Mv<9a|sfmrQT)a0~I7y>dYbh4$zvN^eJP2=_2)Kk!H zbM8QV@l>IpH!U)dX=vc(`#X|)BohIXY;;s-5Yu9H+Msar#G~>FGY2k}1CD?fGeWY{ zDdH51UvgW2svM!;HqIU^_LTAdd&uoXo9=#{ z{u#97VMe$TX2OWIPKj-OS`$rB=~qQbjF+_*3qg>=`hI=UvRNArHFO(zCN1n&SVq#< z?d`r|)~X*H-PgfhpG~N-E-CSWmc>r$${4&MA^)@{c!SJ>d80q2oDgCX6If`bCBQq|vDCNs@slX#c4u~vWRKY#8`#%sYs1fEp*QU5+dk4Yn?I~jxX!iujA7A5 zy+4IRJ(AOAbL>zJKlV=iu;}j=h{5G56(ebLg4-DEHDhd#4S6pfWwyrkyEHJ91h)Og V`gRq~qyPf8|I~FK)~Q-Y{U4=vt$qLi literal 0 HcmV?d00001 From da2c5e4bc1fbe62d135090aef96f28bee1e93056 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:09 +0800 Subject: [PATCH 272/314] Use the 1.21.1 pack formats in the mod's pack.mcmeta --- minecraft-1.21.1/src/main/resources/pack.mcmeta | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 minecraft-1.21.1/src/main/resources/pack.mcmeta diff --git a/minecraft-1.21.1/src/main/resources/pack.mcmeta b/minecraft-1.21.1/src/main/resources/pack.mcmeta new file mode 100644 index 0000000..de03af8 --- /dev/null +++ b/minecraft-1.21.1/src/main/resources/pack.mcmeta @@ -0,0 +1,7 @@ +{ + "pack": { + "description": "${mod_name} resources", + "pack_format": 34, + "supported_formats": [34, 48] + } +} From d54a892701b6148af1714a6b2b5424439b52fcea Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:10 +0800 Subject: [PATCH 273/314] Add the forge 1.21.1 module --- forge-1.21.1/build.gradle | 40 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 forge-1.21.1/build.gradle diff --git a/forge-1.21.1/build.gradle b/forge-1.21.1/build.gradle new file mode 100644 index 0000000..17131ee --- /dev/null +++ b/forge-1.21.1/build.gradle @@ -0,0 +1,40 @@ +plugins { + id 'java' + id 'idea' + id 'eclipse' + id 'net.minecraftforge.gradle' +} + +apply from: rootProject.file('gradle/loader.gradle') + +// The original MCHeli 1.0.3 download. Players supply their own copy; development runs use this +// one (git-ignored, see docs/DEVELOPMENT.md). +def legacyZip = rootProject.file('Mod/MC1.7.10_mcheli_1.0.3.zip').absolutePath + +minecraft { + mappings channel: 'official', version: minecraft_version + + runs { + configureEach { + workingDir = rootProject.layout.projectDirectory.dir("run-${minecraft_version}") + systemProperty 'mcheli.legacyZip', legacyZip + } + + register('client') + + register('server') { + args '--nogui' + } + } +} + +repositories { + minecraft.mavenizer(it) + maven fg.forgeMaven + maven fg.minecraftLibsMaven + mavenCentral() +} + +dependencies { + implementation minecraft.dependency("net.minecraftforge:forge:${minecraft_version}-${forge_version}") +} From 6260592431be0b2fffd5bd941a605f93616c5684 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:10 +0800 Subject: [PATCH 274/314] Add the forge 1.21.1 module properties --- forge-1.21.1/gradle.properties | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 forge-1.21.1/gradle.properties diff --git a/forge-1.21.1/gradle.properties b/forge-1.21.1/gradle.properties new file mode 100644 index 0000000..5bd863d --- /dev/null +++ b/forge-1.21.1/gradle.properties @@ -0,0 +1,7 @@ +# Minecraft 1.21.1 on Forge +loader=forge +minecraft_version=1.21.1 +minecraft_version_range=[1.21.1,1.21.2) +forge_version=52.1.20 +loader_version_range=[52,) +java_version=21 From 2a90b946cfb4c88025d079566eb8f419c6121c8a Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:11 +0800 Subject: [PATCH 275/314] Add ForgeClient to the forge 1.21.1 module --- .../jnhprojects/mcheli/forge/ForgeClient.java | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java diff --git a/forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java b/forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java new file mode 100644 index 0000000..eaf21a8 --- /dev/null +++ b/forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeClient.java @@ -0,0 +1,111 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.client.CameraAngles; +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.client.particle.GlowParticle; +import com.jnhprojects.mcheli.registry.ModParticles; +import net.minecraft.client.Camera; +import net.minecraftforge.client.event.AddGuiOverlayLayersEvent; +import net.minecraftforge.client.event.ClientPlayerNetworkEvent; +import net.minecraftforge.client.event.EntityRenderersEvent; +import net.minecraftforge.client.event.InputEvent; +import net.minecraftforge.client.event.RegisterClientReloadListenersEvent; +import net.minecraftforge.client.event.RegisterKeyMappingsEvent; +import net.minecraftforge.client.event.RegisterParticleProvidersEvent; +import net.minecraftforge.client.event.RenderHandEvent; +import net.minecraftforge.client.event.RenderLivingEvent; +import net.minecraftforge.client.event.ViewportEvent; +import net.minecraftforge.client.gui.overlay.ForgeLayeredDraw; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.eventbus.api.EventPriority; +import net.minecraftforge.eventbus.api.IEventBus; + +/** Client events on Forge. Only loaded on a physical client. */ +final class ForgeClient { + private ForgeClient() { + } + + static void init(IEventBus modBus) { + ClientHooks.init(); + modBus.addListener((EntityRenderersEvent.RegisterRenderers event) -> ClientHooks.registerEntityRenderers(event::registerEntityRenderer)); + modBus.addListener((RegisterParticleProvidersEvent event) -> event.registerSpriteSet(ModParticles.GLOW.get(), GlowParticle.Provider::new)); + modBus.addListener((RegisterKeyMappingsEvent event) -> ClientHooks.keyMappings().forEach(event::register)); + modBus.addListener((RegisterClientReloadListenersEvent event) -> + ClientHooks.reloadListeners().values().forEach(event::registerReloadListener)); + modBus.addListener((AddGuiOverlayLayersEvent event) -> event.getLayeredDraw() + .add(ForgeLayeredDraw.POST_SLEEP_STACK, ClientHooks.HUD_LAYER, ClientHooks::drawHud)); + + IEventBus forgeBus = MinecraftForge.EVENT_BUS; + forgeBus.addListener((TickEvent.ClientTickEvent event) -> { + if (event.phase == TickEvent.Phase.END) { + ClientHooks.clientTickEnd(); + } + }); + forgeBus.addListener((ViewportEvent.ComputeCameraAngles event) -> ClientHooks.computeCameraAngles(new Angles(event))); + forgeBus.addListener((RenderHandEvent event) -> event.setCanceled(ClientHooks.hidesHand())); + forgeBus.addListener(EventPriority.NORMAL, false, RenderLivingEvent.Pre.class, + event -> ClientHooks.beforeRenderLiving(event.getEntity(), event.getPoseStack(), event.getPartialTick())); + forgeBus.addListener(ForgeClient::onInteractionKey); + forgeBus.addListener((ClientPlayerNetworkEvent.LoggingIn event) -> ClientHooks.loggedIn(event.getPlayer())); + forgeBus.addListener((ClientPlayerNetworkEvent.LoggingOut event) -> ClientHooks.loggedOut()); + } + + private static void onInteractionKey(InputEvent.InteractionKeyMappingTriggered event) { + if (event.isUseItem()) { + switch (ClientHooks.onUseKey(event.getHand())) { + case PASS -> { + } + case CONSUME -> { + event.setSwingHand(false); + event.setCanceled(true); + } + case CONSUME_AND_SWING -> event.setCanceled(true); + } + } else if (event.isPickBlock() && ClientHooks.onPickBlockKey()) { + event.setCanceled(true); + } + } + + private record Angles(ViewportEvent.ComputeCameraAngles event) implements CameraAngles { + @Override + public Camera camera() { + return event.getCamera(); + } + + @Override + public float partialTick() { + return (float) event.getPartialTick(); + } + + @Override + public float yaw() { + return event.getYaw(); + } + + @Override + public float pitch() { + return event.getPitch(); + } + + @Override + public float roll() { + return event.getRoll(); + } + + @Override + public void setYaw(float yaw) { + event.setYaw(yaw); + } + + @Override + public void setPitch(float pitch) { + event.setPitch(pitch); + } + + @Override + public void setRoll(float roll) { + event.setRoll(roll); + } + } +} From 9f4e013b79bc6755682e44c865d52dad1c9a2383 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:11 +0800 Subject: [PATCH 276/314] Add ForgeNetwork to the forge 1.21.1 module --- .../mcheli/forge/ForgeNetwork.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java diff --git a/forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java b/forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java new file mode 100644 index 0000000..883dd91 --- /dev/null +++ b/forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeNetwork.java @@ -0,0 +1,58 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.network.ModNetwork; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.network.Channel; +import net.minecraftforge.network.ChannelBuilder; +import net.minecraftforge.network.payload.PayloadFlow; + +/** + * Registers the shared packets on a Forge payload channel. Each packet travels under its own id, + * as on NeoForge and Fabric; the channel only adds Forge's version check. + */ +final class ForgeNetwork { + private static Channel channel; + + private ForgeNetwork() { + } + + static void register() { + PayloadFlow flow = ChannelBuilder + .named(ResourceLocation.fromNamespaceAndPath(MCHeli.MOD_ID, "main")) + .networkProtocolVersion(ModNetwork.PROTOCOL_VERSION) + .payloadChannel() + .play() + .serverbound(); + for (ModNetwork.Serverbound packet : ModNetwork.SERVERBOUND) { + flow = serverbound(flow, packet); + } + flow = flow.clientbound(); + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + flow = clientbound(flow, packet); + } + channel = flow.build(); + } + + static Channel channel() { + return channel; + } + + private static PayloadFlow serverbound( + PayloadFlow flow, ModNetwork.Serverbound packet) { + return flow.add(packet.type(), packet.codec(), (msg, ctx) -> { + ctx.enqueueWork(() -> packet.handler().accept(msg, ctx.getSender())); + ctx.setPacketHandled(true); + }); + } + + private static PayloadFlow clientbound( + PayloadFlow flow, ModNetwork.Clientbound packet) { + return flow.add(packet.type(), packet.codec(), (msg, ctx) -> { + ctx.enqueueWork(() -> packet.handler().accept(msg)); + ctx.setPacketHandled(true); + }); + } +} From 24c60f8c20117060c273ae5ebe6db7b30d4083bf Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:11 +0800 Subject: [PATCH 277/314] Add ForgePlatform to the forge 1.21.1 module --- .../mcheli/forge/ForgePlatform.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java diff --git a/forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java b/forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java new file mode 100644 index 0000000..d8b9d11 --- /dev/null +++ b/forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/ForgePlatform.java @@ -0,0 +1,58 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.loading.FMLEnvironment; +import net.minecraftforge.fml.loading.FMLPaths; +import net.minecraftforge.network.PacketDistributor; + +import java.nio.file.Path; + +final class ForgePlatform implements Platform { + @Override + public String loaderName() { + return "forge"; + } + + @Override + public Path gameDir() { + return FMLPaths.GAMEDIR.get(); + } + + @Override + public Path configDir() { + return FMLPaths.CONFIGDIR.get(); + } + + @Override + public String modVersion() { + return ModList.get().getModContainerById(MCHeli.MOD_ID) + .map(c -> c.getModInfo().getVersion().toString()).orElse(""); + } + + @Override + public boolean isClient() { + return FMLEnvironment.dist == Dist.CLIENT; + } + + @Override + public void sendToServer(CustomPacketPayload payload) { + ForgeNetwork.channel().send(payload, PacketDistributor.SERVER.noArg()); + } + + @Override + public void sendToPlayer(ServerPlayer player, CustomPacketPayload payload) { + ForgeNetwork.channel().send(payload, PacketDistributor.PLAYER.with(player)); + } + + @Override + public CreativeModeTab.Builder creativeTab(ResourceKey after) { + return CreativeModeTab.builder().withTabsBefore(after); + } +} From 0a9118133ed436d452733f6f396f5b8f1dd95eff Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:12 +0800 Subject: [PATCH 278/314] Add ForgeVehicleIngredient to the forge 1.21.1 module --- .../mcheli/forge/ForgeVehicleIngredient.java | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java diff --git a/forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java b/forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java new file mode 100644 index 0000000..057d006 --- /dev/null +++ b/forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/ForgeVehicleIngredient.java @@ -0,0 +1,80 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.mojang.serialization.MapCodec; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraftforge.common.crafting.ingredients.AbstractIngredient; +import net.minecraftforge.common.crafting.ingredients.IIngredientSerializer; +import org.jspecify.annotations.Nullable; + +/** {@link VehicleIngredient} as a Forge ingredient, read from the {@code type} key. */ +final class ForgeVehicleIngredient extends AbstractIngredient { + static final MapCodec CODEC = VehicleIngredient.codec(ForgeVehicleIngredient::new, ForgeVehicleIngredient::vehicle); + static final StreamCodec STREAM_CODEC = + VehicleIngredient.streamCodec(ForgeVehicleIngredient::new, ForgeVehicleIngredient::vehicle); + static final IIngredientSerializer SERIALIZER = new IIngredientSerializer<>() { + @Override + public MapCodec codec() { + return CODEC; + } + + @Override + public void write(RegistryFriendlyByteBuf buffer, ForgeVehicleIngredient value) { + STREAM_CODEC.encode(buffer, value); + } + + @Override + public ForgeVehicleIngredient read(RegistryFriendlyByteBuf buffer) { + return STREAM_CODEC.decode(buffer); + } + }; + + private final String vehicle; + + private ForgeVehicleIngredient(String vehicle) { + this.vehicle = vehicle; + } + + String vehicle() { + return vehicle; + } + + @Override + public boolean test(@Nullable ItemStack stack) { + return VehicleIngredient.matches(vehicle, stack); + } + + /** Read when shown, so a vehicle loaded after the recipes still appears. */ + @Override + public ItemStack[] getItems() { + return VehicleIngredient.displayStacks(vehicle).toArray(ItemStack[]::new); + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public boolean isSimple() { + return false; + } + + @Override + public IIngredientSerializer serializer() { + return SERIALIZER; + } + + @Override + public boolean equals(Object o) { + return o instanceof ForgeVehicleIngredient other && vehicle.equals(other.vehicle); + } + + @Override + public int hashCode() { + return vehicle.hashCode(); + } +} From e844150ed946c352f5b1d0bac66da9047a074c59 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:12 +0800 Subject: [PATCH 279/314] Add McheliForge to the forge 1.21.1 module --- .../jnhprojects/mcheli/forge/McheliForge.java | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java diff --git a/forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java b/forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java new file mode 100644 index 0000000..f072658 --- /dev/null +++ b/forge-1.21.1/src/main/java/com/jnhprojects/mcheli/forge/McheliForge.java @@ -0,0 +1,74 @@ +package com.jnhprojects.mcheli.forge; + +import com.jnhprojects.mcheli.CommonHooks; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.AddPackFindersEvent; +import net.minecraftforge.event.RegisterCommandsEvent; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.event.server.ServerAboutToStartEvent; +import net.minecraftforge.event.server.ServerStoppedEvent; +import net.minecraftforge.eventbus.api.IEventBus; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.registries.ForgeRegistries; +import net.minecraftforge.registries.RegisterEvent; + +/** The Forge entry point: everything here only connects Forge to the shared code. */ +@Mod(MCHeli.MOD_ID) +public final class McheliForge { + + public McheliForge(FMLJavaModLoadingContext context) { + IEventBus modBus = context.getModEventBus(); + Platform.install(new ForgePlatform()); + MCHeli.init(); + + modBus.addListener((RegisterEvent event) -> { + for (ResourceKey> registry : Registration.registries()) { + register(event, Registration.typed(registry)); + } + event.register(ForgeRegistries.Keys.INGREDIENT_SERIALIZERS, + helper -> helper.register(VehicleIngredient.ID, ForgeVehicleIngredient.SERIALIZER)); + }); + ForgeNetwork.register(); + modBus.addListener((AddPackFindersEvent event) -> event.addRepositorySource(CommonHooks.packSource())); + + IEventBus forgeBus = MinecraftForge.EVENT_BUS; + forgeBus.addListener((ServerAboutToStartEvent event) -> CommonHooks.serverAboutToStart(event.getServer())); + forgeBus.addListener((ServerStoppedEvent event) -> CommonHooks.serverStopped()); + forgeBus.addListener((TickEvent.ServerTickEvent event) -> { + if (event.phase == TickEvent.Phase.END) { + CommonHooks.serverTickEnd(event.getServer()); + } + }); + forgeBus.addListener((RegisterCommandsEvent event) -> CommonHooks.registerCommands(event.getDispatcher())); + forgeBus.addListener((PlayerEvent.PlayerLoggedInEvent event) -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedIn(player); + } + }); + forgeBus.addListener((PlayerEvent.PlayerLoggedOutEvent event) -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedOut(player); + } + }); + + if (Platform.get().isClient()) { + ForgeClient.init(modBus); + } + } + + /** Only once Forge asks for that registry, with our entries in declaration order. */ + private static void register(RegisterEvent event, ResourceKey> registry) { + if (event.getRegistryKey().equals(registry)) { + event.register(registry, helper -> Registration.registerAll(registry, helper::register)); + } + } +} From b7ff32078973eb8fed26dd5158663d100ae637ea Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:13 +0800 Subject: [PATCH 280/314] Add the forge 1.21.1 mod metadata --- .../src/main/resources/META-INF/mods.toml | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 forge-1.21.1/src/main/resources/META-INF/mods.toml diff --git a/forge-1.21.1/src/main/resources/META-INF/mods.toml b/forge-1.21.1/src/main/resources/META-INF/mods.toml new file mode 100644 index 0000000..1fdc3b4 --- /dev/null +++ b/forge-1.21.1/src/main/resources/META-INF/mods.toml @@ -0,0 +1,33 @@ +modLoader="javafml" +loaderVersion="${loader_version_range}" +license="${mod_license}" +issueTrackerURL="https://github.com/JnH-Projects/MCHeli-Rework-Forge/issues" + +[[mods]] +modId="${mod_id}" +version="${mod_version}" +displayName="${mod_name}" +displayURL="https://github.com/JnH-Projects/MCHeli-Rework-Forge" +logoFile="mcheli_logo.png" +authors="${mod_authors}" +credits="EMB4 created the original MCHeli mod and all of its vehicle content. Fureniku maintains the CurseForge project." +description=''' +Helicopters, planes, tanks and vehicles for modern Minecraft Forge. +A ground-up rework of MCHeli 1.0.3 by EMB4, which stopped at Minecraft 1.7.10. +The vehicles come from your own copy of the original: put MC1.7.10_mcheli_1.0.3.zip from the +MCHeli CurseForge page into the "mcheli" folder of the game directory. +''' + +[[dependencies.${mod_id}]] + modId="forge" + mandatory=true + versionRange="${loader_version_range}" + ordering="NONE" + side="BOTH" + +[[dependencies.${mod_id}]] + modId="minecraft" + mandatory=true + versionRange="${minecraft_version_range}" + ordering="NONE" + side="BOTH" From 9c50ea69a5b4a7712196d528de9a7ec29d44c3c3 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:13 +0800 Subject: [PATCH 281/314] Add the neoforge 1.21.1 module --- neoforge-1.21.1/build.gradle | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 neoforge-1.21.1/build.gradle diff --git a/neoforge-1.21.1/build.gradle b/neoforge-1.21.1/build.gradle new file mode 100644 index 0000000..60829c3 --- /dev/null +++ b/neoforge-1.21.1/build.gradle @@ -0,0 +1,40 @@ +plugins { + id 'java' + id 'idea' + id 'eclipse' + id 'net.neoforged.moddev' +} + +apply from: rootProject.file('gradle/loader.gradle') + +// The original MCHeli 1.0.3 download. Players supply their own copy; development runs use this +// one (git-ignored, see docs/DEVELOPMENT.md). +def legacyZip = rootProject.file('Mod/MC1.7.10_mcheli_1.0.3.zip').absolutePath + +neoForge { + version = neoforge_version + + runs { + configureEach { + gameDirectory = rootProject.file("run-${minecraft_version}") + systemProperty 'mcheli.legacyZip', legacyZip + } + client { + client() + } + server { + server() + programArgument '--nogui' + } + } + + mods { + mcheli { + sourceSet sourceSets.main + } + } +} + +repositories { + mavenCentral() +} From 34ada45d9f58bee3601688bdf99b754de82b7f27 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:14 +0800 Subject: [PATCH 282/314] Add the neoforge 1.21.1 module properties --- neoforge-1.21.1/gradle.properties | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 neoforge-1.21.1/gradle.properties diff --git a/neoforge-1.21.1/gradle.properties b/neoforge-1.21.1/gradle.properties new file mode 100644 index 0000000..f9272dd --- /dev/null +++ b/neoforge-1.21.1/gradle.properties @@ -0,0 +1,7 @@ +# Minecraft 1.21.1 on NeoForge +loader=neoforge +minecraft_version=1.21.1 +minecraft_version_range=[1.21.1,1.21.2) +neoforge_version=21.1.265 +loader_version_range=[21.1.200,) +java_version=21 From 438229400cca04e1613de99dab0a5d0c8417cd76 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:14 +0800 Subject: [PATCH 283/314] Add McheliNeoForge to the neoforge 1.21.1 module --- .../mcheli/neoforge/McheliNeoForge.java | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java diff --git a/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java b/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java new file mode 100644 index 0000000..734b0ed --- /dev/null +++ b/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/McheliNeoForge.java @@ -0,0 +1,91 @@ +package com.jnhprojects.mcheli.neoforge; + +import com.jnhprojects.mcheli.CommonHooks; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registration; +import net.minecraft.core.Registry; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.AddPackFindersEvent; +import net.neoforged.neoforge.event.RegisterCommandsEvent; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import net.neoforged.neoforge.event.server.ServerAboutToStartEvent; +import net.neoforged.neoforge.event.server.ServerStoppedEvent; +import net.neoforged.neoforge.event.tick.ServerTickEvent; +import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; +import net.neoforged.neoforge.network.registration.PayloadRegistrar; +import net.neoforged.neoforge.registries.NeoForgeRegistries; +import net.neoforged.neoforge.registries.RegisterEvent; + +/** The NeoForge entry point: everything here only connects NeoForge to the shared code. */ +@Mod(MCHeli.MOD_ID) +public final class McheliNeoForge { + + public McheliNeoForge(IEventBus modBus) { + Platform.install(new NeoForgePlatform()); + MCHeli.init(); + + modBus.addListener(RegisterEvent.class, event -> { + for (ResourceKey> registry : Registration.registries()) { + register(event, Registration.typed(registry)); + } + event.register(NeoForgeRegistries.Keys.INGREDIENT_TYPES, + helper -> helper.register(VehicleIngredient.ID, NeoForgeVehicleIngredient.TYPE)); + }); + modBus.addListener(RegisterPayloadHandlersEvent.class, McheliNeoForge::registerPackets); + modBus.addListener(AddPackFindersEvent.class, event -> event.addRepositorySource(CommonHooks.packSource())); + + NeoForge.EVENT_BUS.addListener(ServerAboutToStartEvent.class, event -> CommonHooks.serverAboutToStart(event.getServer())); + NeoForge.EVENT_BUS.addListener(ServerStoppedEvent.class, event -> CommonHooks.serverStopped()); + NeoForge.EVENT_BUS.addListener(ServerTickEvent.Post.class, event -> CommonHooks.serverTickEnd(event.getServer())); + NeoForge.EVENT_BUS.addListener(RegisterCommandsEvent.class, event -> CommonHooks.registerCommands(event.getDispatcher())); + NeoForge.EVENT_BUS.addListener(PlayerEvent.PlayerLoggedInEvent.class, event -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedIn(player); + } + }); + NeoForge.EVENT_BUS.addListener(PlayerEvent.PlayerLoggedOutEvent.class, event -> { + if (event.getEntity() instanceof ServerPlayer player) { + CommonHooks.playerLoggedOut(player); + } + }); + + if (Platform.get().isClient()) { + NeoForgeClient.init(modBus); + } + } + + /** Only once NeoForge asks for that registry, with our entries in declaration order. */ + private static void register(RegisterEvent event, ResourceKey> registry) { + if (event.getRegistryKey().equals(registry)) { + event.register(registry, helper -> Registration.registerAll(registry, helper::register)); + } + } + + /** Handlers run on the main thread, which is NeoForge's default. */ + private static void registerPackets(RegisterPayloadHandlersEvent event) { + PayloadRegistrar registrar = event.registrar(Integer.toString(ModNetwork.PROTOCOL_VERSION)); + for (ModNetwork.Serverbound packet : ModNetwork.SERVERBOUND) { + serverbound(registrar, packet); + } + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + clientbound(registrar, packet); + } + } + + private static void serverbound(PayloadRegistrar registrar, ModNetwork.Serverbound packet) { + registrar.playToServer(packet.type(), packet.codec(), + (msg, context) -> packet.handler().accept(msg, (ServerPlayer) context.player())); + } + + private static void clientbound(PayloadRegistrar registrar, ModNetwork.Clientbound packet) { + registrar.playToClient(packet.type(), packet.codec(), (msg, context) -> packet.handler().accept(msg)); + } +} From fa9b716f1e619f4330a5819eaab8a0d0713eb302 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:15 +0800 Subject: [PATCH 284/314] Add NeoForgeClient to the neoforge 1.21.1 module --- .../mcheli/neoforge/NeoForgeClient.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java diff --git a/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java b/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java new file mode 100644 index 0000000..31b238a --- /dev/null +++ b/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeClient.java @@ -0,0 +1,65 @@ +package com.jnhprojects.mcheli.neoforge; + +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.client.particle.GlowParticle; +import com.jnhprojects.mcheli.registry.ModParticles; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent; +import net.neoforged.neoforge.client.event.ClientTickEvent; +import net.neoforged.neoforge.client.event.EntityRenderersEvent; +import net.neoforged.neoforge.client.event.InputEvent; +import net.neoforged.neoforge.client.event.RegisterClientReloadListenersEvent; +import net.neoforged.neoforge.client.event.RegisterGuiLayersEvent; +import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent; +import net.neoforged.neoforge.client.event.RegisterParticleProvidersEvent; +import net.neoforged.neoforge.client.event.RenderHandEvent; +import net.neoforged.neoforge.client.event.RenderLivingEvent; +import net.neoforged.neoforge.client.gui.VanillaGuiLayers; +import net.neoforged.neoforge.common.NeoForge; + +/** + * Client events on NeoForge. Only loaded on a physical client. The camera hook is a mixin + * ({@code GameRendererMixin}): NeoForge's camera event fires before vanilla positions the camera, + * so a cockpit eye set from it would be overwritten. + */ +final class NeoForgeClient { + private NeoForgeClient() { + } + + static void init(IEventBus modBus) { + ClientHooks.init(); + modBus.addListener(EntityRenderersEvent.RegisterRenderers.class, + event -> ClientHooks.registerEntityRenderers(event::registerEntityRenderer)); + modBus.addListener(RegisterParticleProvidersEvent.class, + event -> event.registerSpriteSet(ModParticles.GLOW.get(), GlowParticle.Provider::new)); + modBus.addListener(RegisterKeyMappingsEvent.class, event -> ClientHooks.keyMappings().forEach(event::register)); + modBus.addListener(RegisterClientReloadListenersEvent.class, + event -> ClientHooks.reloadListeners().values().forEach(event::registerReloadListener)); + modBus.addListener(RegisterGuiLayersEvent.class, + event -> event.registerAbove(VanillaGuiLayers.SLEEP_OVERLAY, ClientHooks.HUD_LAYER, ClientHooks::drawHud)); + + NeoForge.EVENT_BUS.addListener(ClientTickEvent.Post.class, event -> ClientHooks.clientTickEnd()); + NeoForge.EVENT_BUS.addListener(RenderHandEvent.class, event -> event.setCanceled(ClientHooks.hidesHand())); + NeoForge.EVENT_BUS.addListener(RenderLivingEvent.Pre.class, + event -> ClientHooks.beforeRenderLiving(event.getEntity(), event.getPoseStack(), event.getPartialTick())); + NeoForge.EVENT_BUS.addListener(InputEvent.InteractionKeyMappingTriggered.class, NeoForgeClient::onInteractionKey); + NeoForge.EVENT_BUS.addListener(ClientPlayerNetworkEvent.LoggingIn.class, event -> ClientHooks.loggedIn(event.getPlayer())); + NeoForge.EVENT_BUS.addListener(ClientPlayerNetworkEvent.LoggingOut.class, event -> ClientHooks.loggedOut()); + } + + private static void onInteractionKey(InputEvent.InteractionKeyMappingTriggered event) { + if (event.isUseItem()) { + switch (ClientHooks.onUseKey(event.getHand())) { + case PASS -> { + } + case CONSUME -> { + event.setSwingHand(false); + event.setCanceled(true); + } + case CONSUME_AND_SWING -> event.setCanceled(true); + } + } else if (event.isPickBlock() && ClientHooks.onPickBlockKey()) { + event.setCanceled(true); + } + } +} From a389b916424ce0f8e2a8f15d6ad77d54c88bbdc6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:15 +0800 Subject: [PATCH 285/314] Add NeoForgePlatform to the neoforge 1.21.1 module --- .../mcheli/neoforge/NeoForgePlatform.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java diff --git a/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java b/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java new file mode 100644 index 0000000..ab35358 --- /dev/null +++ b/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgePlatform.java @@ -0,0 +1,58 @@ +package com.jnhprojects.mcheli.neoforge; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.fml.ModList; +import net.neoforged.fml.loading.FMLEnvironment; +import net.neoforged.fml.loading.FMLPaths; +import net.neoforged.neoforge.network.PacketDistributor; + +import java.nio.file.Path; + +final class NeoForgePlatform implements Platform { + @Override + public String loaderName() { + return "neoforge"; + } + + @Override + public Path gameDir() { + return FMLPaths.GAMEDIR.get(); + } + + @Override + public Path configDir() { + return FMLPaths.CONFIGDIR.get(); + } + + @Override + public String modVersion() { + return ModList.get().getModContainerById(MCHeli.MOD_ID) + .map(c -> c.getModInfo().getVersion().toString()).orElse(""); + } + + @Override + public boolean isClient() { + return FMLEnvironment.dist == Dist.CLIENT; + } + + @Override + public void sendToServer(CustomPacketPayload payload) { + PacketDistributor.sendToServer(payload); + } + + @Override + public void sendToPlayer(ServerPlayer player, CustomPacketPayload payload) { + PacketDistributor.sendToPlayer(player, payload); + } + + @Override + public CreativeModeTab.Builder creativeTab(ResourceKey after) { + return CreativeModeTab.builder().withTabsBefore(after); + } +} From 47f891e0d036ec778f137b8a80bc82bb9a848877 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:16 +0800 Subject: [PATCH 286/314] Add NeoForgeVehicleIngredient to the neoforge 1.21.1 module --- .../neoforge/NeoForgeVehicleIngredient.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java diff --git a/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java b/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java new file mode 100644 index 0000000..0e7ddc9 --- /dev/null +++ b/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/NeoForgeVehicleIngredient.java @@ -0,0 +1,35 @@ +package com.jnhprojects.mcheli.neoforge; + +import com.jnhprojects.mcheli.item.VehicleIngredient; +import net.minecraft.world.item.ItemStack; +import net.neoforged.neoforge.common.crafting.ICustomIngredient; +import net.neoforged.neoforge.common.crafting.IngredientType; + +import java.util.stream.Stream; + +/** {@link VehicleIngredient} as a NeoForge ingredient, read from the {@code neoforge:ingredient_type} key. */ +record NeoForgeVehicleIngredient(String vehicle) implements ICustomIngredient { + static final IngredientType TYPE = new IngredientType<>( + VehicleIngredient.codec(NeoForgeVehicleIngredient::new, NeoForgeVehicleIngredient::vehicle), + VehicleIngredient.streamCodec(NeoForgeVehicleIngredient::new, NeoForgeVehicleIngredient::vehicle)); + + @Override + public boolean test(ItemStack stack) { + return VehicleIngredient.matches(vehicle, stack); + } + + @Override + public Stream getItems() { + return VehicleIngredient.displayStacks(vehicle); + } + + @Override + public boolean isSimple() { + return false; + } + + @Override + public IngredientType getType() { + return TYPE; + } +} From a47a82f66696dcb8817d5c62cd73238323d80a33 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:16 +0800 Subject: [PATCH 287/314] Add CameraInvoker to the neoforge 1.21.1 module --- .../mcheli/neoforge/mixin/CameraInvoker.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/CameraInvoker.java diff --git a/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/CameraInvoker.java b/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/CameraInvoker.java new file mode 100644 index 0000000..4c289d0 --- /dev/null +++ b/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/CameraInvoker.java @@ -0,0 +1,12 @@ +package com.jnhprojects.mcheli.neoforge.mixin; + +import net.minecraft.client.Camera; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +/** NeoForge adds a roll to {@code Camera#setRotation}, but leaves it protected. */ +@Mixin(Camera.class) +public interface CameraInvoker { + @Invoker("setRotation") + void mcheli$setRotation(float yRot, float xRot, float roll); +} From 1f4539c56c6765a6fba91b42ab2960d45c2cf65b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:17 +0800 Subject: [PATCH 288/314] Add GameRendererMixin to the neoforge 1.21.1 module --- .../neoforge/mixin/GameRendererMixin.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererMixin.java diff --git a/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererMixin.java b/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererMixin.java new file mode 100644 index 0000000..bfc4308 --- /dev/null +++ b/neoforge-1.21.1/src/main/java/com/jnhprojects/mcheli/neoforge/mixin/GameRendererMixin.java @@ -0,0 +1,35 @@ +package com.jnhprojects.mcheli.neoforge.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.renderer.GameRenderer; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * The camera hook, where Forge fires its camera angle event on 1.21.1: once the camera is placed + * for the frame, just before its rotation becomes the view matrix. NeoForge fires its own event + * before vanilla sets the camera position, which would undo a cockpit eye. + */ +@Mixin(GameRenderer.class) +abstract class GameRendererMixin { + @Shadow + @Final + private Camera mainCamera; + + @Inject(method = "renderLevel", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;rotation()Lorg/joml/Quaternionf;", + ordinal = 0)) + private void mcheli$beforeViewRotation(DeltaTracker deltaTracker, CallbackInfo ci) { + ClientHooks.computeCameraAngles(mainCamera, mainCamera.getPartialTickTime(), GameRendererMixin::setRotation); + } + + /** NeoForge turns the camera the other way round its roll axis than Forge does. */ + private static void setRotation(Camera camera, float yaw, float pitch, float roll) { + ((CameraInvoker) camera).mcheli$setRotation(yaw, pitch, -roll); + } +} From 2837f7ab226c9ef8e61bc61fc9f1f5072eb3d469 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:17 +0800 Subject: [PATCH 289/314] Add the neoforge 1.21.1 mod metadata --- .../resources/META-INF/neoforge.mods.toml | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 neoforge-1.21.1/src/main/resources/META-INF/neoforge.mods.toml diff --git a/neoforge-1.21.1/src/main/resources/META-INF/neoforge.mods.toml b/neoforge-1.21.1/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..ecb64df --- /dev/null +++ b/neoforge-1.21.1/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,36 @@ +modLoader="javafml" +loaderVersion="[1,)" +license="${mod_license}" +issueTrackerURL="https://github.com/JnH-Projects/MCHeli-Rework-Forge/issues" + +[[mods]] +modId="${mod_id}" +version="${mod_version}" +displayName="${mod_name}" +displayURL="https://github.com/JnH-Projects/MCHeli-Rework-Forge" +logoFile="mcheli_logo.png" +authors="${mod_authors}" +credits="EMB4 created the original MCHeli mod and all of its vehicle content. Fureniku maintains the CurseForge project." +description=''' +Helicopters, planes, tanks and vehicles for modern Minecraft. +A ground-up rework of MCHeli 1.0.3 by EMB4, which stopped at Minecraft 1.7.10. +The vehicles come from your own copy of the original: put MC1.7.10_mcheli_1.0.3.zip from the +MCHeli CurseForge page into the "mcheli" folder of the game directory. +''' + +[[mixins]] +config="mcheli.mixins.json" + +[[dependencies.${mod_id}]] + modId="neoforge" + type="required" + versionRange="${loader_version_range}" + ordering="NONE" + side="BOTH" + +[[dependencies.${mod_id}]] + modId="minecraft" + type="required" + versionRange="${minecraft_version_range}" + ordering="NONE" + side="BOTH" From c9951cca22e4289dbdb34006029ee86c5af0162d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:17 +0800 Subject: [PATCH 290/314] Add the neoforge 1.21.1 mixin config --- .../src/main/resources/mcheli.mixins.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 neoforge-1.21.1/src/main/resources/mcheli.mixins.json diff --git a/neoforge-1.21.1/src/main/resources/mcheli.mixins.json b/neoforge-1.21.1/src/main/resources/mcheli.mixins.json new file mode 100644 index 0000000..19d6b73 --- /dev/null +++ b/neoforge-1.21.1/src/main/resources/mcheli.mixins.json @@ -0,0 +1,13 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "com.jnhprojects.mcheli.neoforge.mixin", + "compatibilityLevel": "JAVA_21", + "client": [ + "CameraInvoker", + "GameRendererMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} From beb7992cbc3845c110277e3f4bb8099f33da6ee5 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:18 +0800 Subject: [PATCH 291/314] Add the fabric 1.21.1 module --- fabric-1.21.1/build.gradle | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 fabric-1.21.1/build.gradle diff --git a/fabric-1.21.1/build.gradle b/fabric-1.21.1/build.gradle new file mode 100644 index 0000000..beeda80 --- /dev/null +++ b/fabric-1.21.1/build.gradle @@ -0,0 +1,32 @@ +plugins { + id 'java' + id 'idea' + id 'eclipse' + id 'net.fabricmc.fabric-loom-remap' +} + +apply from: rootProject.file('gradle/loader.gradle') + +// The original MCHeli 1.0.3 download. Players supply their own copy; development runs use this +// one (git-ignored, see docs/DEVELOPMENT.md). +def legacyZip = rootProject.file('Mod/MC1.7.10_mcheli_1.0.3.zip').absolutePath + +loom { + runs { + configureEach { + runDir = "../run-${minecraft_version}" + vmArg "-Dmcheli.legacyZip=${legacyZip}" + } + server { + programArgs '--nogui' + } + } +} + +dependencies { + minecraft "com.mojang:minecraft:${minecraft_version}" + // 1.21.1 is obfuscated; Mojang's names keep the shared code the same as on Forge and NeoForge. + mappings loom.officialMojangMappings() + modImplementation "net.fabricmc:fabric-loader:${fabric_loader_version}" + modImplementation "net.fabricmc.fabric-api:fabric-api:${fabric_api_version}" +} From 911e85c6f8e254f5168dbe6b20fe240de6b6866d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:18 +0800 Subject: [PATCH 292/314] Add the fabric 1.21.1 module properties --- fabric-1.21.1/gradle.properties | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 fabric-1.21.1/gradle.properties diff --git a/fabric-1.21.1/gradle.properties b/fabric-1.21.1/gradle.properties new file mode 100644 index 0000000..edfae98 --- /dev/null +++ b/fabric-1.21.1/gradle.properties @@ -0,0 +1,8 @@ +# Minecraft 1.21.1 on Fabric +loader=fabric +minecraft_version=1.21.1 +minecraft_version_range=~1.21.1 +fabric_loader_version=0.20.1 +loader_version_range=>=0.16.0 +fabric_api_version=0.116.17+1.21.1 +java_version=21 From 00fbc979035ccf833789480048955eceb3076585 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:19 +0800 Subject: [PATCH 293/314] Add FabricPlatform to the fabric 1.21.1 module --- .../mcheli/fabric/FabricPlatform.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java diff --git a/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java new file mode 100644 index 0000000..a7114f1 --- /dev/null +++ b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/FabricPlatform.java @@ -0,0 +1,59 @@ +package com.jnhprojects.mcheli.fabric; + +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.platform.Platform; +import net.fabricmc.api.EnvType; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.itemgroup.v1.FabricItemGroup; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.CreativeModeTab; + +import java.nio.file.Path; + +final class FabricPlatform implements Platform { + @Override + public String loaderName() { + return "fabric"; + } + + @Override + public Path gameDir() { + return FabricLoader.getInstance().getGameDir(); + } + + @Override + public Path configDir() { + return FabricLoader.getInstance().getConfigDir(); + } + + @Override + public String modVersion() { + return FabricLoader.getInstance().getModContainer(MCHeli.MOD_ID) + .map(c -> c.getMetadata().getVersion().getFriendlyString()).orElse(""); + } + + @Override + public boolean isClient() { + return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT; + } + + @Override + public void sendToServer(CustomPacketPayload payload) { + ClientPlayNetworking.send(payload); + } + + @Override + public void sendToPlayer(ServerPlayer player, CustomPacketPayload payload) { + ServerPlayNetworking.send(player, payload); + } + + /** Fabric orders tabs by registration, which already puts ours one after another. */ + @Override + public CreativeModeTab.Builder creativeTab(ResourceKey after) { + return FabricItemGroup.builder(); + } +} From 0d0a768569ab7824fbfe357c728ea172e8f774dc Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:19 +0800 Subject: [PATCH 294/314] Add FabricVehicleIngredient to the fabric 1.21.1 module --- .../fabric/FabricVehicleIngredient.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java diff --git a/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java new file mode 100644 index 0000000..5ad9f82 --- /dev/null +++ b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/FabricVehicleIngredient.java @@ -0,0 +1,57 @@ +package com.jnhprojects.mcheli.fabric; + +import com.jnhprojects.mcheli.item.VehicleIngredient; +import com.mojang.serialization.MapCodec; +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredient; +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; + +import java.util.List; + +/** {@link VehicleIngredient} as a Fabric ingredient, read from the {@code fabric:type} key. */ +record FabricVehicleIngredient(String vehicle) implements CustomIngredient { + static final CustomIngredientSerializer SERIALIZER = new CustomIngredientSerializer<>() { + private final MapCodec codec = + VehicleIngredient.codec(FabricVehicleIngredient::new, FabricVehicleIngredient::vehicle); + private final StreamCodec streamCodec = + VehicleIngredient.streamCodec(FabricVehicleIngredient::new, FabricVehicleIngredient::vehicle); + + @Override + public ResourceLocation getIdentifier() { + return VehicleIngredient.ID; + } + + @Override + public MapCodec getCodec(boolean allowEmpty) { + return codec; + } + + @Override + public StreamCodec getPacketCodec() { + return streamCodec; + } + }; + + @Override + public boolean test(ItemStack stack) { + return VehicleIngredient.matches(vehicle, stack); + } + + @Override + public List getMatchingStacks() { + return VehicleIngredient.displayStacks(vehicle).toList(); + } + + @Override + public boolean requiresTesting() { + return true; + } + + @Override + public CustomIngredientSerializer getSerializer() { + return SERIALIZER; + } +} From a29f32b7b2af446d74d6bddb7dc7b18463a29734 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:20 +0800 Subject: [PATCH 295/314] Add McheliFabric to the fabric 1.21.1 module --- .../mcheli/fabric/McheliFabric.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java diff --git a/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java new file mode 100644 index 0000000..382aacc --- /dev/null +++ b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabric.java @@ -0,0 +1,73 @@ +package com.jnhprojects.mcheli.fabric; + +import com.jnhprojects.mcheli.CommonHooks; +import com.jnhprojects.mcheli.MCHeli; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.platform.Platform; +import com.jnhprojects.mcheli.platform.Registration; +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceKey; + +/** + * The Fabric entry point: everything here only connects Fabric to the shared code. The imported + * content pack is added by {@code PackRepositoryMixin}, since Fabric has no pack finder event. + */ +public final class McheliFabric implements ModInitializer { + + @Override + public void onInitialize() { + Platform.install(new FabricPlatform()); + MCHeli.init(); + + for (ResourceKey> registry : Registration.registries()) { + register(Registration.typed(registry)); + } + CustomIngredientSerializer.register(FabricVehicleIngredient.SERIALIZER); + registerPackets(); + + ServerLifecycleEvents.SERVER_STARTING.register(CommonHooks::serverAboutToStart); + ServerLifecycleEvents.SERVER_STOPPED.register(server -> CommonHooks.serverStopped()); + ServerTickEvents.END_SERVER_TICK.register(CommonHooks::serverTickEnd); + CommandRegistrationCallback.EVENT.register((dispatcher, context, selection) -> CommonHooks.registerCommands(dispatcher)); + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> CommonHooks.playerLoggedIn(handler.getPlayer())); + ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> CommonHooks.playerLoggedOut(handler.getPlayer())); + } + + @SuppressWarnings("unchecked") + private static void register(ResourceKey> key) { + Registry registry = (Registry) BuiltInRegistries.REGISTRY.get(key.location()); + if (registry == null) { + throw new IllegalStateException("No registry " + key.location()); + } + Registration.registerAll(key, (id, value) -> Registry.register(registry, id, value)); + } + + /** Client handlers are registered by {@link McheliFabricClient}; handlers run on the main thread. */ + private static void registerPackets() { + for (ModNetwork.Serverbound packet : ModNetwork.SERVERBOUND) { + serverbound(packet); + } + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + clientbound(packet); + } + } + + private static void clientbound(ModNetwork.Clientbound packet) { + PayloadTypeRegistry.playS2C().register(packet.type(), packet.codec()); + } + + private static void serverbound(ModNetwork.Serverbound packet) { + PayloadTypeRegistry.playC2S().register(packet.type(), packet.codec()); + ServerPlayNetworking.registerGlobalReceiver(packet.type(), (msg, context) -> packet.handler().accept(msg, context.player())); + } +} From 57e8d2a8447d4d4797f2f87680fdc95e39df65f6 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:20 +0800 Subject: [PATCH 296/314] Add McheliFabricClient to the fabric 1.21.1 module --- .../mcheli/fabric/McheliFabricClient.java | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java diff --git a/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java new file mode 100644 index 0000000..859952f --- /dev/null +++ b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/McheliFabricClient.java @@ -0,0 +1,69 @@ +package com.jnhprojects.mcheli.fabric; + +import com.jnhprojects.mcheli.client.ClientHooks; +import com.jnhprojects.mcheli.client.particle.GlowParticle; +import com.jnhprojects.mcheli.network.ModNetwork; +import com.jnhprojects.mcheli.registry.ModParticles; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.client.particle.v1.ParticleFactoryRegistry; +import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry; +import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; +import net.fabricmc.fabric.api.resource.IdentifiableResourceReloadListener; +import net.fabricmc.fabric.api.resource.ResourceManagerHelper; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.resources.PreparableReloadListener; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.util.profiling.ProfilerFiller; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +/** + * Client side of the Fabric entry point. The camera, hand, key and rider hooks are mixins (see + * {@code com.jnhprojects.mcheli.fabric.mixin}), since Fabric API has no events for them. + */ +public final class McheliFabricClient implements ClientModInitializer { + + @Override + public void onInitializeClient() { + ClientHooks.init(); + ClientHooks.registerEntityRenderers(EntityRendererRegistry::register); + ParticleFactoryRegistry.getInstance().register(ModParticles.GLOW.get(), GlowParticle.Provider::new); + ClientHooks.keyMappings().forEach(KeyBindingHelper::registerKeyBinding); + ResourceManagerHelper resources = ResourceManagerHelper.get(PackType.CLIENT_RESOURCES); + ClientHooks.reloadListeners().forEach((id, listener) -> resources.registerReloadListener(new Identified(id, listener))); + // Drawn after the vanilla HUD, which is where Forge's post sleep layer ends up too. + HudRenderCallback.EVENT.register(ClientHooks::drawHud); + for (ModNetwork.Clientbound packet : ModNetwork.CLIENTBOUND) { + clientbound(packet); + } + + ClientTickEvents.END_CLIENT_TICK.register(client -> ClientHooks.clientTickEnd()); + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> ClientHooks.loggedIn(client.player)); + ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> ClientHooks.loggedOut()); + } + + private static void clientbound(ModNetwork.Clientbound packet) { + ClientPlayNetworking.registerGlobalReceiver(packet.type(), (msg, context) -> packet.handler().accept(msg)); + } + + /** Fabric wants an id on every reload listener. */ + private record Identified(ResourceLocation id, PreparableReloadListener listener) implements IdentifiableResourceReloadListener { + @Override + public ResourceLocation getFabricId() { + return id; + } + + @Override + public CompletableFuture reload(PreparationBarrier barrier, ResourceManager manager, ProfilerFiller preparations, + ProfilerFiller reload, Executor background, Executor main) { + return listener.reload(barrier, manager, preparations, reload, background, main); + } + } +} From 4792c37141f89c223987e58314ac97f0a67825bb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:21 +0800 Subject: [PATCH 297/314] Add CameraMixin to the fabric 1.21.1 module --- .../mcheli/fabric/mixin/CameraMixin.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java diff --git a/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java new file mode 100644 index 0000000..62bf841 --- /dev/null +++ b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/CameraMixin.java @@ -0,0 +1,53 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import net.minecraft.client.Camera; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +/** + * Vanilla's camera has no roll. This is {@code Camera#setRotation} with the roll Forge patches in, + * so a banked aircraft tilts the view the same on every loader. + */ +@Mixin(Camera.class) +abstract class CameraMixin implements RollableCamera { + @Shadow + @Final + private static Vector3f FORWARDS; + @Shadow + @Final + private static Vector3f UP; + @Shadow + @Final + private static Vector3f LEFT; + + @Shadow + private float xRot; + @Shadow + private float yRot; + @Shadow + @Final + private Vector3f forwards; + @Shadow + @Final + private Vector3f up; + @Shadow + @Final + private Vector3f left; + @Shadow + @Final + private Quaternionf rotation; + + @Override + public void mcheli$setRotation(float yRot, float xRot, float roll) { + this.xRot = xRot; + this.yRot = yRot; + this.rotation.rotationYXZ((float) Math.PI - yRot * (float) (Math.PI / 180.0), -xRot * (float) (Math.PI / 180.0), + roll * (float) (Math.PI / 180.0)); + FORWARDS.rotate(this.rotation, this.forwards); + UP.rotate(this.rotation, this.up); + LEFT.rotate(this.rotation, this.left); + } +} From 14355c53a4aa352f1e6ad69880ae7da916ecdf77 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:21 +0800 Subject: [PATCH 298/314] Add GameRendererMixin to the fabric 1.21.1 module --- .../fabric/mixin/GameRendererMixin.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java diff --git a/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java new file mode 100644 index 0000000..faba98b --- /dev/null +++ b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/GameRendererMixin.java @@ -0,0 +1,40 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import net.minecraft.client.Camera; +import net.minecraft.client.renderer.GameRenderer; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * The camera hook, where Forge fires its camera angle event on 1.21.1 (the camera is placed for + * the frame, and its rotation is about to become the view matrix), and the first person hand, + * which is hidden in some seats. + */ +@Mixin(GameRenderer.class) +abstract class GameRendererMixin { + @Shadow + @Final + private Camera mainCamera; + + @Inject(method = "renderLevel", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;rotation()Lorg/joml/Quaternionf;", + ordinal = 0)) + private void mcheli$beforeViewRotation(CallbackInfo ci) { + ClientHooks.computeCameraAngles(mainCamera, mainCamera.getPartialTickTime(), GameRendererMixin::setRotation); + } + + @Inject(method = "renderItemInHand", at = @At("HEAD"), cancellable = true) + private void mcheli$renderItemInHand(CallbackInfo ci) { + if (ClientHooks.hidesHand()) { + ci.cancel(); + } + } + + private static void setRotation(Camera camera, float yaw, float pitch, float roll) { + ((RollableCamera) camera).mcheli$setRotation(yaw, pitch, roll); + } +} From 1499a6ac75c78c3e5f1801f7e9b73a4d0ea2b145 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:22 +0800 Subject: [PATCH 299/314] Add LivingEntityRendererMixin to the fabric 1.21.1 module --- .../mixin/LivingEntityRendererMixin.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java diff --git a/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java new file mode 100644 index 0000000..2cd967b --- /dev/null +++ b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/LivingEntityRendererMixin.java @@ -0,0 +1,23 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.entity.LivingEntityRenderer; +import net.minecraft.world.entity.LivingEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** Tilts riders with their vehicle, where Forge fires its living entity render event. */ +@Mixin(LivingEntityRenderer.class) +abstract class LivingEntityRendererMixin { + + @Inject(method = "render(Lnet/minecraft/world/entity/LivingEntity;FFLcom/mojang/blaze3d/vertex/PoseStack;" + + "Lnet/minecraft/client/renderer/MultiBufferSource;I)V", at = @At("HEAD")) + private void mcheli$beforeRender(LivingEntity entity, float yaw, float partialTicks, PoseStack pose, MultiBufferSource buffers, + int light, CallbackInfo ci) { + ClientHooks.beforeRenderLiving(entity, pose, partialTicks); + } +} From bb75d9c112e20b6bd6d474eb56c9766c1e99a470 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:22 +0800 Subject: [PATCH 300/314] Add MinecraftMixin to the fabric 1.21.1 module --- .../mcheli/fabric/mixin/MinecraftMixin.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java diff --git a/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java new file mode 100644 index 0000000..b4685f8 --- /dev/null +++ b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/MinecraftMixin.java @@ -0,0 +1,47 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.ClientHooks; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.phys.HitResult; +import org.jspecify.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** The use and pick block keys, which Forge and NeoForge let mods take through an input event. */ +@Mixin(Minecraft.class) +abstract class MinecraftMixin { + @Shadow + public @Nullable LocalPlayer player; + @Shadow + public @Nullable HitResult hitResult; + @Shadow + private int rightClickDelay; + + @Inject(method = "startUseItem", at = @At("HEAD"), cancellable = true) + private void mcheli$startUseItem(CallbackInfo ci) { + if (player == null) { + return; + } + ClientHooks.UseKey result = ClientHooks.onUseKey(InteractionHand.MAIN_HAND); + if (result != ClientHooks.UseKey.PASS) { + // Vanilla sets the same delay before it looks at the hands. + rightClickDelay = 4; + if (result == ClientHooks.UseKey.CONSUME_AND_SWING) { + player.swing(InteractionHand.MAIN_HAND); + } + ci.cancel(); + } + } + + @Inject(method = "pickBlock", at = @At("HEAD"), cancellable = true) + private void mcheli$pickBlock(CallbackInfo ci) { + if (hitResult != null && hitResult.getType() != HitResult.Type.MISS && ClientHooks.onPickBlockKey()) { + ci.cancel(); + } + } +} From 978fe5b636c31bf6e32a40d711f67eebdbb5a2c2 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:22 +0800 Subject: [PATCH 301/314] Add PackRepositoryMixin to the fabric 1.21.1 module --- .../fabric/mixin/PackRepositoryMixin.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java diff --git a/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java new file mode 100644 index 0000000..965719b --- /dev/null +++ b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/PackRepositoryMixin.java @@ -0,0 +1,25 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.CommonHooks; +import net.minecraft.server.packs.repository.PackRepository; +import net.minecraft.server.packs.repository.RepositorySource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +import java.util.Arrays; + +/** + * Adds the imported original content to every pack repository, client resources and server data + * alike, which is what Forge and NeoForge do through their pack finder event. + */ +@Mixin(PackRepository.class) +abstract class PackRepositoryMixin { + + @ModifyVariable(method = "", at = @At("HEAD"), argsOnly = true) + private static RepositorySource[] mcheli$addImportedContent(RepositorySource[] sources) { + RepositorySource[] withOurs = Arrays.copyOf(sources, sources.length + 1); + withOurs[sources.length] = CommonHooks.packSource(); + return withOurs; + } +} From dc2145d203901eac4e9d6d080e5f1e79faed8128 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:23 +0800 Subject: [PATCH 302/314] Add RollableCamera to the fabric 1.21.1 module --- .../jnhprojects/mcheli/fabric/mixin/RollableCamera.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RollableCamera.java diff --git a/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RollableCamera.java b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RollableCamera.java new file mode 100644 index 0000000..f31feae --- /dev/null +++ b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RollableCamera.java @@ -0,0 +1,7 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +/** Implemented on {@code Camera} by {@link CameraMixin}. */ +public interface RollableCamera { + /** Like vanilla's {@code setRotation}, with a roll in degrees as Forge applies it. */ + void mcheli$setRotation(float yRot, float xRot, float roll); +} From d275ec1aca819fd93eb40bb276ac9e7a192b1ad9 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:23 +0800 Subject: [PATCH 303/314] Add RuntimeSoundInstanceMixin to the fabric 1.21.1 module --- .../mixin/RuntimeSoundInstanceMixin.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java diff --git a/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java new file mode 100644 index 0000000..35f4032 --- /dev/null +++ b/fabric-1.21.1/src/main/java/com/jnhprojects/mcheli/fabric/mixin/RuntimeSoundInstanceMixin.java @@ -0,0 +1,20 @@ +package com.jnhprojects.mcheli.fabric.mixin; + +import com.jnhprojects.mcheli.client.sound.RuntimeSoundInstance; +import net.fabricmc.fabric.api.client.sound.v1.FabricSoundInstance; +import net.minecraft.client.sounds.AudioStream; +import net.minecraft.client.sounds.SoundBufferLibrary; +import net.minecraft.resources.ResourceLocation; +import org.spongepowered.asm.mixin.Mixin; + +import java.util.concurrent.CompletableFuture; + +/** Streams content pack sounds from disk through Fabric's sound instance hook. */ +@Mixin(RuntimeSoundInstance.class) +abstract class RuntimeSoundInstanceMixin implements FabricSoundInstance { + + @Override + public CompletableFuture getAudioStream(SoundBufferLibrary loader, ResourceLocation id, boolean repeatInstantly) { + return ((RuntimeSoundInstance) (Object) this).openStream(loader, id, repeatInstantly); + } +} From 7a757db8ac0de19719e4d5e1443db7d923b88fbf Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:24 +0800 Subject: [PATCH 304/314] Add the fabric 1.21.1 mod metadata --- .../src/main/resources/fabric.mod.json | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 fabric-1.21.1/src/main/resources/fabric.mod.json diff --git a/fabric-1.21.1/src/main/resources/fabric.mod.json b/fabric-1.21.1/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..8346f4c --- /dev/null +++ b/fabric-1.21.1/src/main/resources/fabric.mod.json @@ -0,0 +1,27 @@ +{ + "schemaVersion": 1, + "id": "${mod_id}", + "version": "${mod_version}", + "name": "${mod_name}", + "description": "Helicopters, planes, tanks and vehicles for modern Minecraft. A ground-up rework of MCHeli 1.0.3 by EMB4, which stopped at Minecraft 1.7.10. The vehicles come from your own copy of the original: put MC1.7.10_mcheli_1.0.3.zip from the MCHeli CurseForge page into the \"mcheli\" folder of the game directory.", + "authors": ["JayMar921 (rework)", "EMB4 (original author)", "Fureniku (CurseForge maintainer)"], + "contact": { + "homepage": "https://github.com/JnH-Projects/MCHeli-Rework-Forge", + "issues": "https://github.com/JnH-Projects/MCHeli-Rework-Forge/issues", + "sources": "https://github.com/JnH-Projects/MCHeli-Rework-Forge" + }, + "license": "LicenseRef-MCHeli-Rework-1.0", + "icon": "mcheli_logo.png", + "environment": "*", + "entrypoints": { + "main": ["com.jnhprojects.mcheli.fabric.McheliFabric"], + "client": ["com.jnhprojects.mcheli.fabric.McheliFabricClient"] + }, + "mixins": ["mcheli.mixins.json"], + "depends": { + "fabricloader": "${loader_version_range}", + "fabric-api": "*", + "minecraft": "${minecraft_version_range}", + "java": ">=${java_version}" + } +} From 1466c71b155db175b5f0aeaedb16642f990c541d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:24 +0800 Subject: [PATCH 305/314] Add the fabric 1.21.1 mixin config --- .../src/main/resources/mcheli.mixins.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 fabric-1.21.1/src/main/resources/mcheli.mixins.json diff --git a/fabric-1.21.1/src/main/resources/mcheli.mixins.json b/fabric-1.21.1/src/main/resources/mcheli.mixins.json new file mode 100644 index 0000000..1b14b27 --- /dev/null +++ b/fabric-1.21.1/src/main/resources/mcheli.mixins.json @@ -0,0 +1,19 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "com.jnhprojects.mcheli.fabric.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [ + "PackRepositoryMixin" + ], + "client": [ + "CameraMixin", + "GameRendererMixin", + "LivingEntityRendererMixin", + "MinecraftMixin", + "RuntimeSoundInstanceMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} From 6ba607c7d80b45b15dd4fe393640267f0cf912f7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:25 +0800 Subject: [PATCH 306/314] Describe how the 1.21.1 code differs from 26.3 --- docs/ARCHITECTURE.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b32f6ae..07a0159 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -76,6 +76,31 @@ fires its event, so the shared code sees the same thing on every loader: NeoForge rolls the camera the other way round from Forge, so its mixin negates the roll. +### Minecraft 1.21.1 + +`minecraft-1.21.1` is a copy of `minecraft-26.3` ported back, so the two read side by side. After +1.1.0 it is frozen (see [supported versions](ROADMAP.md#supported-versions)), so the copy is not +kept in step by hand beyond that. What differs: + +- **Rendering.** 1.21.1 has no render states or submit step. Each renderer fills the same state + object itself at the start of `render` and draws straight into the `MultiBufferSource`, so + `PartRenderer`, `MeshRenderer` and `GlassSplit` are unchanged apart from that. The glow uses a + render type built from `RenderType`'s public constructor (vanilla keeps `RenderType.create` + package private) and a custom `ParticleRenderType` for additive particles. +- **Vehicle icons.** There is no `item_model` component. The import writes one item model per kind + whose `custom_model_data` overrides pick each vehicle's model, and lists the numbers in + `mcheli/imported/item_icons.properties`; `AircraftItem.stackOf` puts the number on the stack. +- **Recipes** take ingredients as `{"item": ...}` and `{"tag": ...}` objects; the import writes + them that way for `ImportTarget.V1_21_1`. The marker file records the target, so a folder + imported for one version is imported again by the other. +- **Entities** extend `Entity` instead of `VehicleEntity`, whose abstract `getDropItem` is package + private in vanilla, and keep their own hurt wobble data. Save data is `CompoundTag`, damage goes + through `hurt` (server only), server position updates are eased in by `lerpTo` like vanilla's + boat, and a click on the hull arrives through `interactAt`. +- **Camera.** Forge 1.21.1 fires its camera event after the camera is placed, so Forge uses the + event. NeoForge and Fabric hook `GameRenderer.renderLevel` just before the camera rotation turns + into the view matrix, the spot Forge fires from. + `client` is only referenced from the loader modules behind a physical client check, so a dedicated server never loads a client class. From a7102c0850bb6f3946041cbec9a5e9246f39c30d Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:25 +0800 Subject: [PATCH 307/314] Document the 1.21.1 toolchain, modules and run folder --- docs/DEVELOPMENT.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index c5db596..0a3cced 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -4,7 +4,7 @@ | Tool | Version | Notes | |------|---------|-------| -| JDK | 25 | Gradle provisions one through the Foojay resolver if none is installed. | +| JDK | 25 (and 21 for the 1.21.1 modules) | Gradle provisions them through the Foojay resolver if none is installed. | | Gradle | 9.8.2 | Use the wrapper (`./gradlew`). Loom 1.19 needs 9.8. | | ForgeGradle | 7.x | Forge modules. Sets up Minecraft through the Mavenizer on first build. | | ModDevGradle | 2.0.x | NeoForge modules. | @@ -12,6 +12,9 @@ | Forge | 66.1.0 for Minecraft 26.3 | | | NeoForge | 26.3.0.99 for Minecraft 26.3 | | | Fabric | Loader 0.20.1, Fabric API 0.165.0+26.3 | | +| Forge | 52.1.20 for Minecraft 1.21.1 | Built with ForgeGradle 7 and the official mappings. | +| NeoForge | 21.1.265 for Minecraft 1.21.1 | | +| Fabric | Fabric API 0.116.17+1.21.1 | Loom remaps the jar, which is obfuscated on 1.21.1. | All three use Mojang's official names, so the shared code reads the same on every loader. @@ -28,6 +31,7 @@ wait for the other process to finish and build again. ./gradlew :forge-26.3:runClient # development client from ./run ./gradlew :neoforge-26.3:runClient ./gradlew :fabric-26.3:runClient +./gradlew :neoforge-1.21.1:runClient # 1.21.1 modules run from ./run-1.21.1 ./gradlew ::runServer # development dedicated server from ./run ``` @@ -51,6 +55,8 @@ minecraft-26.3/ code shared by every loader on Minecraft 26.3 (no forge-26.3/ Forge 66 glue: entry point, events, payload channel, ingredient neoforge-26.3/ NeoForge glue, plus a camera mixin fabric-26.3/ Fabric glue, plus mixins for what Fabric API has no event for +minecraft-1.21.1/ the same shared code, backported to Minecraft 1.21.1 +forge-1.21.1/, neoforge-1.21.1/, fabric-1.21.1/ the loader glue for 1.21.1 gradle/loader.gradle what every loader module shares docs/ documentation and content format guides .github/workflows CI and release pipelines @@ -82,7 +88,8 @@ so each loader and version ships as one jar and runs as one module in developmen A feature that is pure logic goes in `common` with a unit test; the part that touches Minecraft goes in `minecraft-`; only what needs a loader API goes in the loader modules. The run -folder `./run` is shared by every module; worlds made by one loader open on the others. +folder `./run` is shared by every 26.3 module and `./run-1.21.1` by every 1.21.1 module; worlds made by +one loader open on the others, but never open a 26.3 world on 1.21.1. IDEs import the project as one Gradle project per module. A file under `minecraft-26.3` belongs to all three loader modules, so the IDE shows it under whichever it picked; it compiles under all From e325277c631d61e278cde7b7e5b168cd949b2601 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:25 +0800 Subject: [PATCH 308/314] Add 1.21.1 checks to the test plan --- docs/TESTING.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/TESTING.md b/docs/TESTING.md index 3b19d2f..e760518 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -6,9 +6,10 @@ hand with this plan before a pre-release is tagged. ## Setup 1. `./gradlew build`, then copy the jar for the loader under test from `build/release` - (`mcheli--26.3-.jar`) into the `mods` folder of a 26.3 profile for that loader, + (`mcheli---.jar`) into the `mods` folder of a profile for that loader + and Minecraft version, and `MC1.7.10_mcheli_1.0.3.zip` into its `mcheli` folder. Fabric also needs Fabric API. - Alternatively run `./gradlew :-26.3:runClient`, which imports the zip from `Mod/`. + Alternatively run `./gradlew :-:runClient`, which imports the zip from `Mod/`. 2. Use a launcher profile with its own game directory. Mods for other loaders or Minecraft versions in the same `mods` folder will prevent the game from starting. 3. Create a creative world with cheats enabled. @@ -46,6 +47,15 @@ Run this for each jar in `build/release`: Forge, NeoForge and Fabric on every Mi - [ ] A Fabric client joins a NeoForge server (both with the mod) and downloads its packs. - [ ] A `serverconfig/mcheli-server.toml` written by 1.1.0-beta.2 (Forge) keeps its values. +### 1.21.1 only + +- [ ] Vehicle icons show in the creative tabs and the hand (the kind model picks them by + `custom_model_data`); a pack vehicle shows the barrier. +- [ ] `mcheli/imported/item_icons.properties` exists, and the AH-6X recipe crafts an AH-6X with + its icon. +- [ ] Glow halos, tracers and the glow particle are additive and do not hide smoke behind them. +- [ ] A vehicle hit by a weapon wobbles (hurt time) and takes damage only once per hit. + ## Phase 1 checklist ### Loading From ed9db1e0c2a28127a176c6ed3fd3e13c7635479b Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:26 +0800 Subject: [PATCH 309/314] Tick the 1.21.1 and importer items of phase 7 --- docs/ROADMAP.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index edbcb4e..6d6b160 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -214,11 +214,11 @@ almost everything, adds NeoForge and Fabric on 26.3, then brings all three to 1. entities, sounds, HUD, which are the same on every loader), and a thin module per loader (registration, events, networking, config) - [x] NeoForge and Fabric on 26.3, with the same features and checklists as Forge -- [ ] 1.21.1 (Java 21) on Forge, NeoForge and Fabric: older rendering (no render states), older +- [x] 1.21.1 (Java 21) on Forge, NeoForge and Fabric: older rendering (no render states), older networking, item models from the item JSON instead of `item_model` - [ ] Content packs and their sync work across loaders on the same Minecraft version (a Fabric client on a NeoForge server) -- [ ] Legacy importer checked on 1.21.1 (pack format numbers and the asset layout differ) +- [x] Legacy importer checked on 1.21.1 (pack format numbers and the asset layout differ) - [x] Jar names carry the loader and the Minecraft version, such as `mcheli-fabric-1.21.1-1.1.0-beta.3.jar`; the release workflow publishes every jar - [x] CI builds and tests every module on every pull request From 1da82cb73300e5e55061443d21606eaf71628b0f Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:26 +0800 Subject: [PATCH 310/314] List the 1.21.1 requirements and jar names --- README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 2be7aac..48f853d 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ MCHeli Rework icon -Helicopters, planes, tanks and ground vehicles for **Minecraft 26.3** on **Forge**, **NeoForge** -and **Fabric**. +Helicopters, planes, tanks and ground vehicles for **Minecraft 26.3** and **1.21.1**, on **Forge**, +**NeoForge** and **Fabric**. MCHeli Rework is a ground-up port of MCHeli 1.0.3, a mod that stopped at Minecraft 1.7.10. Every system is rewritten for the modern game, while the original vehicle definitions, models, @@ -125,20 +125,20 @@ Reserved original, the code is source available rather than open source. ## Requirements -| | Version | -|---|---------| -| Minecraft | 26.3 | -| Loader | Forge 66.1.0 or newer in the 66.x line, NeoForge 26.3.0.99 or newer, or Fabric Loader 0.20 with Fabric API | -| Java | 25 (bundled with the Minecraft launcher) | +| | Minecraft 26.3 | Minecraft 1.21.1 | +|---|---|---| +| Forge | 66.1.0 or newer in the 66.x line | 52.1.0 or newer | +| NeoForge | 26.3.0.99 or newer | 21.1.200 or newer | +| Fabric | Loader 0.20 and Fabric API | Loader 0.16 and Fabric API | +| Java | 25 (bundled with the launcher) | 21 (bundled with the launcher) | ## Installing -1. Install Forge, NeoForge or Fabric for Minecraft 26.3. On Fabric, also install +1. Install Forge, NeoForge or Fabric for Minecraft 26.3 or 1.21.1. On Fabric, also install [Fabric API](https://modrinth.com/mod/fabric-api). 2. Download the jar for your loader from the [releases](https://github.com/JnH-Projects/MCHeli-Rework-Forge/releases): - `mcheli-forge-26.3-.jar`, `mcheli-neoforge-26.3-.jar` or - `mcheli-fabric-26.3-.jar`. + `mcheli---.jar`, such as `mcheli-fabric-1.21.1-.jar`. 3. Drop it into the `mods` folder of that profile. 4. Download `MC1.7.10_mcheli_1.0.3.zip` from the [MCHeli CurseForge page](https://www.curseforge.com/minecraft/mc-mods/mcheli-minecraft-helicopter-mod/files) @@ -146,7 +146,7 @@ Reserved original, the code is source available rather than open source. ``` / - mods/mcheli--26.3-.jar + mods/mcheli---.jar mcheli/MC1.7.10_mcheli_1.0.3.zip ``` @@ -158,8 +158,8 @@ zip goes. Use a separate game directory for each loader and version. Mods built for other loaders or older versions in a shared `mods` folder will stop the game from starting. Worlds and the `mcheli` -folder can move between loaders: the imported content, content packs and configs are the same -files on all three. +folder can move between loaders on the same Minecraft version: the imported content, content packs +and configs are the same files on all three. Do not open a 26.3 world in 1.21.1. ## Controls From efe6b143680b86299ad21870b72b25049fad40cb Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:14:26 +0800 Subject: [PATCH 311/314] Note 1.21.1 under Unreleased --- CHANGELOG.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09f2653..122e411 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,25 +7,30 @@ way to 1.0.0; see [docs/ROADMAP.md](docs/ROADMAP.md). ## [Unreleased] -Phase 7 of the road to 1.1.0: the mod on NeoForge and Fabric as well as Forge. +Phase 7 of the road to 1.1.0: the mod on NeoForge and Fabric as well as Forge, and on Minecraft +1.21.1 as well as 26.3. ### Added - NeoForge and Fabric jars for Minecraft 26.3, with the same features as the Forge one. Fabric needs Fabric API. Worlds, the `mcheli` folder, content packs and configs carry over between loaders. +- Minecraft 1.21.1 on Forge, NeoForge and Fabric, with the same vehicles, weapons, HUD and + content packs as on 26.3. Vehicle icons come from the kind's item model, which picks each + vehicle by `custom_model_data`. - The log names the loader the mod runs on, next to the version. ### Changed -- Jar names carry the loader: `mcheli-forge-26.3-.jar`, `mcheli-neoforge-26.3-.jar` - and `mcheli-fabric-26.3-.jar`. +- Jar names carry the loader and the Minecraft version, such as `mcheli-forge-26.3-.jar` + and `mcheli-fabric-1.21.1-.jar`. - The configs are read by the mod itself instead of Forge's config system, which Fabric does not have. The files, their place and their keys stay the same, so existing settings are kept. Edits to `serverconfig/mcheli-server.toml` now apply on the next world start or `/mcheli reload` instead of while the server runs; `/mcheli reload` now reads the server config again too. - The original content is imported again on the first start, since the recipes now name the - vehicle ingredient for every loader. + vehicle ingredient for every loader. The import writes the layout of the Minecraft version it runs + on, and imports again when the folder was made for another version. - Every packet travels under its own id (`mcheli:pilot_state` and so on) with the same encoding on every loader. Clients and servers need the same version, since the network protocol changed. - Glass panes that bullets shoot through come from the `c:glass_panes` tag, which every loader @@ -38,6 +43,8 @@ Phase 7 of the road to 1.1.0: the mod on NeoForge and Fabric as well as Forge. code reaches the loader through `Platform` and `Registration`, and the loaders call `CommonHooks` and `ClientHooks` from their events. Where NeoForge or Fabric has no event, a mixin hooks in where Forge fires its own. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). +- `minecraft-1.21.1` holds the shared code backported to 1.21.1; `forge-1.21.1`, `neoforge-1.21.1` + and `fabric-1.21.1` are its loader modules. 1.21.1 development runs use `./run-1.21.1`. - Gradle 9.8.2, for Fabric Loom 1.19. NeoForge modules build with ModDevGradle. ## [1.1.0-beta.2] - 2026-10-29 From 68c16125c3d4305422f73e7065021f7f6629cb63 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:16:13 +0800 Subject: [PATCH 312/314] Set the version to 1.1.0-beta.3 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index a239d67..e2d93a9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ net.minecraftforge.gradle.merge-source-sets=true mod_id=mcheli mod_name=MCHeli Rework mod_group=com.jnhprojects.mcheli -mod_version=1.1.0-beta.2 +mod_version=1.1.0-beta.3 mod_authors=JayMar921 (rework), EMB4 (original author), Fureniku (CurseForge maintainer) mod_license=MCHeli Rework Licence 1.0 (see LICENSE) From 4ee264b042337c7acc9ccd9f8da086804c45acf7 Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:16:13 +0800 Subject: [PATCH 313/314] Release the changelog section as 1.1.0-beta.3 --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 122e411..4a13537 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,12 @@ All notable changes to this project are documented here. The format follows [Semantic Versioning](https://semver.org/). Pre-releases (`1.0.0-alpha.N`) mark the phases on the way to 1.0.0; see [docs/ROADMAP.md](docs/ROADMAP.md). -## [Unreleased] +## [1.1.0-beta.3] - 2026-10-30 Phase 7 of the road to 1.1.0: the mod on NeoForge and Fabric as well as Forge, and on Minecraft -1.21.1 as well as 26.3. +1.21.1 as well as 26.3. Betas are not uploaded to CurseForge; download them from the GitHub +releases, one jar per loader and Minecraft version. Worlds from 1.1.0-beta.2 and 1.0.0 load as +before on 26.3. Players and servers need the same version, since the network protocol changed. ### Added From b3a860ca344d951c613c65b7ba2c49e0de85a1cf Mon Sep 17 00:00:00 2001 From: jaymar921 Date: Fri, 30 Oct 2026 12:16:14 +0800 Subject: [PATCH 314/314] Mark phase 7 as released in beta.3 --- docs/ROADMAP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 6d6b160..6cb6f24 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -133,7 +133,7 @@ Rules that hold for every beta: |-------|---------|-------|-------| | 5 | `1.1.0-beta.1` | Groundwork: runtime registry, config, multi-version build | Released | | 6 | `1.1.0-beta.2` | Content packs and multiplayer sync | Released | -| 7 | `1.1.0-beta.3` | Loaders (Forge, NeoForge, Fabric) and 1.21.1 | Planned | +| 7 | `1.1.0-beta.3` | Loaders (Forge, NeoForge, Fabric) and 1.21.1 | Released | | 8 | `1.1.0-beta.4` | 1.20.1 | Planned | | 9 | `1.1.0-beta.5` | Equipment and the rest of phase 4 | Planned | | 10 | `1.1.0-beta.6` | Workshop: model, parts and tuning | Planned |