← Back to Dev Blog
Dev Blog #03

Dev Blog #03 — Every Round Was Survival, and Nobody Knew

·The person building SOLREIGN

I want to talk about a bug that never threw an error, never showed up in a log, and never once looked like a bug from the inside of a round. It just made the station a little less itself, shift after shift, and nobody — including me — noticed, because the thing it broke doesn't announce itself. It just quietly doesn't happen.

Here's the shape of it: a two- or three-person shift, the kind SOLREIGN runs a lot of, almost always rolled Survival. Meteors, some atmo trouble, a slow calm shift. Rarely a traitor. Never the chaos preset. I'd chalked it up to small-crew math — of course a handful of people mostly gets the calm mode, that's just what low pop does. Except it wasn't math. It was a wire that was never connected.

How the roll actually works

SOLREIGN, like upstream, decides the round type through Secret — a weighted table of presets that gets rolled at round start. The roll isn't a flat dice throw, though. SecretRuleSystem.TryPickPreset picks a preset from the table, checks whether the current player count clears that preset's GameRule.minPlayers gate, and if it doesn't, throws that pick out and rolls again from what's left. It keeps doing that until something clears the bar. It's a sensible fail-safe — you never want a preset with a 20-player minimum stalling the server because it got picked at pop 3.

The problem is what's in the table it's rolling from, and what those gates actually are.

Upstream's Secret table (Resources/Prototypes/secret_weights.yml) weights Nukeops at 0.20, Traitor at 0.55, Zombie at 0.05, Survival at 0.10, and Revolutionary at 0.05. Reasonable numbers — for the game upstream is tuned for. But look at the gates each of those presets carries in roundstart.yml: Nukeops needs 20 players. Zombie needs 20. Revolutionary needs 15. Traitor needs 5. Survival needs nobody at all.

Run that through a 2-4 person shift and only one entry survives every single re-roll: Survival. Not "usually Survival." Not "weighted toward Survival." Always Survival, deterministically, because everything else in the table is mathematically excluded before the dice even matter. The roll isn't broken. It's working exactly as designed — it's just being asked to choose from a table where four of five doors are locked at our numbers.

The part that actually stings

Here's what makes this one different from a normal missing-feature story: we'd already fixed it. Or rather, I had — I just never turned it on.

Sitting in Resources/Prototypes/_Solreign/GameRules/secret_solreign.yml is SolreignSecret, a second weighted table built specifically for shifts this size. It drops Nukeops, Zombie, and Revolutionary entirely — dead weight at our population, nothing but re-rolls — and replaces them with presets that actually clear the bar: Traitor at 0.35, SolreignAntagsOnSpawn (the chaos preset, antags seated before anyone even spawns) at 0.35 with a minimum of 3, Survival at 0.15 as a legitimate calm option rather than the only option, Kessler Syndrome at 0.10 with no antag minimum at all, and a small 0.05 sliver for the Changeling content pack once it's had a few rounds to prove itself. Every number in that table was picked, argued with myself over, and play-tested specifically against a 3-8 person shift.

The table works. It was never broken. The problem is that which table gets rolled isn't decided by which one exists in the prototypes folder — it's decided by a server config value, game.secret_weight_prototype, and that CVar defaults to the literal string "Secret". Upstream's table. Our tuned one doesn't get touched unless something in the server config explicitly points at it by name.

Nothing did. I built the fix and then never wired it to anything. The station has been running on the stock upstream odds since the day SolreignSecret was written, quietly re-rolling itself down to the one preset that was always going to clear the gate anyway.

Why nothing caught it

This is the genuinely humbling part. There's no exception here — TryPickPreset doesn't error when it excludes four options in a row, it just logs an info line and moves on, because from the code's point of view nothing went wrong. A round started. A preset was selected, legally, from the table it was told to use. Every system downstream of that decision — the crew manifest, the antag selection, the event scheduler — did exactly what it was supposed to do with a Survival round. There was no crash to chase, no red text in the console, no failed assertion. Just a station that quietly never showed you the other four doors, because you never asked it to check the room where they're kept.

The fix, once you've actually found the mismatch, is one line in the server config — the same line the file's own header comment has been describing since the day I wrote it: under [game], set secret_weight_prototype = "SolreignSecret".

That's the whole change. No new code, no migration, nothing to test beyond what SolreignSecret already went through before it was written. It's a config value pointing at a name instead of a config value pointing at the default. It went out with this release, and I pulled the live config afterward just to watch the value actually read back as SolreignSecret instead of taking it on faith. A small shift stops being a coin that only has one side.

What it teaches

If you're running your own SS14 fork and you've built a tuned weightedRandom table for anything gated by minPlayers — round presets, event tables, whatever — the deployment isn't done when the prototype validates and the tests go green. It's done when you've confirmed, on the actual live config, which CVar is reading which table. A prototype file sitting in the repo doesn't do anything by itself; it's inert until something points a string at its ID. Grep your own live config for the CVar name before you assume your own work shipped. I didn't, for longer than I want to admit.

And if a game mode on your server feels suspiciously uniform at a particular population — not "usually the same," but always the same — don't assume that's just how the math shakes out. Go read the actual weights and the actual gates together. Sometimes the station isn't telling you what it thinks. It's just rolling from a table you forgot to hand it.

— See you on shift.

← All Dev Blog entries