Fallout 2 Dedicated Server (co-op) v0.3
Third release, and it has two halves.
The first half is whole systems that were not broken — they were never wired. v0.2 made
every player their own character. v0.3 went looking for what a co-op party still could not
do, and the answer was uncomfortable: the game has a set of hooks the server never
overrode, and each unanswered hook read as "the player cancelled" rather than as an error.
No crash, no log, nothing on screen. That is why elevators, the town map, stealing and
sneaking had all been quietly absent for months. Elevators alone locked the party out of
Sierra, Navarro, the Military Base, Vault City's vault, Vault 15, the Shi Temple and the
Wanamingo Mine.
The second half came out of playing it, and it has its own shape: the server was right and
the screen never heard about it. A value was computed correctly on the authority and then
lost, ignored, or overwritten on the way to a display — so the logs looked clean while the
player saw nothing. Damage that landed for zero. A perk you spent that was rolled back when
you closed the sheet. A save that was fine and refused to load. Proving a value reached the
wire proves nothing about what you see.
Systems that now exist in co-op
- Elevators. ~15 live elevators in the game and none of them worked. Your client draws
the real panel; the server resolves the button against its own destination table and the
whole party rides, as an instant transition. - Resting. No way to pass time or heal on purpose, which also blocked every "come back
in N days" quest. Resting is a group act — one clock, so the night passes for everyone,
every player heals at their own rate, and the others are told who did it. - The town map. Co-op could reach a city but always arrived at its first map, so the
district picker every city has (Vault City courtyard vs downtown, the Den's two halves,
New Reno's four blocks) did not exist anywhere in the game. Now you pick your gate, and
the arrival spot rides with it. Only the districts your party has actually discovered are
offered. - Talking to each other. Press ENTER out of combat, type, ENTER sends. The line floats
over your head for everyone and lands in every message log. Built as a non-blocking
overlay rather than a modal, so it structurally cannot starve the wire, and it takes first
refusal on every key while open — typing no longer toggles combat or swaps your weapon
hands mid-sentence. - Stealing, pickpocketing and planting. The server owns the session and rolls the dice
— your Steal skill, Pickpocket, the victim's Perception, vanilla's per-item difficulty
ramp, the same 10/20/30 XP ladder, the same caught-ends-it rule. Every player opens the
same screen anchored on the thief, so the party watches the robbery instead of guessing.
Planting a live explosive in somebody's pocket works again. - Sneaking. Per-player, on key
1or the skilldex. One player's blown roll no longer
un-hides another, the AI's perception test asks about the target it is actually looking
at, and Silent Running / Silent Death are per-player — both were held back by the same
shared flag. - Spending a level-up. Every co-op player was a viewer of their own character sheet:
the screen opened and then rolled your edits back, so nobody — host included — could put
a point in a skill or take a perk. Now the +/- buttons, the perk dialog, Tag! and Mutate!
send intents the server validates and streams back. And the thing that made it moot:
nobody was ever awarded anything to spend, because vanilla hands out skill points on
the character screen and a dedicated server has no screen. That award moved to where the
XP is credited, per player, and an owed perk pick is now per-player instead of a single
one the first player to open their sheet consumed for everybody. - Cutscene movies play. They had been finished and shipped off — the environment
variable defaulted to disabled, so nobody saw a cutscene unless they knew it existed.
On by default now, projected to every player, with the first acknowledgement releasing
the server. (F2_MOVIES=0remains as a kill switch, because a viewer stuck on a black
screen parks the server in a barrier that the operator's command channel cannot release.) - The pipboy.
Pand the PIP button were both dead on a connected player (the viewer
runs its own key dispatch and had no branch for them) — one missing branch, two dead
controls, and everything behind them unreachable: holodisks, quests, status, automaps.
Also server-authored holodisks, with the first one shipped: SERVER INFORMATION —
name, platform, seats, and the crew with host and online markers, read in your own pipboy
instead of scrolled back through chat. - The pipboy MAPS tab records maps. Nothing had ever told a connected player to record
one, so the list was empty for every map in the game. - The Motion Sensor is an ordinary carryable item, and using one ended everybody's
session — it opens the automap, which was an aborting stub on the server. It now opens
on the screen of whoever waved it.
The other players are people standing there
Extra players are not in the party list, so every "how good is the party at X" question
answered as if they were not present:
- an extra's Barter was worth exactly nothing — they paid host prices;
- an extra's Outdoorsman never avoided an encounter, and a scanner in their pack did
nothing; - the party count under-reported, so a four-player group walked through doors meant for
two, the worldmap sized encounters for a loner, and the pipboy withheld
rest-until-the-party-is-healed; - an extra's own skill use was substituted by whichever companion was best, even when
the extra was the best in the group.
With one deliberate exception, and it matters: players do not count toward a party
size a script can refuse you on. Vault City, the Sierra doors, escort quests — "leave
some of your friends outside" — would each become a wall, and the game stops being playable
at three seats. A truthful count that locks the party out of a map is worse than vanilla's
undercount. Sizing and offering: players count. Refusing: they do not.
There is a matching version of the same mistake on the display side: every viewer showed
the host's perks as its own. The lookup short-circuited on "is this the player character"
before consulting the registry — and on a client that test is true for your actor, so it
read seat 0's row while your own row sat somewhere nothing local ever looked. Level, XP and
SPECIAL were right, which is exactly why only perks looked wrong.
Also finally reaching the right person:
- Empathy. The perk's whole visible effect is colour-coding dialog options by how the
listener will take them — and the client was feeding the renderer a hardcoded NEUTRAL for
every option, so anyone who spent a perk on Empathy saw a grey list and correctly
concluded it did nothing. - The combat log is written for you. "You were damaged for 12" was one line written for
player one and mailed to everybody, so a hit on the host told every player they had been
hit. The narration is now rendered once per reader, so "you" means you. Plus a header
line vanilla structurally cannot carry, because in single-player there is only one thing
swinging at you: "Sulik throws the Spear at you." (F2_NO_ATTACK_HEADER=1to turn it
off.) - Looking at something told the whole server. Every description any player read printed
on everyone's screen — and with Awareness that line carries the target's exact hit points
and weapon, so one player's perk was quietly leaking to seats that had not earned it. - Level-ups, XP, radiation warnings, geiger clicks and the poison line are addressed to
the player they happened to. All were second-person and broadcast, so everyone read that
they were the one vomiting. - Screen fades work at all. The server has emitted them since the beginning and the
client had no case for them, so a script's fade — reading a book, a Doctor session,
digging a grave — did nothing. They are now decoded, addressed to the one player whose
time is passing, ordered around the thing they are supposed to hide, and bounded: a
fade-out whose fade-in never arrives can no longer leave you permanently black. - A scripted cutscene locks input again. The two opcodes a script uses to take the
controls away were no-ops on a dedicated server and streamed nothing, so players could
simply walk out of a scene while it played. - The camera unlock is on. It was built and shipped default-off, which means it did not
exist. (F2_UNLOCK_CAMERA=0to leash it again.)
Combat
- Every attack in the first turn of every fight landed for exactly zero damage. Reported
for a long time as "no damage, no damage, no damage" and then it starts working — which is
why it survived: anyone who checks once a fight is underway sees correct numbers. Our
combat start synthesises a zero-initialised copy of vanilla's scripted-attack parameter
block purely to seat initiative, and in that block a maximum damage of zero means a cap
of zero, applied to every attack until the block clears after the first turn. A punch
computing 2 and a shotgun computing 21 both landed for 0. Vanilla never reaches it because
that block only exists for a scripted attack, which fills it in deliberately. - Your critically-destroyed weapon stayed in your hands, and stayed reloadable. The
narration was never lying — it prints the computed outcome — but the only code that
applies a destroyed or dropped weapon hangs off animation callbacks a dedicated server
does not run, and only on the recorded-attack path, so the result was mode-dependent even
in principle. Applied where every attack goes through instead. The same fix covers the
exploded-weapon critical, and the sprite that went on holding the gun after it was gone. - Stationary things walked. A spore plant never moves in vanilla, and no rule anywhere
says so — its art simply has no walk frames, the animation refuses to register, and the
AI gives up on the turn. Our server could not fail a sequence for any reason, so it
walked them; the client, which does check the art, refused to animate, and the sprite
snapped to the server's tile. That is the teleport. Six of the game's 96 critter art sets
are stationary props implemented as critters (spore plants, both gun turrets, the deathclaw
egg) and we were walking all of them. Critters with no run art are also walked at walk
pace now instead of running on the server and walking on your screen. - "I couldn't shoot one of the deathclaws and there was no reason why." Three different
failures printed the same sentence about the target's type, including the one that matters
— the client aimed at an object this server does not have, which is the stale-mirror
case and is invisible from a message blaming the target. It now names which case it is and
what the server actually sees there. - NPCs attacked with an empty-handed body while holding a weapon, and a knocked-down
critter was never seen getting up. - Opening your inventory in combat could cost you 4 AP for a turn nobody had announced
yet — the previous fighter's shots were still playing out. Open, close, open and you
arrived at your own turn wondering where your AP went. The AP dots also went stale after
equipping armour or a launcher mid-fight.
The worldmap, and trips that ended nowhere
This was one bug wearing five faces. Vanilla's worldmap has exactly one exit — enter a
location — so a map load always follows it and rebuilds whatever leaving tore down. Ours has
several exits that lead nowhere in particular, and every one of them left the world in a
state nothing was written to handle.
- The map you walked out of was dismantled for a trip that went nowhere. Press ESC on
the worldmap and, depending on the map, either every object was gone while viewers kept
drawing a world the server no longer had (the "black map" report), or the objects survived
with every script on the map deleted — solid, visible, completely inert, and you could
walk. Measured on a spore-plant encounter: 26 scripted critters before, 1 after. Behind
that ESC key sat a deterministic server crash, because the one script that survives the
purge keeps a pointer to memory the purge freed. Only the reversible half of leaving runs
at worldmap entry now. - The second worldmap trip crashed the client, for the same reason, on the client's own
copy of that teardown: trip one freed the scripts, trip two ran the corpse. - A cancelled trip left the world saying you were somewhere else. Leave Vault City,
travel toward NCR, stop, press ESC — you are standing in Vault City with your marker on
NCR, and that marker is where the next exit grid drops you. Same for the in-a-car flag,
which left the party driving while on foot. Both are undone now, and the undo is keyed on
the trip having ended nowhere rather than on the ESC key, so the much worse version — the
last client dropping mid-trip — is covered by the same code. - ESC on the worldmap now asks first, naming where it would put you, because that
keypress is not a local close: it cancels the session the whole server is parked on. - Two ways the client cancelled the trip with nobody touching a key. The viewer
force-closes its screens when a fight starts — correct for every screen except this one,
where ESC is an intent — and it read that from a combat flag that is legitimately stale at
that exact moment. The other one was a hard softlock: after any arrival the viewer injected
ESC every single frame, forever, because the state it was waiting to apply could not apply
until the screen closed and the screen would not close. Observed as the server logging
ignored worldmap-cancel intents in a loop while the player pressed nothing. - One player quitting mid-trip no longer drags the party back. Verified rather than
changed, and worth stating because the predicate that decides it does not mean what its
name says. - Entering a random encounter said nothing. Vanilla names the encounter you walked into;
co-op printed nothing, behind two independent gates — console lines are suppressed during a
map load, and the client then discards events during the whole arrival window. - The encounter accept/decline prompt declined itself in about 16 milliseconds. It opened
from inside the network pump; the box's own input loop pumps again mid-frame, misreads the
half-consumed frame as "the server is gone", and answers with an ESC into the box it is
standing in. Two neighbouring handlers already carried a comment saying not to do this. - A viewer staged its own duplicate ambush. Arriving at an encounter, the client re-ran
the spawn the server had already run and put down a second set of hostiles with no network
identity, which no verb could address and no server had heard of.
Saves and checkpoints
- Every save you made while playing alone was unloadable, and nothing said why. Five of
eight slots on disk, including the autosave. A solo save deliberately writes no co-op
appendix so it stays byte-for-byte a vanilla save, and the loader's "clean end of file
means the appendix is simply absent" probe was unreachable: the read it tested can never
report failure at end of file, so it returned success with uninitialised stack garbage, the
magic number read as nonsense and the loader refused the whole file as corrupt. The saves
were always fine. They load now — including the ones v0.2 already refused. - The server now says why a boot failed. The dedicated server installs no debug output
handler, so every diagnostic on the load path was dropped and a failed boot printed one
useless line. An operator whose save will not load had nothing to act on. - One autosave slot, overwritten at the worst possible moment. The autosave fires on
every map change and wrote to the same slot in place, forever — so walking into a random
encounter replaced the only checkpoint in existence with a mid-encounter save on a
transient map, hostiles already inside perception range, and nothing older to go back to.
One bad checkpoint could eat a session. It rotates through slots 11-15 now, still past
everything the load and save screens enumerate, and both the announcement and the log
name the slot, because a rotating window is useless if you have to guess which of five
to reload.
Things you clicked that did nothing
- A shovel, a stimpak or any non-weapon could not be put in your hand at all — the
wield gate misread a union field and refused the art. Then, with it finally in hand,
using it started a fight, because the hand slot has three jobs and only one was wired.
A pair of boots in the weapon slot drew on the room. - Closing your character sheet threw away every point you had just spent. Vanilla ends
that screen by restoring its open-time snapshot on cancel — and cancel is ESC, orC
again, or anything except Done. The snapshot rewrites skills, perks, tags, traits and the
owed pick together, so it could not lose part of a level-up, which is why relogging or
changing map looked like the cure: those re-seeded your row from the server. - A level-up worth three levels granted one perk. The owed pick was a yes/no flag, so
crossing levels 3, 6 and 9 in a single award owed you exactly one. It counts now. (Vanilla
loses them too — but vanilla awards lazily on the character screen, and co-op hands out XP
in lumps.) - A plot teleport moved one player and stranded the rest. In single-player the engine,
not the script, keeps the group together; co-op player actors deliberately are not party
members, so a script that relocates the player character relocated exactly the one who
triggered it. Now it moves everyone. Checked against all 123 relocation sites in the
shipped scripts: the risk is asymmetric — fanning out wrongly leaves a body somewhere odd,
not fanning out leaves a player outside the game. - A same-floor script teleport left the camera behind, looking at where you used to be.
- Clicking a container or a dug grave did nothing, twenty times in a row, with an empty
log — the server fired the interaction and the client independently re-judged whether you
were adjacent yet. Two judges, one question. The server now says the screen is open. - Dialog options past the fold were unreachable. The panel is a fixed size and the
renderer silently drops any option that does not fit — no button, no hotkey, no text.
Co-op crowds dialogs in a way the base game never does. PageUp/PageDown and the mouse
wheel now scroll the option list. Separately: the tenth option was dead on click, at
any time, which is exactly the case crowded dialogs hit. - Inspect in the trade screen. Right-click to switch HAND↔ARROW and read an item
description while bartering, which was impossible because we had torn down the window it
draws into (and then crashed on it). - The Steal hotkey. Every digit 2-8 worked;
3did nothing. - The walk it refused for no reason. No distance check anywhere — the pathfinder gives
up after expanding 2000 tiles, and a route feeling its way around scenery on a big map
exhausts that while the destination is plainly walkable. A player's own walk now gets a
much wider budget; AI reachability, script movement and followers keep vanilla's, because
those are the simulation. - The world keeps moving behind an open inventory, instead of freezing and then snapping
when you close it. That was our regression, not a missing feature — vanilla animated the
world behind its modals, and the dedicated server never stops the simulation. - Missing gunshot sounds. The mixer would spend at most four slots on effects; vanilla's
combat is strictly one animation at a time so it never noticed, but a paced co-op turn
fires a dozen cues together and the ones that found no slot were simply silent — usually
the shot, because the shot came last. Raised to 16. - Your character faces the door before reaching for it. The approach was not recorded,
so the gesture played on whatever facing the walk ended with — reaching sideways through
a wall. - The dialog REVIEW button opened a blank box (a viewer never has the history — the
script runs on the server) and stalled the connection while it was up. It is a no-op now.
Stability
Zero paths in the enumerated server-severance surface can abort the process any more. The
ones that were left were all "the server dies at the worst possible moment":
- Radiation death took the whole server down — and that is ordinary Fallout 2 (the Toxic
Caves goo, the Gecko reactor, Broken Hills). One player letting their rad counter run out
ended everyone's session. - The Motion Sensor (above).
- Winning the game. The endgame movie was an abort stub, so victory killed the server.
Winning is survivable now; watching the slideshow is still a feature. float_msg(obj, "", type)— vanilla's idiom for clearing a floating message, and
one of the most-used script opcodes in the game.- A mod script could kill the server by asking a cosmetic question. Thirteen client-only
routines are reachable from script — screen size, cursor position, key state — and each
aborted. They answer the honest headless truth now, and log once so it stays diagnosable. - A wrong working directory printed a fatal internal symbol name instead of "Could not find
the master datafile".
Plus: a use-after-free with an exact repro (throw a rock, pick it back up, press I) —
a network id was reassigned and the pickup's cleanup freed whatever answered to it. And
mid-fight, items you had thrown stayed listed in your own inventory as phantoms the server
then refused verbs for. A duplicate login is now refused and kicked with a clear line on
both sides, instead of leaving a connected client that receives the whole world and can
drive nothing; the incumbent is untouched.
The client that looked like it crashed
Three separate ways to lose your client with no crash, no core file and nothing in the log —
which is exactly what made them look like crashes:
- One key ended the session. The frame loop quit on a bare ESC, no confirmation, no way
back. What makes that untenable in co-op is that ESC is also the get-me-out-of-this-screen
key, and a streamed screen does not close on the keypress — it sends an intent and waits
for the server. So the natural thing a player does, press ESC until something happens,
closes the screen and then puts the next buffered ESC into the frame loop. One live log had
twelve of them. It asks now, the way vanilla's own exit does. - Walk off the edge of a map during a fight and the client was simply gone. The viewer
scanned the tile it arrived on, found the exit grid, and latched its own map transition —
the authority's job. That code path opens by setting the engine's "get out of the loop you
are in" flag when combat is running, which vanilla consumes again a moment later; a viewer
has no such loop, so the flag lived on and ended the process. The identical bug had already
been fixed sixty lines further down, on the symptom somebody happened to notice. The
general question, now written down: while your player character is a mirror, is this
branch observing it or deciding for it? Deciding belongs to the authority. The
viewer also swallows a stray in-band flag now and says so loudly instead of dying. - The worldmap ESC loop under "The worldmap" above.
Things the world now tells you about
- Global variables were never on the wire. A connected player's copy was whatever the
join blob carried and then it froze — so the pipboy's holodisk list, the quest list, and
the character screen's karma, reputation and addictions were stale until the next
rebaseline. Pick up a holodisk mid-session and it never appeared. - An object can become a different thing. A script can swap an object's prototype in
place, and the engine does it itself when you arm an explosive. The new art streamed but
every question that matters — name, item type, weight, can-I-use-this — still resolved
the old prototype. That is the most confusing shape a desync takes: it looks right and
answers wrong, and it "fixes itself" on a map reload. - The car trunk went permanently invisible the first time you opened it — and that is
vanilla's bug, faithfully mirrored to every client by v0.2's art streaming. The trunk's art
has exactly one frame, and opening a container assigns frame 1 directly, bypassing the
bounds check that would have refused it. An out-of-range frame draws nothing, so the trunk
became invisible and unclickable — and the frame is saved, so the damage rode every save
forever. A one-frame container stays on frame 0 now, andfixcarrepairs a save that
already has it. - The steal screen offered you things the server had already put away. Vanilla detaches
the victim's equipped weapon and armour before the screen opens; the panel went on drawing
the pre-detach view, so clicking a pistol that was never there got you "That item is gone." - Under the hood: the gates were checking two fields. The reconstructed object in our
replay tooling had exactlytileandelev, so "the live stream is validated" meant
position and nothing else — a stream could be wrong about an object's prototype, art
frame, flags, hit points or entire inventory and every gate stayed green. That is why the
bugs above were all found by a person clicking something and getting nothing. The server
can now be asked for its own view of every object (audit), and both a live client and
the replay tool diff the full field set against it.
For operators
- Every feature is ON by default. A dedicated server needed six opt-in environment
variables to reach a playable state, which made the shipped default a crippled server.
Quick start is now a map, two ports and a pace; everything else is a kill switch
(F2_SOMETHING=0). See DEDICATED_HOWTO.md §1. The presentation pace itself now defaults to
the production value instead of zero, for the same reason. fixcarreports and repairs a Highwayman lost to the vanilla car bug: where the engine
thinks the car is parked, whether the trunk is present, off-map or gone, and it can park the
car at a named area or where you stand. It also says what it cannot fix — a trunk already
dropped from the party roster was never written to the save, so only an older save has its
contents.xp <slot> <amount>grants experience to a seat, levelling it as many times as the
amount crosses thresholds.encnextarms one detected random encounter on the next travel check, so the
accept/decline prompt is reachable without waiting on two dice. One-shot, and it keeps the
detection roll and the prompt — it is not a forced encounter.crithit <slot>/critfail <slot> [0-4]arm one seat's next attack roll, one-shot, so
criticals are reproducible. The severity pin is the useful part: the effect is a second
roll that high Luck makes rare, and only the worst column carries the weapon explosion, so
without pinning it the interesting outcomes are effectively unreachable. Two gameplay
notes: nothing fires in the first six in-game days, because vanilla suppresses player
critical failures there, and severity 3 has no consumer anywhere in the engine.partyreports both party numbers side by side (the vanilla follower count and the
real group size), since the difference is the load-bearing part.stress Nspawns hostiles that can actually reach you, and armed. Spawns only needed
to be on a non-blocking tile, so they landed off-map or in sealed rooms; and a critter
built from its prototype carries no inventory, so every stress hostile punched. Each
candidate tile now has to path to a player, and each hostile gets a random weapon its
body can actually hold, plus ammunition for it.- No more
/tmpscratch files. The join blob and the per-player sheet delta serialized
to disk and read straight back on every baseline and every stat change. Both are in-memory
now, and the client's scratch-file setting turned out to have been dead for some time —
removed, along with the two-clients recipe in the HOWTO that depended on it. - An object leaving the world is traceable. The disconnect path emitted nothing at all,
while its opposite had been traced since it was written — and it is the more consequential
of the two, because on a viewer it destroys the mirror outright. "The item vanished and I
do not know who took it" was unanswerable from a server log.
Where this leaves us
A coverage sweep this cycle put roughly 75% of the feature inventory built, but only
~60-65% of a real Temple→Enclave co-op playthrough reachable — and the gap between
those two numbers was the finding, because four of the absent entries were gates rather
than features. Three of them (elevators, the town map, the endgame abort) are closed in this
release; the fourth, rejoin corruption, is not. A feature count cannot see that one dead
level-picker outweighs twenty working perks.
Known issues
- A full party wipe still soft-locks. Reviving needs a living player to use a healing
item on a downed one, so if everyone is down there is no in-game way back. Load a save. - Rejoining a game in progress can still corrupt state, and an occasional crash right
after a map transition (roughly one in ten) is still open. - A rejoining player's wielded weapon is sometimes not re-wielded (intermittent; armour
survives). The weapon is correct in the save — it is lost in the client's reconstruction. - You can win but not watch it. The endgame slideshow is not streamed yet.
- No chat in combat. ENTER already means "attempt to end combat" there, and that binding
is load-bearing. A separate key would buy it. - Addiction display is shared — it reads nine shared globals, so a per-player marker
would light for everyone and never clear. - Dead players' bodies follow the group to whatever map the living arrive at.
- Getting caught stealing produces no visible reaction from the victim. This may simply
be vanilla; it has not been proven either way. - An area-of-effect kill can narrate "killed for no damage." The kill line is real; the
damage figure beside it is reading live state instead of the computed result. - Long fights can still reach a state where ending your turn does not take, and gibbed
corpses settle to the wrong frame and cannot be looted. Walk and run animations can also
drift out of step with the server. - A lot of this release is UI that no automated gate can see (the character screen's +/-
buttons and perk dialog, the elevator panel, the town map, dialog option scrolling, the
pipboy, chat). It is code-verified and smoke-run, and reports from real sessions are the
thing that closes it out.
Upgrading
Saves from v0.2 load — including the solo saves v0.2 itself refused to load, which were
never actually damaged. The per-player sheet block grew a field (v1 blocks are still read),
so a v0.3 save is not expected to load on v0.2 — keep a backup before switching versions, as
always.
Note for anyone who scripted around the old behaviour: the autosave now rotates through
slots 11-15 rather than always writing slot 11.