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
|
||||
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.*`).
|
||||
- `neoforge-26_1/` — MC 26.1.2 (NeoForge, assumed). Lowest MVP priority. Structurally scaffolded,
|
||||
not yet wired into the default build (see `settings.gradle`).
|
||||
- `neoforge-26_1/` — MC 26.1.2 (Phase 10; loader confirmed NeoForge). Lowest MVP priority, fully
|
||||
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
|
||||
|
||||
@@ -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
|
||||
take a while / needs network access. `neoforge-26_1` is not yet buildable from the root — it
|
||||
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.
|
||||
take a while / needs network access.
|
||||
|
||||
## 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:
|
||||
|
||||
@@ -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
|
||||
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
|
||||
mutations event hooks miss (world-gen, other mods, `/fill`). Only the overworld is tracked —
|
||||
other dimensions and `neoforge-26_1` land in a later phase. The WS client and JSON encoding are
|
||||
hand-rolled (no third-party dependency) — see
|
||||
mutations event hooks miss (world-gen, other mods, `/fill`). Only the overworld is tracked. The
|
||||
WS client and JSON encoding are hand-rolled (no third-party dependency) — see
|
||||
`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
|
||||
has no `MultiPlaceEvent` hook (that class doesn't reliably exist at this Forge version), so
|
||||
multi-block placements like doors/beds are only caught by the reconciliation sweep rather than
|
||||
immediately, unlike `forge-1_12_2`.
|
||||
|
||||
`forge-1_7_10` and `neoforge-26_1` implement the identical config/flush/reconciliation/
|
||||
player-tracking behavior against their own API generation (see `Forge1710ChunkAdapter`'s and
|
||||
`Neoforge261ChunkAdapter`'s javadocs for what differs). Neither has a `MultiPlaceEvent`-equivalent
|
||||
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)
|
||||
|
||||
|
||||
+4
-2
@@ -20,6 +20,8 @@ minecraft_1_12_2_version=1.12.2
|
||||
forge_1_12_2_version=14.23.5.2847
|
||||
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
|
||||
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 ecosystem before Phase 10). Uses the modern ModDevGradle plugin; requires a recent JDK
|
||||
// (21+) unlike the two legacy leaves.
|
||||
// Loader confirmed NeoForge (Phase 10) — versions like 26.1.2.94 are published on
|
||||
// maven.neoforged.net. Uses the modern ModDevGradle plugin. Minecraft itself now requires Java
|
||||
// 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 {
|
||||
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 {
|
||||
main {
|
||||
@@ -17,7 +33,7 @@ sourceSets {
|
||||
}
|
||||
|
||||
neoForge {
|
||||
version = project.neoforge_26_1_version
|
||||
version = '26.1.2.94'
|
||||
|
||||
runs {
|
||||
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;
|
||||
|
||||
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.SubscribeEvent;
|
||||
import net.neoforged.fml.ModContainer;
|
||||
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.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.
|
||||
* Loader assumed NeoForge; see root plan's Open Assumptions for the verification note.
|
||||
* Entry point for the 26.1.2 leaf (Phase 10, lowest MVP priority) — post-Flattening, mixin-era
|
||||
* 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)
|
||||
public class MCMapperMod {
|
||||
@@ -15,7 +45,180 @@ public class MCMapperMod {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(MCMapperMod.class);
|
||||
|
||||
public MCMapperMod(IEventBus modEventBus) {
|
||||
LOGGER.info("MCMapper (neoforge-26_1 leaf) scaffolding loaded — no-op until Phase 10");
|
||||
private final ModConfigSpec.ConfigValue<String> backendUrlSpec;
|
||||
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
|
||||
// (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
|
||||
// gradle-wrapper.properties). ModDevGradle (used by neoforge-26_1) needs Gradle 8+ and JDK 17+
|
||||
// to even apply the plugin — incompatible with that daemon within one invocation. Until Phase 10
|
||||
// gives it a proper isolated build setup, it can only be built by pointing org.gradle.java.home
|
||||
// at a JDK 21+ install and invoking Gradle (8+) from inside the neoforge-26_1/ directory
|
||||
// directly, with its own wrapper. The module itself is fully scaffolded and ready.
|
||||
// gradle-wrapper.properties). ModDevGradle (used by neoforge-26_1) needs Gradle 8+ and a Java 25
|
||||
// toolchain (Minecraft itself now requires Java 25 as of the 26.x cycle) — incompatible with
|
||||
// that daemon within one invocation. Phase 10 gave it its own standalone build instead: it has
|
||||
// its own wrapper + settings.gradle (with the Foojay toolchain resolver, so Gradle
|
||||
// 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