TortalPortal
Total Portal Control System for Valheim. Pick destinations from a map UI listing biome, distance, and owner. Features destination photos, PIN locks, tame teleportation, travel filters, and a procedural runic gateway.Changelog - TortalPortal
[1.6.1]
No floor change. MinimumRequiredVersion stays at 1.5.0 β this is a window that opened itself at the far end of a trip: entirely local, nothing on the wire and nothing written to the world. A 1.6.0 client behaves identically apart from being greeted by its own destination window on arrival.
πͺ The destination window greeted you at the far end again
- Fixed (#146, and this time at the cause): the menu came back the instant you arrived. The same symptom 1.4.6 went after, and the thing keeping it alive was 1.4.6's own fix.
- What 1.4.6 had right. Arriving somewhere leaves you standing well inside the destination gate's range, so the gate being travelled to is noted down and holds its menu shut when it reports you in range. 1.4.6 widened that from one way of recognising the gate to three, and that part stands.
- What it had wrong. It also put all three behind a stopwatch started when you left β a few seconds, called generous, to cover the loading fade. It was not generous, it was unreachable. Vanilla will not end a distant teleport before eight seconds have gone by, and every trip this mod starts is a distant one; it then holds on until the destination area reports itself ready and solid ground is found under you, up to fifteen. So the note expired before you could possibly have landed β every time, on every route β the gate you stepped out of was never recognised as an arrival at all, and it did the only other thing it knows how to do.
- Changed: the grace is measured from the landing now, not from the departure, and on the clock the game already keeps for exactly this: vanilla zeroes a timer when a trip begins, pins it at zero for as long as the trip runs, and counts up from zero the moment it ends. That figure is "how long since this player landed", whatever the loading screen cost β so no world, however large or slow to stream in, can outlast the window again.
- Changed: the note is no longer eaten by the first gate to read it. It used to be cleared as soon as any one portal answered with it, which left it good for one gate and one instance of that gate: a second gate near enough to where you land opened its menu in the same frame, and a destination whose object was rebuilt by the zone still loading in behind you came back with nothing left holding it shut. It expires on its own clock instead, so every gate and every rebuild inside the window reads the same answer. The blanket "you cannot have walked anywhere yet" answer keeps a far shorter leash than the two that actually name the gate, so landing in a hub and stepping to the gate next door still opens that gate's window.
- Changed: every trip is noticed now, whoever starts it. The guard used to be told about a trip by the two routes that ask for one, so any other way of moving a player β a console teleport, another mod, whatever gets added here next β landed you unguarded. It watches the teleport itself now, and there is no route left that can forget to mention it.
- The debug log carries the figure each decision turned on, and says so outright when a note goes stale before the gate it names has had its say. That line is the fingerprint of this entire family of bug: if it ever comes back, the log names the cause instead of handing over another hunt.
[1.6.0]
Gates the world was seeded with that nobody has been told about. No floor change - MinimumRequiredVersion stays at 1.5.0: concealed gates are withheld from the published index, so a 1.5.x client is never sent one and simply never has them as destinations, and the reveal rides a routed RPC an older client never registered, which the receiving peer drops as an unknown method. Nothing existing changed shape on the wire or in the world.
π―οΈ Concealed gates
- Added: a second kind of seeded portal, one that has to be found.
tortal_scatterconcealed [count] [radius]raises real portals across the world that are deliberately kept out of the published portal index - so no client is sent their position, their name, or the fact that they exist. They are in nobody's destination list and on nobody's map. You find one by walking into it and pressing E, and at that point the server hands that one gate to that one player. - The secret is kept by the machine that owns it, not by the window that draws it. The destination index is a single ServerSync value, one blob broadcast identically to every peer at login - there is no per-player variant of it. A concealed gate riding in that blob would land in every client's memory the moment they joined, and "concealed" would mean no more than our own list declines to draw it: one config edit, one rival mod or one teleport-to-coordinate away from nothing. So they are withheld from the blob entirely and each player's own finds are sent to them over their own routed RPC. The filter that does it is the same one that has always kept corpse gates out of the index, the map, the export and the portal cap - one place, four consequences.
- A claim is checked against the world, not taken on trust. The only thing a client sends is which gate it touched. The server reads everything else for itself: that the gate exists, that it is concealed, who the claimant actually is (
s_playerIDoff their character ZDO, never the connection id), and that they are genuinely standing within 30 m of the gate they named. A claim it cannot resolve - a peer whose character has not arrived yet - is unknown, not refused, and can simply be made again. - Discovery is recorded on the gate itself, as a list of player IDs in the world save. No side file, so it survives a client reinstall, cannot be edited by the player it is about, and is still true after a server rebuild. A client standing close enough to hold that ZDO can read the list off it, which costs nothing - they are at the gate, they have already found it - and says nothing whatsoever about any other concealed gate in the world.
- Added:
EnableConcealedPortals(synced, default on - and dormant until somebody seeds a world). Switching it off leaves standing gates exactly where they are and stops them being discovered or handed out; it does not make them visible, andtortal_concealedclearis how you actually get rid of them. - Added:
ConcealedDiscoveryShared(synced, default off). Off, every player finds every gate for themselves. On, the first player to touch one reveals it for the whole server. Turning it on does not retroactively publish what individuals had already found on their own - it applies from the next discovery onwards. - Added:
tortal_concealedclear(removes every concealed gate, found or not, and pushes the emptied list so nobody keeps a destination that is no longer there) andtortal_concealed(how many are standing, how many are found, and by whom). - The health scan reports a
Concealedline with those counts, and holds these gates apart from every other check - a gate that records no builder, carries a name nobody is meant to read and is in no index by design would otherwise trip half the findings meant for public portals. The one check it keeps is buried, which matters more here than anywhere: the whole purpose of one of these is to be walked up to.
π± Seeded where a player can actually stand
- Spots are settled by the corpse run's own placement scanner rather than by the coordinate they were sampled at: clearance measured edge to edge from every tree, rock, building piece, portal and generated location, on dry, level, contiguous ground, at least 128 m from every portal already standing.
- Ground height comes from the server-side terrain recipe (
GroundProbe, terraforming included) and not fromWorldGenerator.GetHeight, which is whattortal_scatteruses. BareGetHeightreturns the un-blended single-biome height, and at a biome border that sits metres above or below the mesh a player actually walks on. Test litter nobody visits can afford that; a gate whose entire purpose is to be found cannot. - Every seeded name is checked for uniqueness against every portal already in the world, because vanilla connects two portals whose tags match exactly - a seeded gate sharing a name with somebody's would quietly hook the two together.
- Seeded gates are immune and cannot be hammered down, and they carry the same refusal of a removal this mod did not ask for that corpse gates do. The removal locks are re-taken at every server start, so a gate seeded by a previous run is protected exactly like one seeded five minutes ago. A landmark the world was seeded with is not one gate's worth of wood to whoever finds it first.
- They are deliberately NOT marked as
tortal_scatterlitter.tortal_scatterclearsweeps everything carrying that marker and the health scan reports it as leftovers to tidy, so a seeded world feature wearing it would have been quietly deleted by the first admin who cleared up after a test.
π§ Under the hood
- The three new commands are registered with
isCheat: false, unliketortal_scatter's.ConsoleCommand.IsValidtestsIsCheatsEnabled(), which returnsZNet.IsServer()- so a cheat command can never be run by an admin on a dedicated server, which is precisely where seeding a world matters. The gate that replaces it is the server's ownadminlist.txt, checked server-side on the one RPC all three share. They also register fromZNet.Startas well as fromTerminal.InitTerminal, because registration is only a write into a static dictionary and a headless host may never build a Terminal at all. - Typed at a client, all three are routed to the server and answer back into your console - including the seeding summary, which arrives when the last gate is placed rather than leaving the admin with "seeding 25 gates" and no outcome. A console command runs on the machine it was typed into, and that machine has no world to seed.
- The destination list is now two sources merged - the server's index and this player's own finds - rebuilt only when one of them actually changes, because it is read once per frame per open window. The persistent map markers gained the same trigger, so a gate just discovered gets its marker immediately instead of waiting for the server's next index publish to happen to differ.
- Seeding spends a frame per gate: one placement search is a couple of thousand ground probes, and one gate's failure can no longer take the rest of the run down with it. The per-player push reads the standing gates once for the whole round rather than once per player, and records a send only after it has gone out - marked before, a send that threw would leave that peer flagged as told and the poll would skip them from then on, which is the one failure a poll exists to heal.
