Lessons from shipping a 160,000-line solo C# game across Steam, SSH, web browsers, and 1993 BBS systems
I just shipped Usurper Reborn, a text-based multiplayer RPG. It's a remake of a 1993 BBS door game, and the final product runs on Steam with a bundled terminal, in web browsers via WebSocket, over SSH as a MUD, and as a door game on actual BBS software from the 90s. Same binary, same codebase. 160,000 lines of C#, two years of solo dev work.
I wanted to share some of the technical and design problems I ran into that I haven't seen talked about much, in case any of it is useful.
One binary, four completely different I/O paths
The game has to work across wildly different transport layers:
- Steam/local: Bundled WezTerm terminal emulator, full Unicode, 256-color ANSI, mouse support doesn't matter because it's keyboard driven but the terminal handles everything
- Web: nginx reverse proxy to a Node.js WebSocket server that bridges to an SSH connection to the game process. The browser runs xterm.js
- SSH: Players connect directly. The game gets a PTY with echo handling. Screen readers work here
- BBS door mode: The game reads a DOOR32.SYS drop file from 1993 that contains a raw TCP socket handle. You write bytes directly to that handle. CP437 encoding, no Unicode, 80x24 terminal assumed
The nightmare case was SSH on BBS systems. A BBS passes you a raw TCP socket handle, but if that BBS is using SSH internally, writing to the raw socket bypasses encryption entirely. The SSH client gets corrupted bytes and disconnects with "Bad packet length." The fix was detecting when stdin/stdout are redirected (meaning the BBS is handling transport) and switching to stdio mode instead of socket mode. Sounds obvious in retrospect but it took days of debugging on hardware I couldn't reproduce locally.
Every output function in the game goes through a terminal abstraction layer, but that abstraction has to handle things like: BBS terminals need CP437 encoding. Web terminals handle their own echo, so the server must not echo keystrokes. Raw TCP MUD clients need the server to echo. ANSI color codes 90-97 (bright colors) don't work on BBS terminals, so those get remapped to bold+color. "Press any key" has to work across all four input methods.
The NPC simulation is the game's best feature and its biggest headache
The game has 60+ NPCs that simulate autonomously. They move between locations, form relationships, join factions, fall in love, get married, have affairs, betray each other, murder each other, and die permanently. All of this happens while players are offline.
The simulation creates genuinely interesting emergent stories. A faction leader gets murdered by a jealous NPC, the faction fractures, three NPCs defect to a rival group, and by the time the player logs in the political landscape has shifted. Players get attached to NPCs and are actually upset when they die.
The problem is persistence in a multiplayer environment. The NPC state is the authoritative world state. Player saves store player data, but the world state (NPCs, king, economy, factions) lives in a shared SQLite table. When a player logs in, their save might have stale NPC data from when they last played. The game has to reconcile player saves against the current world state on every login. Things like: the player's save says they're married to an NPC who has since been murdered by another player. The player's save says they work for a faction that was dissolved. The player's save says someone is king who was overthrown three days ago.
Every piece of shared state needs a clear authority. I spent weeks chasing bugs where two systems both thought they were authoritative for the same data.
Accessibility was easier than expected and harder than expected
A blind player connected over SSH with a screen reader and couldn't play at all. Box-drawing characters, ANSI art, Unicode decorations, progress bars. All garbage through a screen reader.
The easy part: adding a ScreenReaderMode flag and branching every display function. Menus go from [X] Label to X. Label. Progress bars become numbers. ANSI art rendering returns immediately. That took a day.
The hard part: the online multiplayer broadcasts messages to grouped players, and each player might have different display preferences. A combat broadcast to a group of 4 needs to be sanitized per recipient. That meant adding a ScreenReaderMode property to each player session and running every broadcast through a per-player sanitizer. The combat system alone had about 30 display paths that needed dual rendering.
The unexpected benefit: the plain text mode is now used by sighted players on mobile SSH clients because it's more compact. The BBS-compatible layout that came from this work became a toggle anyone can use.
Things I'd do differently
- Start with the multiplayer architecture. I built single-player first and bolted multiplayer on. The amount of refactoring to make everything session-safe (AsyncLocal for per-player state, thread-safe world access, save reconciliation) was massive. If I'd designed for multiplayer from day one, half the bugs in my changelog wouldn't exist.
- Don't use static singletons for game systems.
QuestSystem.Instance,NPCSpawnSystem.Instance, etc. Works fine in single-player. In multiplayer with concurrent sessions, every singleton becomes a potential race condition. I've been slowly migrating to per-session instances but it's painful. - Write the terminal abstraction layer first. I wrote game logic with direct
Console.Writecalls and then had to retrofit everything through the abstraction when BBS support came in. Every place I missed became a bug report from a BBS sysop. - Test with real BBS software early. I didn't try running on actual BBS systems until late in development. The encoding issues, socket handle problems, and timing assumptions would have been caught much earlier.
Numbers
- 160,000 lines of C#
- .NET 8.0 self-contained, single binary per platform
- ~425 automated tests
- 11 base classes, 5 prestige classes, 100 dungeon floors, 7 boss fights, 50+ achievements
- Server runs on a single small AWS instance (t3.small), 512MB heap limit
- Online since January 2026
- Full source is GPL v2
Links
- Steam: https://store.steampowered.com/app/3301370/Usurper_Reborn/
- Play free online: https://usurper-reborn.net
- Source: https://github.com/jknight-developer/usurper-reborn
Happy to answer questions about any of this. The BBS door mode stuff in particular is surprisingly undocumented online considering how many of these games existed in the 90s.
Posted Mar 14, 2026, 6:17 AM UTC