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) <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-08-10 15:01:10 -05:00
co-authored by Claude Opus 5
parent a5c1e0291c
commit 487eaa3a36
+69 -2
View File
@@ -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;