Skip to content

Add Waystones compatibility (teleport + distance on sub-levels)#1143

Open
ComfyFluffy wants to merge 5 commits into
ryanhcode:mainfrom
ComfyFluffy:waystones-sublevel-compat-rebased
Open

Add Waystones compatibility (teleport + distance on sub-levels)#1143
ComfyFluffy wants to merge 5 commits into
ryanhcode:mainfrom
ComfyFluffy:waystones-sublevel-compat-rebased

Conversation

@ComfyFluffy

Copy link
Copy Markdown

Summary

Makes Waystones work with sub-levels. Without this, a waystone placed on an assembled sub-level (e.g. an aircraft) is broken in three ways: warping to it freezes the server, warping to an unloaded contraption is refused outright, and its distance/XP cost read as a meaningless ~20,000 km. After this PR you can place a waystone on a contraption and warp to it normally — loaded or unloaded.

All Waystones-specific code is a self-contained compatibility/waystones mixin package, auto-gated by SableMixinPlugin (no hard dependency — Sable loads fine without Waystones). Tested against Waystones 21.1.34 on NeoForge 1.21.1.

The problem

A waystone stores its block position, which for a block on a sub-level is the hidden plot-yard coordinate (DEFAULT_ORIGIN → ~20.48M blocks out). Waystones treats that as a normal world position:

  • Freeze: the same-dimension warp path calls ServerGamePacketListenerImpl#teleport directly, bypassing Sable's existing ServerPlayer#teleportTo projection, so the player is flung into the plot-yard.
  • Refusal: WaystoneImpl#isValidInLevel reads getBlockState(pos), which is air while the contraption is unloaded → "currently being moved or has gone missing".
  • Bad distance: while unloaded there's no live pose to project against, so the plot-yard coordinate leaks into distance/cost on both server and client.

Changes

Core (used by the compat mixins, but generally applicable):

  • SubLevelContainer now remembers a last-known pose and UUID per plot, set on allocate and kept while a sub-level is merely unloaded (cleared only on REMOVED). Persisted alongside plot occupancy in SubLevelOccupancySavedData. This lets a held sub-level's position/identity be resolved without loading it.
  • ActiveSableCompanion#projectOutOfSubLevel falls back to that last-known pose when no sub-level is loaded at the position (previously it returned the raw plot-yard coordinate). This flows through distanceSquaredWithSubLevels → the Entity#distanceToSqr overwrite, so server-side distance/cost (is_within_distance, the distance cost variable, /waystones list) become correct for unloaded sub-levels.

Compat mixins (compatibility/waystones):

  • WaystoneTeleportManagerMixin — projects the teleport target out of the sub-level; and if the target is on an unloaded contraption, freezes the player to (subLevelId, localAnchor) after the teleport.
  • WaystoneImplMixin — treats a waystone on a reserved-but-unloaded plot as a valid teleport target.
  • WaystoneButtonMixin (client) — omits the distance for a waystone whose sub-level isn't loaded here, reusing the same "no distance shown" path Waystones already uses for cross-dimension waystones.

How the unloaded warp works

It reuses the exact mechanism Sable already uses for bed respawns onto sub-levels: held sub-levels are stored in holding chunks keyed by their global world position, so teleporting the player to the contraption's last-known position re-activates it; the freeze then holds the player until the sub-level is live and places them precisely on the deck.

Testing

Manually, on a real modpack (Create: Aeronautics-style + Waystones, NeoForge 1.21.1):

  • Warp to a waystone on a loaded aircraft → lands on the deck (previously froze the server).
  • Warp to a waystone on an unloaded aircraft → world loads at its last position, player lands on the deck (previously "missing").
  • XP cost → correct for both loaded and unloaded.
  • GUI distance → real distance when loaded, blank when unloaded, unchanged for ordinary far-away waystones.

