Reference · Updated 2552.08.07.08.17

Multiplayer, Team Slayer, Scoring & Dedicated Servers — Plan

High-level shape of the finished product, the order to build it, and a detailed spec for the first item. Written 2026-08-01 as a planning pass; nothing here is built yet.

High-level shape of the finished product, the order to build it, and a detailed spec for the first item. Written 2026-08-01 as a planning pass; nothing here is built yet.

Reads on top of: Docs/SystemLinkVision.md (the north star), Docs/MenusAndOnline.md (EOS/hosting decisions already locked 2026-06-07), Docs/RespawnSystem.md, Docs/DamagePipeline.md.


#1. What "finished" actually means here

The vision doc is unusually specific and it changes the target. SystemLink is not a matchmaking shooter that happens to have friends lists:

The session is the game. A Friday night of five matches is the primary unit of experience.
The match is a chapter, not the book.

So "team slayer with score" is not the finished product — it is the first complete chapter. The finished product is:


An evening: invite friends → play several matches across maps and modes →

            the night is remembered → that history accumulates over months

Concretely, four layers, only one of which is "a match":

LayerQuestion it answersStatus
Identity & PartyWho agreed to play tonight?Designed (EOS), not built
Match HostingWhere does this match run?Listen server decided; abstraction designed
Match RulesWhat are we playing, who won?Nothing built — teams, score, match flow
Session & HistoryWhat happened tonight, and last month?Nothing built — the vision's actual payoff

The differentiator is the fourth layer. Team slayer with a scoreboard is table stakes that every shooter has. The session summary, the rivalries, the "remember when" — that is the reason this project exists, and it is the layer most likely to get deferred forever if the plan doesn't name it.

#2. ⚠ On "dedicated server" — I think it should be last, not first

You named it first, so this is worth stating plainly rather than quietly reordering.

MenusAndOnline.md §0 already locked player-hosted listen server now, AWS GameLift later, with ISLSessionService (§7) as the seam so hosting can be swapped without touching party or menu code. That decision looks right, and building dedicated servers first would work against it:

  • It delivers no gameplay. A dedicated server with no teams, no score and no match end is an
  • empty room that costs money to keep running.

  • Listen server already solves the hard part. Over EOS P2P/relay, friends join across the
  • internet with no port forwarding. That is the thing that normally kills hobby multiplayer.

  • The vision caps the scale. Private, invite-only, friends-sized lobbies. Host advantage matters
  • at 64-player competitive scale; at eight friends it is a rounding error next to whether the night was fun.

  • It costs real money, forever. Recurring infra spend for a project whose stated goal is
  • memorable nights, not concurrency.

  • The seam already exists, so doing it later is a contained change, not a rewrite.

What would genuinely justify dedicated servers, and is worth watching for: host-migration pain when the host rage-quits, host FPS advantage becoming a running joke rather than a laugh, cheating once the group widens beyond people you trust, or wanting a session to persist while players come and go. Build it when one of those bites, not on principle.

If you want dedicated servers early anyway — e.g. you already have infra, or you want a persistent
"clubhouse" server always up for the group — say so and the order changes. That is a legitimate
product decision, just a different one from what's in MenusAndOnline.md.

#3. Build order

Each phase is playable at its end. That matters more than usual here, because the whole point is whether an evening is fun, and that can only be judged by playing one.

Revised 2026-08-01: LAN moved to first. Beepers' testers are on the same physical network, so real humans on real machines are reachable immediately — no EOS, no accounts, no infrastructure.

