diff --git a/network/entities.cpp b/network/entities.cpp index d8c78b74..2e33aee1 100644 --- a/network/entities.cpp +++ b/network/entities.cpp @@ -16,6 +16,7 @@ http://mozilla.org/MPL/2.0/. #include "vehicle/Driver.h" #include "vehicle/Train.h" #include "utilities/Logs.h" +#include "utilities/Globals.h" namespace network { @@ -28,6 +29,16 @@ PeerId allocate_peer_id() return next++; } +bool is_multiplayer() +{ + return Global.network_client.has_value() || !Global.network_servers.empty(); +} + +bool is_authority() +{ + return !Global.network_client.has_value(); +} + bool is_drivable(TDynamicObject const *Vehicle) { if (Vehicle == nullptr || Vehicle->MoverParameters == nullptr) diff --git a/network/entities.h b/network/entities.h index 4b87fdc0..2e749a65 100644 --- a/network/entities.h +++ b/network/entities.h @@ -92,6 +92,12 @@ extern entity_registry Entities; // hands out ids for connecting peers; PEER_HOST is reserved for the local participant PeerId allocate_peer_id(); +// true when this run takes part in a session at all, as host or as client +bool is_multiplayer(); +// true when this peer decides what happens in the world. a standalone game and the host +// of a session do; a client does not, it only carries out what the server hands it +bool is_authority(); + // true for vehicles a player can take over bool is_drivable(TDynamicObject const *Vehicle); diff --git a/scene/scene.cpp b/scene/scene.cpp index 14825705..871c79be 100644 --- a/scene/scene.cpp +++ b/scene/scene.cpp @@ -21,6 +21,7 @@ http://mozilla.org/MPL/2.0/. #include "scene/sn_utils.h" #include "rendering/renderer.h" #include "widgets/map_objects.h" +#include "network/entities.h" namespace scene { @@ -121,20 +122,20 @@ basic_cell::update_traction( TDynamicObject *Vehicle, int const Pantographindex // legacy method, updates sounds and polls event launchers within radius around specified point void -basic_cell::update_events() { +basic_cell::update_events( event_poll_point const &Point ) { // event launchers for( auto *launcher : m_eventlaunchers ) { - glm::dvec3 campos = Global.pCamera.Pos; + glm::dvec3 referencepoint = Point.location; double radius = launcher->dRadius; - if (launcher->train_triggered && simulation::Train) { - campos = simulation::Train->Dynamic()->HeadPosition(); - radius *= Timer::GetDeltaTime() * simulation::Train->Dynamic()->GetVelocity() * 0.277; + if( launcher->train_triggered && Point.vehicle != nullptr ) { + referencepoint = Point.vehicle->HeadPosition(); + radius *= Timer::GetDeltaTime() * Point.vehicle->GetVelocity() * 0.277; } if( launcher->check_conditions() && ( radius < 0.0 - || glm::distance2( launcher->location(), campos ) < launcher->dRadius ) ) { + || glm::distance2( launcher->location(), referencepoint ) < launcher->dRadius ) ) { if( launcher->check_activation() ) launch_event( launcher, true ); if( launcher->check_activation_key() ) @@ -728,13 +729,17 @@ basic_section::update_traction( TDynamicObject *Vehicle, int const Pantographind // legacy method, polls event launchers within radius around specified point void -basic_section::update_events( glm::dvec3 const &Location, float const Radius ) { +basic_section::update_events( std::vector const &Points, float const Radius ) { for( auto &cell : m_cells ) { - if( glm::length2( cell.area().center - Location ) < sq(cell.area().radius + Radius) ) { - // we reject cells which aren't within our area of interest - cell.update_events(); + for( auto const &point : Points ) { + if( glm::length2( cell.area().center - point.location ) < sq(cell.area().radius + Radius) ) { + cell.update_events( point ); + // a cell is polled at most once per frame, no matter how many players are + // standing near it - launcher activation counters are stateful + break; + } } } } @@ -1053,11 +1058,42 @@ basic_region::update_events() { if( false == simulation::is_ready ) { return; } - // render events and sounds from sectors near enough to the viewer + if( false == network::is_authority() ) { + // a client does not decide what fires; it carries out what the server sends it + return; + } + auto const range = EU07_SECTIONSIZE; // arbitrary range - auto const §ionlist = sections( Global.pCamera.Pos, range ); + + std::vector points; + points.emplace_back( event_poll_point{ + Global.pCamera.Pos, + ( simulation::Train != nullptr ? simulation::Train->Dynamic() : nullptr ) } ); + + if( true == network::is_multiplayer() ) { + // gameplay may not hang on where the host happens to be looking, so the launchers + // are polled around every vehicle somebody is actually working + for( auto const &entry : network::Entities.entries() ) { + if( entry.crew_count == 0 ) { continue; } + auto const *vehicle { simulation::Vehicles.find( entry.name ) }; + if( vehicle == nullptr ) { continue; } + if( simulation::Train != nullptr && simulation::Train->Dynamic() == vehicle ) { continue; } + + points.emplace_back( event_poll_point{ vehicle->HeadPosition(), vehicle } ); + } + } + + // sections() hands back a scratchpad it reuses, so the union has to be collected first + std::vector sectionlist; + for( auto const &point : points ) { + auto const &nearby = sections( point.location, range ); + sectionlist.insert( sectionlist.end(), nearby.begin(), nearby.end() ); + } + std::sort( sectionlist.begin(), sectionlist.end() ); + sectionlist.erase( std::unique( sectionlist.begin(), sectionlist.end() ), sectionlist.end() ); + for( auto *section : sectionlist ) { - section->update_events( Global.pCamera.Pos, range ); + section->update_events( points, range ); } } diff --git a/scene/scene.h b/scene/scene.h index 9f5201f8..37779a08 100644 --- a/scene/scene.h +++ b/scene/scene.h @@ -31,6 +31,14 @@ class opengl33_renderer; namespace scene { +// a place the event launchers are polled around. in a single player game there is just +// one of these - the camera - but a session has to look at every crewed vehicle, because +// gameplay may not depend on where any single player happens to be pointing their camera +struct event_poll_point { + glm::dvec3 location; + TDynamicObject const *vehicle { nullptr }; +}; + int constexpr EU07_CELLSIZE = 250; int constexpr EU07_SECTIONSIZE = 1000; int constexpr EU07_REGIONSIDESECTIONCOUNT = 500; // number of sections along a side of square region @@ -93,7 +101,7 @@ public: update_traction( TDynamicObject *Vehicle, int const Pantographindex ); // legacy method, polls event launchers within radius around specified point void - update_events(); + update_events( event_poll_point const &Point ); // legacy method, updates sounds within radius around specified point void update_sounds(); @@ -264,7 +272,7 @@ public: update_traction( TDynamicObject *Vehicle, int const Pantographindex ); // legacy method, updates sounds and polls event launchers within radius around specified point void - update_events( glm::dvec3 const &Location, float const Radius ); + update_events( std::vector const &Points, float const Radius ); // legacy method, updates sounds and polls event launchers within radius around specified point void update_sounds( glm::dvec3 const &Location, float const Radius ); diff --git a/simulation/simulation.cpp b/simulation/simulation.cpp index a1493f63..16bf9377 100644 --- a/simulation/simulation.cpp +++ b/simulation/simulation.cpp @@ -295,16 +295,26 @@ void state_manager::process_commands() { std::string event_name; std::string vehicle_name; + std::string delay_text; std::getline(ss, event_name, '%'); std::getline(ss, vehicle_name, '%'); + std::getline(ss, delay_text, '%'); basic_event *ev = Events.FindEvent(event_name); TDynamicObject *vehicle = nullptr; if (!vehicle_name.empty()) vehicle = simulation::Vehicles.find(vehicle_name); + double delay = 0.0; + if (!delay_text.empty()) { + try { delay = std::stod(delay_text); } + catch (std::exception const &) { delay = 0.0; } + } + + // this is the authority speaking, whether it reached us over the wire or came + // straight from our own event manager if (ev) - Events.AddToQuery(ev, vehicle); + Events.AddToQuery(ev, vehicle, delay, event_launch::authority); } if (commanddata.command == user_command::setlight) { diff --git a/world/Event.cpp b/world/Event.cpp index 4e042b29..1b9ad488 100644 --- a/world/Event.cpp +++ b/world/Event.cpp @@ -33,6 +33,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Timer.h" #include "utilities/Logs.h" #include "widgets/map_objects.h" +#include "network/entities.h" void basic_event::event_conditions::bind( basic_event::node_sequence *Nodes ) { @@ -1306,13 +1307,15 @@ multi_event::run_() { if( std::get( childwrapper ) != conditiontest ) { continue; } if( childevent != this ) { - // normalnie dodać - simulation::Events.AddToQuery( childevent, m_activator ); + // normalnie dodać. + // the parent already ran on every peer, so its children are not a local + // decision any more and must not be sent around a second time + simulation::Events.AddToQuery( childevent, m_activator, 0.0, event_launch::authority ); } else { // jeśli ma być rekurencja to musi mieć sensowny okres powtarzania if( m_delay >= 5.0 ) { - simulation::Events.AddToQuery( this, m_activator ); + simulation::Events.AddToQuery( this, m_activator, 0.0, event_launch::authority ); } } } @@ -2288,8 +2291,15 @@ event_manager::queue_receivers( radio_message const Message, glm::dvec3 const &L void event_manager::update() { - // process currently queued events + // process currently queued events. this runs on every peer: a client still has a + // queue to service, with its delays and its execution CheckQuery(); + + if( false == network::is_authority() ) { + // evaluating the conditions, however, is the authority's job alone + return; + } + // test list of global events for possible new additions to the queue for( auto *launcher : m_launcherqueue ) { if (launcher->check_conditions() && launcher->Event1) { @@ -2357,8 +2367,9 @@ event_manager::insert( basic_event *Event ) { m_eventmap.emplace( Event->m_name, m_events.size() - 1 ); if( Event->m_ignored != true && contains(Event->m_name, "onstart") ) { - // event uruchamiany automatycznie po starcie - AddToQuery( Event, nullptr ); + // event uruchamiany automatycznie po starcie. + // scenario init runs identically on every peer, so it needs no replication + AddToQuery( Event, nullptr, 0.0, event_launch::authority ); } } @@ -2393,9 +2404,58 @@ event_manager::FindEvent( std::string const &Name ) return FindEventById(GetEventId(Name)); } +// how long a request handed to the session is considered still on its way. it only has to +// outlast the round trip through the command queue; the ceiling keeps a request that got +// lost somewhere from silencing its event for good +double const REPLICATION_TIMEOUT { 5.0 }; + +// hands a locally evaluated launch over to the session authority +bool +event_manager::defer_to_authority( basic_event *Event, TDynamicObject const *Owner, double delay ) { + + if( Event->m_passive ) { return false; } + if( Event->m_inqueue > 0 ) { return false; } + + // an event whose request is still travelling must not be requested again, or a + // condition that stays true would flood the session. instant events are exempt: they + // are edge driven and every single one of their additions has to get through + if( ( false == Event->is_instant() ) + && ( Event->m_replicationtime >= 0.0 ) + && ( Timer::GetTime() - Event->m_replicationtime < REPLICATION_TIMEOUT ) ) { + return false; + } + + Event->m_replicationtime = Timer::GetTime(); + + // the very same command a scenario or a script would use; it comes back through the + // command queue and is carried out by every peer, this one included + auto const payload { + Event->m_name + + "%" + ( Owner != nullptr ? Owner->name() : std::string() ) + + "%" + std::to_string( delay ) }; + + m_relay.post( user_command::queueevent, 0.0, 0.0, GLFW_PRESS, 0, glm::vec3( 0.0f ), &payload ); + + WriteLog( "net: event replicated: " + Event->m_name + ( Owner != nullptr ? " (by " + Owner->name() + ")" : "" ), logtype::net ); + + return true; +} + // legacy method, inserts specified event in the event query bool -event_manager::AddToQuery( basic_event *Event, TDynamicObject const *Owner, double delay ) { +event_manager::AddToQuery( basic_event *Event, TDynamicObject const *Owner, double delay, event_launch const Source ) { + + if( Event == nullptr ) { return false; } + + if( ( Source == event_launch::evaluation ) + && ( true == network::is_multiplayer() ) ) { + // the world does not decide on its own what happens in a session: the authority + // evaluates the condition and hands the outcome to everybody + if( false == network::is_authority() ) { return false; } + return defer_to_authority( Event, Owner, delay ); + } + + Event->m_replicationtime = -1.0; if( Event->m_passive ) { return false; } // jeśli może być dodany do kolejki (nie używany w skanowaniu) if( Event->m_inqueue > 0 ) { return false; } // jeśli nie dodany jeszcze do kolejki @@ -2512,7 +2572,8 @@ event_manager::InitEvents() { //łączenie eventów z pozostałymi obiektami for( auto *event : m_events ) { event->init(); - if( event->m_delay < 0 ) { AddToQuery( event, nullptr ); } + // scenario init runs identically on every peer, so it needs no replication + if( event->m_delay < 0 ) { AddToQuery( event, nullptr, 0.0, event_launch::authority ); } } } diff --git a/world/Event.h b/world/Event.h index 52ee3468..540a01cf 100644 --- a/world/Event.h +++ b/world/Event.h @@ -21,6 +21,14 @@ http://mozilla.org/MPL/2.0/. #include "scripting/lua.h" #endif +// where a request to put an event in the queue came from. in a session only the +// authority evaluates conditions; everybody, the authority included, runs the event when +// it comes back as a replicated queueevent, so it fires exactly once on every peer +enum class event_launch { + evaluation, // somebody's local condition check + authority // decided by the session authority, or by deterministic scenario init +}; + // common event interface class basic_event { @@ -98,6 +106,7 @@ public: double m_delay { 0.0 }; double m_delayrandom { 0.0 }; // zakres dodatkowego opóźnienia // standardowo nie będzie dodatkowego losowego opóźnienia double m_delaydeparture { std::numeric_limits::quiet_NaN() }; // departure-based event delay + double m_replicationtime { -1.0 }; // when the authority last handed this one to the session protected: // types @@ -683,7 +692,8 @@ public: } // legacy method, inserts specified event in the event query bool - AddToQuery( basic_event *Event, TDynamicObject const *Owner, double delay = 0.0 ); + AddToQuery( basic_event *Event, TDynamicObject const *Owner, double delay = 0.0, + event_launch const Source = event_launch::evaluation ); // legacy method, executes queued events bool CheckQuery(); @@ -719,6 +729,11 @@ private: basic_table m_radiodrivenlaunchers; eventlauncher_sequence m_launcherqueue; command_relay m_relay; +// methods + // hands a locally evaluated launch over to the session, so that it reaches every peer + // through the ordinary queueevent command instead of firing here alone + bool + defer_to_authority( basic_event *Event, TDynamicObject const *Owner, double delay ); };