diff --git a/network/entities.cpp b/network/entities.cpp index 2e33aee1..270eeab5 100644 --- a/network/entities.cpp +++ b/network/entities.cpp @@ -44,10 +44,11 @@ bool is_drivable(TDynamicObject const *Vehicle) if (Vehicle == nullptr || Vehicle->MoverParameters == nullptr) return false; - // anything with its own drive, plus anything the scenario already put a driver in - // (this covers driving trailers of multiple units, which carry no engine of their own) - // TODO: a cab count exposed by TMoverParameters would be a more precise test - return Vehicle->MoverParameters->EngineType != TEngineType::None || Vehicle->Mechanik != nullptr; + auto const &mover = *Vehicle->MoverParameters; + + // a driving position is what counts, not an engine: the driving trailer of a multiple + // unit has a full desk and no traction of its own, and the scenario puts nobody in it + return mover.EngineType != TEngineType::None || Vehicle->Mechanik != nullptr || mover.MainCtrlPosNo > 0 || mover.BrakeCtrlPosNo > 0; } void entity_registry::clear() @@ -61,10 +62,12 @@ void entity_registry::build() { clear(); + // every vehicle gets an id, not only the ones worth driving. commands are addressed by + // it, and a player working a train may walk into any of its cars std::vector names; for (TDynamicObject *vehicle : simulation::Vehicles.sequence()) { - if (!is_drivable(vehicle)) + if (vehicle == nullptr) continue; names.emplace_back(vehicle->name()); } @@ -88,10 +91,22 @@ void entity_registry::build() m_byname.emplace(name, entry.id); } + if (m_entries.size() > 0xffff) + { + // the command channel packs the recipient into sixteen bits + ErrorLog("net: scenario holds more vehicles than the command channel can address", logtype::net); + } + reindex(); refresh(); - WriteLog("net: assigned network ids to " + std::to_string(m_entries.size()) + " drivable vehicles", logtype::net); + uint32_t drivable{0}; + for (auto const &entry : m_entries) + if (entry.drivable) + ++drivable; + + WriteLog("net: assigned network ids to " + std::to_string(m_entries.size()) + " vehicles, " + std::to_string(drivable) + " of them with a driving position", + logtype::net); } void entity_registry::adopt(std::vector const &Entries) @@ -112,15 +127,110 @@ void entity_registry::reindex() void entity_registry::refresh() { + // only there so that a malformed consist cannot spin us forever + int const CONSIST_LIMIT{256}; + for (auto &entry : m_entries) { TDynamicObject const *vehicle = resolve(entry.id); + entry.consist_id = entry.id; + entry.consist_size = 1; + entry.consist_lead = false; + entry.drivable = is_drivable(vehicle); entry.ai_active = (vehicle != nullptr && vehicle->Mechanik != nullptr && vehicle->Mechanik->AIControllFlag); - entry.crew_count = Crews.count(entry.id); entry.crew_capacity = CREW_CAPACITY; - entry.claimable = (vehicle != nullptr && entry.crew_count < entry.crew_capacity); } + + // group the vehicles into the trains they are currently coupled into. this is redone + // every time, so a train that comes apart turns into two trains here, and the crews + // follow whichever half each player happens to be sitting in + std::unordered_set visited; + + for (auto const &entry : m_entries) + { + TDynamicObject *vehicle = resolve(entry.id); + if (vehicle == nullptr || visited.count(vehicle) > 0) + continue; + + TDynamicObject *front = vehicle; + for (int step = 0; step < CONSIST_LIMIT; ++step) + { + TDynamicObject *previous = front->Prev(); + if (previous == nullptr || previous == vehicle || visited.count(previous) > 0) + break; + front = previous; + } + + std::vector members; + TDynamicObject *member = front; + for (int step = 0; step < CONSIST_LIMIT && member != nullptr; ++step, member = member->Next()) + { + if (visited.count(member) > 0) + break; + visited.emplace(member); + + auto const id = id_of(member->name()); + if (id != ENTITY_NONE) + members.emplace_back(id); + } + + if (members.empty()) + continue; + + // the lowest id in the set names the train, and the lowest one that can actually + // be driven stands for it in the lobby + NetworkEntityId consist{members.front()}; + NetworkEntityId lead{ENTITY_NONE}; + uint8_t crew{0}; + + for (auto const id : members) + { + consist = std::min(consist, id); + crew = (uint8_t)std::min(255, crew + Crews.count(id)); + + auto const *candidate = find(id); + if (candidate != nullptr && candidate->drivable && (lead == ENTITY_NONE || id < lead)) + lead = id; + } + if (lead == ENTITY_NONE) + lead = consist; + + for (auto const id : members) + { + auto const lookup = m_byid.find(id); + if (lookup == m_byid.end()) + continue; + + auto &target = m_entries[lookup->second]; + target.consist_id = consist; + target.consist_size = (uint8_t)std::min(255, members.size()); + target.consist_lead = (id == lead); + target.crew_count = crew; + target.claimable = (crew < target.crew_capacity); + } + } +} + +NetworkEntityId entity_registry::consist_of(NetworkEntityId Id) const +{ + auto const *entry = find(Id); + return entry != nullptr ? entry->consist_id : ENTITY_NONE; +} + +std::vector entity_registry::consist_members(NetworkEntityId Consist) const +{ + std::vector members; + if (Consist == ENTITY_NONE) + return members; + + for (auto const &entry : m_entries) + { + if (entry.consist_id == Consist) + members.emplace_back(entry.id); + } + + return members; } NetworkEntityId entity_registry::id_of(std::string const &Name) const diff --git a/network/entities.h b/network/entities.h index 2e749a65..d743a8fb 100644 --- a/network/entities.h +++ b/network/entities.h @@ -36,20 +36,35 @@ constexpr NetworkEntityId ENTITY_NONE = 0; // so raising this number does not call for a structural change constexpr uint8_t CREW_CAPACITY = 2; -// authoritative description of a single drivable vehicle, as published by the server +// authoritative description of a single vehicle, as published by the server. +// every vehicle is listed, not only the ones worth driving: commands are addressed by +// this id, and a player may walk into any member of the train they are working struct vehicle_entry { NetworkEntityId id{ENTITY_NONE}; std::string name; + // the train this vehicle is currently coupled into, named by the lowest id in the set. + // a player belongs to a train, not to a single car, so this is what the crew, the + // permissions and the lobby all work with. it is recomputed as vehicles couple and + // uncouple, which is what splits a crew between the halves of a train that came apart + NetworkEntityId consist_id{ENTITY_NONE}; + // how many vehicles that train has, and whether this entry is the one that stands for + // it in the lobby + uint8_t consist_size{1}; + bool consist_lead{false}; + // crew of the whole train, not of this vehicle alone uint8_t crew_count{0}; uint8_t crew_capacity{CREW_CAPACITY}; bool ai_active{false}; bool claimable{false}; + // has a driving position of its own + bool drivable{false}; bool operator==(vehicle_entry const &Other) const { - return id == Other.id && name == Other.name && crew_count == Other.crew_count && crew_capacity == Other.crew_capacity && ai_active == Other.ai_active && - claimable == Other.claimable; + return id == Other.id && name == Other.name && consist_id == Other.consist_id && consist_size == Other.consist_size && consist_lead == Other.consist_lead && + crew_count == Other.crew_count && crew_capacity == Other.crew_capacity && ai_active == Other.ai_active && claimable == Other.claimable && + drivable == Other.drivable; } bool operator!=(vehicle_entry const &Other) const { return !(*this == Other); } }; @@ -74,6 +89,9 @@ public: NetworkEntityId id_of(std::string const &Name) const; NetworkEntityId id_of(TDynamicObject const *Vehicle) const; + // the train a vehicle belongs to, and everything coupled into it + NetworkEntityId consist_of(NetworkEntityId Id) const; + std::vector consist_members(NetworkEntityId Consist) const; std::string name_of(NetworkEntityId Id) const; TDynamicObject *resolve(NetworkEntityId Id) const; vehicle_entry const *find(NetworkEntityId Id) const; @@ -98,7 +116,8 @@ bool is_multiplayer(); // 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 +// true for vehicles that carry a driving position of their own. note that a driving +// trailer has one without having an engine, and without the scenario putting a driver in bool is_drivable(TDynamicObject const *Vehicle); } // namespace network diff --git a/network/message.cpp b/network/message.cpp index b525b80d..87a27def 100644 --- a/network/message.cpp +++ b/network/message.cpp @@ -57,9 +57,11 @@ void network::vehicle_list::serialize(std::ostream &stream) const { sn_utils::ls_uint32(stream, entry.id); sn_utils::s_str(stream, entry.name); + sn_utils::ls_uint32(stream, entry.consist_id); + sn_utils::s_uint8(stream, entry.consist_size); sn_utils::s_uint8(stream, entry.crew_count); sn_utils::s_uint8(stream, entry.crew_capacity); - sn_utils::s_uint8(stream, (uint8_t)((entry.ai_active ? 1 : 0) | (entry.claimable ? 2 : 0))); + sn_utils::s_uint8(stream, (uint8_t)((entry.ai_active ? 1 : 0) | (entry.claimable ? 2 : 0) | (entry.drivable ? 4 : 0) | (entry.consist_lead ? 8 : 0))); } } @@ -75,11 +77,15 @@ void network::vehicle_list::deserialize(std::istream &stream) vehicle_entry entry; entry.id = sn_utils::ld_uint32(stream); entry.name = sn_utils::d_str(stream); + entry.consist_id = sn_utils::ld_uint32(stream); + entry.consist_size = sn_utils::d_uint8(stream); entry.crew_count = sn_utils::d_uint8(stream); entry.crew_capacity = sn_utils::d_uint8(stream); uint8_t const flags = sn_utils::d_uint8(stream); entry.ai_active = (flags & 1) != 0; entry.claimable = (flags & 2) != 0; + entry.drivable = (flags & 4) != 0; + entry.consist_lead = (flags & 8) != 0; vehicles.emplace_back(entry); } } diff --git a/network/network.cpp b/network/network.cpp index 266c7be5..1f2ec880 100644 --- a/network/network.cpp +++ b/network/network.cpp @@ -524,8 +524,12 @@ void network::client::handle_message(std::shared_ptr conn, const mes // the routine correction is what keeps us in line; a full resync is only worth // asking for when even that is not catching up if (result.worst_position_error > RESYNC_POSITION_ERROR) { - if (++bad_corrections >= RESYNC_BAD_CORRECTIONS) { + ++bad_corrections; + + if ((bad_corrections >= RESYNC_BAD_CORRECTIONS) + && (last_resync_tick == 0 || Global.simulation_tick > last_resync_tick + RESYNC_COOLDOWN_TICKS)) { bad_corrections = 0; + last_resync_tick = Global.simulation_tick; WriteLog("net: " + std::to_string((int)result.worst_position_error) + " m out of place after a correction, asking for a full resync", logtype::net); send_resync_request(Global.simulation_tick, 0); diff --git a/network/network.h b/network/network.h index e29c9360..436cd74b 100644 --- a/network/network.h +++ b/network/network.h @@ -101,9 +101,12 @@ namespace network size_t reconnect_delay = 0; const size_t RECONNECT_DELAY_FRAMES = 60; - const float MAX_BUFFER_SIZE = 60.0f; + // the queue of authoritative frames waiting to be played out. every frame sitting + // in here is a frame of delay between pressing a key and seeing it happen, so it + // is kept as short as the connection allows rather than a comfortable second deep + const float MAX_BUFFER_SIZE = 12.0f; const float JITTERINESS_MIX = 0.998f; - const float TARGET_MIN = 2.0f; + const float TARGET_MIN = 1.0f; const float TARGET_MIX = 0.98f; const float JITTERINESS_MULTIPIER = 2.0f; const float CONSUME_MULTIPIER = 0.05f; @@ -113,10 +116,13 @@ namespace network // how far out of place a correction found us, in a row. a full resync is only // worth asking for when the routine stream is not catching up on its own int bad_corrections = 0; + uint64_t last_resync_tick = 0; static constexpr double RESYNC_POSITION_ERROR = 25.0; static constexpr int RESYNC_BAD_CORRECTIONS = 5; + // a correction needs time to take hold; asking again before it has is pointless + static constexpr uint64_t RESYNC_COOLDOWN_TICKS = 600; - float last_target = 20.0f; + float last_target = 2.0f; float jitteriness = 1.0f; float consume_counter = 0.0f; diff --git a/network/session.cpp b/network/session.cpp index 75200076..0022657b 100644 --- a/network/session.cpp +++ b/network/session.cpp @@ -70,8 +70,7 @@ command_verdict validate_command(PeerId Peer, user_command Command, uint32_t Rec if (target == command_target::vehicle) { - // a player works the controls of the vehicle they are on, and of no other - return Crews.is_member(Peer, (NetworkEntityId)(Recipient & 0xffff)) ? command_verdict::accepted : command_verdict::not_in_crew; + return may_control(Peer, (NetworkEntityId)(Recipient & 0xffff)) ? command_verdict::accepted : command_verdict::not_in_crew; } if (target == command_target::simulation) @@ -93,11 +92,24 @@ claim_result claim_by_name(PeerId Peer, std::string const &Vehicle, NetworkEntit return claim_result::unknown_vehicle; auto const previous = Crews.vehicle_of(Peer); + if (previous == Entity) + return claim_result::already_member; + + if ((previous != ENTITY_NONE) && (Entities.consist_of(previous) == Entities.consist_of(Entity))) + { + // walking to another car of the train you are already working is not a new claim, + // so it asks nothing of the capacity. the seat still moves, because which car a + // player sits in decides which half they keep when the train comes apart + Crews.leave(Peer, previous); + Crews.take_seat(Peer, Entity); + return claim_result::granted; + } + auto const result = Crews.claim(Peer, Entity); - if ((result == claim_result::granted) && (previous != ENTITY_NONE) && (previous != Entity)) + if ((result == claim_result::granted) && (previous != ENTITY_NONE)) { - // nobody works two vehicles at once + // nobody works two trains at once Crews.leave(Peer, previous); } @@ -152,6 +164,21 @@ void filter_commands(PeerId Peer, command_queue::commands_map &Commands) } } +bool may_control(PeerId Peer, NetworkEntityId Entity) +{ + auto const seat = Crews.vehicle_of(Peer); + if (seat == ENTITY_NONE) + return false; + + auto const train = Entities.consist_of(Entity); + if (train == ENTITY_NONE) + return false; + + // the whole set the player is sitting in, so that the desk of a driving trailer works + // on the motor cars behind it and the cab switches of a multiple unit go through + return Entities.consist_of(seat) == train; +} + vehicle_crew &crew_registry::entry(NetworkEntityId Id) { auto lookup = m_vehicles.find(Id); @@ -178,7 +205,8 @@ claim_result crew_registry::claim(PeerId Peer, NetworkEntityId Id) if (std::find(vehicle.crew.begin(), vehicle.crew.end(), Peer) != vehicle.crew.end()) return claim_result::already_member; - if (vehicle.crew.size() >= published->crew_capacity) + // capacity counts the people on the whole train, not on this one car + if (published->crew_count >= published->crew_capacity) return claim_result::crew_full; if (vehicle.crew.empty()) @@ -373,6 +401,23 @@ void crew_registry::mirror(NetworkEntityId Id, std::vector const &Crew) entry(Id).crew = Crew; } +void crew_registry::take_seat(PeerId Peer, NetworkEntityId Id) +{ + auto &vehicle = entry(Id); + if (std::find(vehicle.crew.begin(), vehicle.crew.end(), Peer) != vehicle.crew.end()) + return; + + if (vehicle.crew.empty()) + { + TDynamicObject const *dynamic = Entities.resolve(Id); + vehicle.ai_before_claim = (dynamic != nullptr && dynamic->Mechanik != nullptr && dynamic->Mechanik->AIControllFlag); + vehicle.ai_restore_pending = false; + } + + vehicle.crew.emplace_back(Peer); + m_pending.emplace_back(Id); +} + std::vector crew_registry::take_pending_updates() { std::vector pending; @@ -425,6 +470,8 @@ void post_authority_command(user_command Command, uint32_t Recipient, glm::vec3 // frames to wait before repeating an authority request that has not taken effect yet static int const ACTION_COOLDOWN_FRAMES = 30; +// how many times to ask a vehicle's AI driver to step aside before giving up on it +static int const AI_HANDOVER_ATTEMPTS = 5; void crew_registry::reconcile() { @@ -457,10 +504,26 @@ void crew_registry::reconcile() if (dynamic->Mechanik != nullptr && dynamic->Mechanik->AIControllFlag) { - // first human on board takes over from the AI - WriteLog("net: " + dynamic->name() + " taken over from the AI driver", logtype::net); - post_authority_command(user_command::aidriverdisable, (uint32_t)command_target::vehicle | train->id(), dynamic->GetPosition(), std::string()); - vehicle.action_cooldown = ACTION_COOLDOWN_FRAMES; + if (vehicle.ai_attempts < AI_HANDOVER_ATTEMPTS) + { + // first human on board takes over from the AI + if (vehicle.ai_attempts == 0) + WriteLog("net: " + dynamic->name() + " taken over from the AI driver", logtype::net); + + ++vehicle.ai_attempts; + post_authority_command(user_command::aidriverdisable, (uint32_t)command_target::vehicle | train->id(), dynamic->GetPosition(), std::string()); + vehicle.action_cooldown = ACTION_COOLDOWN_FRAMES; + } + else if (vehicle.ai_attempts == AI_HANDOVER_ATTEMPTS) + { + // asking again would only fill the log; say so once and leave it + ++vehicle.ai_attempts; + ErrorLog("net: " + dynamic->name() + " will not let go of its AI driver", logtype::net); + } + } + else + { + vehicle.ai_attempts = 0; } continue; } diff --git a/network/session.h b/network/session.h index 937aeb33..73878318 100644 --- a/network/session.h +++ b/network/session.h @@ -34,6 +34,9 @@ struct vehicle_crew // keeps reconcile() from re-posting the same request every single frame while the // previous one is still on its way through the command queue int action_cooldown{0}; + // how many times we have asked the AI to hand this vehicle over. if it will not, that + // is worth saying once rather than asking forever + int ai_attempts{0}; }; enum class claim_result @@ -74,6 +77,9 @@ public: // client side: takes over the crew of one vehicle as published by the server void mirror(NetworkEntityId Id, std::vector const &Crew); + // puts a peer on a vehicle without asking about capacity; used when somebody walks to + // another car of the train they are already working + void take_seat(PeerId Peer, NetworkEntityId Id); // server side: brings the world in line with the crew roster - creates the cab of a // claimed vehicle, hands the vehicle over from the AI and hands it back when the last @@ -138,6 +144,10 @@ std::string describe(command_verdict Verdict); // simply holds every right - there is no separate path for the local participant command_verdict validate_command(PeerId Peer, user_command Command, uint32_t Recipient); +// a player works a train, not a single car: they may operate anything coupled into the +// set they are sitting in, which is what makes the cab switches of a multiple unit work +bool may_control(PeerId Peer, NetworkEntityId Entity); + // walking into a cab with the in-game key is the same request as pressing the button in // the lobby, so it goes through the crew registry rather than being treated as a command // that reaches the whole world. returns the vehicle it resolved to, if any diff --git a/network/snapshot.cpp b/network/snapshot.cpp index dbc4d65a..c2c3482c 100644 --- a/network/snapshot.cpp +++ b/network/snapshot.cpp @@ -38,6 +38,45 @@ double const REPOSITION_TOLERANCE_CORRECTION{2.5}; // --------------------------------------------------------------------------- // what the authority last sent, so that a routine correction only carries changes +// most tracks in a scenery carry no name at all, so a name is useless as an identity. +// their order in the path table comes straight from the scenery file and is therefore the +// same on every peer, which makes the index a usable one +std::vector g_trackindex; +std::unordered_map g_trackids; + +void refresh_track_index() +{ + auto const &paths = simulation::Paths.sequence(); + if (g_trackindex.size() == paths.size()) + return; + + g_trackindex.assign(paths.begin(), paths.end()); + g_trackids.clear(); + for (uint32_t i = 0; i < g_trackindex.size(); ++i) + { + if (g_trackindex[i] != nullptr) + g_trackids.emplace(g_trackindex[i], i); + } +} + +uint32_t track_id_of(TTrack const *Track) +{ + if (Track == nullptr) + return 0xffffffff; + + refresh_track_index(); + + auto const lookup = g_trackids.find(Track); + return lookup != g_trackids.end() ? lookup->second : 0xffffffff; +} + +TTrack *track_by_id(uint32_t const Id) +{ + refresh_track_index(); + + return (Id < g_trackindex.size() ? g_trackindex[Id] : nullptr); +} + std::unordered_map g_lastvehicles; std::unordered_map g_lastmemcells; std::unordered_map g_lastswitches; @@ -137,7 +176,7 @@ void read_session(std::istream &Stream, network::snapshot_mode const Mode) struct vehicle_state { std::string name; - std::string track; + uint32_t track{0xffffffff}; double translation{0.0}; uint8_t axlefirst{0}; uint8_t direction{0}; @@ -164,8 +203,6 @@ struct vehicle_state bool battery{false}; bool converterallow{false}; bool compressorallow{false}; - int32_t cabactive{0}; - int32_t caboccupied{0}; std::array pantenabled{{false, false}}; std::array pantdisabled{{false, false}}; std::array pantactive{{false, false}}; @@ -179,7 +216,7 @@ struct vehicle_state void serialize_vehicle(std::ostream &Stream, vehicle_state const &State) { sn_utils::s_str(Stream, State.name); - sn_utils::s_str(Stream, State.track); + sn_utils::ls_uint32(Stream, State.track); sn_utils::ls_float64(Stream, State.translation); sn_utils::s_uint8(Stream, State.axlefirst); sn_utils::s_uint8(Stream, State.direction); @@ -204,8 +241,6 @@ void serialize_vehicle(std::ostream &Stream, vehicle_state const &State) sn_utils::s_bool(Stream, State.battery); sn_utils::s_bool(Stream, State.converterallow); sn_utils::s_bool(Stream, State.compressorallow); - sn_utils::ls_int32(Stream, State.cabactive); - sn_utils::ls_int32(Stream, State.caboccupied); for (int i = 0; i < 2; ++i) { sn_utils::s_bool(Stream, State.pantenabled[i]); @@ -227,7 +262,7 @@ vehicle_state deserialize_vehicle(std::istream &Stream) vehicle_state state; state.name = sn_utils::d_str(Stream); - state.track = sn_utils::d_str(Stream); + state.track = sn_utils::ld_uint32(Stream); state.translation = sn_utils::ld_float64(Stream); state.axlefirst = sn_utils::d_uint8(Stream); state.direction = sn_utils::d_uint8(Stream); @@ -252,8 +287,6 @@ vehicle_state deserialize_vehicle(std::istream &Stream) state.battery = sn_utils::d_bool(Stream); state.converterallow = sn_utils::d_bool(Stream); state.compressorallow = sn_utils::d_bool(Stream); - state.cabactive = sn_utils::ld_int32(Stream); - state.caboccupied = sn_utils::ld_int32(Stream); for (int i = 0; i < 2; ++i) { state.pantenabled[i] = sn_utils::d_bool(Stream); @@ -279,7 +312,7 @@ vehicle_state read_from(TDynamicObject const &Vehicle) vehicle_state state; state.name = Vehicle.name(); - state.track = (track != nullptr ? track->name() : std::string()); + state.track = track_id_of(track); state.translation = Vehicle.RaTranslationGet(); state.axlefirst = (uint8_t)(Vehicle.iAxleFirst ? 1 : 0); state.direction = (uint8_t)(Vehicle.iDirection ? 1 : 0); @@ -304,8 +337,6 @@ vehicle_state read_from(TDynamicObject const &Vehicle) state.battery = mover.Battery; state.converterallow = mover.ConverterAllow; state.compressorallow = mover.CompressorAllow; - state.cabactive = mover.CabActive; - state.caboccupied = mover.CabOccupied; for (int i = 0; i < 2; ++i) { state.pantenabled[i] = mover.Pantographs[i].valve.is_enabled; @@ -324,19 +355,41 @@ vehicle_state read_from(TDynamicObject const &Vehicle) return state; } +// true for the train the local player is working. its controls are driven by the commands +// its crew issue, which every peer already receives, so the correction must keep its hands +// off them - otherwise it keeps snapping a lever back a fraction of a second after the +// player moved it, and the control looks broken +bool is_own_train(std::string const &Name) +{ + auto const seat = network::Crews.vehicle_of(Global.network_peer_id); + if (seat == network::ENTITY_NONE) + return false; + + auto const train = network::Entities.consist_of(network::Entities.id_of(Name)); + return (train != network::ENTITY_NONE) && (network::Entities.consist_of(seat) == train); +} + // applies everything except where the vehicle is; position is dealt with per consist, // because moving one vehicle of a coupled set on its own tears the couplers apart void apply_controls(TDynamicObject &Vehicle, vehicle_state const &State) { auto &mover = *Vehicle.MoverParameters; + // what the physics produced is always taken from the authority; what a person set is + // not, on the train that person is working + bool const ours = is_own_train(State.name); + mover.V = State.velocity; mover.Vel = State.velocitykmh; - mover.DirActive = State.diractive; - mover.MainCtrlPos = State.mainctrl; - mover.ScndCtrlPos = State.scndctrl; - mover.BrakeCtrlPos = State.brakectrl; - mover.LocalBrakePosA = State.localbrake; + + if (!ours) + { + mover.DirActive = State.diractive; + mover.MainCtrlPos = State.mainctrl; + mover.ScndCtrlPos = State.scndctrl; + mover.BrakeCtrlPos = State.brakectrl; + mover.LocalBrakePosA = State.localbrake; + } mover.PipePress = State.pipepress; mover.BrakePress = State.brakepress; @@ -345,6 +398,14 @@ void apply_controls(TDynamicObject &Vehicle, vehicle_state const &State) mover.Compressor = State.compressor; mover.CompressedVolume = State.compressedvolume; + if (ours) + { + // the rest of it is what the crew set, and their commands reach us on their own + if (Vehicle.Mechanik != nullptr && Vehicle.Mechanik->AIControllFlag != State.aiactive) + Vehicle.Mechanik->TakeControl(State.aiactive); + return; + } + // the appliances go through the vehicle's own switches wherever it has them, so that // whatever they drag along stays consistent. range local: every vehicle carries its // own state in the update, there is no need to push it down the consist twice @@ -357,8 +418,9 @@ void apply_controls(TDynamicObject &Vehicle, vehicle_state const &State) if (mover.CompressorAllow != State.compressorallow) mover.CompressorSwitch(State.compressorallow, range_t::local); - mover.CabActive = State.cabactive; - mover.CabOccupied = State.caboccupied; + // CabActive and CabOccupied are deliberately not replicated: which cab a player is + // sitting in is theirs alone, and two people sharing a vehicle each have their own. + // forcing the server's value on them is what made the camera jump in and out for (int i = 0; i < 2; ++i) { @@ -388,9 +450,17 @@ void apply_controls(TDynamicObject &Vehicle, vehicle_state const &State) // its leading bogie; this inverts the arithmetic that function itself does void reposition(TDynamicObject &Vehicle, vehicle_state const &State) { - TTrack *track = (State.track.empty() ? nullptr : simulation::Paths.find(State.track)); + TTrack *track = track_by_id(State.track); if (track == nullptr) + { + static bool reported{false}; + if (!reported) + { + reported = true; + ErrorLog("net: cannot place " + Vehicle.name() + ", track " + std::to_string(State.track) + " is unknown here", logtype::net); + } return; + } double const half = Vehicle.fAxleDist * 0.5; double const axleoffset = (State.axlefirst ? -half : half); diff --git a/widgets/multiplayer_lobby.cpp b/widgets/multiplayer_lobby.cpp index 08ae6fc2..4e3d9261 100644 --- a/widgets/multiplayer_lobby.cpp +++ b/widgets/multiplayer_lobby.cpp @@ -32,7 +32,14 @@ ui::multiplayer_lobby_panel::multiplayer_lobby_panel() std::string ui::multiplayer_lobby_panel::describe_crew(network::NetworkEntityId const Entity) const { - auto const crew = network::Crews.crew_of(Entity); + // the people on the whole train, wherever in it they happen to be sitting + std::vector crew; + for (auto const member : network::Entities.consist_members(network::Entities.consist_of(Entity))) + { + for (auto const peer : network::Crews.crew_of(member)) + crew.emplace_back(peer); + } + if (crew.empty()) return std::string(); @@ -93,7 +100,7 @@ void ui::multiplayer_lobby_panel::render_contents() ImGui::SetColumnWidth(2, statewidth); ImGui::SetColumnWidth(3, actionwidth); - ImGui::TextDisabled("%s", STR_C("Vehicle")); + ImGui::TextDisabled("%s", STR_C("Train")); ImGui::NextColumn(); ImGui::TextDisabled("%s", STR_C("Crew")); ImGui::NextColumn(); @@ -102,11 +109,21 @@ void ui::multiplayer_lobby_panel::render_contents() ImGui::NextColumn(); ImGui::Separator(); + network::NetworkEntityId const owntrain{network::Entities.consist_of(ownvehicle)}; + for (auto const &entry : entries) { + // one row per train. a player takes the whole set, not a single car, and moves + // between its cars freely once they are on it + if (!entry.consist_lead) + continue; + ImGui::PushID(static_cast(entry.id)); - ImGui::TextUnformatted(entry.name.c_str()); + if (entry.consist_size > 1) + ImGui::Text("%s +%u", entry.name.c_str(), (unsigned)(entry.consist_size - 1)); + else + ImGui::TextUnformatted(entry.name.c_str()); ImGui::NextColumn(); ImGui::Text("%u/%u", (unsigned)entry.crew_count, (unsigned)entry.crew_capacity); @@ -119,10 +136,10 @@ void ui::multiplayer_lobby_panel::render_contents() ImGui::TextDisabled("%s", entry.ai_active ? STR_C("AI") : STR_C("free")); ImGui::NextColumn(); - if (entry.id == ownvehicle) + if (owntrain != network::ENTITY_NONE && entry.consist_id == owntrain) { if (ImGui::Button(STR_C("Leave"))) - Application.request_vehicle_leave(entry.id); + Application.request_vehicle_leave(ownvehicle); } else if (entry.claimable) { @@ -143,7 +160,7 @@ void ui::multiplayer_lobby_panel::render_contents() ImGui::Columns(1); ImGui::Separator(); - ImGui::TextDisabled("%s", STR_C("You can also walk up to a vehicle and use the enter-vehicle key.")); + ImGui::TextDisabled("%s", STR_C("You can also walk up to a train and use the enter-vehicle key.")); } void ui::multiplayer_lobby_panel::claim(network::NetworkEntityId const Entity, network::NetworkEntityId const Current)