#PhaseWhy herePlayable result
1LAN host/join + first menu screenGets real people playing this week, and doubles as the main menu's first contentFour friends in one room, in one match
2TeamsEverything downstream needs team identity. Scoring, friendly fire, spawns, the reticle, nameplates all branch on it.Red vs Blue, correct colours, friendly fire rules honoured
3Match flow, scoring & the game type assetTurns "shooting each other" into "a match with a winner"Score limit / time limit, a match that ends and declares a winner
4Scoreboard & post-matchThe feedback loop that makes score mean anythingLive scoreboard, end-of-match results screen
5Session layerThe vision's payoff. Multiple matches as one evening, with a summaryA Friday night that is remembered as a night
6EOS party & invitesOnly once the people you want to play with are not in the roomInvite a remote friend over the internet
7Dedicated serversOnly when §2's triggers biteSame game, hosted elsewhere

Why LAN-first is stronger than it first appears:

  • It is not a detour. MenusAndOnline.md §4.6 already puts the LAN path behind the same
  • ISLSessionService. This picks which implementation to build first — exactly what the abstraction was for. Nothing is thrown away when EOS lands.

  • It is thematically the actual product. The vision opens with "recreate the feeling of a classic
  • Halo LAN party." For one night in one room, LAN is the product, not a stepping stone.

  • This project's bug history is host-vs-client divergence. BUG-023 (anim notifies never fire for
  • remote clients), BUG-029 (ragdoll never cleared on observers), dash direction resolved on the wrong machine, melee target not surviving the RPC, the listen-host separate fire path. Those surface in proportion to how many real clients exist. Three friends on real hardware will find more in an evening than weeks of PIE.

The one real objection, and its answer. LAN latency (~1 ms) hides netcode problems that appear over the internet. UE's built-in emulation removes that gap:


NetEmulation.PktLag 80

NetEmulation.PktLagVariance 20

NetEmulation.PktLoss 2

Run on the host, the LAN behaves like a poor internet connection. Internet-condition testing with no internet infrastructure — and worth doing deliberately once the basics work, because this project's predicted-ability bugs (dash, melee lunge) are exactly the class that only appears under latency.

#4. Phase 1 in detail — LAN play and the first menu screen

#4.1 The insight that makes this cheap

"Host Game / Join Game" is also the answer to the other open goal. MainMenu currently has a diorama and no menu; the first screen it needs is Host/Join. So this is not two projects — the LAN work is the main menu's first content, built on the CommonUI stack that already exists, with the settings screen as a worked example of the pattern.

#4.2 Scope, smallest first

  1. Main menu screen — Host, Join, Settings (already built), Quit. On the MenuStack layer of
  2. USLPrimaryGameLayout, page base WBP_SL_Page.

  1. Host — travel to the gameplay map as a listen server.
  1. Join — v1 a plain IP text box; v1.1 a LAN session search that auto-discovers the host.
  1. Leave — return to the menu map.

For sessions, OnlineSubsystemNull supports LAN session search out of the box with no config and no accounts, which is the standard zero-infrastructure path. The blunter alternative — OpenLevel(Map, true, "listen") on the host and ClientTravel("<ip>") on the client — needs no subsystem at all and is a legitimate v1 if the session search fights back.

Either way it goes behind ISLSessionService (MenusAndOnline.md §7) as USLSessionService_LAN, so Phase 6 adds EOS by registering a different implementation.

#4.3 ⚠ Immediate blocker: Level 1 has two PlayerStarts

Verified during the level import — Level1 contains exactly 2 PlayerStart actors. That was fine when the only tester was PIE. Four people joining a two-start map will fight over spawns or fail to spawn, and it will look like a networking bug when it is a level-content bug.

Place 8 PlayerStarts before the first LAN test. Team-tagging them can wait for Phase 2; count cannot. This is a five-minute job that will otherwise eat an evening of misdiagnosis.