Notes for reviewers

  • The projectOutOfSubLevel change looks wide but is provably safe for loaded sub-levels: subLevels[index] is written only in allocateSubLevel (set) and removeSubLevel (null), so getContaining never returns null during a loaded sub-level's lifetime — the new fallback branch is unreachable while loaded, and behaves identically across the load/unload boundary (lastKnownPose == logicalPose at unload). If you'd prefer it explicit, I'm happy to split it into a dedicated projectOutOfSubLevelOrLastKnown so the shared primitive is untouched.
  • lastKnownUuids is new (respawn uses tracking points for identity instead). It's needed because waystones aren't proactively registered like beds, so we need a position→identity lookup for an unloaded sub-level.

Known limitations

  • The warp trusts that a reserved plot still holds the waystone (no disk-read validation of the block while unloaded).
  • Re-activation must land within the freeze window (~160 ticks); a very slow chunk load would drop the player at the contraption's last position.
  • Client GUI distance is omitted (not computed) while the sub-level is unloaded; an accurate value would require streaming last-known poses to clients.

Waystones treats a waystone's block position as a stable world position,
but a waystone placed on an assembled sub-level (e.g. an aircraft) is
stored at the hidden plot-yard coordinate ~20.48M blocks out. Two problems
followed:

- Teleporting to such a waystone froze the server. Waystones' same-dimension
  warp calls ServerGamePacketListenerImpl#teleport directly, bypassing
  Sable's ServerPlayer#teleportTo projection, so the player was dropped into
  the plot-yard. Add a compatibility mixin that projects the teleport target
  out of the sub-level before the entity is moved.

- Distance and XP cost were wrong while the sub-level was unloaded: with no
  live pose to project against, the raw plot-yard coordinate leaked through.
  Track a per-plot last-known pose in SubLevelContainer (set on allocate,
  refreshed on unload), persist it alongside plot occupancy, and have
  projectOutOfSubLevel fall back to it when no loaded sub-level is present.
  Server-side distance checks (cost, is_within_distance, /waystones list)
  now resolve an unloaded sub-level's approximate position load-free.

Known limitation: the client GUI distance text still shows the raw distance
for sub-levels never loaded on the client; fixing that needs last-known
poses synced to clients.
Building on the last-known pose work, allow warping to a waystone whose
contraption is currently unloaded, reusing the same machinery Sable uses for
bed respawns onto sub-levels.

- Track the sub-level UUID per plot alongside the last-known pose
  (SubLevelContainer), persisted with plot occupancy, so a held sub-level can
  be identified without loading it.

- WaystoneImplMixin: a waystone on a reserved-but-unloaded plot is treated as
  a valid teleport target instead of "currently being moved or has gone
  missing".

- WaystoneTeleportManagerMixin: after projecting the target to the sub-level's
  last-known world position (which re-activates the held sub-level by loading
  the world chunk there), freeze the player to (subLevelId, localAnchor). The
  freeze places the player precisely on the deck once the sub-level goes live,
  then releases - the same path as respawn.

Known limitations: the warp trusts that a reserved plot still holds the
waystone (no disk-read validation), and relies on the re-activation landing
within the freeze window.
A waystone on a sub-level stores the plot-yard coordinate. When the contraption
is loaded the client projects it via Sable's distanceToSqr overwrite and the
distance is accurate, but while it is unloaded here the client has no pose to
project against and the distance renders as a meaningless ~20,000 km.

Recovering the real distance would require the server to stream last-known
poses to clients. Instead, detect that the waystone sits in a reserved plot-yard
position that isn't loaded on this client and omit the distance - reusing the
existing path Waystones already uses for cross-dimension waystones, which show
no distance either.
MixinExtras requires the wrapped operation's receiver parameter to match the
call's exact owner type. The distanceToSqr call in WaystoneButton#renderWidget
has a LocalPlayer receiver (the static type of Minecraft#player), not Entity,
so the handler must take LocalPlayer or mixin application fails with
"unexpected argument type ... at index 0" and the screen crashes on open.
@CLAassistant

CLAassistant commented Jun 8, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@ComfyFluffy ComfyFluffy changed the title Waystones sublevel compat rebased Add Waystones compatibility (teleport + distance on sub-levels) Jun 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants