Live, parts in testing
Legacy rescue on a live server
Rebuilding inherited systems on a server that never stops: phones and radios from scratch, every plugin’s data onto MySQL, equipment that can’t lose items, and a trunk migration that moves every player’s storage without losing a thing.
- Role
- Design and implementation, working solo
- Stack
- Lua (GLua), NutScript, MySQL, Python for tooling
- Context
- Diverge Networks, a long-running roleplay server
- Status
- Phones, data layer and equipment live; trunks and speed in testing
The approach
Most of a long-running server is code someone else wrote, code that was never finished, or code written for a server that has since changed. I enjoy that. Sometimes the job is finding the one clean fix without waking all the other monsters. Sometimes it’s turning a hedge maze into a modern apartment.
Either way, players have to keep playing while it happens, and nothing they own can go missing on the way.
Phones and radios, rebuilt from scratch
Live since 2025
The old phone and radio system was unreliable enough that the server asked for a full rewrite: calls that never ended, players stuck in calls that no longer existed, and police channels that could be listened in on.
- Numbers are assigned on first use with a check for numbers already taken, and supporters can pick custom numbers.
- Calls move through explicit states, and unanswered calls close themselves after 30 seconds.
- Leaving a payphone call runs its cleanup in a protected block, so the call is severed and its networked state cleared even if something errors halfway.
- A periodic check finds players marked as being in calls that no longer exist, and releases them.
- Radio channels with encryption and a new voice bubble. 911 frequencies are police-only, and a way around that check was closed.
- Full support for in-game languages on both phones and radios.
Screenshot coming soon
The phone interface while dialling.
assets/projects/phone-radio/phone-dial.png (tight crop)
Video coming soon
One call from dialling to answering to hanging up, with a second client answering.
assets/projects/phone-radio/phone-call.mp4 (about 15 seconds)
Screenshot coming soon
The radio voice bubble while someone speaks.
assets/projects/phone-radio/radio-bubble.png (tight crop)
Moving every plugin’s data onto MySQL
Live since 2025
Plugins saved their data to files on the server. I moved doors first, then every plugin, onto MySQL, with an upload command that brought the old file data across.
The new data layer guarantees each plugin’s reads and writes happen in the order they were made:
- Each plugin gets its own queue, and its database operations run strictly one at a time. A read can never overtake the write before it.
- Every call returns a promise that settles when that particular operation is done.
- The queue moves on after both successes and failures, so one bad query can never stall a plugin.
- Values are cached, so reads only hit the database when asked to refresh.
- Stored data is decoded inside a protected call, so corrupt data is rejected cleanly instead of crashing the caller.
Code excerpt coming soon
The per-plugin queue: enqueue, run the next operation, and advance on success or failure.
assets/excerpts/plugin-data-queue.lua
Equipment that can’t get lost
Live since September 2026
Equipping clothing moves an item out of the inventory grid into a hidden state. The inherited equip code had no safe way back when something went wrong halfway, so items vanished.
I rebuilt equipping and unequipping as one promise-based API used by every item type, with safeguards at each step, and added a check on character load that finds items stuck in that hidden state, puts them back and tells the player what happened.
Code excerpt coming soon
Equipping as one promise chain, and the load-time check that rescues items stuck out of the grid.
assets/excerpts/equip-recovery.lua
Every trunk into one, without losing an item
Built, in testing
Each vehicle used to have its own trunk. They’re being replaced by one shared trunk per character, migrated automatically the first time each character loads after the update.
- Items move one at a time, in sequence, so no two transfers ever race each other.
- If the shared trunk is full, the item goes to a lost-and-found inventory at the car dealer instead of failing.
- If a move fails, the item goes back to its original slot. If even that fails, the server raises an error naming the item.
- The vehicle’s database row only switches to the shared trunk after every item has moved, so a failure halfway through leaves the old trunk as the source of truth.
- Every step is written to a dated migration log.
Video coming soon
A test character with items spread across several vehicle trunks loads in. The shared trunk opens with everything consolidated, then the lost-and-found at the car dealer shows the overflow.
assets/projects/legacy-rescue/trunk-migration.mp4 (20 to 30 seconds)
Screenshot coming soon
The shared 10 by 10 trunk inventory, partly filled.
assets/projects/legacy-rescue/shared-trunk.png (tight crop to the panel)
Code excerpt coming soon
The item transfer with fallback and revert, and the migration that only updates the database once every item has moved.
assets/excerpts/trunk-migration.lua
The trade-off
Removing an item and adding it elsewhere isn’t one database transaction, so a server crash between the two steps could still lose that one item. The revert path and the migration log keep that window small and visible, and it’s an acceptable risk for a one-time migration.
Speed, from last-writer-wins to composable modifiers
Built, in testing
Every system on the server set player speed directly. Carrying a lot of cash, being drunk and being tased all overwrote each other, and nothing restored the right value afterwards.
- Each system sets and clears its own named modifier. Setting the same name again replaces it instead of stacking.
- The final speed is recomputed from a base value and every active modifier: multipliers multiply together, flat adjustments add, and the result never drops below zero. Because order doesn’t matter, removing one effect never disturbs another.
- The API is generated in one loop for all eight movement speeds the engine has, from walking to ladder climbing.
- The engine’s old speed setters still work, but now set the base value and log a warning naming the call site. Plugins that haven’t been converted keep behaving correctly, and the warnings show exactly what’s left to migrate.
Code excerpt coming soon
The generated modifier API, the recompute step, and the legacy setter that makes gradual migration possible.
assets/excerpts/speed-modifiers.lua
Video (optional) coming soon
A player slowed by carrying cash and by being drunk at the same time, then recovering from one without losing the other.
assets/projects/legacy-rescue/speed-stacking.mp4 (about 15 seconds)
Smaller rescues
- Inventory and item syncs that overflowed network buffers, moved to encoded, compressed payloads.
- MySQL 8.0 compatibility.
- A melee rework that untangled the order of combat-logging hooks.
- Item definitions merged into one file with a Python converter, and later split again.