Bump Endstone to a new BDS version
Two phases:
- Phase 1 regenerates the symbol offset tables - which hook resolves to
which address. It tells you which symbols broke.
- Phase 2 ports
src/bedrock/ to the new ABI using the header diff. It
tells you why they broke and how to fix them.
NDA boundary (read first)
This workflow may use two private Mojang-derived artifacts:
bedrock-headers - required for ABI porting. This private repo contains C++
headers reconstructed from BDS binaries.
bedrock_server.pdb - useful when available for Windows symbol resolution,
but not guaranteed to be published for every BDS release.
Both artifacts are NDA-protected. Never copy header bodies, class
definitions, full member layouts, PDB dumps, symbol listings, or other private
artifact contents into the public endstone repo, its commits, PRs, issues,
logs, or this skill. Endstone's src/bedrock/ is a hand-written, minimal
reimplementation - only what Endstone needs, in Endstone's own naming - which
is the DMCA-safe form. Treat headers, PDBs, generated dumps, and diffs as
private working references only.
Phase 1 - regenerate the symbol tables
How the symbol pipeline works
scripts/configs/{windows,linux}.toml signature configs, hand-maintained
| scripts/dump_symbols.py
v
src/bedrock/symbol_generator/symbols/{windows,linux}.toml resolved offsets
| symbol_generator (CMake target)
v
bedrock_symbols.generated.h std::array of name -> offset
src/bedrock/symbol.h get_symbol() looks a symbol up by __FUNCDNAME__ - the
mangled name of Endstone's own declaration in src/bedrock/. The symbol-table
key IS the signature of Endstone's reimplementation. Unresolved symbols are
written as 0 and skipped by the generator (that hook is disabled; the build
still succeeds).
Prerequisites
uv - runs dump_symbols.py (PEP 723 inline deps, no manual install).
pdbtool - cargo install pdbtool (Microsoft pdb-rs). Reads the Windows PDB
when one is available.
bedrock-headers for the target version. Headers are mandatory for Phase 2
ABI porting and must remain private.
- Optional: the Windows BDS PDB (
bedrock_server.pdb) for the target version.
Use it when available; some BDS releases do not have a public/accessible PDB.
- The target version must be published in the
EndstoneMC/bedrock-server-data
repo - the Linux path downloads the binary from it. Check its versions.json.
Procedure
- Branch off the current release branch (e.g.
v0.11):
git checkout -b feat/<NN.NN>-support (naming follows feat/26.10-support).
- Bump the config versions - set
version = "<X.Y.Z>" in both
scripts/configs/windows.toml and scripts/configs/linux.toml, using the
3-component release string from bedrock-server-data versions.json.
- Regenerate (run in the background, 25 s to a few minutes):
- Windows:
uv run --script scripts/dump_symbols.py scripts/configs/windows.toml --pdb <path>/bedrock_server.pdb
- Linux:
uv run --script scripts/dump_symbols.py scripts/configs/linux.toml
- If the Windows PDB is unavailable, skip the Windows PDB regeneration for
now and rely on Linux pattern results plus the required header diff to
identify ABI changes. Update Windows symbols later if/when a PDB becomes
available.
- Triage the failures. The two platforms fail differently:
- Windows (PDB by name, then byte pattern): each entry is looked up by
mangled
name in the PDB; entries the PDB has no public record for
(lambdas, function-local statics) fall back to scanning the entry's
pattern in the server binary. A miss thus means both failed - the
mangled name is gone (MSVC encodes the full signature incl. return type,
const-ness and access) and the byte pattern no longer matches. A PDB hit
is name-verified; a fallback hit ("Found signature (fallback)") is only
pattern-verified, like a Linux hit.
- Linux (byte-pattern scan): a miss = the
pattern in configs/linux.toml
no longer matches. The function usually still exists - the pattern went
stale. A Linux hit is a pattern match labelled with the config name; it
is not name-verified.
- Failed on both -> real signature/API change.
- Windows only -> the mangled name changed: signature, return type,
const-ness or access specifier. Itanium omits the return type, so a pure
return-type change leaves the Linux name intact. A const/access change is
fixed in Endstone's
src/bedrock/ declaration (__FUNCDNAME__ derives
from it), not the config alone.
- Linux only -> stale byte pattern; re-extract it (IDA / Ghidra).
The failure list is the input to Phase 2.
Phase 2 - port src/bedrock to the new ABI
Source: the header diff
dwarf2cpp reconstructs C++ headers from a DWARF-bearing BDS build (the Android
build libminecraftpe.so carries DWARF; the Windows/Linux server binaries are
stripped). Output lands in bedrock-headers, one branch per BDS release
(android/r26_u1, android/r26_u2, ...).
dwarf2cpp <libminecraftpe.so> --base-dir <build-root> -o <out> (or uvx dwarf2cpp).
- In
bedrock-headers: git checkout -b android/r<NN>_u<N>, place the output, commit.
git diff android/r<prev> android/r<new> is the change set.
The actionable set
src/bedrock/ is ~655 hand-maintained headers - a small subset of BDS. Most of
a release diff (5000+ files) touches nothing Endstone declares. So:
actionable work = (changed headers) intersect (the 655 src/bedrock headers)
Match by normalized basename (lowercase, strip _ and -): bedrock-headers
Mob.h <-> Endstone mob.h; BlockSource.h <-> block_source.h.
Staged review order
Review the diff in stages - foundational types first, so later stages do not
rework. Scope: the handheld/ tree and the top-level src/base/ tree;
skip handheld/src-client/ (game client) and the other top-level src/
subtrees (account, external, gui - client / Xbox / third-party, nothing
Endstone mirrors). Within each stage, deep-dive only the intersection. (The
first attempt used 3 coarse stages; "handheld/src non-world" alone was 467
files / 44 intersecting - too big. Use this finer split:)
src/base (top-level, not under handheld/) - the shared Core library:
foundational utilities and low-level types (BinaryStream, ...). Easy to
miss because every staged path below lives under handheld/ while this tree
is separate; a missed change here (e.g. a new BinaryStream virtual)
silently shifts a vtable that Phase 1 can never flag.
src-deps/SharedTypes - shared types and enums
src/common/network - packets, network types, packet-id / disconnect enums
src/common/server (incl. server/commands) - server and command system
src/common/entity - ECS components
src/common/{certificates,resources,scripting,platform,locale,gameplayhandlers,...} - remaining non-world
src/common/world/actor
src/common/world/item
src/common/world/level/block
src/common/world/level/{dimension,biome} and remaining src/common/world/level/* (chunk, material, storage, level core)
src/common/world/* - remaining world (attribute, effect, events, inventory, response, ...)
src-deps other than SharedTypes (Certificates, VanillaComponents, ...), then anything else
- Cross-validate - once every ABI change is in, re-review the whole
src/bedrock/ diff against the bedrock-headers diff. Every edited function
signature, vtable slot, member type/order, and structural change must trace
to a concrete change in git diff android/r<prev> android/r<new>. Reject
anything not backed by the diff: no invented types, no guessed members, no
hallucinated signatures, no "looks-right" edits. A change that cannot be
matched to the header diff is wrong - revert or fix it. This stage exists
because the porting stages, especially when parallelised across agents, can
introduce plausible but unfounded edits - they must all be matched up.
Applying a change
For each intersecting header with a real change (use Gotchas to rule out
noise first), update Endstone's src/bedrock/ declaration:
- Function signatures (especially hooked /
ENDSTONE_HOOK) - parameter
types, const/ref, return type must match BDS exactly, or __FUNCDNAME__
stops matching the symbol.
- Virtual functions - the vtable order must match BDS. An added / removed /
reordered virtual shifts every slot below it; mirror the new order (use
= 0
placeholders for virtuals Endstone does not implement).
- Members - type, order and size must match for layout. Member names stay
Endstone's own (
lower_case_); do not copy Mojang's names. Declare a new
member's type for real - never a same-size stand-in (std::string etc.). If
the real type would drag in a heavy include chain, add a forward declaration
to src/bedrock/forward.h in alphabetic order and use the type incomplete
(fine for pointers, references, and container value types like
std::vector / std::unordered_map).
- Width-ambiguous integers - bedrock-headers are reconstructed from a
Linux/Android build, where
unsigned long is 64-bit; on Windows (LLP64) it
is 32-bit. Whenever the diff shows unsigned long, port it as
std::uint64_t so the type is 64-bit on both of Endstone's target platforms.
- Template arguments - a class template's default arguments are part of
its declaration: copy them from bedrock-headers verbatim, never guess (e.g.
brstd::bitset's word-type parameter defaults to unsigned int). And never
drop an explicit template argument to lean on a default - a given
instantiation often overrides the default, so check the actual instantiation
in the diff and spell every argument it spells. If the explicit argument is a
width-ambiguous integer, still apply the rule above (brstd::bitset<N, unsigned long> -> brstd::bitset<N, std::uint64_t>).
- One type per corresponding file - when a change needs a BDS type Endstone
does not yet declare, add it in its own
src/bedrock/ header mirroring the
BDS file that defines it (snake_case path), then #include it - do not paste
a foreign type's definition inline into whatever header happens to need it.
(A forward declaration of a helper type may sit in the header that uses it,
as BDS does; a forward declaration needed across many headers goes in
src/bedrock/forward.h.)
- Structural refactors - when BDS introduces a base class (e.g. 1.26.20's
BaseConnectionRequest), mirror it: add the base header, re-parent the
derived classes, move the shared members into the base. When BDS removes a
class, git rm the Endstone header once grep confirms nothing else
references it. Follow the BDS structure closely; only the file name differs
(snake_case). Keep it minimal - declare only what Endstone uses.
Then update the mangled name in scripts/configs/{windows,linux}.toml,
re-run the dumper, and update any affected hook in
src/endstone/runtime/bedrock_hooks/.
ABI edits are easy to get subtly wrong - a wrong vtable slot or member offset
corrupts memory silently, caught by neither a compile nor a PR review. Build
and test iteratively; do not batch many unverified ABI edits.
Finish
Re-run the dumper until all symbols resolve (or remaining 0s are understood).
Build (cmake --build --preset conan-release), run tests, add a CHANGELOG
entry ("Added support for BDS X.Y.Z"), commit, open the PR.
Gotchas
dwarf2cpp diff noise - no real BDS change, skip these
- Versioned-namespace churn -
SharedTypes/v1_26_10/... becomes
v1_26_20/...; most of that subtree's diff is just the version bump.
- Template-instantiation churn -
SharedPtr.h / SharedCounter and
similar enumerate concrete instantiations (CopperBlock<ThinFenceBlock>,
GlazedTerracottaBlock, ...). The set churns every release; Endstone uses
its own templates - ignore entirely.
- File regrouping - dwarf2cpp regroups types into different generated
files. A file shown as fully deleted (e.g.
CommonTypes.h) often just means
its types moved. Confirm a type is genuinely gone, not relocated.
- Lambda source-location churn -
match<(lambda at .../Foo.cpp:47:3)> -
lambda line/column numbers shift every build. Pure noise.
- Declaration reordering - declarations get reordered within a file; the
diff shows -/+ pairs of identical content moved.
Real BDS changes that still need no src/bedrock/ action
A change can be a genuine BDS change yet need nothing in Endstone, because
Endstone deliberately models only a subset. Rule these out before editing:
- Additive enum values beyond Endstone's declared subset. Endstone declares
only the enumerators it uses; the enum's underlying width is unchanged, so
values added past Endstone's range need nothing.
- A class Endstone truncates with
// ... - e.g. NetEventCallback.
Virtuals or members added in the undeclared tail are already covered by the
// ...; touch nothing. (One added in the middle of the declared part does
still need handling.)
- An empty-stubbed type - Endstone declares some types it never models as
struct X {}; (e.g. NetworkPeer::NetworkStatus). Internal changes to such a
type need no action.
- New free functions or non-virtual methods Endstone does not declare - no
layout or vtable impact.
Tooling
- lief's free build cannot read PDBs -
lief.pdb.load() fails with
"DebugInfo are not available for this build" (PDB/DWARF is LIEF-extended,
licensed). The Windows path shells out to pdbtool instead.
- dbghelp is too slow - loading a ~2 GB BDS PDB through dbghelp takes
minutes;
pdbtool dump ... psi takes ~25 s.
- PowerShell
> writes UTF-16 - a redirected pdbtool dump will not grep
with ASCII patterns; capture via the script's subprocess (UTF-8) or run
iconv -f UTF-16 -t UTF-8 first.
- Windows symbol offsets are RVAs:
RVA = section RVA + symbol offset.
- Some symbols are never public - function-local statics, lambdas, some data -
and never appear in
pdbtool ... psi. The --pdb path handles this: any
entry it cannot resolve by name falls back to scanning that entry's pattern
in the server binary (bedrock_server.exe, read from beside the PDB or
downloaded). Such an entry just needs a valid byte pattern like a Linux one;
its name can be an arbitrary unique key.
- When a function has no usable symbol at all - an overload dropped from the
PDB publics, or a lambda - resolve a callee instead: reimplement the
function in
src/bedrock/ from the decompilation and BEDROCK_CALL a
function it calls that does have a stable symbol. Verify the reimplementation
against the decomp; never guess the body.
- A function inlined on one platform but out-of-line on the other has a real
symbol on only one side. Split resolution with
#ifdef - e.g. call the
out-of-line function on one platform and read its function-local static on
the other. If the inlined-away function is hooked (not just called),
#ifdef-calling the original does not help - there is no entry point to
detour; relocate the hook on that platform to the still-out-of-line caller
it was inlined into and reconstruct the trigger condition there.
- To recover a renamed Windows symbol (re-signed, not removed), dump every
public once with
pdbtool --quiet dump <pdb> psi and grep for the scope
(?<method>@<Class>@@); the surviving line is the new mangled name. Verify it
against the bedrock-headers signature before copying it into the config so you
do not pick the wrong overload.
Re-extracting a stale Linux pattern
A "Linux only" miss is a stale byte pattern; the function still exists.
Re-cut it from the new binary:
- Prefer a prologue pattern over a call-site one. A call-site pattern
(
E8 ?? ?? ?? ?? + rip_relative / rip_offset) resolves the function via
a call displacement, so it breaks whenever the caller changes - even
when the target function is untouched; that is why one release breaks a
whole cluster of them. Re-anchor on the function's own prologue (the push
sequence + sub rsp) wherever the function is not inlined - the match
offset is then the function start itself.
- Converting a call-site entry to a prologue entry: delete
rip_relative
and rip_offset. A prologue pattern's match offset already is the
function; leaving rip_relative = true makes the dumper RIP-decode bytes
inside the prologue and resolve to a silently-wrong, non-zero address -
worse than a 0 miss, because it passes the build and corrupts at runtime.
- A prologue miss and a call-site miss mean different things: a
prologue miss = the function's own codegen changed (callee-saved register
set, argument shuffle); a call-site miss = a caller changed. Neither implies
a signature change - a different saved-register set is pure codegen churn.
Confirm against the Windows mangled name before assuming the config
name
must change.
- Cross-version dump to triage. Run the dumper against the previous BDS
version (copy the config, set the old
version; the output path is fixed,
so git checkout the regenerated table afterward). A pattern that matches
the previous version but not the new one confirms it went stale at this
bump - re-extract it; one that misses both was already wrong.
- RVA-delta plausibility. Cross-check resolved offsets against the
previous version's table: deltas should all be positive (the binary grows)
and rise broadly monotonically with RVA (code accumulates ahead of each
function). A negative delta, a wild outlier, or a function pair whose
relative order flipped flags a pattern that matched the wrong location -
verify those before trusting the dump. Sub-MB local non-monotonic wiggles
are normal linker-layout noise.
BDS itself
- BDS sometimes does structural refactors, not just signature tweaks - e.g.
1.26.20 split packets into a
<Name>Payload struct plus
cerealizer/serialize specializations, and re-parented ConnectionRequest
under BaseConnectionRequest. These need real design work in src/bedrock/,
not a mechanical edit - flag them and do them deliberately. When one packet
shows the <Name>Payload split, assume every network/packet/* got it -
port them all; inventory_slot_packet.h is a good reference for the pattern.
- A function missing from the PDB publics may have been inlined, not removed.
- BDS is built with whole-program optimization, so the compiler can drop the
unused
this from a non-virtual member function that never touches it -
every argument then shifts down one integer register (on Linux the first
real arg moves from rsi to rdi; spot it as mov <reg>, rdi where the
old prologue had mov <reg>, rsi). Mirror it by declaring that function
static in Endstone's src/bedrock/ (#ifdef-split if only one platform
optimized it). This changes __FUNCDNAME__: a static member has no this,
so the Itanium name loses its cv-qualifier (_ZNK... -> _ZN...) and the
MSVC name loses its B const code. Updating only the config pattern is
not enough - the name must change to the static-form mangling, or
get_symbol() throws.
- BDS sometimes promotes a type alias to a concrete type - e.g. 1.26.20
turned
using DimensionType = AutomaticID<Dimension, int>; into a standalone
struct DimensionType. The mangled name of every symbol taking it changes
(...V?$AutomaticID@VDimension@@H@@... -> ...UDimensionType@@...) with no
logic change, so it surfaces as a Windows miss. The fix is structural: make
Endstone's type a real struct too (mirror the BDS layout - often a single
integer member), replace every direct use of the old underlying type, and
delete the now-orphaned alias/template header. Editing only the config name
is not enough - __FUNCDNAME__ comes from Endstone's declaration, so the
declared type itself must change.
- Removing a class is a multi-file cascade, not just one
git rm. When BDS
deletes a class, also remove: its src/endstone/runtime/bedrock_hooks/*.cpp
hook, the source-list entries in the relevant CMakeLists.txt files, the
[[signatures]] blocks in both scripts/configs/*.toml, the entries in the
generated src/bedrock/symbol_generator/symbols/*.toml, and any dependent
declaration that named the type. grep the tree for the class name afterward
to confirm nothing dangles.
Symbol table is not optional for the build
get_symbol() is consteval and throws when a name is absent; the
generator drops every 0-valued entry from bedrock_symbols.generated.h.
So an unresolved symbol that any TU consumes via BEDROCK_CALL /
BEDROCK_VAR / BEDROCK_CTOR is a hard compile error
(C7595: call to immediate function is not a constant expression), not a
silently-disabled hook. Phase 1 must resolve every consumed symbol before
the build can go green - Phase 2 alone never produces a buildable tree.
- When a hooked function's signature changes, its
__FUNCDNAME__ changes, so
the old name in scripts/configs/{windows,linux}.toml goes stale - update
it and re-dump, or that hook's TU stops compiling.
- Temporary verification trick. To compile-check the Phase-2 port before
Phase 1 is finished, temporarily comment out the
if (val == 0) { ... continue; } skip in src/bedrock/symbol_generator/main.cpp so every entry -
including unresolved 0s - lands in the generated table. get_symbol then
finds every key and the build goes green, so real ABI/source errors surface.
Revert it before committing - the generator must keep dropping 0s and
get_symbol must keep throwing so a genuinely missing symbol stays a loud
error. The real fix is always Phase 1 (re-dump / update stale names).
Resuming a partially-done bump branch
- Don't trust that an earlier stage is finished. A prior commit may have ported
only some files of a stage (e.g. some
network/packet/* but not all) and
left .cpp files referencing the pre-refactor shape. Build early; the first
compile pass is what surfaces these gaps.
Record findings here
Append durable, version-independent lessons to Gotchas. Keep per-version
specifics (which overload was chosen, what replaced a removed function) in
commit messages and the CHANGELOG, not here.
Source: EndstoneMC/endstone — distributed by TomeVault.
1---2name: bump-bds3description: Update Endstone to support a new Bedrock Dedicated Server (BDS) version - regenerate the symbol offset tables and port src/bedrock to the new ABI. Use when bumping the supported BDS version (e.g. "add support for BDS 1.26.x", "bump the BDS version"). Use when this capability is needed.4---56# Bump Endstone to a new BDS version78Two phases:910- **Phase 1** regenerates the symbol offset tables - which hook resolves to11 which address. It tells you *which* symbols broke.12- **Phase 2** ports `src/bedrock/` to the new ABI using the header diff. It13 tells you *why* they broke and *how* to fix them.1415## NDA boundary (read first)1617This workflow may use two private Mojang-derived artifacts:1819- `bedrock-headers` - required for ABI porting. This private repo contains C++20 headers reconstructed from BDS binaries.21- `bedrock_server.pdb` - useful when available for Windows symbol resolution,22 but not guaranteed to be published for every BDS release.2324**Both artifacts are NDA-protected.** Never copy header bodies, class25definitions, full member layouts, PDB dumps, symbol listings, or other private26artifact contents into the public `endstone` repo, its commits, PRs, issues,27logs, or this skill. Endstone's `src/bedrock/` is a hand-written, minimal28reimplementation - only what Endstone needs, in Endstone's own naming - which29is the DMCA-safe form. Treat headers, PDBs, generated dumps, and diffs as30private working references only.3132---3334# Phase 1 - regenerate the symbol tables3536## How the symbol pipeline works3738```39scripts/configs/{windows,linux}.toml signature configs, hand-maintained40 | scripts/dump_symbols.py41 v42src/bedrock/symbol_generator/symbols/{windows,linux}.toml resolved offsets43 | symbol_generator (CMake target)44 v45bedrock_symbols.generated.h std::array of name -> offset46```4748`src/bedrock/symbol.h` `get_symbol()` looks a symbol up by `__FUNCDNAME__` - the49mangled name of Endstone's own declaration in `src/bedrock/`. The symbol-table50key IS the signature of Endstone's reimplementation. Unresolved symbols are51written as `0` and skipped by the generator (that hook is disabled; the build52still succeeds).5354## Prerequisites5556- `uv` - runs `dump_symbols.py` (PEP 723 inline deps, no manual install).57- `pdbtool` - `cargo install pdbtool` (Microsoft pdb-rs). Reads the Windows PDB58 when one is available.59- `bedrock-headers` for the target version. Headers are mandatory for Phase 260 ABI porting and must remain private.61- Optional: the Windows BDS PDB (`bedrock_server.pdb`) for the target version.62 Use it when available; some BDS releases do not have a public/accessible PDB.63- The target version must be published in the `EndstoneMC/bedrock-server-data`64 repo - the Linux path downloads the binary from it. Check its `versions.json`.6566## Procedure67681. **Branch** off the current release branch (e.g. `v0.11`):69 `git checkout -b feat/<NN.NN>-support` (naming follows `feat/26.10-support`).702. **Bump the config versions** - set `version = "<X.Y.Z>"` in both71 `scripts/configs/windows.toml` and `scripts/configs/linux.toml`, using the72 3-component release string from bedrock-server-data `versions.json`.733. **Regenerate** (run in the background, 25 s to a few minutes):74 - Windows: `uv run --script scripts/dump_symbols.py scripts/configs/windows.toml --pdb <path>/bedrock_server.pdb`75 - Linux: `uv run --script scripts/dump_symbols.py scripts/configs/linux.toml`76 - If the Windows PDB is unavailable, skip the Windows PDB regeneration for77 now and rely on Linux pattern results plus the required header diff to78 identify ABI changes. Update Windows symbols later if/when a PDB becomes79 available.804. **Triage the failures.** The two platforms fail differently:81 - **Windows (PDB by name, then byte pattern):** each entry is looked up by82 mangled `name` in the PDB; entries the PDB has no public record for83 (lambdas, function-local statics) fall back to scanning the entry's84 `pattern` in the server binary. A miss thus means *both* failed - the85 mangled name is gone (MSVC encodes the full signature incl. return type,86 const-ness and access) *and* the byte pattern no longer matches. A PDB hit87 is name-verified; a fallback hit ("Found signature (fallback)") is only88 pattern-verified, like a Linux hit.89 - **Linux (byte-pattern scan):** a miss = the `pattern` in `configs/linux.toml`90 no longer matches. The function usually still exists - the pattern went91 stale. A Linux hit is a pattern match *labelled* with the config name; it92 is not name-verified.93 - Failed on **both** -> real signature/API change.94 - **Windows only** -> the mangled name changed: signature, return type,95 const-ness or access specifier. Itanium omits the return type, so a pure96 return-type change leaves the Linux name intact. A const/access change is97 fixed in Endstone's `src/bedrock/` *declaration* (`__FUNCDNAME__` derives98 from it), not the config alone.99 - **Linux only** -> stale byte pattern; re-extract it (IDA / Ghidra).100101The failure list is the input to Phase 2.102103---104105# Phase 2 - port src/bedrock to the new ABI106107## Source: the header diff108109`dwarf2cpp` reconstructs C++ headers from a DWARF-bearing BDS build (the Android110build `libminecraftpe.so` carries DWARF; the Windows/Linux server binaries are111stripped). Output lands in `bedrock-headers`, one branch per BDS release112(`android/r26_u1`, `android/r26_u2`, ...).1131141. `dwarf2cpp <libminecraftpe.so> --base-dir <build-root> -o <out>` (or `uvx dwarf2cpp`).1152. In `bedrock-headers`: `git checkout -b android/r<NN>_u<N>`, place the output, commit.1163. `git diff android/r<prev> android/r<new>` is the change set.117118## The actionable set119120`src/bedrock/` is ~655 hand-maintained headers - a small subset of BDS. Most of121a release diff (5000+ files) touches nothing Endstone declares. So:122123> **actionable work = (changed headers) intersect (the 655 src/bedrock headers)**124125Match by normalized basename (lowercase, strip `_` and `-`): bedrock-headers126`Mob.h` <-> Endstone `mob.h`; `BlockSource.h` <-> `block_source.h`.127128## Staged review order129130Review the diff in stages - foundational types first, so later stages do not131rework. Scope: the `handheld/` tree **and** the top-level `src/base/` tree;132**skip `handheld/src-client/`** (game client) and the other top-level `src/`133subtrees (`account`, `external`, `gui` - client / Xbox / third-party, nothing134Endstone mirrors). Within each stage, deep-dive only the intersection. (The135first attempt used 3 coarse stages; "handheld/src non-world" alone was 467136files / 44 intersecting - too big. Use this finer split:)1371381. `src/base` (top-level, *not* under `handheld/`) - the shared `Core` library:139 foundational utilities and low-level types (`BinaryStream`, ...). Easy to140 miss because every staged path below lives under `handheld/` while this tree141 is separate; a missed change here (e.g. a new `BinaryStream` virtual)142 silently shifts a vtable that Phase 1 can never flag.1432. `src-deps/SharedTypes` - shared types and enums1443. `src/common/network` - packets, network types, packet-id / disconnect enums1454. `src/common/server` (incl. `server/commands`) - server and command system1465. `src/common/entity` - ECS components1476. `src/common/{certificates,resources,scripting,platform,locale,gameplayhandlers,...}` - remaining non-world1487. `src/common/world/actor`1498. `src/common/world/item`1509. `src/common/world/level/block`15110. `src/common/world/level/{dimension,biome}` and remaining `src/common/world/level/*` (chunk, material, storage, level core)15211. `src/common/world/*` - remaining world (`attribute`, `effect`, `events`, `inventory`, `response`, ...)15312. `src-deps` other than SharedTypes (Certificates, VanillaComponents, ...), then anything else15413. **Cross-validate** - once every ABI change is in, re-review the whole155 `src/bedrock/` diff against the bedrock-headers diff. Every edited function156 signature, vtable slot, member type/order, and structural change must trace157 to a concrete change in `git diff android/r<prev> android/r<new>`. Reject158 anything not backed by the diff: no invented types, no guessed members, no159 hallucinated signatures, no "looks-right" edits. A change that cannot be160 matched to the header diff is wrong - revert or fix it. This stage exists161 because the porting stages, especially when parallelised across agents, can162 introduce plausible but unfounded edits - they must all be matched up.163164## Applying a change165166For each intersecting header with a *real* change (use Gotchas to rule out167noise first), update Endstone's `src/bedrock/` declaration:168169- **Function signatures** (especially hooked / `ENDSTONE_HOOK`) - parameter170 types, const/ref, return type must match BDS exactly, or `__FUNCDNAME__`171 stops matching the symbol.172- **Virtual functions** - the vtable order must match BDS. An added / removed /173 reordered virtual shifts every slot below it; mirror the new order (use `= 0`174 placeholders for virtuals Endstone does not implement).175- **Members** - type, order and size must match for layout. Member *names* stay176 Endstone's own (`lower_case_`); do not copy Mojang's names. Declare a new177 member's type for real - never a same-size stand-in (`std::string` etc.). If178 the real type would drag in a heavy include chain, add a forward declaration179 to `src/bedrock/forward.h` in alphabetic order and use the type incomplete180 (fine for pointers, references, and container value types like181 `std::vector` / `std::unordered_map`).182- **Width-ambiguous integers** - bedrock-headers are reconstructed from a183 Linux/Android build, where `unsigned long` is 64-bit; on Windows (LLP64) it184 is 32-bit. Whenever the diff shows `unsigned long`, port it as185 `std::uint64_t` so the type is 64-bit on both of Endstone's target platforms.186- **Template arguments** - a class template's *default* arguments are part of187 its declaration: copy them from bedrock-headers verbatim, never guess (e.g.188 `brstd::bitset`'s word-type parameter defaults to `unsigned int`). And never189 drop an *explicit* template argument to lean on a default - a given190 instantiation often overrides the default, so check the actual instantiation191 in the diff and spell every argument it spells. If the explicit argument is a192 width-ambiguous integer, still apply the rule above (`brstd::bitset<N,193 unsigned long>` -> `brstd::bitset<N, std::uint64_t>`).194- **One type per corresponding file** - when a change needs a BDS type Endstone195 does not yet declare, add it in its *own* `src/bedrock/` header mirroring the196 BDS file that defines it (snake_case path), then `#include` it - do not paste197 a foreign type's definition inline into whatever header happens to need it.198 (A forward declaration of a helper type may sit in the header that uses it,199 as BDS does; a forward declaration needed across many headers goes in200 `src/bedrock/forward.h`.)201- **Structural refactors** - when BDS introduces a base class (e.g. 1.26.20's202 `BaseConnectionRequest`), mirror it: add the base header, re-parent the203 derived classes, move the shared members into the base. When BDS removes a204 class, `git rm` the Endstone header once `grep` confirms nothing else205 references it. Follow the BDS structure closely; only the file name differs206 (snake_case). Keep it minimal - declare only what Endstone uses.207208Then update the mangled `name` in `scripts/configs/{windows,linux}.toml`,209re-run the dumper, and update any affected hook in210`src/endstone/runtime/bedrock_hooks/`.211212ABI edits are easy to get subtly wrong - a wrong vtable slot or member offset213corrupts memory silently, caught by neither a compile nor a PR review. Build214and test iteratively; do not batch many unverified ABI edits.215216# Finish217218Re-run the dumper until all symbols resolve (or remaining `0`s are understood).219Build (`cmake --build --preset conan-release`), run tests, add a CHANGELOG220entry ("Added support for BDS X.Y.Z"), commit, open the PR.221222## Gotchas223224### dwarf2cpp diff noise - no real BDS change, skip these225226- **Versioned-namespace churn** - `SharedTypes/v1_26_10/...` becomes227 `v1_26_20/...`; most of that subtree's diff is just the version bump.228- **Template-instantiation churn** - `SharedPtr.h` / `SharedCounter` and229 similar enumerate concrete instantiations (`CopperBlock<ThinFenceBlock>`,230 `GlazedTerracottaBlock`, ...). The set churns every release; Endstone uses231 its own templates - ignore entirely.232- **File regrouping** - dwarf2cpp regroups types into different generated233 files. A file shown as fully deleted (e.g. `CommonTypes.h`) often just means234 its types moved. Confirm a type is genuinely gone, not relocated.235- **Lambda source-location churn** - `match<(lambda at .../Foo.cpp:47:3)>` -236 lambda line/column numbers shift every build. Pure noise.237- **Declaration reordering** - declarations get reordered within a file; the238 diff shows -/+ pairs of identical content moved.239240### Real BDS changes that still need no `src/bedrock/` action241242A change can be a genuine BDS change yet need nothing in Endstone, because243Endstone deliberately models only a subset. Rule these out before editing:244245- **Additive enum values** beyond Endstone's declared subset. Endstone declares246 only the enumerators it uses; the enum's underlying width is unchanged, so247 values added past Endstone's range need nothing.248- **A class Endstone truncates with `// ...`** - e.g. `NetEventCallback`.249 Virtuals or members added in the undeclared tail are already covered by the250 `// ...`; touch nothing. (One added *in the middle* of the declared part does251 still need handling.)252- **An empty-stubbed type** - Endstone declares some types it never models as253 `struct X {};` (e.g. `NetworkPeer::NetworkStatus`). Internal changes to such a254 type need no action.255- **New free functions or non-virtual methods** Endstone does not declare - no256 layout or vtable impact.257258### Tooling259260- **lief's free build cannot read PDBs** - `lief.pdb.load()` fails with261 "DebugInfo are not available for this build" (PDB/DWARF is LIEF-extended,262 licensed). The Windows path shells out to `pdbtool` instead.263- **dbghelp is too slow** - loading a ~2 GB BDS PDB through dbghelp takes264 minutes; `pdbtool dump ... psi` takes ~25 s.265- **PowerShell `>` writes UTF-16** - a redirected `pdbtool` dump will not grep266 with ASCII patterns; capture via the script's subprocess (UTF-8) or run267 `iconv -f UTF-16 -t UTF-8` first.268- Windows symbol offsets are RVAs: `RVA = section RVA + symbol offset`.269- Some symbols are never public - function-local statics, lambdas, some data -270 and never appear in `pdbtool ... psi`. The `--pdb` path handles this: any271 entry it cannot resolve by name falls back to scanning that entry's `pattern`272 in the server binary (`bedrock_server.exe`, read from beside the PDB or273 downloaded). Such an entry just needs a valid byte `pattern` like a Linux one;274 its `name` can be an arbitrary unique key.275- When a function has no usable symbol at all - an overload dropped from the276 PDB publics, or a lambda - resolve a *callee* instead: reimplement the277 function in `src/bedrock/` from the decompilation and `BEDROCK_CALL` a278 function it calls that does have a stable symbol. Verify the reimplementation279 against the decomp; never guess the body.280- A function inlined on one platform but out-of-line on the other has a real281 symbol on only one side. Split resolution with `#ifdef` - e.g. call the282 out-of-line function on one platform and read its function-local static on283 the other. If the inlined-away function is *hooked* (not just called),284 `#ifdef`-calling the original does not help - there is no entry point to285 detour; relocate the hook on that platform to the still-out-of-line caller286 it was inlined into and reconstruct the trigger condition there.287- To recover a **renamed** Windows symbol (re-signed, not removed), dump every288 public once with `pdbtool --quiet dump <pdb> psi` and grep for the scope289 (`?<method>@<Class>@@`); the surviving line is the new mangled name. Verify it290 against the bedrock-headers signature before copying it into the config so you291 do not pick the wrong overload.292293### Re-extracting a stale Linux pattern294295A "Linux only" miss is a stale byte `pattern`; the function still exists.296Re-cut it from the new binary:297298- **Prefer a prologue pattern over a call-site one.** A call-site pattern299 (`E8 ?? ?? ?? ??` + `rip_relative` / `rip_offset`) resolves the function via300 a `call` displacement, so it breaks whenever the *caller* changes - even301 when the target function is untouched; that is why one release breaks a302 whole cluster of them. Re-anchor on the function's own prologue (the `push`303 sequence + `sub rsp`) wherever the function is not inlined - the match304 offset is then the function start itself.305- **Converting a call-site entry to a prologue entry: delete `rip_relative`306 and `rip_offset`.** A prologue pattern's match offset already *is* the307 function; leaving `rip_relative = true` makes the dumper RIP-decode bytes308 inside the prologue and resolve to a silently-wrong, non-zero address -309 worse than a `0` miss, because it passes the build and corrupts at runtime.310- A **prologue miss** and a **call-site miss** mean different things: a311 prologue miss = the function's own codegen changed (callee-saved register312 set, argument shuffle); a call-site miss = a caller changed. Neither implies313 a signature change - a different saved-register set is pure codegen churn.314 Confirm against the Windows mangled name before assuming the config `name`315 must change.316- **Cross-version dump to triage.** Run the dumper against the *previous* BDS317 version (copy the config, set the old `version`; the output path is fixed,318 so `git checkout` the regenerated table afterward). A pattern that matches319 the previous version but not the new one confirms it went stale *at* this320 bump - re-extract it; one that misses both was already wrong.321- **RVA-delta plausibility.** Cross-check resolved offsets against the322 previous version's table: deltas should all be positive (the binary grows)323 and rise broadly monotonically with RVA (code accumulates ahead of each324 function). A negative delta, a wild outlier, or a function pair whose325 relative order flipped flags a pattern that matched the wrong location -326 verify those before trusting the dump. Sub-MB local non-monotonic wiggles327 are normal linker-layout noise.328329### BDS itself330331- BDS sometimes does **structural refactors**, not just signature tweaks - e.g.332 1.26.20 split packets into a `<Name>Payload` struct plus333 `cerealizer`/`serialize` specializations, and re-parented `ConnectionRequest`334 under `BaseConnectionRequest`. These need real design work in `src/bedrock/`,335 not a mechanical edit - flag them and do them deliberately. When one packet336 shows the `<Name>Payload` split, assume *every* `network/packet/*` got it -337 port them all; `inventory_slot_packet.h` is a good reference for the pattern.338- A function missing from the PDB publics may have been inlined, not removed.339- BDS is built with whole-program optimization, so the compiler can **drop the340 unused `this`** from a non-virtual member function that never touches it -341 every argument then shifts down one integer register (on Linux the first342 real arg moves from `rsi` to `rdi`; spot it as `mov <reg>, rdi` where the343 old prologue had `mov <reg>, rsi`). Mirror it by declaring that function344 `static` in Endstone's `src/bedrock/` (`#ifdef`-split if only one platform345 optimized it). This changes `__FUNCDNAME__`: a static member has no `this`,346 so the Itanium name loses its cv-qualifier (`_ZNK...` -> `_ZN...`) and the347 MSVC name loses its `B` const code. Updating only the config `pattern` is348 not enough - the `name` must change to the static-form mangling, or349 `get_symbol()` throws.350- BDS sometimes promotes a **type alias to a concrete type** - e.g. 1.26.20351 turned `using DimensionType = AutomaticID<Dimension, int>;` into a standalone352 `struct DimensionType`. The mangled name of every symbol taking it changes353 (`...V?$AutomaticID@VDimension@@H@@...` -> `...UDimensionType@@...`) with no354 logic change, so it surfaces as a Windows miss. The fix is structural: make355 Endstone's type a real `struct` too (mirror the BDS layout - often a single356 integer member), replace every direct use of the old underlying type, and357 delete the now-orphaned alias/template header. Editing only the config `name`358 is not enough - `__FUNCDNAME__` comes from Endstone's declaration, so the359 declared type itself must change.360- Removing a class is a **multi-file cascade**, not just one `git rm`. When BDS361 deletes a class, also remove: its `src/endstone/runtime/bedrock_hooks/*.cpp`362 hook, the source-list entries in the relevant `CMakeLists.txt` files, the363 `[[signatures]]` blocks in both `scripts/configs/*.toml`, the entries in the364 generated `src/bedrock/symbol_generator/symbols/*.toml`, and any dependent365 declaration that named the type. `grep` the tree for the class name afterward366 to confirm nothing dangles.367368### Symbol table is not optional for the build369370- `get_symbol()` is `consteval` and **throws** when a name is absent; the371 generator **drops every `0`-valued entry** from `bedrock_symbols.generated.h`.372 So an unresolved symbol that any TU consumes via `BEDROCK_CALL` /373 `BEDROCK_VAR` / `BEDROCK_CTOR` is a hard **compile** error374 (`C7595: call to immediate function is not a constant expression`), not a375 silently-disabled hook. Phase 1 must resolve *every consumed* symbol before376 the build can go green - Phase 2 alone never produces a buildable tree.377- When a hooked function's signature changes, its `__FUNCDNAME__` changes, so378 the old `name` in `scripts/configs/{windows,linux}.toml` goes stale - update379 it and re-dump, or that hook's TU stops compiling.380- **Temporary verification trick.** To compile-check the Phase-2 port before381 Phase 1 is finished, *temporarily* comment out the `if (val == 0) { ...382 continue; }` skip in `src/bedrock/symbol_generator/main.cpp` so every entry -383 including unresolved `0`s - lands in the generated table. `get_symbol` then384 finds every key and the build goes green, so real ABI/source errors surface.385 **Revert it before committing** - the generator must keep dropping `0`s and386 `get_symbol` must keep throwing so a genuinely missing symbol stays a loud387 error. The real fix is always Phase 1 (re-dump / update stale `name`s).388389### Resuming a partially-done bump branch390391- Don't trust that an earlier stage is finished. A prior commit may have ported392 only some files of a stage (e.g. some `network/packet/*` but not all) and393 left `.cpp` files referencing the pre-refactor shape. Build early; the first394 compile pass is what surfaces these gaps.395396## Record findings here397398Append durable, version-independent lessons to **Gotchas**. Keep per-version399specifics (which overload was chosen, what replaced a removed function) in400commit messages and the CHANGELOG, not here.401402---403> Source: [EndstoneMC/endstone](https://github.com/EndstoneMC/endstone) — distributed by [TomeVault](https://tomevault.io).404<!-- tomevault:4.0:skill_md:2026-06-23 -->