#116 THE REAL STEAM CAUSE: experience never rode the lobby -- joiners' pages were authored by the HOST as the "veteran" fallback

The user's question broke my first story: "did lynx host tho?"  He did (Oracle
joined LYNX's lobby that night) -- but more importantly the affected players'
logs read mode=steam, which gets the FULL menu, so the FeJoinOnly fix in
3109cfc patched a real gap (mode=join) that is NOT the path the affected trio
ran.  Oracle COULD see and cycle the selector; it still didn't matter.  Traced:

  * The steam lobby serialized exactly three member fields: nm / vh / cl.
    Experience never travelled.
  * The steam HOST rebuilds mission.pilots[] from the lobby roster
    (name/vehicle/color only), so every non-self page has EMPTY experience --
    and BTFeMission_WriteEgg stamps the "veteran" fallback on empty.
  * The steam JOINER writes NO egg at all (spec->eggPath[0]=0); his mission
    arrives from the HOST's console feed -- i.e. the host's egg, where HIS
    page says veteran no matter what his own console shows.
  * The host's own page keeps his menu choice only as slot-0 residue of the
    pre-roster self fill -- why BOTH hosts (Sauron AND Lynx) were clean while
    all three joiners heated in "standard".

FIX: experience is now published as lobby member data ("xp") beside nm/vh/cl,
unpacked into BTLobbyMember, and stamped onto each member's egg page in the
host's roster loop.  Both exported lobby entry points + PublishSelf gain the
parameter; the fe passes the local menu selection.  An older client in the
lobby simply has no "xp" -- the veteran fallback applies to THAT member only,
exactly the pre-fix behavior, so mixed-build lobbies do not break.

Per-player mixing (the original design, per the user: the sysop set each
user's tier; mixed matches were legal) now works over steam end to end: each
member's OWN choice -> lobby -> the host-authored egg page -> the console feed
-> that node's master player.

VERIFICATION BOUNDARY: builds both configs; the chain is code-traced; the
lobby legs need a real Steam session -- field re-test is a JOINER selecting
Standard and logging [exp] experience=1 heatModelOn=0 (plus the [fe] line on
both sides).  The 3109cfc join-trim selector stays: correct for mode=join.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joe DiPrima
2026-08-02 10:50:19 -05:00
co-authored by Claude Opus 5
parent 3109cfc49a
commit 2df1369168
3 changed files with 30 additions and 5 deletions
+8 -2
View File
@@ -1199,7 +1199,7 @@ int
if (menu.steamAction == 1)
{
BTLobbyRoster roster;
if (BTLobby_HostAndRoom(self.name, self.vehicle, self.color,
if (BTLobby_HostAndRoom(self.name, self.vehicle, self.color, self.experience,
&roster, spec->steamMyToken, sizeof(spec->steamMyToken),
spec->steamMap, sizeof(spec->steamMap)) != 0 ||
roster.memberCount == 0)
@@ -1220,6 +1220,12 @@ int
strncpy(pilot.color,
member.color[0] ? member.color : kColors[0].key,
sizeof(pilot.color) - 1);
// #116: the member's OWN experience choice rides the lobby now;
// absent (older build in the lobby) leaves the field empty and the
// egg writer's "veteran" fallback applies -- the old behavior,
// but now only for clients that never published a choice.
strncpy(pilot.experience, member.experience,
sizeof(pilot.experience) - 1);
sprintf(pilot.address, "%s:%d",
member.fakeAddress, member.gamePort);
if (!member.isSelf)
@@ -1238,7 +1244,7 @@ int
}
if (menu.steamAction == 2)
{
if (BTLobby_JoinAndWait(self.name, self.vehicle, self.color,
if (BTLobby_JoinAndWait(self.name, self.vehicle, self.color, self.experience,
spec->steamMyToken, sizeof(spec->steamMyToken),
spec->steamMap, sizeof(spec->steamMap)) != 0)
{
+15 -3
View File
@@ -83,7 +83,8 @@ static int
static CSteamID currentLobby;
static void
PublishSelf(const char *pilot_name, const char *vehicle, const char *color)
PublishSelf(const char *pilot_name, const char *vehicle, const char *color,
const char *experience)
{
//
// Identity is implicit (the member's SteamID); the roster TOKENS are
@@ -93,6 +94,7 @@ static void
matchmaking->SetLobbyMemberData(currentLobby, "nm", pilot_name);
matchmaking->SetLobbyMemberData(currentLobby, "vh", vehicle);
matchmaking->SetLobbyMemberData(currentLobby, "cl", color);
matchmaking->SetLobbyMemberData(currentLobby, "xp", experience);
}
static int
@@ -118,6 +120,14 @@ static int
strncpy(out->color,
matchmaking->GetLobbyMemberData(lobby, member, "cl"),
sizeof(out->color) - 1);
{
// #116: experience rides the lobby too -- absent (an older build in
// the lobby) leaves it empty and the egg writer's "veteran" fallback
// applies, same as before this field existed.
const char *xp = matchmaking->GetLobbyMemberData(lobby, member, "xp");
if (xp != NULL)
strncpy(out->experience, xp, sizeof(out->experience) - 1);
}
out->isSelf = (member == SteamUser()->GetSteamID());
if (out->name[0] == 0)
{
@@ -312,6 +322,7 @@ static int
int
BTLobby_HostAndRoom(
const char *pilot_name, const char *vehicle, const char *color,
const char *experience,
BTLobbyRoster *roster_out,
char *my_token_out, int my_token_capacity,
char *steam_map_out, int steam_map_capacity)
@@ -335,7 +346,7 @@ int
}
currentLobby = created.m_ulSteamIDLobby;
SteamMatchmaking()->SetLobbyData(currentLobby, "btl4", "1");
PublishSelf(pilot_name, vehicle, color);
PublishSelf(pilot_name, vehicle, color, experience);
LobbyLog("host: lobby up (%llu)", (unsigned long long)created.m_ulSteamIDLobby);
if (RunRoom(1) != 0)
@@ -403,6 +414,7 @@ int
int
BTLobby_JoinAndWait(
const char *pilot_name, const char *vehicle, const char *color,
const char *experience,
char *my_token_out, int my_token_capacity,
char *steam_map_out, int steam_map_capacity)
{
@@ -438,7 +450,7 @@ int
return -1;
}
currentLobby = lobby;
PublishSelf(pilot_name, vehicle, color);
PublishSelf(pilot_name, vehicle, color, experience);
LobbyLog("join: in lobby, waiting for GO");
int result = RunRoom(0);
+7
View File
@@ -29,6 +29,11 @@ struct BTLobbyMember
int gamePort; // token game port (1502)
char vehicle[16];
char color[16];
char experience[16]; // #116: per-player by design (sysop-set
// in the pod; mixed matches legal) --
// without it on the wire, the host's
// egg authored every JOINER as the
// WriteEgg "veteran" fallback
int isSelf;
unsigned long long steamID;
};
@@ -46,6 +51,7 @@ struct BTLobbyRoster
int
BTLobby_HostAndRoom(
const char *pilot_name, const char *vehicle, const char *color,
const char *experience,
BTLobbyRoster *roster_out,
char *my_token_out, int my_token_capacity,
char *steam_map_out, int steam_map_capacity);
@@ -57,5 +63,6 @@ int
int
BTLobby_JoinAndWait(
const char *pilot_name, const char *vehicle, const char *color,
const char *experience,
char *my_token_out, int my_token_capacity,
char *steam_map_out, int steam_map_capacity);