Uh oh!
There was an error while loading. Please reload this page.
Restore world saving, fix five server crashes, and persist dropped items - #5
Restore world saving, fix five server crashes, and persist dropped items#5beleata wants to merge 5 commits into
Conversation
Persistence.Save built the whole world, deleted old save files and then logged "Saved world..." without writing anything: the call to SerializeToFile has been commented out with a "TODO: FIX" since c89a61f (2023-07-23). Nothing has been persisted since, so every restart started from a clean world. Restoring the write exposed the load path, which had never run: - PersistenceJsonConverter.WriteJson serialised through the same JsonSerializer the converter is registered on, so writing recursed until the stack overflowed. The concrete type is already written by PersistenceData.__ObjectType, so writing needs no converter at all and CanWrite is now false. - Save wrote relative to the working directory, while Load and the save-file cleanup scan the directory next to the assembly. With -configdir set, saves went where Load never looks. - Vessels were loaded with Parallel.ForEachAsync, but the game code that drives mutates shared state through plain List<T>. Docking corrupted the shared docked-vessels tree into cycles, which overflowed the stack in BulletPhysicsController.RemoveRigidBody and in OrbitParameters.RelativePosition, and repair points corrupted Room.AirConsumers, throwing out of List.Remove. Loading is now sequential. - Docking and stabilisation refer to other vessels by GUID, so they now run in a second pass once every vessel exists, rather than reaching for vessels that had not been created yet. - RemoveRigidBody walks the docking tree iteratively with a visited set, so a malformed tree is reported instead of taking the server down. - Room.AddAirConsumer rejects null instead of storing it for a later NullReferenceException in UpdateCompoundRooms. Verified end to end: a generated world is saved, loaded again after a restart, runs and saves again, including reloading a save that was itself written by a loaded world. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Everything here was found by playing the game against a local server: a Nakama instance, the game server, and the client built from the Client repository. Restoring world saving (previous commit) made the load path run for the first time, and each of these turned up behind it. Crashes, all of which end the server process: - Killing a player compares every vessel's quest trigger id against one another through QuestTriggerID's == operator, which dereferences both sides. Almost no vessel carries one, so the first comparison throws. Any death took the server down. Its GetHashCode also built a multi-dimensional array out of the field values. - Destroying a vessel ran its docked vessels and its crew through Parallel.ForEachAsync, and the spawn system and name generator it reaches into keep plain dictionaries. Two vessels going at once corrupt them. - A player floating with no vessel nearby reports none, and UpdateMovementListener dereferenced that before the guard against it six lines further down. - GameTransport.DisconnectInternal ran its body twice for one client and threw on the already disposed socket before the entry could be removed, leaving the client marked as connected and the server refusing its next login as a duplicate. - An item on a pivot is deleted when no client has spoken for it in five minutes, but lastSenderTime started at DateTime.MinValue, so anything restored from a save looked abandoned for two millennia and was deleted on the first tick. Items left lying in a vessel: Such an item belongs to no vessel and to nobody's inventory - it rides on its own pivot - so nothing that is persisted could reach it and it was lost on every shutdown. This is issue OpenHellion#3. The vessel it was released in is now recorded, along with where, and on load the item is put back on a pivot in that vessel. Verified across restarts: the item is written to the save, restored, and saved again. PlayerReady's setter compared with a single '=', so its body ran on every assignment rather than only on a change, firing anything hanging off it many times a second. Known limitation: a client is told about these items when it joins, but does not draw them. The server places such an item within a metre of the station's centre while the client draws it where it was dropped, tens of metres away - the two measure in different frames and neither converts. That is issue OpenHellion#1's territory, and it is not addressed here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Makkkkus
commented
Aug 21, 2026
Sorry for not replying earlier! Got back to this now when the weekend started. I really appreciate you both taking the time to test and edit the codebase. I especially appreciate you looking into saving and loading as it has been a bother for me for some while. I currently have a local working tree with over eight months of changes for a separation of the world into local and solar system space (#1). In doing this I have made an immense number of changes: touching 418 files on the client and 125 files on the server, totalling in thousands of lines of edits. I regret not pushing this so you could look at it. I will am very close to finishing most of the remaining bugs from the rewrite. I will push the changes at the end of this weekend, even if they are incomplete. I will add a .todo file if they still are broken. |
beleata
commented
Aug 22, 2026
Thank you for the reply, and no rush at all — eight months of work on a world-space Take these whenever it suits you, and only if you think they fit. We are not The one reason we mention ordering at all: all fourteen are small and independent of What we changed, and whyEverything traces back to one thing. Server, saving and loading. Saving restored, and the write pointed at the same Server, five ways to kill the process. Every player death, through Client, two faults that stop a loaded world from ever finishing. Nakama. The plugin does not load at all against a current Nakama: All of it was found by playing rather than by reading: one of us plays and reports A questionDo you know about the Hellion Revival Project — Zorkind's work, at hellionrp.space? I ask because it seems a waste for two efforts to solve the same game twice without And one practical thing: is there a Discord, or anywhere else you would rather talk? |
Makkkkus
commented
Aug 22, 2026
I have looked briefly through your changes. All of them seem good and pretty self-explainatory. I appreciate you taking the time to look at this. I am familiar with Zorkind and his revival project; I have written a bit about the differences between the two projects on our about page. I have talked a bit to Zorkind on his Discord server, and I am not very impressed with how things are run there. Hellion Rescue Project (HRP) is essensially a dictatorship with him having complete control over all aspects of the project. He argues that is is the only correct way to do things because it is "illegal" to distribute the original game; while ignoring that he himself distributes modified binaries of the original server, which is just as illegal. Zorkind has created a fake impression that our project is illegal, when it is not. I think the reason why Zorkind keeps repeating this is that he wants control (corroborated in that he has spread fake stories of contacting the original owners offering to buy the game many times and falsely claimed to have talked to me about the legality of our project). The main reason why this project exists is to let new players access Hellion, which is not possible with HRP's approach. We have a Discord! The link is: https://discord.gg/9nGWgQ8Uyf |
Items left lying in a vessel were persisted through a list of pivots. That was treating a symptom. Removed, along with everything that propped it up, and replaced with the rule the shipped client already expects. A client whose item is resting on the floor reports that it has left the vessel roughly once a second, for as long as the item exists. Its room trigger fires an exit every time the item is re-parented, and the reply re-parents it again. Believing it leaves the item on a pivot of its own, and a pivot's contents are described exactly once - in the message announcing the drop - because the movement message walks vessels and a pivot is not a vessel. From then on the item is invisible to anyone arriving later, including the player who dropped it once they reconnect, and there is nothing under any vessel to write down when the world is saved. The shipped game hides all of this: floating items are destroyed after five minutes and were never saved, so nobody could see that they had already stopped existing for everyone else. Measured against a 2018 server and the retail client, the same drop sends two messages and stops. - A release inside a vessel now leaves the item owned by that vessel, and the reply carries that back to the client. Released in open space it still goes on a pivot, and the five minute cleanup still applies to it. - An object with no master client was described to nobody. Restoring one from a save leaves it with no master, so the server told no one where it was. With no master the server is the only authority there is, so everyone hears from it. - A player who has just connected gives up whatever they were the master of. A new client knows nothing, and while it held that title the server would not tell it where those objects were. - A pivot's dynamic objects are now walked when building the movement message, the same as a vessel's, so a tool dropped in open space beside you is not invisible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
beleata
commented
Aug 22, 2026
We were wrong about dropped items, and I have pushed the correction. The pivot What is actually brokenA client whose item is resting on the floor of a vessel reports that the item has Believing it puts the item on a pivot of its own. A pivot's contents are described That is why our first attempt looked like a persistence problem. It never was. Why nobody has hit this beforeThe shipped game destroys a floating item five minutes after anyone last touched it, How we measured itWe ran the identical action against two servers, logging every attach message: The client code along that whole path — What the commit doesFour changes, all on the server, none of which depend on finding that client fault:
Tested: drop, watch, reconnect, restart the server, reconnect again. The items are One thing we noticed and did not chasePicking a restored item up gave the wrong item: a repair tool came back as a stim A question about open spaceAnything let go of outside a vessel is destroyed five minutes after it was last |
* Based on research in #5 Co-Authored-By: beleata <o885407257@gmail.com>
Makkkkus
commented
Aug 27, 2026
Picked the persistence findings and commited it as 7613843. The other fixes might need to be looked into later. Closing this as done. |
Fourteen fixes, and one thing we could not finish
Hello. We are two people who wanted to play Hellion again, found OpenHellion, and
spent two days on it. We are not asking you to merge anything blind — everything
below sits in branches on our own forks so you can look first.
Everything here was found by actually playing: a local Nakama, the game server,
and the client built from your repository. One of us played and reported what he
saw; the other read logs and wrote code. Every fix below was reproduced in the
running game, not deduced from reading.
A question before anything else: are the three repositories on GitHub what you
are working from, or do you have newer local work? We branched from
mainon each(
Serverat84dda51, plusClientandNakamaat their current heads). If youhave unpushed changes, some of this may already be solved or may conflict, and we
would rather rebase than hand you noise.
Where it started
Persistence.Savebuilds the whole world, deletes old save files, logs"Saved world..."— and writes nothing. The call toSerializeToFilehas beencommented out with a
TODO: FIXsincec89a61f(2023-07-23).So nothing has been saved for two years, nothing has been loaded, and the entire
load path has never run. Everything else we found was hiding behind that.
The fourteen
Server — saving and loading
1. World saving was disabled. Restored. The write also went to a path relative
to the working directory while
Loadand the save-file cleanup scan the directorynext to the assembly, so with
-configdirset, saves landed whereLoadneverlooks.
2.
PersistenceJsonConverter.WriteJsonrecursed into itself through the sameJsonSerializerit is registered on. The concrete type is already written byPersistenceData.__ObjectType, so writing needs no converter:CanWriteis nowfalse.3. Loading ran in parallel over code that is not thread safe. Vessels were
loaded through
Parallel.ForEachAsync, and the game code it drives mutates sharedstate through plain
List<T>. Docking corrupted the shared docked-vessels treeinto cycles, which overflowed the stack in
BulletPhysicsController.RemoveRigidBodyand in
OrbitParameters.RelativePosition; repair points corruptedRoom.AirConsumers, throwing out ofList.Remove. Loading is now sequential.We checked the save file itself first: 1566 vessels, 1222 with a docking parent,
zero cycles. The file was clean; the cycles were made on load.
4. Docking and stabilisation are resolved by GUID, so they now run in a second
pass once every vessel exists, rather than reaching for vessels not yet created.
5.
RemoveRigidBodywalked the docking tree recursively with no guard. It nowiterates with a visited set, so a malformed tree is reported instead of taking the
server down.
6.
Room.AddAirConsumeraccepted null, which surfaced much later as aNullReferenceExceptioninUpdateCompoundRooms— only in a depressurised room,because a pressure check short-circuits the dereference one line earlier.
Server — five ways to kill the process
7. Every player death.
QuestTriggerID's==operator dereferences bothsides, and killing a player compares it against every vessel in the world. Almost
no vessel has one. First comparison, exception,
async void, server gone.Its
GetHashCodealso built a multi-dimensional array out of the field values.8. Destroying a vessel. Its docked vessels and crew went through
Parallel.ForEachAsync, and the spawn system and name generator it reaches intokeep plain dictionaries. Two vessels at once corrupt them.
InvalidOperationException, server gone.9. A player floating with no vessel nearby.
UpdateMovementListenerdereferenced
nearestVesselbefore the guard against it six lines further down.10. Reconnecting.
GameTransport.DisconnectInternalran its body twice for oneclient and threw on the already disposed socket before the entry could be
removed — so the client stayed marked as connected and the server refused its next
login as a duplicate. The entry is now taken out first.
11.
PlayerReady's setter compared with a single=. Its body ran on everyassignment instead of only on a change, firing whatever hangs off it many times a
second.
Client — two faults that stop a saved world from loading
12.
Ship.FixedUpdateisasyncand awaits a network send. The continuationcan resume off the main thread, and reading
Time.fixedDeltaTimeafter it throws.The exception is never observed and loading simply stops. The value is now read
before the await.
13.
QuestIndicators.IndicatorPrefabis not assigned in the scene. A restoredcharacter has quest progress, so
World.OnLoginplaces quest markers,Instantiatethrows out of the login handler, and the client sits on "LoadingWorld" for ever — with nothing in any log to say why. This one cost us an
evening. A missing prefab now skips the marker.
Nakama — the plugin does not load at all
14. Against a current Nakama,
InitModulefails and the whole runtime refusesto start: no game server can register, no client can find one.
registerStorageIndexgained asortableFieldsparameter beforemaxEntries,so the numeric
maxEntrieslands in the array position and the runtime rejectsit with "expects an array of strings".
storageIndexListnow returns a result object with anobjectsarray ratherthan a bare array.
Also:
register_serverrecordsctx.clientIp, and a server reaching Nakama throughanything that rewrites the source address gets stored under an address no client can
reach — with no way to correct it.
GameServer.inidocuments anip_addresssetting, but the server never sends one and the plugin never reads one. We added a
SERVER_IP_OVERRIDEruntime variable, falling back toctx.clientIpas before.Issue #3: items left lying in a vessel
Solved on the server. Not solved on the client — and we want to be exact about
which is which.
An item a player drops belongs to no vessel and to nobody's inventory: it rides on
its own
Pivot. Nothing inPersistencecould reach it, so every dropped item waslost on shutdown. It is now recorded — which vessel it was released in, and where —
and put back on a pivot in that vessel on load.
Verified, repeatedly: the item is written to the save, survives a restart, is
restored, and is saved again. We watched the counts in the save files across
restarts.
What does not work: a joining client is told about these items but does not draw
them. The server thinks the item is ~0.2 m from the station's centre; the client
draws it ~25 m away, next to where it was dropped. Both are "right" in their own
frame, and nothing converts between them.
That is issue #1 — "fully embrace two coordinate space types" — in your own
words. We do not think it can be patched around, and we stopped rather than paper
over it.
What we tried and abandoned
We are listing these so nobody repeats them.
Parenting the dropped item to the vessel instead of a pivot. The item vanished
within a second. There is no representation for an item lying loose at an arbitrary
position inside a vessel: of 6983 item records in a world, every single one is
on an attach point. Only five had a free position, and those were ours.
Sending the item as a child of the vessel at spawn time. The client places it
by
transform.localPositionunder that vessel, which is a different frame from theone the drop was measured in. Consistently ~25 m out.
Having the client fall back to the player's vessel when it cannot resolve the
pivot. It resolves, and the item lands at the station's origin.
Rebuilding the pivot on each join. This worked — 1.4 m from the player — but
it leaks an empty artificial body per join into the solar system's body list, which
is walked for every movement message; it makes the item jump for anyone already
watching it; and two players joining at once both rebuild the same pivot with no
locking. We took it out.
Pinning the pivot's orbit to the vessel every movement message. The pivot lands
at the station's centre, because the item's own offset lives in a frame we cannot
read.
Deriving the drop position from the server's own data. We tried the client's
attach position (a client-side root), the player's world position (returns the
vessel's own), and the player's
LocalPosition(a different small-scale frame).None of them is the item's offset within the station. The server does not know
where a dropped item is, in any frame it can reason about — dropped items are
client-authoritative.
That last line is the real finding, and it is why we stopped.
The branches
beleata/Serverfix/persistence-not-savedbeleata/Clientfix/loading-a-saved-worldbeleata/Nakamafix/load-against-current-nakamaTwo commits on the server: the first restores saving and the crashes it exposed,
the second the five process-killers and the dropped-item persistence.
Take whatever is useful, in whatever shape suits you — squash it, rewrite it,
or just take the diagnosis and write it yourself. We are glad the game is still
alive, and we would rather it stayed that way than get credited for anything.
Two smaller things we did not touch
OrbitParameters.cs:71has aDebug.LogFormatinside a property getter that isread constantly. It produced ~16,000 log lines in two minutes while a world
loaded.
-shutdowncannot start while a server is running from the same directory:Debug.Initialize()holdsoutput_log.txtexclusively, so the shutdown helperdies before it can send the message.
One question about the repository itself
Serverhas no licence file, whileClientandNakamaare GPL-3.0. That leavescontributions there in an awkward spot. Would you consider adding one?