Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
TortalPortal LITE
Strictly server-side Portal-network mod: progression-gated gates, one-way corpse-run recovery, fast-transit terminals, per-portal ore upgrades, portal caps, and self-repairing network health checks. Zero client install, full PlayFab/Xbox/crossplay.
| Date uploaded | 2 weeks ago |
| Version | 1.0.9 |
| Download link | Wubarrk-TortalPortal_LITE-1.0.9.zip |
| Downloads | 37 |
| Dependency string | Wubarrk-TortalPortal_LITE-1.0.9 |
This mod requires the following mods to function
denikson-BepInExPack_Valheim
BepInEx pack for Valheim. Preconfigured with the correct entry point for mods and preferred defaults for the community.
Preferred version: 5.4.2350README
🌀 TortalPortal Lite
No client install, ever. Every gate, terminal, and health check runs entirely on the server. Built and verified against Valheim 1.0.7, 1.0.12, 1.0.15 and 1.0.16.
TortalPortal Lite is a strictly server-side portal-network mod (GUID wubarrk.tortalportallite):
every feature below runs entirely on the dedicated server, operating directly on the world's raw
ZDO data instead of live game objects. Steam, Xbox, PlayFab, and full crossplay parties all get the
identical experience, with nothing to download. Forked from the sibling mod Wonderland's own
architecture, this is TortalPortal Lite's v1.0.0 — the first packaged release. A large internal
R&D pass explored 263 possible portal mechanisms; the product owner picked exactly the 14 below to
actually ship. Everything else was cut from the codebase entirely, not just disabled — down from
296 files / ~44.8k lines to 83 files / ~10k lines. A v1.0.1 follow-up added a 15th feature,
BarrkBOT Portal Export, for admins who want portal data fed into their own tooling.
📜 Contents
- 🚪 Features
- 👻 Phantom Anchor Fabrication
- 🔒 Progression-Gated Sealed Gate
- 🩺 HealthScan
- 🩹 Repair
- 📊 Metrics
- 🚦 Version Migration
- 🤝 Compat
- 🧩 Architecture
- 🪨 The Ore Gate
- 🚧 Server-Enforced Portal Caps
- 💀 Corpse-Run Gate
- 📡 DestinationPrewarm
- 🚄 Fast-Transit Mode & Parked Terminal
- 📍 Live Portal Markers
- 📤 BarrkBOT Portal Export
- 💻 Admin Console
- 🔧 Configuration
- 📦 Dependencies
- 📥 Installation
🚪 Features
👻 Phantom Anchor Fabrication
A destination that doesn't need a builder. An admin declares a named phantom anchor — Name,
Position (X/Y/Z), RotationY, and a Void flag — directly in routing.json's
phantomAnchors section, and the server creates and maintains it automatically from there.
Void:true keeps it invisible and non-portal, a pure coordinate for other features to point at;
Void:false turns it into a real, visible portal anchor players can walk up to. Either way, nobody
has to physically carry portal materials out and build the thing by hand — Sealed Gates and Parked
Terminals below both lean on phantom anchors as destinations that already exist the moment they're
declared.
🔒 Progression-Gated Sealed Gate
A portal that only opens once it's earned. Declared in routing.json's sealedGates section
(Name, Position, GlobalKey, Destination, SealedTag, OpenTag), a Sealed Gate resolves to
nothing at all until a named global key — a boss defeat, or any custom flag the server sets —
becomes true, then opens straight to its declared destination. The server re-checks that key on its
own schedule rather than waiting on a player to interact with it: SealedGateEvalSeconds, in config
section 11 - Routing: Schedules and Conditions, controls how often (default 1 second).
🩺 HealthScan
A standing check for the portal pairs vanilla itself leaves broken. Running on its own timer
(HealthIntervalSeconds, config section 4 - Foundations: Health, default 30 seconds), HealthScan
walks the managed portal network looking for two specific problems: odd-count same-tag groups —
which vanilla's own pairwise-only pairing always leaves one member of permanently orphaned — and
stale, dangling connections. It's deliberately narrow about what counts as broken: a portal whose
one-way connection is intentional, like the Corpse-Run Gate below, is excluded from this report
entirely, because that's a designed feature, not a defect.
🩹 Repair
Turns HealthScan's findings into fixes you can trust, and undo. Repair only ever acts on what
HealthScan actually flagged: retagging an orphaned portal, clearing a dangling link. It's dry-run
by default (RepairDryRunDefault, config section 5 - Foundations: Repair) — removekey tpl repair on its own just reports what it would do — and only removekey tpl repair --apply writes
anything. Before it does, it automatically snapshots the world's portal state first, so removekey tpl restore <name> can always undo the whole batch if the fix wasn't what you wanted. If
AutoRepair is on but Version Migration has the destructive gate closed, the scheduled --apply
pass logs that it's idle once and then waits quietly, resuming on its own when the gate opens.
📊 Metrics
Know your portal network at a glance. removekey tpl metrics (config section 7 - Foundations: Metrics) reports total, connected, and managed portal counts, breakdowns per tag, per biome, and
per builder, plus a position-based heuristic for transit counts and "top routes" across the world.
It's a read-only view straight off server-side ZDO data — nothing to install to see any of it.
🚦 Version Migration
A boot-time seatbelt before anything destructive runs. At startup the mod checks two things: is
this a Valheim build it has actually been verified against, and does the game's own portal-prefab
registry look intact. If either check fails, destructive passes — specifically removekey tpl repair --apply — refuse to run at all rather than risk corrupting the world. An admin can
explicitly opt back in via GlobalConfig.AcceptUnverifiedBuild, but that's a deliberate override,
never a default.
🤝 Compat
One command's worth of answer to "my portals keep unpairing." removekey tpl compat reports
which Harmony patches exist on the handful of vanilla methods this mod also touches, plus an
explicit, named check against two specific sibling mods this server is expected to run alongside —
Wonderland and GetOffMyLawn. Both were read directly: both patch ZDO.SetOwner, but both vetoes are
scoped only to their own tracked objects (Wonderland's buoyant water items, GetOffMyLawn's
ward-held building pieces) and neither ever fires on a portal ZDO. Confirmed: no interference.
🧩 Architecture
Not a player-facing feature — the frame everything above is built on. Under the hood, the mod runs a fixed subsystem tick order with disjoint write ownership at every stage — Census first (what portals exist), then Audit, then HealthScan, and on down the line — each stage reading what came before it and never fighting another stage for the same data. Nothing here has a console verb or a config key of its own; it's simply the reason the other twelve features can coexist without stepping on each other.
🪨 The Ore Gate
A perk written onto one specific portal, not a global setting. Declared in economy.json's
oreGates section against a portal's own position, AllowAllItems (bool, default true) lets ore
and metal — normally barred from portal travel — actually pass through that one gate, and an
optional ExitDistance (float) extends how far in front of it a traveller steps out on arrival.
It's a real, vanilla-client-honoured gameplay effect: the client just sees ore go through and a
further step-out distance at that gate, because the data lives on that portal's own record, not in
a global rule.
🚧 Server-Enforced Portal Caps
A per-player portal limit an unmodified client genuinely cannot bypass. Off by default
(PortalCapEnabled, config section 155 - WildcardB: Portal Caps), turning it on caps each player
at PortalCapPerCreator portals (default 6). Once a player is at the cap, any portal they build
after that is removed about a second after it appears and they're told why — enforced entirely
server-side against raw ZDO ownership, unlike a client-side placement limit a modded client could
simply ignore. Portals that already exist are never removed: switching the cap on, or lowering it,
only affects what's built afterwards.
💀 Corpse-Run Gate
Gear recovery without a bare-handed run back into danger. The instant a tracked player dies, the server automatically raises a private, temporary portal near their claimed bed (or the world spawn, if they have none) leading straight to their tombstone. This is a genuine one-way trip by design, not a round-trip pair — there is no automatic portal back from the grave to the bed.
Both ends always appear. A death is picked up from the tombstone itself, not from the list of connected players, so dying and immediately logging off still gets you a gate — it is standing when you come back, and that is when you are told about it. If a raise can't complete on the first try it is retried rather than dropped, and every maintenance pass afterwards checks both ends and re-builds whichever one is missing, in the same spot it was placed.
Both ends are placed as clear as the ground allows, and on the ground. The server scans outward
from the tombstone (and from the bed) in 1 m rings for a spot with ClearanceMeters (default 10 m)
of room — measured edge to edge from every tree trunk, boulder, building piece, portal and
generated location, using each object's real collider extents rather than its pivot, so a boulder
that spans eight metres around its centre counts as eight metres of boulder. Tiny props (mushrooms,
flowers, berry bushes) only need 2 m. That figure is a preference: nowhere in a Valheim forest has
10 m of room from everything, so the search steps the requirement down and takes the closest spot
honouring the best figure that patch of map can offer — never below MinRoomMeters (default 2 m) of
real room, and widening its radius before it settles for less. The ground under the gate is the
client's own heightmap recipe re-run on the server — biome blending and hoe/pickaxe terraforming
included — and the frame's four corners and surroundings are checked so it neither floats off a slope
nor wedges against a cliff. A death inside a dungeon opens the grave-side gate at the dungeon's
entrance. A gate raised by a previous server run and forgotten is reaped automatically, so restarts
never leave stray portals.
Both gates are immune to all damage — not de-targeted, immune. Valheim never lets an attacker
apply damage itself: every source, a troll's AoE swing and a fire's damage-over-time included, ends
by sending an RPC to whichever machine owns the portal, and a routed RPC that finds no object on
the receiving machine is dropped on the floor. A dedicated server never builds objects where players
are, so a portal the server owns throws away every hit aimed at it. TortalPortal Lite holds that
ownership against the server's own 2-second hand-off to nearby players, refuses both ways a portal
can be deleted (including the one vanilla applies from any client without checking who sent it),
no-ops every WearNTear path that could hurt the piece, writes 1,000,000,000 HP and the full wear
immunity set for the moments a client holds a copy, blocks hammer removal, and keeps a watchdog that
puts health and ownership back and counts every time it had to. Every 5 minutes the log says both
ends are still standing and what the protection refused.
The gate self-destroys once the grave is emptied or despawned. It no longer expires on a timer while
the grave still stands: GateTtlMinutes (config section 41 - Targeted: Corpse Run) defaults to 0,
meaning no limit, because a deep Mistlands or Ashlands corpse run routinely takes longer than the old
30-minute cap allowed.
📡 DestinationPrewarm
No walking through into a portal that just quietly stopped working. The instant any managed
portal's destination changes, DestinationPrewarm force-sends the new destination data to nearby
players' clients immediately, so a re-point is never followed by someone walking through into a
"dead" transit because their client hadn't received the update yet. PrewarmRadius (config section
16 - Routing: Delivery Pipeline, default 30m) controls how far out that push reaches. It's
always-on — a safety net, not an opt-in feature.
🚄 Fast-Transit Mode & Parked Terminal
A portal the server walks you through, no swirl required. A Parked Terminal is a managed portal
kept permanently "unconnected" on purpose; instead of the normal portal-connection walk-through, the
server itself detects a player stepping up to it and teleports them directly. Fast-Transit is the
quicker variant of that same mechanism — a roughly 2-4 second fade instead of vanilla's roughly
8-second swirl — used once the destination zone is confirmed already generated and safe to land in;
otherwise it automatically falls back to the normal, slower transit rather than risk a bad landing.
Both are declared in routing.json's parkedTerminals section: Distant:false is Fast-Transit,
Distant:true is the plain Parked Terminal base mechanism underneath it.
📍 Live Portal Markers
A map pin that can actually disappear again. Name any portal with a leading # in vanilla's own
rename box — no client mod, just the game's stock UI — and it gets a labelled marker on every
connected player's map, injected through vanilla's own persistent-event system. Rename the portal
away from that # prefix and the marker vanishes with it; every other feedback surface this mod
family uses is permanent once placed, which is exactly why this one exists. Off by default
(LiveMarkersEnabled, config section 40 - Ux: Advanced Schemes) and kept to a small radius, since
a persistent event's radius also touches nearby spawners and weather.
📤 BarrkBOT Portal Export
A live feed for your own tooling, not just your own eyes. Writes
BepInEx/config/TortalPortalLite/barrkbot_portals.json — per-player portal counts, cap/over-cap
flags, and world totals — atomically (temp-file-then-swap, so nothing reading it ever catches a
half-written file), on a floor of once every 60 seconds. Built directly against BarrkBOT's own
published ingestion contract: no HTTP, no webhook, no auth — purely a file on this server's own disk
for BarrkBOT (or anything else you point at it) to read on its own schedule. Force an immediate
rewrite any time with removekey tpl barrkbot. Config section 84 - Ops: BarrkBot Export
(Enabled, default on; IntervalSeconds, default 60, floored at 60).
💻 Admin Console
Every command is typed as removekey tpl <verb> [args] into an admin's own in-game console (F5)
or chat — this piggybacks entirely on vanilla's own remote-command channel, so driving TortalPortal
Lite from in-game needs no client mod either.
| Verb | What it does |
|---|---|
list |
Lists every portal the mod currently manages. |
status |
A one-line summary of the mod's own running state. |
health |
Runs an on-demand HealthScan pass immediately, instead of waiting for the next scheduled interval. |
lock <x> <y> <z> |
Locks the portal at that position out of automated passes. |
unlock <x> <y> <z> |
Reverses lock. |
uninstall --clean-keys |
Prepares for removal, clearing the mod's own tracked keys and data before the plugin is pulled. |
help |
Lists every verb with its arguments. |
repair [--apply] |
See Repair above — dry-run without --apply, writes fixes with it. |
snapshot [name] |
Saves a snapshot of the world's current portal state, named or auto-named. |
snapshots |
Lists saved snapshots. |
restore <name> [--recreate] |
Restores a named snapshot; --recreate re-creates any portal the snapshot had that no longer exists in the world. |
compat |
See Compat above. |
metrics |
See Metrics above. |
barrkbot |
Forces an immediate rewrite of barrkbot_portals.json — see BarrkBOT Portal Export above. |
report <x> <y> <z> |
A detailed, single-portal readout for the portal at that position. |
🔧 Configuration
Settings live in BepInEx/config/wubarrk.tortalportallite.cfg, split into numbered sections, each
entry documented with its own description and range right in the file. Edits are picked up
live: the file is polled every 5 seconds and applied automatically — no server restart needed for
a config change to take effect.
Server-Synced (Admin Controlled)
| Section | What it covers |
|---|---|
1 - General |
Enabled — the master switch for the entire mod (default on). AcceptUnverifiedBuild — admin opt-in to let destructive passes run against a Valheim build or portal-prefab registry the mod hasn't itself verified (see Version Migration). Also the periodic server-log "still alive" Heartbeat. |
40 - Ux: Advanced Schemes |
LiveMarkersEnabled (off by default), LiveMarkersRadius, LiveMarkersReassertSeconds — see Live Portal Markers above. |
4 - Foundations: Health |
HealthIntervalSeconds — how often the background HealthScan pass re-checks the portal network (default 30s). |
5 - Foundations: Repair |
RepairDryRunDefault — whether removekey tpl repair reports-only or writes fixes by default. |
7 - Foundations: Metrics |
Settings behind removekey tpl metrics — portal counts, per-tag/biome/builder breakdowns, and route heuristics. |
11 - Routing: Schedules and Conditions |
SealedGateEvalSeconds — how often a Sealed Gate's GlobalKey is re-checked (default 1s). |
16 - Routing: Delivery Pipeline |
PrewarmRadius — how far out DestinationPrewarm force-sends a changed destination to nearby clients (default 30m). |
41 - Targeted: Corpse Run |
GateTtlMinutes — how long a Corpse-Run Gate stands if the grave is never emptied; 0 (the default) means no limit, so it lives exactly as long as the grave. ClearanceMeters — edge-to-edge room every gate prefers from trees, rocks, pieces, portals and locations (default 10, range 1–30). MinRoomMeters — the hard floor of real room it will accept (default 2, range 0.5–10). SearchRadiusMeters — how far out from the tombstone / bed to look, widened automatically if nothing in it qualifies (default 40, range 5–150). OffsetMeters — minimum distance of the grave-side gate from the tombstone (default 2). MaxGraveAgeMinutes — skip graves older than this; 0 (default) means no limit. |
155 - WildcardB: Portal Caps |
PortalCapEnabled (off by default) and PortalCapPerCreator (default 6) — the per-player portal limit and its enforcement. |
Local to Your Game
| Setting | Section | What it does |
|---|---|---|
| File-system paths | (various) | Where this server keeps routing.json, economy.json, and its own snapshot/report output. Read straight off this machine's disk, never synced to any client, since they're specific to this server. |
Enabled, IntervalSeconds |
84 - Ops: BarrkBot Export |
See BarrkBOT Portal Export above — local because it's writing a file to this server's own disk. |
Declaration Files
Two of the features above aren't driven by the .cfg at all — they're declared directly in JSON
files sitting next to it:
routing.json—phantomAnchors(Phantom Anchor Fabrication),sealedGates(Progression-Gated Sealed Gate), andparkedTerminals(Fast-Transit Mode & Parked Terminal).economy.json—oreGates(The Ore Gate), one entry per upgraded portal.
📦 Dependencies
⚠️ Requires: BepInEx (the Valheim pack,
denikson-BepInExPack_Valheim-5.4.2350or newer) — and only BepInEx.
| Dependency | Why |
|---|---|
| denikson-BepInExPack_Valheim (5.4.2350) | The mod loader (also provides HarmonyX, which every patch in this mod runs on). |
📥 Installation
TortalPortal Lite is a server-side-only mod — install it once, on the server, and every connected player benefits with nothing to download, on any platform.
- Install BepInExPack Valheim (
denikson-BepInExPack_Valheim-5.4.2350or newer) on the server. - Drop
TortalPortalLite.dllinto the server'sBepInEx/pluginsfolder. - Restart the server. That's it — no client install, no client config, nothing for players to do.
A Valheim mod by Wubarrk.
CHANGELOG
Changelog
1.0.10
Changed
1.0.17added to the verified-build list. On 1.0.17 the version gate refused the reassert/repair passes ("outside this mod's verified build list") until it was added. An asmdiff of the 1.0.16 and 1.0.17 server assemblies shows six changed types (ItemDrop,ItemDrop.ItemData,Inventory,VariantDialog,Version,ZDOMan) and no removed or re-signed member. The only one this mod calls into isZDOMan, and its single change is the body of the private save helperAddObjectsPerChunk, which this mod neither patches nor calls;Versionchanged only in its.cctor(CurrentVersion= 1.0.17). SeeSubsystems/Foundations/VersionMigration.cs.- Rebuilt against Valheim 1.0.17 (client build 25730771 / dedicated server build 25730807, network version 40, unchanged from 1.0.16) and re-verified: a static reference check against both the client and dedicated-server assemblies, plus a headless 1.0.17 dedicated-server boot.
- No behaviour change on 1.0.16 or earlier. Includes the 1.0.9 portal-cap fix below for any server still on 1.0.8.
1.0.9
Server-Enforced Portal Caps (feature 10) now removes the portal that was just built, never an older one, and never touches a portal that already exists.
Fixed
- A player at the cap lost their other portals and kept the new one. The engine chose its victims
by sorting each creator's portals on
ZDO.DataRevisionand taking the highest, on the belief that it is a creation-time proxy. It is a per-ZDO write counter -ZDO.IncreaseDataRevisionisDataRevision++(:73879) andZDO.Loadresets it to 0 (:74601) - so a portal that has been tagged and paired outranks one placed a second ago. On the live server a player already holding 12 who built three more in a few seconds lost a portal built 14 minutes earlier, then one built about two hours earlier, and only then the portal they had just placed. The players' report was exactly this: "it destroys your other portals rather than preventing you from building".
Changed
- A portal is judged once, when it first appears.
PortalCapTrackerkeeps the set of portals it has already seen. A portal that first shows up while its creator already holdscapof them is the one removed; when several arrive together the first-built is kept. Nothing is ranked any more, so an existing portal can never be picked. (A stock client still cannot be stopped from placing the piece - it builds locally and the server only sees the ZDO arrive - so removing it about a second later is the server-side form of "you can't build another".) - Existing portals are never removed. Switching the cap on, lowering it, or restarting with someone already over it used to destroy the excess; now that player just can't build more until they are back under the cap. Everything present when watching starts counts as already existing: the first census snapshot after startup, a cap switched off and on again, a gap of 15 s or more between engine updates, and any snapshot taken while no player is connected. The log says how many portals it is watching and names any creator already over the cap.
- The message says what happened: "Portal limit reached (12 of 12). The portal you just built was removed - dismantle an old one to make room." (was "Your newest extra portal was removed."). One message per burst, and the removal log line now carries the portal's tag.
- Each census snapshot is evaluated once instead of re-tallied on nearly every frame (the
RpcZdoDataHookfast path fires after every incoming ZDO packet, and the old sweep rebuilt its per-creator lists each time). Counting is unchanged: every portal in the census, charged to itss_creator; a linked pair is two; portals with no creator are not counted.
1.0.8
Corpse-Run Gate (feature 11) hardened on both of its promises: both ends always appear, and neither end can be destroyed.
Fixed
- The bed-side gate was being destroyed within seconds of every death, by this mod and vanilla
working against each other. Vanilla's own
Game.ConnectPortalsreconciler tears down any portal link whose partner points atNone(:100474-100481), and a one-way gate's grave-side end points atNoneby design - so within 5 s of every raise vanilla cleared the origin's connection, and the phantom factory's generic "connection target is gone, reap the phantom" rule then deleted the bed-side portal outright. Both halves are fixed: the ends are mintedTPL_engineownedso the generic sweep leaves them to the engine that owns them, and the one-way link is re-asserted from aGame.ConnectPortalspostfix - synchronously, inside vanilla's own call, so the intermediate disconnected state is never sent to a client. - Only the grave-side end was protected. The bed-side end was an ordinary phantom, so
ZDOMan.ReleaseNearbyZDOShanded it to whichever player was nearby every 2 s and it took damage like any other piece. Both ends are now registered with the newTargetedPortalProtection. - Nothing stopped a client from deleting a gate.
ZDOMan.RPC_DestroyZDOapplies whatever ZDOIDs a client sends it with no sender check, andZDOMan.DestroyZDObroadcasts a removal to every client whenever the server itself owns the ZDO - either one deleted a corpse-run portal outright, with health and ownership irrelevant. The mod already had the right primitive (Core/Hooks/HandleDestroyedZdoHook) but onlyAuditEnginewas registered against it, observe-only. Both removal paths are now vetoed for a protected portal, and a blocked removal force-sends the ZDO so the client that had already dropped its local copy gets the portal back. - A death could be dropped silently and never retried.
RaiseGatereturned without a word if the tombstone had gone invalid in the 3 s settle window, and a failed mint logged an error and gave up - in both cases after the death had already been recorded as seen, so it was never reconsidered. Raises are now retried with backoff, and a death is only marked handled once a gate genuinely stands. - A death was only noticed while the player was connected. Detection now runs off the tombstone table instead of the connected-player list, so dying and immediately disconnecting still produces a gate - standing and waiting, with the notification deferred until the player is back.
- Nothing re-built an end that went missing. The maintenance pass only ever looked at the destination, and only when its ZDO was still valid. It now checks both ends every pass and re-mints whichever one is gone, at the position it was placed at, re-linking and re-asserting afterwards.
ResolveOrigincould return(0,0,0)- open ocean on most seeds - for a player with no claimed bed when the Start Temple could not be resolved. It now falls back to the player's last known position, and says so.- Gates expired mid-run.
TtlMinutesdefaulted to 30, which is shorter than a deep Mistlands or Ashlands corpse run. Replaced byGateTtlMinutes, default0= no limit, so a gate lives exactly as long as the grave does. (A new key rather than a new default: BepInEx keeps whatever value an existing.cfgalready holds, so a changed default never reaches a server that has run before.)
Changed
- Damage immunity is now argued from where damage is actually arbitrated, and enforced in four
independent places. Every damage source in the game - melee, arrows, a troll's AoE swing, an
Aoecomponent's fire damage-over-time - ends atWearNTear.Damage(HitData), whose entire body ism_nview.InvokeRPC("RPC_Damage", hit)(:150275), andZNetView.InvokeRPCroutes tom_zdo.GetOwner()(:82901).ZRoutedRpc.HandleRoutedRPCthen drops the call outright whenZNetScene.FindInstancefinds no object (:83692-83711). A dedicated server pins its reference position at(1e6,0,1e6)every physics tick (:100340) and so never instantiates anything at a real player coordinate: a portal ZDO the server owns receives every damage call aimed at it and discards 100% of them, with no health threshold involved. So the mod (1) holds server ownership againstReleaseNearbyZDOS, (2) vetoes both removal paths, (3) no-ops everyWearNTearentry point that can lower health or destroy the piece, for the case where a server does hold an instance, and (4) keeps the 1e9 health and wear-immunity field overrides for the window where a client holds a copy. De-targeting (Piece.m_randomTarget/m_primaryTarget) is retained but is now documented as a courtesy, not a defence - it never stopped area damage. - A watchdog now counts what it had to fix, and a status line every 5 minutes reports both ends standing plus every damage call, removal and ownership steal the protection refused. "Is anything getting through" is an answerable question instead of a guess.
ClearanceMetersis a preference, scanned in descending tiers, instead of all-or-nothing. On the live server 40 of 40 logged placements came backCompromiseand not one reachedClear: atClearanceMeters=15(and at the 10 m default) nowhere in a Valheim forest has that much room from every tree, rock and building piece, so every gate fell through to a last-ditch "most open spot" that weighs a metre of room against thirty metres of walking - which is how gates ended up 26-40 m from the bed they were meant to stand beside, and why the warning it logged every single time carried no signal. The search now tries the configured clearance, then progressively smaller requirements, and takes the closest spot honouring the best one available, never accepting less than the newMinRoomMetershard floor while any spot in range can meet it - widening the search radius (up to 4x, capped at 200 m) first. NewReducedquality for "met a smaller requirement in full", so a warning again means something. Log lines now read as room actually obtained versus room needed.- Death-detection latency cut from tens of seconds to a tick or two. Respawning destroys the
player's character ZDO and mints a new one (
Game._RequestRespawn:100278-100293), so a character that is replaced or disappears opens a short watch that scans the player's last known position - which is where a fresh tombstone is - instead of waiting for the paced background sweep's cursor to come round. That sweep's own budget went from 20 to 48 sectors per tick. - New config in section
41 - Targeted: Corpse Run:MinRoomMeters(default 2),MaxGraveAgeMinutes(default 0 = no limit),GateTtlMinutes(default 0 = no limit, replacesTtlMinutes).
Known gaps
Core/Hooks/RpcZdoDataHook's leading-ZDOID capture reads theintcount field thatZDOMan.RPC_ZDODatawrites first (:77133), not a ZDOID, so the value it hands its postfix handlers is garbage. Nothing in the corpse-run path depends on it;AuditEngineis the only consumer. Left alone in this release rather than changed blind.- Older
Subsystems/Foundations/doc comments still cite 1.0.12 decompile line numbers, now ~32-43 lines low against 1.0.15/1.0.16.
1.0.7
Changed
1.0.16added to the verified-build list. An asmdiff of the 1.0.15 and 1.0.16 server assemblies shows every type this mod patches or calls (Game,ZDOMan,ZDO,ZDOExtraData,ZDOVars,ZRoutedRpc,ZRpc,ZNetScene,ZNetView,TeleportWorld,ZDOID,ZPackage,ZNet,FejdStartup) is unchanged; of the 19 types that did change, onlyTerrainCompis one this mod has any relationship with (TargetedGroundProbereads the_TerrainCompilerwire format directly rather than calling the class), and the changed members (Awake,ApplyOperation,PaintCleared,.cctor) are notLoad/Save/ApplyToHeightmap, the ones that format depends on. SeeSubsystems/Foundations/VersionMigration.cs.- Rebuilt against Valheim 1.0.16 (client build 25527674 / dedicated server build 25527701, network version 40, unchanged from 1.0.15) and re-verified: a static reference check against both the client and dedicated-server assemblies, and a headless 1.0.16 dedicated-server boot.
1.0.6
Fixed
- A modded client was kicked for not having TortalPortalLite installed — a server-only mod.
(
Plugin.csConfigSyncinitializer.) The plugin setDisplayName,CurrentVersionandMinimumRequiredVersionon itsConfigSyncbut neverModRequired, which ServerSync defaults totrue(ServerSync.cs:1148).GetFailedServerfails every check whereModRequired && !ValidatedClients.Contains(rpc)(ServerSync.cs:1248), and a client without the DLL never validates, so it was disconnected withConnectionStatus.ErrorVersionnaming this mod. A vanilla client has no ServerSync and sends no version list, so the check never ran and the bug stayed invisible on vanilla servers; it only bites a modded client whose pack correctly omits the server-side mods.ModRequired = falseis now set explicitly.MinimumRequiredVersionstays: not required, but a client that does load it must match versions.
1.0.5
Fixed
- Automatic repair never ran on Valheim 1.0.15 — and said so every five minutes.
1.0.15was outside the verified-build list, so withAutoRepair = trueevery scheduled--applypass was refused by the Version Migration gate and the identical refusal line was logged each interval (230 times on one server).1.0.15is now verified: a per-class diff of the decompiled 1.0.12 and 1.0.15 server assemblies shows the entire portal write path (Game,ZDOMan,ZDO,ZDOExtraData,ZDOVars,ZRoutedRpc,ZNetScene,TeleportWorld) is byte-identical, and the classes that did change (Terminal,ZNet,TerrainComp,Inventory,Piece,Player,Character) changed outside every member this mod patches or calls.
Changed
- The automatic repair pass now idles quietly while the destructive gate is closed. It logs one
warning explaining why (and what to do about it) the first time a scheduled
--applypass would be refused, skips silently after that, and logs a "resumed" line the moment the gate opens — whether byAcceptUnverifiedBuildhot-reloading,DryRunDefaultflipping to report-only, or a future build being verified. The console/RemoteCommandrepair --applyverb still refuses with its full message every time it's asked.
1.0.4
Fixed
- Corpse-run gates no longer spawn inside rocks, trees or buildings. The 1.0.3 scanner measured clearance 1.3 m from each object's ZDO pivot, so a black-forest boulder whose pivot sat 4 m away but whose mesh spanned 8 m passed as "clear" and gates were planted inside rock walls. Every obstacle is now a disc sized from the prefab's real collider extents (read off the prefab asset and scaled by the object's stored scale), generated locations count with their exterior radius (their static geometry has no ZDOs), and clearance is measured edge-to-edge from the portal frame. Creatures, items, projectiles and the tombstone itself are not obstacles.
- Gates sit on the real ground. Height now comes from a headless re-run of the client's own
heightmap build —
HeightmapBuilder.Build's corner-biome blending plus the zone's_TerrainCompilerterraforming deltas — instead ofWorldGenerator.GetHeight, which returns the un-blended single-biome height and knows nothing about hoe/pickaxe work; at biome borders and on levelled bases that was metres off. The pivot, the frame's four base corners and a 2 m ring of surroundings must all be dry, lava-free, level and contiguous. - A death inside a dungeon re-anchors the grave-side gate to the dungeon's exterior entrance (same 64 m zone as the interior) instead of inside a room wall; the player is told.
- Stray gates after a restart — gate bookkeeping lived only in memory while the portal ZDOs persist, so every restart with an unrecovered tombstone left the old pair standing and raised a second. Any corpse-run portal the running server did not raise is now reaped.
- Both ends of one gate can no longer share a clearing when a player dies beside their own bed.
- A new tombstone now gets 3 s to stop falling/sliding before its position is used.
Added
ClearanceMeters(section41 - Targeted: Corpse Run, default10, range1–30): edge-to-edge room every gate (both ends) must have from every tree, rock, building piece, portal and location. Sub-metre props (mushrooms, flowers, berry bushes) only need 2 m. When nothing inside the search radius honours it, the most open dry spot found is used and the server log says so — with distance, nearest obstacle and shortfall — so you can widen the search.SearchRadiusMeters(same section, default40, range5–150) replaces 1.0.3'sMaxOffsetMeters(default 8, which could never satisfy a 10 m clearance; the rename stops a savedMaxOffsetMeters = 8from silently capping the new search). The scan is rings outward, so the gate still lands as close to the grave as the clearance allows; the search also auto-extends past any obstacle that covers the anchor itself (a death inside the start temple or a ruin).- One
[CorpseRun]log line per gate end on every death: quality (Clear/Compromise/WaterSurface/LastResort), position, distance, slope, margin vs nearest obstacle, the obstacle census, and the anchor's height above the modelled ground as a sanity readout.
Changed
28 - Targeted: Bed/OffsetMetersand41/OffsetMetersare now the minimum distance from the bed / tombstone (ranges widened to 1–20); the maximum isSearchRadiusMeters.- Bed-side gates no longer accept underwater ground; an over-water bed gets a shoreline spot or a surface-floating gate like any ocean death.
- The death toast reads "near your bed" / "at the world spawn" and mentions when the grave gate exits at a dungeon entrance.
1.0.3
Added
- Configurable Corpse-Run Offset (
CorpseRunMaxOffsetMeters). AddedCorpseRunMaxOffsetMeters(default8.0, range3.0–25.0meters) to config section41 - Targeted: Corpse Run, governing the outer radius searched when seeking unobstructed ground for the recovery gate.
Improved
- Intelligent Corpse-Run Gate Placement. Replaced simple fixed-offset positioning with an
adaptive concentric-ring clearance algorithm adapted from Wonderland's starting grant placement:
- Concentric scanning rings (2.5m, 4.0m, 6.0m, up to
CorpseRunMaxOffsetMeters) testing angular candidate points around the tombstone or player bed. - Multi-directional slope and clearance checks rejecting steep drop-offs, cliffs, and walls.
- Proximity collision checks against trees, rocks, buildings, and existing structures via
ZdoSpatialQuery.FindNearensuring a 2.5m clear buffer. - Shoreline and water handling ensuring the portal never spawns submerged under water while gracefully accommodating coastal beach ground above the waterline.
- Interior dungeon detection: preserves elevated interior elevations (
y > 1000m) without snapping down to exterior world heightmap levels. - Automatic yaw alignment orienting the destination portal to face directly toward the tombstone upon arrival.
- Concentric scanning rings (2.5m, 4.0m, 6.0m, up to
Changed
- Indestructible Tombstone Recovery Portal. Prevented monsters and world damage from destroying
the corpse-run destination portal at the grave before the player can retrieve their gear:
- Mob AI de-targeting: sets Piece
m_randomTarget = falseandm_primaryTarget = falseso mobs ignore the portal and do not agro or attack it. - Full client damage immunity: configures
WearNTearfields on the portal ZDO with immune damage modifiers across all physical and elemental damage types, disables structural support wear, and sets health to 1,000,000,000 HP. - Server ownership pinning: patches
ZDO.SetOwnerandZDO.SetOwnerInternalto keep the destination portal pinned to the dedicated server (ZDO owner 0L). Because Valheim clients only execute and broadcast damage to objects they own, unowned server pieces drop client-side monster attack damage. - Autonomous health watchdog: a 2-second background check ensures any transient damage is immediately healed until the grave is claimed or the TTL expires.
- Mob AI de-targeting: sets Piece
1.0.2
Fixed
- Version Migration falsely flagged verified builds as unverified. The running-build check
compared
Version.GetVersionString()— which carries a platform prefix on some builds (l-1.0.12on Steam Linux,dw-/dl-on Deck,ms-on Microsoft Store) — against the bare1.0.12in the verified list, so it never matched. Now comparesVersion.CurrentVersion(bare major.minor.patch); the prefixed string is still logged for humans. - Version Migration always reported the portal-prefab registry as empty. The
Game.instance.PortalPrefabHashcheck ran during the plugin's own Awake, before the game'sGameobject exists — so it always failed, andrepair --applywas permanently refused regardless ofAcceptUnverifiedBuild. The check now runs once on the first tickGame.instanceexists (whichGame.Awakepopulates synchronously), logs the verified prefab count, and only errors on a genuinely empty registry.
1.0.1
Added
- BarrkBOT Portal Export. Writes
barrkbot_portals.json— per-player portal counts, cap/over-cap flags, and world totals — atomically toBepInEx/config/TortalPortalLite/, on a floor of once every 60 seconds, built directly against BarrkBOT's own published ingestion contract (no HTTP, no webhook, no auth — file-only; BarrkBOT reads, it never writes). Force an immediate rewrite withremovekey tpl barrkbot. Config section84 - Ops: BarrkBot Export(Enabled,IntervalSeconds).
1.0.0
Added
- Phantom Anchor Fabrication. An admin declares a named "phantom anchor" — a destination point
with no real player-built portal behind it — in
routing.json'sphantomAnchorssection (fields:Name,Position{X,Y,Z},RotationY,Void).Void:truemakes it invisible and non-portal;Void:falsemakes it a real, visible portal anchor. The server creates and maintains it automatically, so Sealed Gates and Parked Terminals have somewhere to point that nobody has to physically build. - Progression-Gated Sealed Gate. A portal that resolves to nothing until a named global key (a
boss defeat, or a custom flag) becomes true, then opens to its declared destination. Declared in
routing.json'ssealedGatessection (Name,Position,GlobalKey,Destination,SealedTag,OpenTag). Config section11 - Routing: Schedules and Conditions, keySealedGateEvalSecondscontrols how often the global key is re-checked (default 1s). - HealthScan. A background pass (config section
4 - Foundations: Health,HealthIntervalSeconds, default 30s) that flags odd-count same-tag portal groups — which always leave one permanently orphaned by vanilla's own pairwise-only pairing — and stale dangling connections. It deliberately does not flag a portal whose one-way connection is intentional, like the Corpse-Run Gate below; those are a designed feature, not a defect, and are excluded from this report. - Repair. Turns HealthScan's findings into safe, bounded, reversible fixes — retag an orphaned
portal, clear a dangling link. Dry-run by default (config section
5 - Foundations: Repair,RepairDryRunDefault), reporting what it would do; runremovekey tpl repair --applyto actually write the fixes. It automatically snapshots the world's portal state first, sorestorecan always undo the whole batch. - Metrics. Portal counts (total/connected/managed), per-tag/per-biome/per-builder breakdowns,
and a position-based heuristic for transit counts and "top routes," read via
removekey tpl metrics. Config section7 - Foundations: Metrics. - Version Migration. A boot-time safety check: is this a Valheim build the mod has actually been
verified against, and does the game's own portal-prefab registry look intact. If either check
fails, destructive passes (
repair --apply) refuse to run rather than risk corrupting the world, unless an admin explicitly opts in viaGlobalConfig.AcceptUnverifiedBuild. - Compat.
removekey tpl compatreports which Harmony patches exist on the handful of vanilla methods this mod also touches, so "my portals keep unpairing" support questions have one command's worth of answer. It also runs an explicit, named check against two specific sibling mods this server is expected to run alongside — Wonderland and GetOffMyLawn. Both were read directly: both patchZDO.SetOwner, but both vetoes are scoped only to their own tracked objects (Wonderland's buoyant water items, GetOffMyLawn's ward-held building pieces) and never fire on a portal ZDO — confirmed no interference. - Architecture. Not a player-facing feature — this is the subsystem/registry/tick-order design the whole mod is built on: Census first, then Audit, then HealthScan, and so on, each with disjoint write ownership over the portal data it touches.
- The Ore Gate. A per-portal upgrade, declared in
economy.json'soreGatessection (Portalposition,AllowAllItemsbool, defaulttrue, optionalExitDistancefloat) that lets ore/metal actually pass through that specific gate and/or extends how far in front of it a traveller steps out. A real, vanilla-client-honoured gameplay perk written directly onto that one portal's data, not a global setting. - Server-Enforced Portal Caps. An optional per-player portal limit (config section
155 - WildcardB: Portal Caps,PortalCapEnabled, off by default,PortalCapPerCreator, default 6). Once a player's live portal count exceeds the cap, their newest excess portals are destroyed automatically and they're toasted about it — genuinely unbypassable by an unmodified client, unlike a client-side placement limit. - Corpse-Run Gate. On a tracked player's death, the server automatically raises a private,
temporary portal near their claimed bed (or the world spawn if they have none) leading straight to
their tombstone, so gear recovery doesn't mean a bare-handed run back into danger. This is a
genuine one-way trip by design, not a round-trip pair — there is no automatic portal back from the
grave to the bed. The gate self-destroys once the grave is emptied/despawned or after a
configurable TTL (config section
41 - Targeted: Corpse Run,CorpseRunTtlMinutes, default 30). - DestinationPrewarm. An always-on safety net: the instant any managed portal's destination
changes, nearby players' clients are immediately force-sent the new destination data, so a
re-point is never followed by someone walking through into a "dead" transit because their client
hadn't received the update yet. Config section
16 - Routing: Delivery Pipeline,PrewarmRadius, default 30m. - Fast-Transit Mode (and Parked Terminal). A managed portal kept permanently "unconnected" on
purpose; instead of the normal portal-connection walk-through, the server itself detects a player
stepping up to it and teleports them directly. Fast-Transit is the quicker variant (roughly 2-4
second fade instead of vanilla's roughly 8 second swirl), used once the destination zone is
confirmed already generated and safe to land in — otherwise it automatically falls back to the
normal, slower transit rather than risking a bad landing. Declared in
routing.json'sparkedTerminalssection (Distant:false= Fast-Transit,Distant:true= the plain Parked Terminal base mechanism). - Live Portal Markers. Any portal named with a leading
#in vanilla's own rename box gets a removable, labelled marker on every connected player's map, via vanilla's own persistent-event system — the marker disappears the moment the portal is renamed away from it. Off by default (LiveMarkersEnabled, config section40 - Ux: Advanced Schemes).
Notes
- This release is the result of narrowing a much larger internal research catalog — 263 candidate portal mechanisms explored during R&D — down to exactly the 14 features above for the shipped product. Everything else was removed from the codebase entirely, not merely disabled.