#4.4 Other things that will bite

  • The menu map needs a menu-shaped GameMode. MainMenu is a diorama with a CineCameraActor and
  • CameraRig_Rail driving LS_MainIntro. The player must view through that camera, not spawn a Chief in the middle of the set. BP_MainMenuGameMode currently uses the stock PlayerController and default pawn.

  • Hard travel, not seamless. Seamless travel needs a transition map and carries more subtlety;
  • hard travel is fine for a first playable.

  • Pause must not pause the server. Already recorded: on a listen server SetGamePaused stops
  • time for everyone, so the in-match pause has to be a UI overlay once networked (MenusAndOnline.md §8.4, UISystem.md 6.12).

  • Max players. Pick a number and enforce it at session creation — with the vision's "friends
  • sized" framing, 8 or 16 is the sane cap.

  • bUseSeamlessTravel, GameSession and MaxPlayers live on the GameMode; expect a pass over
  • ASLGameModeBase here.

#4.5 ⚠ Record now for Phase 6: the net driver conflict

EOS configuration sets the EOS NetDriver as the default and routes traffic through SocketSubsystemEOS (MenusAndOnline.md §4.2). LAN wants the standard IP net driver. The two must coexist and be selected per transport, or switching EOS on silently breaks the LAN path that everything was tested against.

Not a problem today — the default net driver is what is in place — but it is exactly the kind of thing that costs an evening when discovered rather than planned.

#5. Phase 2 in detail — Teams

#5.1 Two architectural decisions to make first

(a) AGameModeBaseAGameMode. ASLGameModeBase currently extends AGameModeBase, which has no match state machine. Team slayer needs at least warmup → in progress → post-match, plus "don't let anyone score before the match starts". AGameMode provides MatchState, ReadyToStartMatch, HandleMatchHasStarted, ReadyToEndMatch — the exact shape needed.

Recommend switching the base class in Phase 2 and not hand-rolling a state enum. Flagged here
because it is easier to decide before teams are wired than after. It is a base-class change on a
class that already ships, so it wants a clean-build + PIE pass of its own.

(b) There is no GameState subclass at all. Team score is per-match, shared by everyone, and must replicate to late joiners — that is AGameStateBase's job, not PlayerState's. Phase 1 creates ASLGameState even though it only holds team definitions at first.

#5.2 Where team identity lives

On ASLPlayerState, replicated — for the same reason the ASC lives there: the pawn is destroyed and recreated on every respawn, so anything pawn-local is lost. This is the exact trap already recorded in feedback_ability_handles_playerstate.

Implement UE's IGenericTeamAgentInterface on ASLPlayerState (and forward from the pawn), so FGenericTeamId and GetTeamAttitudeTowards work. This is worth doing even with no AI today — perception, and any later bot work, understand that interface for free.


UENUM(BlueprintType)

enum class ESLTeam : uint8

{

    None  = 0,   // unassigned / spectator

    Red   = 1,

    Blue  = 2,

};

Deliberately an enum with room to grow, not a bool — same reasoning ESLAimTarget records for itself. FFA is then simply "every player on ESLTeam::None", not a separate code path.

#5.3 The change list

