Add Waystones compatibility (teleport + distance on sub-levels)#1143
Open
ComfyFluffy wants to merge 5 commits into
Open
Add Waystones compatibility (teleport + distance on sub-levels)#1143ComfyFluffy wants to merge 5 commits into
ComfyFluffy wants to merge 5 commits into
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/waystonesmixin package, auto-gated bySableMixinPlugin(no hard dependency — Sable loads fine without Waystones). Tested against Waystones21.1.34on 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:ServerGamePacketListenerImpl#teleportdirectly, bypassing Sable's existingServerPlayer#teleportToprojection, so the player is flung into the plot-yard.WaystoneImpl#isValidInLevelreadsgetBlockState(pos), which is air while the contraption is unloaded → "currently being moved or has gone missing".Changes
Core (used by the compat mixins, but generally applicable):
SubLevelContainernow remembers a last-known pose and UUID per plot, set on allocate and kept while a sub-level is merely unloaded (cleared only onREMOVED). Persisted alongside plot occupancy inSubLevelOccupancySavedData. This lets a held sub-level's position/identity be resolved without loading it.ActiveSableCompanion#projectOutOfSubLevelfalls 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 throughdistanceSquaredWithSubLevels→ theEntity#distanceToSqroverwrite, so server-side distance/cost (is_within_distance, thedistancecost 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):
Notes for reviewers
projectOutOfSubLevelchange looks wide but is provably safe for loaded sub-levels:subLevels[index]is written only inallocateSubLevel(set) andremoveSubLevel(null), sogetContainingnever 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 == logicalPoseat unload). If you'd prefer it explicit, I'm happy to split it into a dedicatedprojectOutOfSubLevelOrLastKnownso the shared primitive is untouched.lastKnownUuidsis 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