From 487eaa3a363ad4fcde7a66b4300b40b899f98260 Mon Sep 17 00:00:00 2001 From: Cyd Date: Mon, 10 Aug 2026 15:01:10 -0500 Subject: [PATCH] The host can watch instead of racing YOUR ROLE joins the setup screen at the head of the loadout column - Racer or Live Cam - so the host still picks the track, the conditions and the length, and still owns the lobby and marshals every pod, but can hand its own grid slot back and watch. It rides the existing group machinery (kRoles through GroupSize/GroupTitle/ItemName), so there is no new UI code, and it persists in pilot.cfg beside the rest of the loadout. Offered only when Steam is configured, the same condition the HOST and JOIN buttons use: the role means nothing without a lobby to host. In the egg the host's own entry becomes hostType=1 and vehicle=camera. Those two are what RPRegistry::MakePlayer reads to build a camera director rather than a racer, and they are all it takes - the arcade selected its camera cabinet exactly this way, from egg data alone. The pick is honoured only when other pods are actually in the race. pilot.cfg remembers Live Cam, so someone who set it for a lobby race and later launched a single-player one would otherwise hand the map load a camera host with no peers, which is the one configuration known to hang it. With no extras the choice is logged and ignored and the host races, which is what pressing LAUNCH on a solo game meant anyway. Still open: a camera host with real racing peers has never been tried, so whether the map-load hang survives contact with a live race is the next thing to find out; and nothing yet tells lobby members that the host is spectating, so their room screen just shows one fewer car. Co-Authored-By: Claude Opus 5 (1M context) --- RP_L4/RPL4FE.cpp | 71 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/RP_L4/RPL4FE.cpp b/RP_L4/RPL4FE.cpp index 34c31b1..9cb3407 100644 --- a/RP_L4/RPL4FE.cpp +++ b/RP_L4/RPL4FE.cpp @@ -166,6 +166,19 @@ namespace { "clear", "Clear" }, { "fog", "Light Fog" }, { "soup", "Heavy Fog" }, }; + // + // What the host does with the race it is setting up. A Live Cam host + // still owns the lobby, picks the track and the conditions, and + // marshals every pod - it just watches instead of racing, which is how + // the arcade ran its camera cabinet. Only offered on a lobby race: + // a camera needs pods to point at, so see HostIsCamera below. + // + const CatalogEntry kRoles[] = + { + { "racer", "Racer" }, { "camera", "Live Cam" }, + }; + enum { kRoleRacer = 0, kRoleCamera = 1 }; + struct LengthEntry { int seconds; // 0 = endless (length line omitted) @@ -199,6 +212,7 @@ namespace GroupTime, GroupWeather, GroupLength, + GroupRole, // racer or Live Cam (lobby races only) GroupScenario, GroupTeam, // football only GroupPosition, // football only @@ -214,6 +228,19 @@ namespace return selection[GroupScenario] == 1; } + // + // Did the host ask to watch rather than race? The selection alone is + // not enough: the option persists in pilot.cfg, so somebody who set + // Live Cam for a lobby race and later launched a single-player one + // would otherwise get a camera with nothing to point at - which is + // the one configuration known to hang the map load. The egg builder + // therefore also requires other pods to be present; see BuildEggText. + // + Logical WantsCamera(const int *selection) + { + return RPL4Lobby_Configured() && selection[GroupRole] == kRoleCamera; + } + const CatalogEntry *ActiveMaps(const int *selection, int *count) { if (IsFootball(selection)) @@ -300,6 +327,7 @@ namespace { GroupBadge, "badge" }, { GroupTeam, "team" }, { GroupPosition, "position" }, { GroupTime, "time" }, { GroupWeather, "weather" }, { GroupLength, "length" }, + { GroupRole, "role" }, }; // @@ -327,6 +355,7 @@ namespace case GroupTime: return FE_COUNT(kTimes); case GroupWeather: return FE_COUNT(kWeather); case GroupLength: return FE_COUNT(kLengths); + case GroupRole: return FE_COUNT(kRoles); } return 0; } @@ -635,6 +664,17 @@ namespace groups[n++] = GroupTime; groups[n++] = GroupWeather; groups[n++] = GroupLength; + // + // Heads the loadout column: what you ARE reads before what you + // are driving, and on Live Cam the lists under it stop applying. + // Only when Steam is configured - the role is meaningless without + // a lobby, and a camera with nothing to watch is worse than + // meaningless (see HostIsCamera). + // + if (RPL4Lobby_Configured()) + { + groups[n++] = GroupRole; + } groups[n++] = GroupVehicle; if (IsFootball(selection)) { @@ -936,6 +976,7 @@ namespace case GroupTime: return "TIME OF DAY"; case GroupWeather: return "WEATHER"; case GroupLength: return "GAME LENGTH"; + case GroupRole: return "YOUR ROLE"; } return ""; } @@ -961,6 +1002,7 @@ namespace case GroupTime: return kTimes[index].name; case GroupWeather: return kWeather[index].name; case GroupLength: return kLengths[index].name; + case GroupRole: return kRoles[index].name; case GroupLaunch: return "L A U N C H G A M E"; case GroupSteamHost: return "HOST STEAM GAME"; case GroupSteamJoin: return "JOIN STEAM GAME"; @@ -1557,18 +1599,43 @@ namespace egg += line; } + // + // The host's Live Cam pick, resolved here because this is where + // the other pods are finally known. A camera needs something to + // watch: with no extras this is a single-player race, the pick is + // stale (pilot.cfg remembers it), and honouring it would hand the + // map load a camera host with no peers - the case that hangs. So + // it is quietly ignored and the host races, which is what someone + // pressing LAUNCH on a solo game meant anyway. + // + // hostType 1 is CameraShipHostType and vehicle 'camera' makes the + // mission's game model 'camera'; together they are what turn a + // station into a camera director (see RP/RPREG.cpp). Everything + // else on the line stays - dropzone and colour cost nothing and + // the egg readers still expect them. + // + Logical owner_is_camera = WantsCamera(fe->selection) && extra_count > 0; + if (WantsCamera(fe->selection) && extra_count == 0) + { + DEBUG_STREAM << "FE: Live Cam ignored - no other pods in this race\n" + << std::flush; + } + for (int p = 0; p < pilot_count; ++p) { + Logical camera_entry = (p == 0 && owner_is_camera); + sprintf(line, "[%s]\n", pilots[p].address); egg += line; - egg += "hostType=0\n"; + egg += camera_entry ? "hostType=1\n" : "hostType=0\n"; egg += "dropzone=one\n"; sprintf(line, "name=%s\n", pilots[p].name); egg += line; sprintf(line, "bitmapindex=%d\n", p + 1); egg += line; egg += "loadzones=1\n"; - sprintf(line, "vehicle=%s\n", pilots[p].vehicle); + sprintf(line, "vehicle=%s\n", + camera_entry ? "camera" : pilots[p].vehicle); egg += line; sprintf(line, "color=%s\n", pilots[p].color); egg += line;