Doors run on the mission clock instead of being replicated
A door's position was an integrated countdown owned by whichever machine the map-entity round-robin happened to deal it to. That left doors one one-way-latency behind on every other machine, re-acquired at each state change; drifting permanently on any frame hitch over a second, which the old code dropped outright rather than clamping; and frozen mid-cycle, collision volumes included, when their owning peer left, since ownership transfer is not implemented. Doors are clockwork with no inputs, and door/VTV physics is already local pointer access - VTV::ProcessCollision reads door->currentVelocity off the local object and the crush test is local VTV state - so a door does not need an owner at all. Door::SlideDoor is now a phase function of Application::GetMissionElapsed(), anchored so phase zero reproduces the original DefaultState entry: fully open, starting to close. Every host builds its own doorframe out of the map stream as a HermitInstance, the instance kind DynamicEntityCreation does not broadcast, so nothing is sent, nothing is received, and a peer leaving takes no doors with it. Verified against a copy of the old integrator at 25fps with the real 10s travel / 3s dead timings: identical 26s cycle, a constant one-frame offset, and no drift across a 3s stall that leaves the old code permanently 3 seconds out of phase. Also fixes a latent bug found on the way: UpdateManager iterates the dynamic master socket, which holds Independant and Hermit instances as well as masters, and handed all of them to EntityUpdateReplicants, which asserts MasterInstance. Doorframes no longer consume a slot in the map-entity ownership cursor, which shifts who owns every map entity dealt after them, so this cannot share a session with an older build. The lobby publishes a simulation revision and refuses to launch a mixed room. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+50
-1
@@ -51,6 +51,20 @@ namespace
|
||||
const char kResultsKey[] = "res";
|
||||
const char kScenarioKey[] = "sc";
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Simulation protocol revision. Bump this whenever a change makes
|
||||
// two builds simulate the same mission differently - it is not the
|
||||
// wire format alone. Map entity ownership is dealt by advancing a
|
||||
// shared cursor once per map entity, so anything that changes which
|
||||
// entities are dealt at all silently desynchronizes who owns what.
|
||||
//
|
||||
// 2 - doorframes became local Hermit clockwork and are no longer
|
||||
// dealt, which shifts every subsequent map entity's owner
|
||||
// 1 - the 3-machine verified Steam build
|
||||
//-------------------------------------------------------------------
|
||||
const char kNetRevision[] = "2";
|
||||
const char kNetRevKey[] = "nr";
|
||||
|
||||
// the owner's mission setup, shown to everyone in the room
|
||||
const char kMapKey[] = "mp";
|
||||
const char kTimeKey[] = "td";
|
||||
@@ -164,6 +178,9 @@ namespace
|
||||
SteamMatchmaking()->SetLobbyMemberData(gLobby, "ps",
|
||||
RPL4FrontEnd_PositionKey(RPL4FrontEnd_GetPositionIndex()));
|
||||
|
||||
// what this build simulates like, so a mismatched room cannot launch
|
||||
SteamMatchmaking()->SetLobbyMemberData(gLobby, kNetRevKey, kNetRevision);
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Only the owner's menu decides the mission, so the owner also
|
||||
// publishes what it picked: the scenario (members need it to know
|
||||
@@ -172,6 +189,8 @@ namespace
|
||||
//---------------------------------------------------------------
|
||||
if (IsOwner())
|
||||
{
|
||||
// members check this before they act on the owner's go
|
||||
SteamMatchmaking()->SetLobbyData(gLobby, kNetRevKey, kNetRevision);
|
||||
SteamMatchmaking()->SetLobbyData(gLobby, kScenarioKey,
|
||||
RPL4FrontEnd_IsFootballSelected() ? "football" : "race");
|
||||
SteamMatchmaking()->SetLobbyData(gLobby, kMapKey,
|
||||
@@ -226,6 +245,7 @@ namespace
|
||||
char badge[24];
|
||||
char team[32]; // football pick
|
||||
char position[16];
|
||||
char netRev[8]; // simulation protocol revision
|
||||
Logical published;
|
||||
};
|
||||
|
||||
@@ -266,6 +286,9 @@ namespace
|
||||
strncpy(member->position,
|
||||
SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "ps"),
|
||||
sizeof(member->position) - 1);
|
||||
strncpy(member->netRev,
|
||||
SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, kNetRevKey),
|
||||
sizeof(member->netRev) - 1);
|
||||
member->published =
|
||||
member->ip[0] != '\0' && member->consolePort > 0 && member->gamePort > 0;
|
||||
}
|
||||
@@ -797,14 +820,22 @@ namespace
|
||||
room.launchClicked = False;
|
||||
room.memberCount = CollectMembers(room.members);
|
||||
Logical all_published = True;
|
||||
Logical all_same_build = True;
|
||||
for (int i = 0; i < room.memberCount; ++i)
|
||||
{
|
||||
if (!room.members[i].published)
|
||||
{
|
||||
all_published = False;
|
||||
}
|
||||
if (strcmp(room.members[i].netRev, kNetRevision) != 0)
|
||||
{
|
||||
all_same_build = False;
|
||||
DEBUG_STREAM << "Lobby: " << room.members[i].name
|
||||
<< " simulates like rev '" << room.members[i].netRev
|
||||
<< "', we are rev '" << kNetRevision << "'\n" << std::flush;
|
||||
}
|
||||
}
|
||||
if (all_published && room.memberCount >= 1)
|
||||
if (all_published && all_same_build && room.memberCount >= 1)
|
||||
{
|
||||
++gLastGoNonce;
|
||||
char go[800];
|
||||
@@ -834,6 +865,24 @@ namespace
|
||||
//
|
||||
if (!IsOwner())
|
||||
{
|
||||
//
|
||||
// A room whose owner simulates differently than we do would
|
||||
// desynchronize silently rather than fail, so sit the race out
|
||||
// instead of flying into it.
|
||||
//
|
||||
const char *owner_rev = LobbyText(kNetRevKey);
|
||||
if (owner_rev[0] != '\0' &&
|
||||
strcmp(owner_rev, kNetRevision) != 0)
|
||||
{
|
||||
DEBUG_STREAM << "Lobby: owner simulates like rev '"
|
||||
<< owner_rev << "', we are rev '" << kNetRevision
|
||||
<< "' - not launching\n" << std::flush;
|
||||
outcome = LobbyRoomLeft;
|
||||
SteamMatchmaking()->LeaveLobby(gLobby);
|
||||
gInLobby = False;
|
||||
break;
|
||||
}
|
||||
|
||||
const char *go = SteamMatchmaking()->GetLobbyData(gLobby, kGoKey);
|
||||
if (go != NULL && go[0] != '\0')
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user