Game
Co-op: the siblings togetherCO
Playing together. One company in the host's save, siblings who join through Steam, everything synced, nobody ever stuck waiting, and the rule every feature follows so solo is just co-op with one sibling.
The same game, with your brother in it. One sibling hosts, the others join from their Steam friends list and walk out of grandma's house next to them. You mow the same lawn, hear each other's engines, fight over the car keys and blame each other for the broken window. Solo is the same game with one sibling: there is no solo-only code path.
The player's view
Getting together. You start or continue your game and wait in the "Who's playing?" room (MN6). Your friend sees "Playing Game Mower" on Steam, clicks Join (or accepts your invite), and their card pops up next to yours. The first time, they make their sibling (SI1) and the car drops them off at the curb with a suitcase (ST5, later). After that, they wake up in their own bed in the siblings' room: they're the sibling on the camping cot, and they always will be.
Playing together. Everything one of you does, the other sees: the lawn you cut turns short on their screen, the stone your mower throws breaks the same window, the phone rings for both, the cutscene plays for everyone and only the host can skip it. Whoever holds grandma's car keys drives; everyone else rides along, and an argument about who gets them is part of the game. Whoever knocks on a client's door starts the job. The day ends when everyone is in bed.
When someone's away. Their sibling sleeps in their bed, day and night, and mumbles if you poke them. When your friend's connection drops, the fuel can they held lands where they stood, the job goes on, and they're back next to you a minute later as the same sibling, with the same pocket money. When the host quits, everyone goes back to the title with a line that says why; the host's save has everything.
It just works. Over a bad connection your sibling still walks smoothly, the lawn never disagrees, and if it ever does, it quietly fixes itself.
Ideas
CO1: one company, the host's
The host's company is the only real one. Its SaveData goes to every guest after each change (NetCompany:
gzipped, in pieces under one packet, CompanyWire). A guest runs its own commands on its copy at once (no wait) and
sends them (type + JSON) to the host, who runs them as that sibling (GameSession.ExecuteFor, the actor recorded on
every ledger line); the guest keeps its copy until the host's state acknowledges them, so nothing flickers.
- The host owns the save. A guest's sibling (name, look, voice, later their pocket money, EC2) lives in the host's save; a guest plays it only when that host hosts. Guests never save; a guest's own save is untouched and comes back when they leave.
- Shared or personal: everything is the company's except what
PlayerDataholds for one sibling (look, voice, pocket, skills). A feature decides which, per field (the golden rule, below). - Secrets: the whole company state goes to every guest. Private things (a sibling's wallet) are hidden by the UI, not the network: fine between siblings, never relied on for anything else.
Needs: - · Done when: built (FishNet prototype, 2026-09-30): a guest's scrub in the house reaches the host's save as that sibling, and the guest's own save is back after leaving.
CO2: one lawn, cut by everyone
Each player cuts their own copy of the lawn (LawnGrid) and sends the cells they changed (CellSync, cellRate);
the host merges them and relays them to the others. MergeCell only ever shortens grass and keeps scalps, so the
copies converge in any order. A guest joining mid-job takes the host's snapshot. Feed and repair seed ride in the
same cells (higher wins); the stripe and band edges too (GR1, CellSync.bandEdge).
Needs: - · Done when: built: two autopilots mow one lawn and both read the same percentage (75.2% on both in the first run).
CO3: the garden's moments
One-off moments in a job go through GardenEvents: a flower cut, a rock hit, a window broken. Every garden object
is numbered by position (GardenObjects), the first hit wins (the host checks), and a short history lets late
joiners see what's already broken. The Sim's JobIncidents turns them into the result's lines, per sibling: who
cut the roses is on record (the blame the whole game leans on: MO3, MO12, IN1).
Needs: CO2 · Done when: built: on two instances, a flower one sibling cuts is gone on both, and the result
names the sibling who cut it; guest logs show mismatches=0.
CO4: the other trades' work areas
Hedges, logs, branches, the pool and the washing surfaces are ISyncAreas (AreaSync.cs): each machine marks the
elements its player changed and merges the others' keeping the most done (trimmed, cut, cleaner, more doses), so
copies converge in any order; GardenAreas numbers them the same everywhere. Leaves move, so every machine replays
every blower stroke and the host sends where its leaves settled once a second. Built in the prototype; each trade
polishes its part when it arrives.
Needs: CO2 · Done when: built: a guest's trimmed hedge, cut logs, pool and washed patio show on the host (m07_villa, m08_storm).
CO5: the hub together
Outside jobs (the house, the garden, the street), the other siblings are always there.
- Bodies: every remote sibling is a
NetAvatardriven by theirNetPose(poseRate), with the flags for what their body is doing (carrying a bag, rolling paint, in bed, asleep, engine on...). PL7 gives them a body you bump into, PL4 what's in their hands. - Moments: one-off things in the hub go through
HubEventslikeGardenEventsin a job (a bag into the dumpster, a scrub, a knock, a call answered), with a shortHubEventLogfor late joiners. - Sounds: another sibling's mower and tools play the same synthesized engine and cut sounds, in 3D (SN).
Needs: - · Done when: built: a guest's bag into the dumpster plays on the host, and the host's on the guest.
CO6: everyone goes together
Until siblings can split up (DY5), everyone is in the same place.
- Scenes: a ride, a job or the way home moves every sibling (
RpcWhere): the guests' games follow the company's location. The night fade plays for everyone (RpcNight). - Cutscenes: the host starts them (
CinematicSync, rules inCinematicClock): every sibling plays the same scene from the same start tick, side by side; only the host skips, and the skip ends it everywhere. A sibling who joins after the start skips it.
Needs: CO1 · Done when: built: two instances watch the intro together, the host's skip ends it on both in under half a second, and a late joiner skips it.
CO7: join a Steam friend
How MVP testers in different houses play together: through Steam's relay, so nobody types an address or opens a port. It moves the joining part of the demo's Steam layer into the MVP; cloud saves, achievements and the rest stay in the Steam spec. The screens are the menus spec's (the title MN1, the "Who's playing?" room MN6, "Open to siblings" in the pause menu MN8); this changes what's behind them.
- Transport: FishNet over Steam's networking (Steam Datagram Relay): FishySteamworks or a maintained fork (its compatibility with our pinned FishNet 4.7.2 is checked first, see Open questions). Before our own app id exists, Valve's test id 480 (Spacewar).
- Hosting: Wait for siblings (MN6) or Open to siblings (MN8) creates a friends-only Steam lobby carrying the host's Steam id and the save's name and day. Steam shows "Playing Game Mower" with a Join button to the host's friends. In the room, Invite a friend opens Steam's invite list where MN6 showed the address.
- Joining: three ways, the same room: Join from the Steam friends list; accept an invite (Shift+Tab overlay), even with the game closed (Steam starts it and it goes straight to joining); or the title's Join, which lists friends with an open game (name, save, day, "1/2") where MN6 had the address field.
- Private only: no public lobbies, no browsing strangers' games.
- Full: past
maxPlayers, "Sam's game is full (2 siblings)". - No Steam: without Steam running, the title says so; solo still plays (it never needed the network).
- Dev: join by address stays in dev builds (two instances on one Mac): F10 host, F11 join, F12 leave,
-netjoin <address> -netauto -profile guest(the test recipe in Testing).
Needs: MN6, the Steamworks app id · Done when: two testers in different homes, neither touching an address or a router, join each other from the friends list and through an invite, and play a day; a third friend gets "full".
CO8: you stay you
Today players are numbered by connection, so a guest who rejoins becomes player 3 and is added to the host's save again. Instead each sibling belongs to a person.
- Who you are: your Steam id; in dev builds without Steam, a random key made once per profile (
-profile) and kept with it, not with the settings, since two instances on one Mac share them.PlayerDatastores it. MN6's room uses it to give a guest back their sibling. - Joining: the host looks the id up. Found: you're that sibling again (name, look, voice, bed, pocket), wherever
you left them. Not found: a new sibling, with SI1's screen, while the save has fewer than
siblingsPerCompany; otherwise "All four siblings are taken in Sam's game" (CO17 frees one). - The host is always sibling 1, by their own id; a host who joins somebody else's game is a guest there like anyone.
- Old saves: the host's sibling takes the host's id at the first load; earlier guest siblings keep their slots without an id, and the first new id to join claims the first one (with its look) rather than making a new sibling.
Needs: CO7 · Done when: a guest leaves and joins again three times and is the same sibling each time, with the save still holding two siblings; an old save with a guest sibling is claimed by the next guest.
CO9: asleep in their bed
A sibling whose player isn't connected sleeps in their own bed in the siblings' room (TU4: the bunk bed, the cot, the
sleeping bag), day and night. The beds go to siblings in order (the host gets the bottom bunk, the second sibling the
top one...). Their body has their look, lying as in NetPose.InBed, snoring softly.
- E on them: a sleepy mumble in their voice (SI2), three per voice ("Five more minutes...", "Not the hedge, grandma..."). Nothing else: they can't be woken, moved or robbed.
- Joining wakes them: a player who joins while the others are in the hub gets up from that bed; in a job, they come in beside the host as today (PL7's spawn rule).
- Bedtime: a sleeping absent sibling is not "connected": they never hold the day up (TU4).
- Never there: siblings nobody ever played (a 2-player company has two) are not in the house (ST5 is how they'd turn up).
Needs: CO8, TU4 · Done when: after a 2-player session, the host plays alone and finds the other sibling asleep in their bed, at noon too, mumbling in their voice; the guest joins and gets out of that bed.
CO10: dropping out and coming back
Connections drop, games crash. Nobody gets stuck because of it, and nothing is lost.
- A guest drops (closed, crashed, or silent for
dropTimeout): what they held goes down where they stood (PL4's rule: anything left goes back to its home when the scene ends), the mower they pushed stops there with the engine off, their car keys go back on the hook (CO11). The others see a line: "Kevin dropped out." The job, the day and the bed rule go on without them; their sibling is asleep in their bed (CO9). - Coming back: their title shows "Lost Sam's game" with a Rejoin button (
rejoinWindow); Steam's Join works too. They're the same sibling (CO8), back beside the host. - The host drops or quits: every guest goes to their title with the reason ("Sam's game closed", "Lost Sam's game (the connection dropped)"), and their own save comes back. The host's game goes back to its last save point (MN2), for everyone alike.
- The host closing on purpose with siblings still in: MN8's confirm says the guests will be sent to their titles.
- No host migration: the host's save is the game (Rejected).
Needs: CO8, CO9, PL4 · Done when: in 2-player co-op, killing the guest mid-job holding the fuel can leaves the can on the lawn, the keys on the hook and the host mowing; Rejoin brings the same sibling back next to the host within a minute; killing the host sends the guest to its title with the reason and its own save intact.
CO11: whoever holds the keys
Grandma's car keys are a real thing, and they decide who drives. They replace "Sam has the van keys": today only the host can start a drive.
- Where they are: on the hook by the front door (grandma's note on it: "KEYS GO ON THE HOOK.", TU2), or in one sibling's pocket. A pocket thing, not in your hands: holding the keys never stops you holding a tool. Whoever has them jingles, and the keyring shows at their hip for everyone.
- Taking and giving: E on the hook takes them or hangs them back. E on a sibling while you have them: "Give the
keys". You can't take them from a sibling, except one who hasn't moved for
afkKeysAfter(hold E: "Pat Kevin down"), so a sibling gone to make a sandwich never strands the others. - Driving: E on the driver's door with the keys starts the drive (a booked job far away, home from a job, later the store and DY2's rides). Without them: "Kevin has the keys" or "The keys are on the hook". Everyone rides along (CO6).
- On foot: walked jobs need no keys: whoever knocks on the client's door starts the job (TU7's knock is open to every sibling; the first knock wins), and anyone can knock to say you're done (EC8). Walking off a job early asks the one who does it ("Leave Mrs. Parker's lawn unfinished?") and the toast names them: "Sam walked off the job."
- What stays the host's: skipping cutscenes (CO6) and the day ending (the bed rule, TU4). The host still runs and checks every command: what changes is who may ask.
- Solo: you take the keys once from the hook and keep them until you hang them up.
- Never lost: keys can't be put down anywhere but the hook or a sibling's pocket; a sibling who drops out leaves them on the hook (CO10).
Needs: CO6, PL3, TU7 · Done when: in 2-player co-op, the guest takes the keys and drives both siblings to a booked job; the host gets "Kevin has the keys" at the car; the keys handed over in the garden drive both home; the host knocks first on a walked job and the guest's knock does nothing; solo plays a day taking the keys once.
CO12: never out of sync
The company can't drift (CO1 sends it whole after every change), but the lawn, the work areas and the garden's moments are merged copies: a missed packet or a bug could leave the two screens disagreeing for good. A safety net finds it and fixes it quietly.
- The check: every
hashEvery, the host sends a hash per chunk ofhashChunkcells of its lawn (and one per work area, CO4, and of the garden events' history). Each guest compares with its own, skipping chunks anyone touched in the lastsettleTime(cuts still in flight differ for good reasons). - The fix: a chunk that differs is sent again whole from the host, and the guest takes it. The player may see a few cells change; nothing else.
- The count: every resync is logged (
[Net] resync) and shown in the dev stats; the two-instance runs expect zero, asmismatches=0today. A resync is a bug found, not a feature working.
Needs: CO2, CO3 · Done when: in the sandbox, a dev button that corrupts a chunk on the guest is fixed within
hashEvery + 1 s; a normal 2-player mow logs no resync.
CO13: the internet test
Two instances on one Mac never lose a packet. Testers on home internet will.
- Simulated internet: the sandbox F1 panel and a
-netsimlaunch argument switch FishNet's latency simulator between presets: Same house (0 ms), Across town (simTown), Across the ocean (simOcean). - Smooth siblings: a remote sibling is drawn
poseDelayin the past, between two received poses, so they never jitter or snap; their mower and hand tool follow the same pose. Your own walking, cutting and commands stay instant (as built: owner-authoritative movement, commands run locally first). - Lag shows nowhere else: a guest's prompt, belt and screens never wait on the host; a refused command (two siblings, one thing, PL3) greys with its reason a moment later rather than freezing.
- The real test: a full Mrs. Higgins job between two homes over Steam (CO7).
Needs: CO2 (CO7 for the real test) · Done when: on Across the ocean, a full Mrs. Higgins job on two
instances shows the other sibling and their mower moving smoothly, the guest's mowing feels the same as the host's,
and the logs show mismatches=0 and no resync.
CO14: three and four siblings
The company has four siblings (SI); this lets all four be played. What two players hide and four show: the beds
(TU4 has four), the car's four seats (grandma's hatchback is tight, and that's the joke), four voices per side
(SI2), the bed rule and the keys with four, the economy's per-sibling scaling (EC: the household bill and demand
already scale), bandwidth (four poses, four cutters on one lawn), and the offer algorithm's capacity. The cap goes
maxPlayers goes from 2 to siblingsPerCompany.
Needs: CO7, CO8 · Done when: four instances play a full day: four ride in the car, four mow one lawn with
mismatches=0, the day ends when all four are in bed.
CO15: a ping
Middle-click (gamepad: a stick press) drops a short marker where you look, "over here!", with a whistle from your position. Two more ways to find each other were offered for the MVP and not taken: your sibling's name under the dot when you look at them, and their outline through walls. The MVP relies on looking around (and Discord).
CO16: proximity voice
Your friend's real voice coming from their sibling's mouth: quieter with distance, muffled through walls, louder when they shout, a little crackle on the phone. Steam's voice or a voice service; push-to-talk or voice activity. Testers use Discord until then.
CO17: the host's sibling list
With four slots, a friend who never comes back holds one forever. The host's game menu lists the siblings with when each last played: the host can rename one or free a slot (the sibling moves out, a car takes them and their pocket money away, their bed is free).
CO18: one screen, two siblings
Split screen on one machine, which would also open Steam Remote Play Together (a friend plays your local co-op over the internet without owning the game). Every view, UI screen and input path would need a second player.
Tuning
| Name | Value | Idea | What it does / why |
|---|---|---|---|
packetLimit | 900 bytes | CO1 | Every message fits in one packet: FishNet 4.7.2's split packets broke a broadcast. |
cellRate | 10 /s | CO2 | How often each player sends the lawn cells they changed. |
poseRate | 20 /s | CO5 | Defined by PL7; listed here because the network budget counts it. |
maxPlayers | 2 | CO7 | Players connected at once (MN6's room uses it); siblingsPerCompany from CO14. |
siblingsPerCompany | 4 | CO8, CO14 | Siblings a save can hold, and players connected at once from CO14. |
dropTimeout | 12 s | CO10 | Silence before a guest counts as gone (FishNet's default was 60 s). |
rejoinWindow | 30 min | CO10 | How long the title's Rejoin button stays after a drop. |
afkKeysAfter | 120 s | CO11 | A sibling still for this long can be patted down for the keys. |
hashEvery | 5 s | CO12 | How often the host sends the state hashes. |
hashChunk | 8 x 8 cells | CO12 | Lawn chunk per hash: small enough to resend in one packet. |
settleTime | 2 s | CO12 | A chunk touched this recently is skipped (cuts in flight). |
poseDelay | 100 ms | CO13 | How far in the past remote siblings are drawn: two poses at poseRate. |
simTown | 40 ms ± 10, 0% loss | CO13 | The "across town" preset (one way). |
simOcean | 150 ms ± 30, 1% loss | CO13 | The worst connection the MVP must handle (one way). |
Co-op
This spec is co-op itself; its own new state, against the golden rule:
- State: the car keys' holder (a player id, 0 = the hook) is company state, changed by commands
(
TakeKeysCommand,HangKeysCommand,GiveKeysCommand). Each sibling's player id (Steam or dev key) is inPlayerData. - Owner: the keys are shared (the company's); the id is per sibling.
- Who decides: anyone may ask; the host checks (the holder is the one asking, the giver holds them, the patted
sibling has been still for
afkKeysAfter, by the host's clock). Who's connected is the host's to say. - Randomness: none (CO9's mumble picks the next line in turn).
- Channels: commands for the keys;
NetPosefor the keyring at the hip;HubEventsfor the jingle, the mumble and "Kevin dropped out"; the CO12 hashes as their own message. - Packets: CO12's chunk resend stays under
packetLimit. - Late joiners: the keys' holder is in the company state; sleeping siblings are drawn from it (who's in the save and not connected).
- Checks: every Done when above runs on two instances; CO7 and CO13 also between two homes.
Tech
The golden rule, for every feature
Solo is co-op with one sibling: there is no solo-only code path. Before writing a feature, answer this list (every spec's Co-op section does); before committing it, test it on two instances.
- State lives in
Scripts/Sim, with no scene dependencies. Every change to it is anICompanyCommandrun throughGameSession.Execute(it carries the actor). Never changeCompany.Statefrom a MonoBehaviour. - Who owns each new field: one sibling (
PlayerData) or the company. Write it in the spec's Co-op section. - Who may ask? A drive goes through the keys (CO11), a job start through the knock (TU7), the day's end through
the beds (TU4). Only what the host must run (cutscene skips, settling a job, the end of the day) is in
CompanyWire.HostOnly, with a friendly reason. Until CO11 is built, drives are host-only too ("Sam has the van keys"). - Randomness is deterministic:
SimRandom.Value(State.seed, uid, salt)(likeHaggleRoll), neverUnityEngine.Randomor an unseededSystem.Random. A guest's local run must give the host's answer (Seeds: how the game rolls its dice). - What do the other siblings see, and how? Pick one channel, never "it only happens on my machine":
- company state (commands) for anything that lasts;
NetPoseflags for the body's state now (carrying, in bed, holding something);- an event channel (
GardenEventsin a job,HubEventsoutside) for one-off moments; - generation from seeds (the street, schedules) for things nobody changes.
- Every message fits in one packet (
packetLimit): chunk big payloads likeNetCompanydoes. - Late joiners get the current state: company state, the event histories, the current cutscene skipped. Guests never save.
- Anyone can drop at any moment (CO10): whatever a sibling holds or blocks must have a home it goes back to.
- Secrets: every guest gets the whole company state. Hide private things in the UI, between siblings only.
- Cutscenes are the host's: the host starts them, every connected sibling watches, only the host skips.
- Save and data versions: new save fields bump
SaveData.CurrentVersionwith a migration; new or changed mission data bumpsGameDatabase.dataVersion. - Voice lines go through
VoiceLines, soVoiceExportlists them (see Siblings: who you are for the cast). - Tests: an EditMode test per new command or rule (
Tests/EditMode), plus a two-instance run (the editor as host, a Mac dev build as guest:-netjoin 127.0.0.1 -netauto -profile guest, recipe in Testing). The guest's log must showmismatches=0(and no resync, CO12). Back up the user's save before any play test.
The code
Assets/_Game/Scripts/Net:NetSession(host, join, leave, the dev autopilot),NetPlayer(one per connection: poses, cells, events),NetAvatar(a remote sibling's body),NetCompany(the company state, cutscenes,RpcWhere,RpcClosing).- Sim:
CompanyWire(encoding,HostOnly),LawnGridSync,AreaSync,GardenEvents,HubEventLog,CinematicClock,JoinAddress,Bedtime. GameSession:Execute/ExecuteFor,Guest,LocalPlayerId,HostOnlyReason,Adopt,RestoreOwnSave.- Prefabs: Game Mower/Rebuild Net Prefabs (
Assets/_Game/Net/Resources/Net). - FishNet 4.7.2 is pinned: 4.7.3 needs Unity 6.3.
Changes these ideas bring
- CO6 + CO11: a guest can now start a drive, so the scene follows the company's location, not the host's own choice: the host's game follows a ride started by a guest like everyone else's.
- CO8:
PlayerData.netId(string: Steam id or dev key), save version bump with the claim rule for old saves. - CO9: a sleeping-sibling body per absent sibling in the house (their look,
NetAvatar's lying pose, no network object). - CO11:
SaveData.keysHolder; the hook, the keyring and the jingle;CompanyWire.HostOnlyloses the drive commands. - CO12: hash helpers on
LawnGrid(per chunk) and on eachISyncArea; a chunk snapshot message.
Tests
- EditMode: the keys' commands (take, hang, give, pat-down timing, refused cases), the identity rules (found, new, full, old-save claim), the chunk hashes (equal grids hash equal; one changed cell changes one chunk).
- Two instances: every Done when above. Between two homes: CO7 and CO13.
Open questions
- Which Steam transport? FishySteamworks 4.1.1 dates from 2024: does it work with FishNet 4.7.2, or is a fork (or FishyFacepunch) better? A first-evening spike in CO7 decides.
- Testers' copies: Steam Playtest or keys on a private branch, once the $100 app fee is paid (needed for the Coming Soon page right after the MVP anyway).
- Public games with strangers, ever? The game is about siblings: friends only so far.
- Two siblings, one mower (PL4's open question): who pushes when both grab the bar.
- Fast-forward in co-op (DY1/DY2: when every sibling agrees) and the keys: does the holder decide alone?
Rejected
- Everyone keeps a copy of the company: any sibling could host it, but copies diverge and need a "newest wins" rule. The host owns the save.
- Host migration (a guest takes over when the host drops): a lot of work for a rare case between friends; the host's save already has everything.
- Bots playing absent siblings: they sleep in their bed (CO9).
- Join by address for players: needs port forwarding or a VPN; Steam's relay does it for free (CO7). It stays in dev builds.
- Only the host drives (as built): a guest would be a passenger forever. The keys decide (CO11).
- A vote before every drive ("Sam wants to go: ready?"): the bed rule already does this once a day; for drives it would be a prompt every few minutes. Walking off or driving off on your own names you instead.
- Dedicated servers: one sibling hosts; nothing to run or pay for.