diff --git a/MUNGA/APP.cpp b/MUNGA/APP.cpp index d372c97..66c301f 100644 --- a/MUNGA/APP.cpp +++ b/MUNGA/APP.cpp @@ -273,6 +273,35 @@ Scalar return mgr->GetFrameRate(); } +// +//############################################################################# +// GetMissionElapsed +//############################################################################# +// +Scalar + Application::GetMissionElapsed() +{ + Check(this); + + // + //-------------------------------------------------------------------------- + // gameStarted is only ever stamped by RunMissionMessageHandler, so before + // the race it is uninitialized - and entities that are pre-runnable do get + // performed before then. Answer zero until the clock actually exists. + //-------------------------------------------------------------------------- + // + if ( + GetApplicationState() != RunningMission + && GetApplicationState() != EndingMission + ) + { + return 0.0f; + } + + Scalar elapsed = Now() - gameStarted; + return (elapsed > 0.0f) ? elapsed : 0.0f; +} + // //############################################################################# // Initialize diff --git a/MUNGA/APP.h b/MUNGA/APP.h index 8728b67..0ea47d4 100644 --- a/MUNGA/APP.h +++ b/MUNGA/APP.h @@ -318,6 +318,15 @@ public: Scalar GetSecondsRemainingInGame() {return secondsRemainingInGame;} + // + // Seconds since the console's RunMission started the race, counting up. + // Every machine anchors this on the same message, so anything derived + // from it agrees across the mesh without being replicated - see the + // clockwork doors in DOOR.cpp. Reads 0 outside a running mission + // (gameStarted holds garbage until RunMission stamps it). + // + Scalar + GetMissionElapsed(); ApplicationID GetApplicationID() {return applicationID;} diff --git a/MUNGA/DOOR.cpp b/MUNGA/DOOR.cpp index b749f5d..622e8cb 100644 --- a/MUNGA/DOOR.cpp +++ b/MUNGA/DOOR.cpp @@ -72,172 +72,96 @@ Door::AttributeIndexSet& Door::GetAttributeIndex() //############################################################################# // Model Support // -void - Door::ReadUpdateRecord(Simulation::UpdateRecord *message) -{ - Check(this); - Check_Pointer(message); - Subsystem::ReadUpdateRecord(message); - UpdateRecord* record = (UpdateRecord*) message; - - percentOpen = record->percentOpen; - switch (GetSimulationState()) - { - case Opening: - case Closing: - phaseTimeRemaining = travelTime; - break; - case Opened: - case Closed: - phaseTimeRemaining = deadTime; - break; - } -// DEBUG_STREAM << GetEntity()->GetEntityID() << " door updated to state " -// << GetSimulationState() << " @ " -// << application->GetSecondsRemainingInGame() << endl; - MoveCollisionVolume(percentOpen); - Check_Fpu(); -} +// There is no ReadUpdateRecord/WriteUpdateRecord pair here on purpose. Doors +// are Hermit instances built independently on every host, so no door state is +// ever sent or received - the phase function below is the only thing that +// decides where a door is, and it reaches the same answer everywhere. +// //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // void - Door::WriteUpdateRecord(Simulation::UpdateRecord *record, int update_model) -{ - Check(this); - Check_Pointer(record); - - Subsystem::WriteUpdateRecord(record, update_model); - - UpdateRecord *update = (UpdateRecord*)record; - update->percentOpen = percentOpen; - update->recordLength = sizeof(*update); - Check_Fpu(); -} - -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// -void - Door::SlideDoor(Scalar time_slice) + Door::SlideDoor(Scalar) { Check(this); // - //------------------------------------------------------------ - // Advance the clock, then branch based upon our current state - //------------------------------------------------------------ + //-------------------------------------------------------------------------- + // The door is clockwork. Its position is a function of how long the race + // has been running, not of a countdown integrated frame by frame, so: // - int new_state; - if (time_slice > 1.0f) + // - every machine puts this door in the same place from the same mission + // clock, without a byte crossing the wire, + // - a frame hitch of any length costs nothing, because there is no + // accumulated state left to fall behind (the old code dropped any slice + // over a second outright and never got that time back). + // + // Phase zero is the instant the door starts to close, fully open, which is + // where the original state machine began from DefaultState: + // + // [0, travel) Closing 1 -> 0 + // [travel, travel+dead) Closed 0 + // [travel+dead, 2travel+dead) Opening 0 -> 1 + // [2travel+dead, cycle) Opened 1 + //-------------------------------------------------------------------------- + // + if (cycleTime <= 0.0f) { + MoveCollisionVolume(0.0f); + SetSimulationState(Closed); Check_Fpu(); return; } - phaseTimeRemaining -= time_slice; - Scalar percent_open; - switch (GetSimulationState()) + + Check(application); + Scalar phase = fmod(application->GetMissionElapsed() - phaseOffset, cycleTime); + if (phase < 0.0f) { + phase += cycleTime; + } // - //------------------------------------------------------------------------ - // If the door is not done opening, set its new position, otherwise branch - // to the opened state - //------------------------------------------------------------------------ + //-------------------------------------------------------------------------- + // Pick the band. Each division below is guarded by the comparison that + // selected the branch, so a door with a zero travelTime or deadTime simply + // loses that band rather than dividing by zero. + //-------------------------------------------------------------------------- // - case Opening: -Door_Opening: - new_state = Opening; - if (phaseTimeRemaining > 0.0f) - { - percent_open = 1.0f - phaseTimeRemaining/travelTime; - } - else - { - phaseTimeRemaining += deadTime; -// DEBUG_STREAM << GetEntity()->GetEntityID() << " door opened @ " -// << application->GetSecondsRemainingInGame() << endl; - goto Door_Opened; - } - currentVelocity.Subtract( - worldExtent, - GetEntity()->localOrigin.linearPosition - ); - currentVelocity /= travelTime; - Check_Fpu(); - break; + Scalar open_start = travelTime + deadTime; + int new_state; + Scalar percent_open; - // - //------------------------------------------------------------- - // If the door is ready to start closing, jump to closing state - //------------------------------------------------------------- - // - case Opened: -Door_Opened: - new_state = Opened; - if (phaseTimeRemaining <= 0.0f) - { - phaseTimeRemaining += travelTime; -// DEBUG_STREAM << GetEntity()->GetEntityID() << " door closing @ " -// << application->GetSecondsRemainingInGame() << endl; - goto Door_Closing; - } - percent_open = 1.0f; - currentVelocity = Vector3D::Identity; - Check_Fpu(); - break; - - // - //------------------------------------------------------------------------ - // If the door is not done closing, set its new position, otherwise branch - // to the closed state - //------------------------------------------------------------------------ - // - case DefaultState: - phaseTimeRemaining = travelTime; -// DEBUG_STREAM << GetEntity()->GetEntityID() << " door default @ " -// << application->GetSecondsRemainingInGame() << endl; - case Closing: -Door_Closing: + if (phase < travelTime) + { new_state = Closing; - if (phaseTimeRemaining > 0.0f) - { - percent_open = phaseTimeRemaining/travelTime; - } - else - { - phaseTimeRemaining += deadTime; -// DEBUG_STREAM << GetEntity()->GetEntityID() << " door closed @ " -// << application->GetSecondsRemainingInGame() << endl; - goto Door_Closed; - } + percent_open = 1.0f - phase/travelTime; currentVelocity.Subtract( GetEntity()->localOrigin.linearPosition, worldExtent ); currentVelocity /= travelTime; - Check_Fpu(); - break; - - // - //------------------------------------------------------------- - // If the door is ready to start opening, jump to opening state - //------------------------------------------------------------- - // - case Closed: -Door_Closed: + } + else if (phase < open_start) + { new_state = Closed; - if (phaseTimeRemaining <= 0.0f) - { - phaseTimeRemaining += travelTime; -// DEBUG_STREAM << GetEntity()->GetEntityID() << " door opening @ " -// << application->GetSecondsRemainingInGame() << endl; - goto Door_Opening; - } percent_open = 0.0f; currentVelocity = Vector3D::Identity; - Check_Fpu(); - break; - + } + else if (phase < open_start + travelTime) + { + new_state = Opening; + percent_open = (phase - open_start)/travelTime; + currentVelocity.Subtract( + worldExtent, + GetEntity()->localOrigin.linearPosition + ); + currentVelocity /= travelTime; + } + else + { + new_state = Opened; + percent_open = 1.0f; + currentVelocity = Vector3D::Identity; } // @@ -344,7 +268,8 @@ Door::Door( // // Initialize variables // - phaseTimeRemaining = 0.0f; + phaseOffset = 0.0f; + cycleTime = 2.0f*(travelTime + deadTime); currentPosition = Point3D::Identity; SetPerformance(&Door::SlideDoor); diff --git a/MUNGA/DOOR.h b/MUNGA/DOOR.h index fd966ba..0b70812 100644 --- a/MUNGA/DOOR.h +++ b/MUNGA/DOOR.h @@ -20,16 +20,11 @@ struct Door__SubsystemResource: collisionID; }; -//########################################################################## -//##################### Chute::UpdateRecord ##################### -//########################################################################## - -struct Door__UpdateRecord : - public Subsystem::UpdateRecord -{ - Scalar - percentOpen; -}; +// +// A door has no update record. It is Hermit clockwork - every host builds +// its own out of the map stream and derives the position from the mission +// clock, so there is nothing to publish and nothing to receive. +// //########################################################################## //######################### CLASS Door ######################## @@ -91,7 +86,6 @@ public: typedef void (Door::*Performance)(Scalar time_slice); - typedef Door__UpdateRecord UpdateRecord; void SetPerformance(Performance performance) @@ -109,12 +103,6 @@ public: GetFirstBoxedSolid() {Check(this); return collisionVolumes;} -protected: - void - WriteUpdateRecord(Simulation::UpdateRecord *message, int update_model); - void - ReadUpdateRecord(Simulation::UpdateRecord *message); - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Construction and Destruction // @@ -152,9 +140,18 @@ private: worldExtent; Scalar - phaseTimeRemaining, travelTime, - deadTime; + deadTime, + // + // Where in the cycle this door sits at mission time zero, and the + // length of one full open-close-open cycle. phaseOffset is not in + // the subsystem resource yet: every door in the game is in lockstep, + // and adding a field to Door__SubsystemResource changes its sizeof, + // which invalidates every prebuilt .res. Wire it to a "PhaseOffset" + // notation entry when there is a reason to rebuild resources. + // + phaseOffset, + cycleTime; int collisionVolumeCount; diff --git a/MUNGA/DOORFRAM.cpp b/MUNGA/DOORFRAM.cpp index 6931710..3c7d2c7 100644 --- a/MUNGA/DOORFRAM.cpp +++ b/MUNGA/DOORFRAM.cpp @@ -126,8 +126,15 @@ Logical } creation_message->classToCreate = RegisteredClass::DoorFrameClassID; + // + // Hermit, not Master: every host builds its own doorframe out of the map + // stream (see the DoorFrameClassID exemption in LoadMapStream) and runs it + // off the mission clock. Hermit is the instance kind DynamicEntityCreation + // does NOT broadcast, which is what stops N machines each announcing the + // same doorframe and producing N-squared of them. + // creation_message->instanceFlags = - MasterInstance|DynamicFlag|MapFlag|TrappedFlag; + HermitInstance|DynamicFlag|MapFlag|TrappedFlag; return true; } diff --git a/MUNGA/INTEREST.cpp b/MUNGA/INTEREST.cpp index e23ea1e..cf04c8d 100644 --- a/MUNGA/INTEREST.cpp +++ b/MUNGA/INTEREST.cpp @@ -411,8 +411,20 @@ void // supposed to //--------------------------------------------------------------------- // + // + // Doorframes are exempt: they are clockwork, computed identically on + // every machine from the mission clock, so each host builds its own + // Hermit copy instead of one host owning it and replicating. That + // also means they survive a peer dropping, which owned doors do not - + // ownership transfer is not implemented. Note this changes how many + // times the cursor below is advanced, so old and new builds deal the + // remaining map entities differently: they cannot share a session. + // Logical post_make_message = True; - if (Entity::EntityFlagsIsMap(message->instanceFlags)) + if ( + Entity::EntityFlagsIsMap(message->instanceFlags) + && message->classToCreate != DoorFrameClassID + ) { Check(application); HostManager *host_manager = application->GetHostManager(); diff --git a/MUNGA/UPDATE.cpp b/MUNGA/UPDATE.cpp index 1ff108b..3347bc8 100644 --- a/MUNGA/UPDATE.cpp +++ b/MUNGA/UPDATE.cpp @@ -168,10 +168,20 @@ void // //----------------------------------------------------------------------- - // If update message is not null then send the change + // If update message is not null then send the change. + // + // The dynamic master socket holds Independant and Hermit instances as + // well as masters, and neither of those publishes: an Independant runs + // its own simulation on every host, and a Hermit is not replicated at + // all. EntityUpdateReplicants asserts MasterInstance, so the caller is + // the one that has to make that true - the clockwork doorframes are + // Hermits and would otherwise arrive there. //----------------------------------------------------------------------- // - if (update_message != NULL) + if ( + update_message != NULL + && entity->GetInstance() == Entity::MasterInstance + ) { Check(update_message); diff --git a/RP_L4/RPL4LOBBY.cpp b/RP_L4/RPL4LOBBY.cpp index 32d6da3..94826ae 100644 --- a/RP_L4/RPL4LOBBY.cpp +++ b/RP_L4/RPL4LOBBY.cpp @@ -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') {