Phase 10: implement neoforge-26_1 leaf (WS, deltas, chat, link, reconciliation, player tracking)
Ports the full feature set proven by the two legacy leaves to modern, post-Flattening NeoForge (loader assumption confirmed: NeoForge 26.1.2.94 is real and published). Neoforge261ChunkAdapter reads BlockState via LevelChunk/LevelChunkSection instead of raw id+meta, carrying the full packed state id in DeltaEvent#blockStateId (a lossless int, unlike the pre-Flattening 16-bit encoding) while documenting a known truncation caveat for SectionData's char[]-based 3D backfill on very large modded registries. MCMapperMod uses constructor-injected event buses (IEventBus/ModContainer) and NeoForge.EVENT_BUS instead of @Mod.EventHandler methods, ModConfigSpec instead of legacy Configuration, and LinkCommand is a Brigadier registration (no CommandBase in this era) fired from RegisterCommandsEvent. Tracks its own loaded-chunk set via ChunkEvent.Load/Unload rather than querying chunk provider internals (no stable public API for that in modern MC). Gets its own standalone Gradle wrapper + settings.gradle (Foojay toolchain resolver) since ModDevGradle needs Gradle 8+ and a Java 25 toolchain (MC itself now requires Java 25), incompatible with the legacy leaves' Gradle-7/JDK-8 pin in one invocation. Verified against real NeoForge 26.1.2.94 + decompiled MC 26.1.2 source via ./gradlew build from inside neoforge-26_1/ (two real API mismatches caught and fixed by the compiler: ChunkPos is now a record — x()/z() methods, not fields — and ResourceLocation was renamed to Identifier, ResourceKey#identifier() not #location()).
This commit is contained in:
@@ -18,8 +18,12 @@ in Claude Code's plan mode.
|
|||||||
`common` interfaces as `forge-1_12_2`. Builds with legacy ForgeGradle 1.2, same fork family, one
|
`common` interfaces as `forge-1_12_2`. Builds with legacy ForgeGradle 1.2, same fork family, one
|
||||||
Forge-tooling generation further back — pre-block-state (raw `Block` + metadata int, no
|
Forge-tooling generation further back — pre-block-state (raw `Block` + metadata int, no
|
||||||
`IBlockState`) and pre-FML-repackage (`cpw.mods.fml.*`, not `net.minecraftforge.fml.*`).
|
`IBlockState`) and pre-FML-repackage (`cpw.mods.fml.*`, not `net.minecraftforge.fml.*`).
|
||||||
- `neoforge-26_1/` — MC 26.1.2 (NeoForge, assumed). Lowest MVP priority. Structurally scaffolded,
|
- `neoforge-26_1/` — MC 26.1.2 (Phase 10; loader confirmed NeoForge). Lowest MVP priority, fully
|
||||||
not yet wired into the default build (see `settings.gradle`).
|
wired against the same `common` interfaces as the two legacy leaves. Post-Flattening, mixin-era
|
||||||
|
API: block reads are `BlockState`, not raw id+meta; events/commands/config live under
|
||||||
|
`net.neoforged.*` with constructor-injected event buses instead of `@Mod.EventHandler` methods
|
||||||
|
and Brigadier commands instead of `CommandBase`. Builds standalone via ModDevGradle — see
|
||||||
|
"Building" below, not part of the root multi-project build (see `settings.gradle`).
|
||||||
|
|
||||||
## Version targets and priority
|
## Version targets and priority
|
||||||
|
|
||||||
@@ -40,12 +44,28 @@ Both legacy leaves share one root build, on Gradle 7.6 / JDK 8 (pinned via
|
|||||||
```
|
```
|
||||||
|
|
||||||
The first build downloads Minecraft/Forge artifacts and MCP mappings from Forge's Maven and can
|
The first build downloads Minecraft/Forge artifacts and MCP mappings from Forge's Maven and can
|
||||||
take a while / needs network access. `neoforge-26_1` is not yet buildable from the root — it
|
take a while / needs network access.
|
||||||
needs Gradle 8+ and JDK 17+ (ModDevGradle), incompatible with the legacy leaves' toolchain
|
|
||||||
within one Gradle invocation; see `settings.gradle`'s comment for the workaround until Phase 10
|
|
||||||
gives it a proper isolated build.
|
|
||||||
|
|
||||||
## Configuration (forge-1_12_2 and forge-1_7_10)
|
`neoforge-26_1` is **not** part of the root build — it needs Gradle 8+ and a Java 25 toolchain
|
||||||
|
(Minecraft itself now requires Java 25 as of the 26.x cycle), incompatible with the legacy
|
||||||
|
leaves' Gradle-7/JDK-8 pin within one Gradle invocation. It has its own wrapper; build it from
|
||||||
|
inside its own directory:
|
||||||
|
|
||||||
|
```
|
||||||
|
cd neoforge-26_1
|
||||||
|
./gradlew build
|
||||||
|
```
|
||||||
|
|
||||||
|
The Gradle *daemon* can run on any modern JDK on `PATH`/`JAVA_HOME` (or pinned via
|
||||||
|
`org.gradle.java.home` in a `$GRADLE_USER_HOME/gradle.properties`, same mechanism as the legacy
|
||||||
|
leaves' JDK 8 pin — see `settings.gradle`'s comment) — it does not need to already be JDK 25
|
||||||
|
itself. `settings.gradle` applies the Foojay toolchain resolver so Gradle auto-provisions an
|
||||||
|
actual JDK 25 (into its own `GRADLE_USER_HOME` cache, not a system-wide install) for the
|
||||||
|
compile/run tasks. The first build also downloads and decompiles/patches Minecraft itself via
|
||||||
|
NeoForge's NeoForm pipeline (the modern equivalent of the legacy leaves' MCP step) — expect it to
|
||||||
|
take several minutes and a real chunk of disk/network the first time; subsequent builds are fast.
|
||||||
|
|
||||||
|
## Configuration (all three leaves)
|
||||||
|
|
||||||
On first server start the leaf writes `config/mcmapper.cfg` with defaults. Set:
|
On first server start the leaf writes `config/mcmapper.cfg` with defaults. Set:
|
||||||
|
|
||||||
@@ -59,15 +79,22 @@ overworld chunks, and streams event-driven column deltas (block place/break) in
|
|||||||
`deltaFlushIntervalTicks` (default 20 = 1s). A periodic reconciliation sweep (Phase 7) also
|
`deltaFlushIntervalTicks` (default 20 = 1s). A periodic reconciliation sweep (Phase 7) also
|
||||||
walks a bounded number of currently-loaded chunks per tick (`reconciliationChunksPerSweep`,
|
walks a bounded number of currently-loaded chunks per tick (`reconciliationChunksPerSweep`,
|
||||||
default 4) and re-sends any that drifted from what the backend last acknowledged, to catch
|
default 4) and re-sends any that drifted from what the backend last acknowledged, to catch
|
||||||
mutations event hooks miss (world-gen, other mods, `/fill`). Only the overworld is tracked —
|
mutations event hooks miss (world-gen, other mods, `/fill`). Only the overworld is tracked. The
|
||||||
other dimensions and `neoforge-26_1` land in a later phase. The WS client and JSON encoding are
|
WS client and JSON encoding are hand-rolled (no third-party dependency) — see
|
||||||
hand-rolled (no third-party dependency) — see
|
|
||||||
`common/src/main/java/.../ws/SimpleWebSocketClient.java` and `.../json/MiniJson.java` for why.
|
`common/src/main/java/.../ws/SimpleWebSocketClient.java` and `.../json/MiniJson.java` for why.
|
||||||
`forge-1_7_10` implements the identical config/flush/reconciliation/player-tracking behavior
|
|
||||||
against 1.7.10's own API generation (see `Forge1710ChunkAdapter`'s javadoc for what differs) — it
|
`forge-1_7_10` and `neoforge-26_1` implement the identical config/flush/reconciliation/
|
||||||
has no `MultiPlaceEvent` hook (that class doesn't reliably exist at this Forge version), so
|
player-tracking behavior against their own API generation (see `Forge1710ChunkAdapter`'s and
|
||||||
multi-block placements like doors/beds are only caught by the reconciliation sweep rather than
|
`Neoforge261ChunkAdapter`'s javadocs for what differs). Neither has a `MultiPlaceEvent`-equivalent
|
||||||
immediately, unlike `forge-1_12_2`.
|
hook wired (1.7.10: that class doesn't reliably exist at this Forge version; 26.1.2: skipped for
|
||||||
|
symmetry, no strong need identified), so multi-block placements like doors/beds are only caught
|
||||||
|
by the reconciliation sweep rather than immediately, unlike `forge-1_12_2`. `neoforge-26_1` writes
|
||||||
|
its config to `config/mcmapper-server.toml` (NeoForge's `ModConfigSpec`/TOML format, not the
|
||||||
|
legacy leaves' `.cfg`), and — being post-Flattening — carries the full registry-wide packed
|
||||||
|
`BlockState` id as `DeltaEvent#blockStateId` (an `int`, so this is lossless for the 2D column
|
||||||
|
pipeline); 3D section backfill (`SectionData#blocks`, a `char[]` for wire-size reasons inherited
|
||||||
|
from the pre-Flattening leaves) truncates to the low 16 bits, a known, documented collision risk
|
||||||
|
for very large modded registries — see `Neoforge261ChunkAdapter`'s javadoc.
|
||||||
|
|
||||||
### Player position tracking (Phase 7b)
|
### Player position tracking (Phase 7b)
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -20,6 +20,8 @@ minecraft_1_12_2_version=1.12.2
|
|||||||
forge_1_12_2_version=14.23.5.2847
|
forge_1_12_2_version=14.23.5.2847
|
||||||
mcp_1_12_2_mappings=stable_39
|
mcp_1_12_2_mappings=stable_39
|
||||||
|
|
||||||
# neoforge-26_1 (loader assumed NeoForge — confirm before Phase 10, see plan's Open Assumptions)
|
# neoforge-26_1 — loader confirmed NeoForge (Phase 10); this properties file isn't read by that
|
||||||
|
# leaf's build (it's a standalone Gradle project, see neoforge-26_1/settings.gradle), kept here
|
||||||
|
# only as a record of the pinned version alongside the other two leaves'.
|
||||||
minecraft_26_1_version=26.1.2
|
minecraft_26_1_version=26.1.2
|
||||||
neoforge_26_1_version=26.1.2
|
neoforge_26_1_version=26.1.2.94
|
||||||
|
|||||||
@@ -1,12 +1,28 @@
|
|||||||
// Loader assumed to be NeoForge (see root plan's Open Assumptions — confirm the actual 26.1.x
|
// Loader confirmed NeoForge (Phase 10) — versions like 26.1.2.94 are published on
|
||||||
// loader ecosystem before Phase 10). Uses the modern ModDevGradle plugin; requires a recent JDK
|
// maven.neoforged.net. Uses the modern ModDevGradle plugin. Minecraft itself now requires Java
|
||||||
// (21+) unlike the two legacy leaves.
|
// 25 (bumped from 21 during the 26.x cycle) — see settings.gradle's comment on how the Java 25
|
||||||
|
// toolchain gets provisioned without needing it pre-installed system-wide.
|
||||||
|
//
|
||||||
|
// Standalone build, not part of the root multi-project build — see settings.gradle.
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'net.neoforged.moddev' version '2.0.+'
|
id 'net.neoforged.moddev' version '2.0.+'
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceCompatibility = targetCompatibility = JavaVersion.VERSION_21
|
group = 'com.octoturge.mcmapper'
|
||||||
|
version = '0.1.0-SNAPSHOT'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
toolchain {
|
||||||
|
languageVersion = JavaLanguageVersion.of(25)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceCompatibility = targetCompatibility = JavaVersion.VERSION_25
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main {
|
main {
|
||||||
@@ -17,7 +33,7 @@ sourceSets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
neoForge {
|
neoForge {
|
||||||
version = project.neoforge_26_1_version
|
version = '26.1.2.94'
|
||||||
|
|
||||||
runs {
|
runs {
|
||||||
client {}
|
client {}
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
Vendored
+252
@@ -0,0 +1,252 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||||
|
' "$PWD" ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
Vendored
+94
@@ -0,0 +1,94 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS=-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// Standalone build — deliberately NOT part of the root multi-project build (see the root
|
||||||
|
// settings.gradle's comment): ModDevGradle needs Gradle 8+ and a Java 25 toolchain, incompatible
|
||||||
|
// with the two legacy leaves' Gradle-7/JDK-8-daemon pin within one Gradle invocation. Build this
|
||||||
|
// leaf from inside this directory, with its own wrapper:
|
||||||
|
//
|
||||||
|
// cd neoforge-26_1 && ./gradlew build
|
||||||
|
//
|
||||||
|
// The Gradle daemon itself can run on any modern JDK on PATH/JAVA_HOME (8.14.x supports up to
|
||||||
|
// JDK 23 as the *daemon* JVM) — it does not need to already be JDK 25. The Foojay resolver below
|
||||||
|
// lets Gradle auto-provision an actual JDK 25 toolchain (into its own GRADLE_USER_HOME cache, not
|
||||||
|
// a system-wide install) for the compile/run tasks that require Minecraft's own Java 25
|
||||||
|
// requirement, without needing one pre-installed.
|
||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
gradlePluginPortal()
|
||||||
|
maven { url = 'https://maven.neoforged.net/releases' }
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = 'neoforge-26_1'
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.octoturge.mcmapper.neoforge261;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.CommandDispatcher;
|
||||||
|
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||||
|
import com.octoturge.mcmapper.common.BackendConnection;
|
||||||
|
import com.octoturge.mcmapper.common.LinkCodeGenerator;
|
||||||
|
import com.octoturge.mcmapper.common.protocol.LinkRequest;
|
||||||
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
|
import net.minecraft.commands.Commands;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code /mcmapper link} — see LinkRequest.java's javadoc and MCMapper-Backend's link.ts. Modern
|
||||||
|
* Brigadier command registration (this era has no {@code CommandBase} to extend), registered from
|
||||||
|
* {@link MCMapperMod}'s {@code RegisterCommandsEvent} handler.
|
||||||
|
*/
|
||||||
|
final class LinkCommand {
|
||||||
|
private LinkCommand() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static void register(CommandDispatcher<CommandSourceStack> dispatcher, BackendConnection connection) {
|
||||||
|
dispatcher.register(Commands.literal("mcmapper")
|
||||||
|
.then(Commands.literal("link")
|
||||||
|
.executes(ctx -> link(ctx.getSource(), connection))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int link(CommandSourceStack source, BackendConnection connection) throws CommandSyntaxException {
|
||||||
|
ServerPlayer player = source.getPlayerOrException();
|
||||||
|
String code = LinkCodeGenerator.generate();
|
||||||
|
LinkRequest.AuthMode authMode = source.getServer().usesAuthentication()
|
||||||
|
? LinkRequest.AuthMode.ONLINE : LinkRequest.AuthMode.OFFLINE;
|
||||||
|
connection.sendLinkRequest(new LinkRequest(player.getUUID().toString(),
|
||||||
|
player.getGameProfile().name(), code, authMode));
|
||||||
|
source.sendSuccess(() -> Component.literal(
|
||||||
|
"Your MCMapper link code: " + code + " — enter it on the map site within 10 minutes."), false);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,43 @@
|
|||||||
package com.octoturge.mcmapper.neoforge261;
|
package com.octoturge.mcmapper.neoforge261;
|
||||||
|
|
||||||
|
import com.octoturge.mcmapper.common.ChunkAdapter;
|
||||||
|
import com.octoturge.mcmapper.common.DefaultBackendConnection;
|
||||||
|
import com.octoturge.mcmapper.common.ReconciliationScheduler;
|
||||||
|
import com.octoturge.mcmapper.common.config.MapperConfig;
|
||||||
|
import com.octoturge.mcmapper.common.protocol.DeltaEvent;
|
||||||
|
import com.octoturge.mcmapper.common.protocol.PlayerPosition;
|
||||||
|
import com.octoturge.mcmapper.common.protocol.SectionData;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
import net.neoforged.bus.api.IEventBus;
|
import net.neoforged.bus.api.IEventBus;
|
||||||
|
import net.neoforged.bus.api.SubscribeEvent;
|
||||||
|
import net.neoforged.fml.ModContainer;
|
||||||
import net.neoforged.fml.common.Mod;
|
import net.neoforged.fml.common.Mod;
|
||||||
|
import net.neoforged.fml.config.ModConfig;
|
||||||
|
import net.neoforged.neoforge.common.ModConfigSpec;
|
||||||
|
import net.neoforged.neoforge.common.NeoForge;
|
||||||
|
import net.neoforged.neoforge.event.RegisterCommandsEvent;
|
||||||
|
import net.neoforged.neoforge.event.ServerChatEvent;
|
||||||
|
import net.neoforged.neoforge.event.server.ServerStartingEvent;
|
||||||
|
import net.neoforged.neoforge.event.server.ServerStoppingEvent;
|
||||||
|
import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entry point for the 26.1.2 leaf — lowest MVP priority (Phase 10), stubbed structurally now.
|
* Entry point for the 26.1.2 leaf (Phase 10, lowest MVP priority) — post-Flattening, mixin-era
|
||||||
* Loader assumed NeoForge; see root plan's Open Assumptions for the verification note.
|
* NeoForge. Implements the same feature set proven by the two legacy leaves against the {@code
|
||||||
|
* common} interfaces; see {@link Neoforge261ChunkAdapter}'s javadoc for what differs API-wise
|
||||||
|
* (block-state reads instead of raw id+meta, {@code net.neoforged.*} packages, Brigadier commands
|
||||||
|
* instead of {@code CommandBase}, {@code ModConfigSpec} instead of legacy Forge's {@code
|
||||||
|
* Configuration}, constructor-injected event buses instead of {@code @Mod.EventHandler} methods).
|
||||||
*/
|
*/
|
||||||
@Mod(MCMapperMod.MOD_ID)
|
@Mod(MCMapperMod.MOD_ID)
|
||||||
public class MCMapperMod {
|
public class MCMapperMod {
|
||||||
@@ -15,7 +45,180 @@ public class MCMapperMod {
|
|||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(MCMapperMod.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(MCMapperMod.class);
|
||||||
|
|
||||||
public MCMapperMod(IEventBus modEventBus) {
|
private final ModConfigSpec.ConfigValue<String> backendUrlSpec;
|
||||||
LOGGER.info("MCMapper (neoforge-26_1 leaf) scaffolding loaded — no-op until Phase 10");
|
private final ModConfigSpec.ConfigValue<String> serverTokenSpec;
|
||||||
|
private final ModConfigSpec.IntValue deltaFlushIntervalTicksSpec;
|
||||||
|
private final ModConfigSpec.IntValue reconciliationIntervalTicksSpec;
|
||||||
|
private final ModConfigSpec.IntValue reconciliationChunksPerSweepSpec;
|
||||||
|
private final ModConfigSpec.BooleanValue playerTrackingEnabledSpec;
|
||||||
|
private final ModConfigSpec.IntValue playerPositionIntervalTicksSpec;
|
||||||
|
|
||||||
|
private final MapperConfig config = new MapperConfig();
|
||||||
|
private DefaultBackendConnection connection;
|
||||||
|
private Neoforge261ChunkAdapter adapter;
|
||||||
|
private final List<DeltaEvent> pendingDeltas = Collections.synchronizedList(new ArrayList<DeltaEvent>());
|
||||||
|
private final Set<Long> pendingSectionChunks = Collections.synchronizedSet(new LinkedHashSet<Long>());
|
||||||
|
private final ReconciliationScheduler reconciliationScheduler = new ReconciliationScheduler();
|
||||||
|
private MinecraftServer mcServer;
|
||||||
|
private int ticksSinceFlush = 0;
|
||||||
|
private int ticksSinceReconciliation = 0;
|
||||||
|
private int ticksSincePlayerPositions = 0;
|
||||||
|
|
||||||
|
public MCMapperMod(IEventBus modEventBus, ModContainer container) {
|
||||||
|
ModConfigSpec.Builder builder = new ModConfigSpec.Builder();
|
||||||
|
backendUrlSpec = builder.comment("WS URL of the MCMapper backend api service")
|
||||||
|
.define("backendUrl", config.backendUrl);
|
||||||
|
serverTokenSpec = builder.comment("Per-server token issued when registering with the backend (see MCMapper-Backend's `bun run seed`)")
|
||||||
|
.define("serverToken", config.serverToken);
|
||||||
|
deltaFlushIntervalTicksSpec = builder.comment("How often (in ticks) to batch and flush block-change deltas to the backend")
|
||||||
|
.defineInRange("deltaFlushIntervalTicks", config.deltaFlushIntervalTicks, 1, 20 * 60);
|
||||||
|
reconciliationIntervalTicksSpec = builder.comment("How often (in ticks) to run a reconciliation sweep, re-reading and resending a " +
|
||||||
|
"rotating slice of loaded chunks to catch changes event hooks miss (world-gen, other mods, /fill, etc.)")
|
||||||
|
.defineInRange("reconciliationIntervalTicks", config.reconciliationIntervalTicks, 20, 20 * 60 * 60);
|
||||||
|
reconciliationChunksPerSweepSpec = builder.comment("Max chunks to re-read and resend per reconciliation sweep")
|
||||||
|
.defineInRange("reconciliationChunksPerSweep", config.reconciliationChunksPerSweep, 1, 5000);
|
||||||
|
playerTrackingEnabledSpec = builder.comment("Whether to send throttled online-player positions to the backend at all. The " +
|
||||||
|
"backend also has its own per-server admin toggle deciding whether it relays this on to web " +
|
||||||
|
"viewers — this setting only controls the mod side.")
|
||||||
|
.define("playerTrackingEnabled", config.playerTrackingEnabled);
|
||||||
|
playerPositionIntervalTicksSpec = builder.comment("How often (in ticks) to send the online-player roster to the backend, when playerTrackingEnabled")
|
||||||
|
.defineInRange("playerPositionIntervalTicks", config.playerPositionIntervalTicks, 5, 20 * 60);
|
||||||
|
container.registerConfig(ModConfig.Type.SERVER, builder.build());
|
||||||
|
|
||||||
|
NeoForge.EVENT_BUS.register(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadConfig() {
|
||||||
|
config.backendUrl = backendUrlSpec.get();
|
||||||
|
config.serverToken = serverTokenSpec.get();
|
||||||
|
config.deltaFlushIntervalTicks = deltaFlushIntervalTicksSpec.get();
|
||||||
|
config.reconciliationIntervalTicks = reconciliationIntervalTicksSpec.get();
|
||||||
|
config.reconciliationChunksPerSweep = reconciliationChunksPerSweepSpec.get();
|
||||||
|
config.playerTrackingEnabled = playerTrackingEnabledSpec.get();
|
||||||
|
config.playerPositionIntervalTicks = playerPositionIntervalTicksSpec.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public void serverStarting(ServerStartingEvent event) {
|
||||||
|
loadConfig();
|
||||||
|
if (config.serverToken == null || config.serverToken.isEmpty()) {
|
||||||
|
LOGGER.warn("MCMapper serverToken is not configured (see config/mcmapper-server.toml) — not connecting to backend");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mcServer = event.getServer();
|
||||||
|
connection = new DefaultBackendConnection(LOGGER::info, LOGGER::warn);
|
||||||
|
connection.connect(config.backendUrl, config.serverToken);
|
||||||
|
LOGGER.info("MCMapper (neoforge-26_1 leaf) connecting to " + config.backendUrl);
|
||||||
|
|
||||||
|
ServerLevel overworld = event.getServer().overworld();
|
||||||
|
adapter = new Neoforge261ChunkAdapter(overworld);
|
||||||
|
adapter.registerEventHooks(new ChunkAdapter.DeltaSink() {
|
||||||
|
@Override
|
||||||
|
public void onDelta(DeltaEvent delta) {
|
||||||
|
pendingDeltas.add(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onChunkDirty(int chunkX, int chunkZ) {
|
||||||
|
pendingSectionChunks.add((((long) chunkX) << 32) | (chunkZ & 0xFFFFFFFFL));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Neoforge261ChatBridge chatBridge = new Neoforge261ChatBridge(event.getServer());
|
||||||
|
connection.setChatListener(chatBridge::injectWebChatMessage);
|
||||||
|
connection.setWaypointShareListener(chatBridge::injectWaypointShare);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public void onRegisterCommands(RegisterCommandsEvent event) {
|
||||||
|
LinkCommand.register(event.getDispatcher(), connection != null ? connection : new com.octoturge.mcmapper.common.BackendConnection.NoOp());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public void onServerChat(ServerChatEvent event) {
|
||||||
|
if (connection == null) return;
|
||||||
|
ServerPlayer player = event.getPlayer();
|
||||||
|
connection.sendChatMessage(player.getUUID().toString(), event.getUsername(), event.getRawText());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public void onServerTick(ServerTickEvent.Post event) {
|
||||||
|
if (connection == null) return;
|
||||||
|
|
||||||
|
if (++ticksSinceFlush >= config.deltaFlushIntervalTicks) {
|
||||||
|
ticksSinceFlush = 0;
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (++ticksSinceReconciliation >= config.reconciliationIntervalTicks) {
|
||||||
|
ticksSinceReconciliation = 0;
|
||||||
|
reconcile();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.playerTrackingEnabled && ++ticksSincePlayerPositions >= config.playerPositionIntervalTicks) {
|
||||||
|
ticksSincePlayerPositions = 0;
|
||||||
|
sendPlayerPositions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendPlayerPositions() {
|
||||||
|
List<PlayerPosition> players = new ArrayList<PlayerPosition>();
|
||||||
|
for (ServerPlayer player : mcServer.getPlayerList().getPlayers()) {
|
||||||
|
if (player.level().dimension() != net.minecraft.world.level.Level.OVERWORLD) continue;
|
||||||
|
players.add(new PlayerPosition(player.getUUID().toString(), player.getGameProfile().name(),
|
||||||
|
player.getBlockX(), player.getBlockY(), player.getBlockZ()));
|
||||||
|
}
|
||||||
|
connection.sendPlayerPositions(0, players);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reconcile() {
|
||||||
|
List<Long> chunkKeys = reconciliationScheduler.next(adapter.loadedChunkKeys(),
|
||||||
|
config.reconciliationChunksPerSweep);
|
||||||
|
if (chunkKeys.isEmpty()) return;
|
||||||
|
|
||||||
|
String dimensionId = adapter.getDimensionId();
|
||||||
|
for (long key : chunkKeys) {
|
||||||
|
int chunkX = (int) (key >> 32);
|
||||||
|
int chunkZ = (int) key;
|
||||||
|
List<DeltaEvent> columns = adapter.readChunk(dimensionId, chunkX, chunkZ);
|
||||||
|
if (!columns.isEmpty()) connection.sendDeltas(columns);
|
||||||
|
List<SectionData> sections = adapter.readSections(chunkX, chunkZ);
|
||||||
|
if (!sections.isEmpty()) connection.sendSections(dimensionId, chunkX, chunkZ, sections);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void flush() {
|
||||||
|
List<DeltaEvent> batch;
|
||||||
|
synchronized (pendingDeltas) {
|
||||||
|
if (pendingDeltas.isEmpty()) {
|
||||||
|
batch = null;
|
||||||
|
} else {
|
||||||
|
batch = new ArrayList<DeltaEvent>(pendingDeltas);
|
||||||
|
pendingDeltas.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (batch != null) connection.sendDeltas(batch);
|
||||||
|
|
||||||
|
List<Long> dirtyChunks;
|
||||||
|
synchronized (pendingSectionChunks) {
|
||||||
|
if (pendingSectionChunks.isEmpty()) return;
|
||||||
|
dirtyChunks = new ArrayList<Long>(pendingSectionChunks);
|
||||||
|
pendingSectionChunks.clear();
|
||||||
|
}
|
||||||
|
String dimensionId = adapter.getDimensionId();
|
||||||
|
for (long key : dirtyChunks) {
|
||||||
|
int chunkX = (int) (key >> 32);
|
||||||
|
int chunkZ = (int) key;
|
||||||
|
List<SectionData> sections = adapter.readSections(chunkX, chunkZ);
|
||||||
|
if (!sections.isEmpty()) {
|
||||||
|
connection.sendSections(dimensionId, chunkX, chunkZ, sections);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public void serverStopping(ServerStoppingEvent event) {
|
||||||
|
if (connection != null) connection.disconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
package com.octoturge.mcmapper.neoforge261;
|
||||||
|
|
||||||
|
import com.octoturge.mcmapper.common.ChatBridge;
|
||||||
|
import com.octoturge.mcmapper.common.protocol.WaypointChatFormatter;
|
||||||
|
import com.octoturge.mcmapper.common.protocol.WaypointShare;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
|
||||||
|
public class Neoforge261ChatBridge implements ChatBridge {
|
||||||
|
private final MinecraftServer server;
|
||||||
|
|
||||||
|
public Neoforge261ChatBridge(MinecraftServer server) {
|
||||||
|
this.server = server;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void injectWebChatMessage(String displayName, String message) {
|
||||||
|
server.getPlayerList().broadcastSystemMessage(Component.literal("[Web] " + displayName + ": " + message), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void injectWaypointShare(WaypointShare waypoint) {
|
||||||
|
// Same rationale as the legacy leaves' ChatBridge: neither JourneyMap's nor Xaero's
|
||||||
|
// chat-waypoint syntax needs a click-event component, plain text is enough.
|
||||||
|
server.getPlayerList().broadcastSystemMessage(Component.literal(WaypointChatFormatter.format(waypoint)), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
+191
@@ -0,0 +1,191 @@
|
|||||||
|
package com.octoturge.mcmapper.neoforge261;
|
||||||
|
|
||||||
|
import com.octoturge.mcmapper.common.ChunkAdapter;
|
||||||
|
import com.octoturge.mcmapper.common.protocol.DeltaEvent;
|
||||||
|
import com.octoturge.mcmapper.common.protocol.SectionData;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.LevelAccessor;
|
||||||
|
import net.minecraft.world.level.block.Block;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.LevelChunk;
|
||||||
|
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import net.neoforged.bus.api.SubscribeEvent;
|
||||||
|
import net.neoforged.neoforge.common.NeoForge;
|
||||||
|
import net.neoforged.neoforge.event.level.BlockEvent;
|
||||||
|
import net.neoforged.neoforge.event.level.ChunkEvent;
|
||||||
|
import net.neoforged.neoforge.event.level.block.BreakBlockEvent;
|
||||||
|
import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 26.1.2's {@link ChunkAdapter} (Phase 10) — post-Flattening: blocks are read as {@link
|
||||||
|
* BlockState}, not raw id+metadata, and this leaf's block identity ({@code Block.getId(state)},
|
||||||
|
* a registry-wide packed int) no longer reliably fits the pre-Flattening leaves' {@code (blockId
|
||||||
|
* << 4) | meta} 16-bit encoding once a heavily-modded registry is in play (see {@link
|
||||||
|
* DeltaEvent}'s javadoc). {@link DeltaEvent#blockStateId} is a plain {@code int}, so 2D
|
||||||
|
* column/reconciliation data (the Phase 1/9/10 parity bar) carries the full packed id losslessly;
|
||||||
|
* {@link SectionData#blocks} is a {@code char[]} for wire-size reasons inherited from the
|
||||||
|
* pre-Flattening leaves, so 3D section backfill truncates to the low 16 bits — a known,
|
||||||
|
* documented collision risk for very large (heavily modded) registries, deferred rather than
|
||||||
|
* redesigning the wire protocol for a Phase whose own verification bar is 2D-tile parity, not 3D.
|
||||||
|
*
|
||||||
|
* <p>Also unlike the two legacy leaves, this adapter tracks its own loaded-chunk set from {@link
|
||||||
|
* ChunkEvent.Load}/{@code Unload} rather than querying a chunk provider's internals directly —
|
||||||
|
* modern {@code ServerChunkCache} doesn't expose a simple public "currently loaded chunks"
|
||||||
|
* iterable, and an event-driven set is arguably more robust anyway.
|
||||||
|
*/
|
||||||
|
public class Neoforge261ChunkAdapter implements ChunkAdapter {
|
||||||
|
private final ServerLevel level;
|
||||||
|
private final String dimensionId;
|
||||||
|
private final Set<Long> loadedChunks = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
|
public Neoforge261ChunkAdapter(ServerLevel level) {
|
||||||
|
this.level = level;
|
||||||
|
this.dimensionId = level.dimension().identifier().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDimensionId() {
|
||||||
|
return dimensionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<DeltaEvent> readChunk(String dimension, int chunkX, int chunkZ) {
|
||||||
|
LevelChunk chunk = level.getChunk(chunkX, chunkZ);
|
||||||
|
List<DeltaEvent> events = new ArrayList<DeltaEvent>(256);
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
for (int lx = 0; lx < 16; lx++) {
|
||||||
|
for (int lz = 0; lz < 16; lz++) {
|
||||||
|
events.add(readColumn(chunkX * 16 + lx, chunkZ * 16 + lz, now, DeltaEvent.Source.RECONCILIATION));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DeltaEvent readColumn(int worldX, int worldZ, long now, DeltaEvent.Source source) {
|
||||||
|
int height = level.getHeight(Heightmap.Types.WORLD_SURFACE, worldX, worldZ);
|
||||||
|
int topY = height - 1;
|
||||||
|
BlockState state = level.getBlockState(new BlockPos(worldX, topY, worldZ));
|
||||||
|
int blockStateId = Block.getId(state);
|
||||||
|
return new DeltaEvent(dimensionId, worldX, topY, worldZ, blockStateId, now, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SectionData> readSections(int chunkX, int chunkZ) {
|
||||||
|
LevelChunk chunk = level.getChunk(chunkX, chunkZ);
|
||||||
|
LevelChunkSection[] storage = chunk.getSections();
|
||||||
|
List<SectionData> sections = new ArrayList<SectionData>();
|
||||||
|
for (int sectionY = 0; sectionY < storage.length; sectionY++) {
|
||||||
|
LevelChunkSection section = storage[sectionY];
|
||||||
|
// hasOnlyAir() is the modern equivalent of the legacy leaves' null/isEmpty()
|
||||||
|
// shortcut — skip fully-air sections without a 4096-position scan.
|
||||||
|
if (section == null || section.hasOnlyAir()) continue;
|
||||||
|
|
||||||
|
char[] blocks = new char[4096];
|
||||||
|
for (int ly = 0; ly < 16; ly++) {
|
||||||
|
for (int lz = 0; lz < 16; lz++) {
|
||||||
|
for (int lx = 0; lx < 16; lx++) {
|
||||||
|
BlockState state = section.getBlockState(lx, ly, lz);
|
||||||
|
int blockStateId = Block.getId(state) & 0xFFFF;
|
||||||
|
blocks[(ly * 16 + lz) * 16 + lx] = (char) blockStateId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sections.add(new SectionData(sectionY, blocks));
|
||||||
|
}
|
||||||
|
return sections;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerEventHooks(DeltaSink sink) {
|
||||||
|
NeoForge.EVENT_BUS.register(new EventHooks(sink));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Long> loadedChunkKeys() {
|
||||||
|
return new ArrayList<Long>(loadedChunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same "mark dirty, drain once per tick" strategy as the legacy leaves (see {@code
|
||||||
|
* Forge1122ChunkAdapter}'s javadoc) — {@code BreakBlockEvent} still fires before the actual
|
||||||
|
* removal. This era's block events expose {@code getLevel()}/{@code getPos()} methods (not
|
||||||
|
* the legacy leaves' direct-field or {@code x}/{@code y}/{@code z} conventions), and block
|
||||||
|
* break/place are two differently-shaped classes ({@code BreakBlockEvent} top-level,
|
||||||
|
* {@code BlockEvent.EntityPlaceEvent} nested) rather than one shared {@code BlockEvent}
|
||||||
|
* subclass pair.
|
||||||
|
*/
|
||||||
|
private class EventHooks {
|
||||||
|
private final DeltaSink sink;
|
||||||
|
private final Set<Long> dirtyColumns = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
|
EventHooks(DeltaSink sink) {
|
||||||
|
this.sink = sink;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public void onBlockBreak(BreakBlockEvent event) {
|
||||||
|
markDirty(event.getLevel(), event.getPos());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public void onBlockPlace(BlockEvent.EntityPlaceEvent event) {
|
||||||
|
markDirty(event.getLevel(), event.getPos());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public void onChunkLoad(ChunkEvent.Load event) {
|
||||||
|
if (event.getLevel() != level) return;
|
||||||
|
LevelChunk chunk = event.getChunk();
|
||||||
|
int chunkX = chunk.getPos().x();
|
||||||
|
int chunkZ = chunk.getPos().z();
|
||||||
|
loadedChunks.add(key(chunkX, chunkZ));
|
||||||
|
for (DeltaEvent e : readChunk(dimensionId, chunkX, chunkZ)) {
|
||||||
|
sink.onDelta(e);
|
||||||
|
}
|
||||||
|
sink.onChunkDirty(chunkX, chunkZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public void onChunkUnload(ChunkEvent.Unload event) {
|
||||||
|
if (event.getLevel() != level) return;
|
||||||
|
LevelChunk chunk = event.getChunk();
|
||||||
|
loadedChunks.remove(key(chunk.getPos().x(), chunk.getPos().z()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public void onServerTick(ServerTickEvent.Post event) {
|
||||||
|
if (dirtyColumns.isEmpty()) return;
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
Set<Long> dirtyChunks = new LinkedHashSet<Long>();
|
||||||
|
Iterator<Long> it = dirtyColumns.iterator();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
long key = it.next();
|
||||||
|
it.remove();
|
||||||
|
int wx = (int) (key >> 32);
|
||||||
|
int wz = (int) key;
|
||||||
|
sink.onDelta(readColumn(wx, wz, now, DeltaEvent.Source.EVENT));
|
||||||
|
dirtyChunks.add(key(wx >> 4, wz >> 4));
|
||||||
|
}
|
||||||
|
for (long chunkKey : dirtyChunks) {
|
||||||
|
sink.onChunkDirty((int) (chunkKey >> 32), (int) chunkKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markDirty(LevelAccessor eventLevel, BlockPos pos) {
|
||||||
|
if (eventLevel != level) return;
|
||||||
|
dirtyColumns.add(key(pos.getX(), pos.getZ()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long key(int a, int b) {
|
||||||
|
return (((long) a) << 32) | (b & 0xFFFFFFFFL);
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-5
@@ -17,8 +17,12 @@ include 'forge-1_12_2'
|
|||||||
// neoforge-26_1 is intentionally NOT included in the default build: both legacy leaves
|
// neoforge-26_1 is intentionally NOT included in the default build: both legacy leaves
|
||||||
// (forge-1_7_10, forge-1_12_2) use anatawa12's ForgeGradle 1.2/2.3 forks, which need Gradle 7.6
|
// (forge-1_7_10, forge-1_12_2) use anatawa12's ForgeGradle 1.2/2.3 forks, which need Gradle 7.6
|
||||||
// and JDK 8 (see gradle.properties' org.gradle.java.home pin and this project's
|
// and JDK 8 (see gradle.properties' org.gradle.java.home pin and this project's
|
||||||
// gradle-wrapper.properties). ModDevGradle (used by neoforge-26_1) needs Gradle 8+ and JDK 17+
|
// gradle-wrapper.properties). ModDevGradle (used by neoforge-26_1) needs Gradle 8+ and a Java 25
|
||||||
// to even apply the plugin — incompatible with that daemon within one invocation. Until Phase 10
|
// toolchain (Minecraft itself now requires Java 25 as of the 26.x cycle) — incompatible with
|
||||||
// gives it a proper isolated build setup, it can only be built by pointing org.gradle.java.home
|
// that daemon within one invocation. Phase 10 gave it its own standalone build instead: it has
|
||||||
// at a JDK 21+ install and invoking Gradle (8+) from inside the neoforge-26_1/ directory
|
// its own wrapper + settings.gradle (with the Foojay toolchain resolver, so Gradle
|
||||||
// directly, with its own wrapper. The module itself is fully scaffolded and ready.
|
// auto-provisions the JDK 25 toolchain rather than needing one pre-installed) — build it from
|
||||||
|
// inside neoforge-26_1/ directly (`cd neoforge-26_1 && ./gradlew build`), pointing
|
||||||
|
// org.gradle.java.home at a JDK 21+ install for the Gradle daemon itself via a separate
|
||||||
|
// $GRADLE_USER_HOME (same mechanism as the legacy leaves' JDK 8 pin) — see neoforge-26_1's
|
||||||
|
// README section for the full command.
|
||||||
|
|||||||
Reference in New Issue
Block a user