WhereChangeNotes
ASLPlayerStateReplicated ESLTeam + IGenericTeamAgentInterfaceOnRep_Team broadcasts so visuals update on clients
ASLGameState (new)Team definitions: display name, colour, and later scoreReplicated; needed for late joiners
ASLGameModeBaseAssign team on PostLogin — balance by current countHost override later from the party screen
SLDamageExecutionFriendly-fire gateThe real gameplay change — see 4.4
USLWeaponsComponent::ClassifyHitReturn Friendly for same-team charactersOne function. ESLAimTarget::Friendly is already reserved for exactly this.
USLReticleFriendly state (don't just reuse the hostile red)Consumer already handles a kind, not a bool
Character materialTeam tintNeeds a decision: tint the existing MC material vs per-team material instances
NameplatesTeam-coloured, friendly-onlyDoesn't exist yet — may defer to Phase 3 with the scoreboard

#5.4 Friendly fire is a design decision, not a toggle

Three viable policies, and the vision points somewhere specific:

  • Off — simplest, no betrayal concept, and kills a genuine source of stories
  • On, full damage — classic Halo; enables betrayals, which the vision explicitly lists as a
  • memorable-moment source

  • On, reduced — a middle ground that mostly satisfies nobody

SystemLinkVision.md lists betrayals under session stats and Nemesis/Target under nightly awards. That argues for friendly fire ON, with betrayals recorded rather than punished — no auto-kick, because among friends the betrayal is the joke. Make it a per-match rule on the game mode so a group can turn it off.

Implementation lands in SLDamageExecution as a gate before damage is applied, alongside the existing Overshield handling. Note the precedent already set there: Overshield forces health damage to zero regardless of magnitude — so the ordering of these gates matters and needs a deliberate read of that execution rather than a blind insert.

#5.5 Spawning

Team spawns are a Phase 1 stretch, not a requirement. Level1 currently has 2 PlayerStarts — fine for testing, wrong for 4v4. The real work is:

  • Tag PlayerStarts with a team
  • Override ChoosePlayerStart to prefer own-team starts, weighted away from enemies
  • Anti-spawn-camp weighting (distance from nearest enemy, recent-death location)

Reasonable to defer the weighting and just do team-tagged starts in Phase 1.

#5.6 Tests worth writing

The automation suite is the right place for the pure logic, and this phase has real pure logic:

  • Team assignment balances (3 players → 2/1, not 3/0)
  • GetTeamAttitudeTowards — same team friendly, other team hostile, None hostile to all
  • Friendly-fire gate: same-team damage is zero when the rule is off, full when on
  • ClassifyHit returns Friendly / Hostile correctly
  • Team survives death and respawn — the PlayerState-not-pawn property, which is exactly the class of
  • bug that has bitten this project before

#5.7 Risks

  • Base class change (AGameModeBaseAGameMode) touches a shipping class. Do it on its own
  • branch, clean build, PIE pass. → feedback_clean_binaries_intermediate.

  • OnRep_Team on proxiesGetPawnASC is documented as null during OnRep for proxies
  • (feedback_gas_tag_callback_patterns). Team visuals must not assume a valid ASC in that callback.

  • Kill attribution already half-existsRequestRespawn(PC, Killer) passes a killer, and
  • GetEffectCauser() vs GetInstigator() has a recorded footgun. Phase 2 should build on that rather than inventing a parallel path.

  • Suicide/environment attribution is already known-wrong — falling out of the world currently
  • records itself as the source and scores as a suicide (CurrentFocus.md). Worth fixing when scoring lands, not before.

#6. Game Type Settings — promoted to a first-class feature

Added 2026-08-01 after Beepers raised it. On reflection this is not plumbing, it is a vision feature, and it deserves higher billing than the original draft gave it.

#6.1 Why it ranks higher than it looks

The thing people actually remember about Halo LAN nights is rarely a standard Slayer match. It is rockets only, no shields, 300% speed, one-shot kills, swords in the dark. Custom game settings are the LAN party. Measured against the stated goal — "maximize memorable nights with friends" — a rich game-type system scores higher than almost anything else on the roadmap, because it lets a group invent their own traditions instead of playing the traditions we shipped.

It is also a hard prerequisite for the session layer. The vision describes a session as several matches "across different maps and game types". A session is literally a list of (map, game type) pairs, so game type has to be a selectable first-class asset before Phase 5 can exist at all.

#6.2 Shape: a DataAsset, matching the existing pattern

Follows USLWeaponDataAsset / USLMobilityModuleDataAsset / USLGrenadeDataAsset — the project's established way of making designer-tunable config that diffs and needs no rebuild.


USLGameTypeDataAsset

├── Identity        DisplayName, Description, Icon

├── Teams           FFA or N teams, team size, auto-balance

├── Victory         ScoreLimit, TimeLimit, what scores and for how much

├── Friendly fire   ESLFriendlyFire { Off, On, Heal }        ← see 5.3

├── Respawn         delay, instant, waves, spawn protection

├── Loadout         starting weapons + ammo (a USLLoadoutDataAsset already exists)

└── Modifiers       damage scale, move speed, gravity, shields on/off,

                    headshot-only, infinite ammo, grenade count …

The Modifiers bucket is where the memorable nights come from. It is also cheap: most entries are a single scalar multiplied into something that already exists.

**Not a settings menu first.** Phase 2 needs the asset and the plumbing that reads it — a handful
of curated game types the host picks from. The in-game editor for making your own comes later, and
is a MenuKit-shaped problem (Docs/MenuKitExtractionPlan.md).

Where it slots: built as part of Phase 2, because match flow is the first thing that needs to read ScoreLimit/TimeLimit from somewhere. Start with only the fields Slayer needs and grow it — but decide the architecture in Phase 1 so it is not retrofitted onto a hardcoded mode.

#6.3 Friendly fire as a three-state enum, including Heal

Beepers' idea: a setting where shooting a teammate heals them. It slots in as a third value rather than a separate toggle, which is the better shape — the three options are mutually exclusive answers to one question, "what happens when you hit a friendly?"


UENUM(BlueprintType)

enum class ESLFriendlyFire : uint8

{

    Off,    // hits on teammates do nothing

    On,     // full damage; betrayals recorded, not punished

    Heal,   // hits on teammates restore health

};

Design notes that fall out of the existing systems:

  • Heal HEALTH, not shields. Shields already regenerate on their own (GE_ShieldRegen), so
  • healing them is nearly a no-op. Health does not regenerate — so a support player topping up a teammate's health is restoring the one pool nothing else can. That makes the mode strategically real rather than a novelty.

  • Do not reuse the damage number. Bone multipliers are applied in
  • SLGameplayAbility_Fire.cpp:421, before SetByCaller, so the execution only ever sees an already-multiplied value — a headshot would heal double, which is absurd. Instead the heal should be a flat per-hit amount from the game type, which sidesteps multipliers entirely and gives a clean designer knob.

  • Put the branch in SLDamageExecution, not the fire ability. Six call sites set
  • SLTags::Data::Damage (fire, melee, grenade, fell-out-of-world, weapons component, test emitter). Branching in the execution covers all of them uniformly and for free; branching in the fire path would have to be repeated five more times.

  • "Guardian" is already in the vision's nightly awards list. Healing done is the stat that award
  • wants. Worth emitting from day one even before awards exist.

  • Ammo is spent healing — a real cost, and good tension.

Level of effort — honest split:

EffortWhat
CoreLow — as estimatedAn early branch in SLDamageExecution that returns before the damage path, plus the team lookup and one game-type field. The execution is ~100 lines and already captures Health.
CompleteMediumFriendly hit-marker, a distinct impact cue (the cue system is per-weapon, so a friendly variant is needed), overheal clamping, healing-done attribution, and deciding whether every weapon can heal or only some

So the instinct is right — the core is genuinely a small change, and it is small precisely because the damage pipeline is already centralised. Just note it depends on teams existing, so it is Phase 1.5 at the earliest, and the feedback layer is where the remaining time goes.

#7. What this plan deliberately does not decide

  • Modes beyond Slayer (CTF, Oddball, KotH). The vision lists them; the mode architecture should
  • be shaped to allow them in Phase 2, but only Slayer gets built.

  • Progression/unlocks. The vision rules them out — noted so nobody adds them by reflex.
  • Where session history is stored (local save vs EOS vs a small backend). A Phase 5 question, and
  • a big one: "history matters" implies durability across reinstalls.

  • Character/loadout select — already flagged open in MenusAndOnline.md §8.5.
  • Split-screen interaction with teams. Split-screen is a committed target
  • (MenusAndOnline.md §9); two local players on the same team is the obvious case and it mostly falls out of PlayerState-based teams, but it needs a deliberate test.