mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-17 14:29:20 +02:00
The renderer draws nothing at all while simulation::is_ready is false, and that flag only goes up once the local player's cab is built. The previous commit made walking into a cab a crew request, so the cab of a claimed vehicle is created by Crews::reconcile() rather than by the command itself. reconcile() runs from the network housekeeping, which was gated on simulation::is_ready - so a host started with -v had its entervehicle turned into a claim, and then the cab waited for is_ready while is_ready waited for the cab. Black screen on both ends, since a client whose server never gets going has nothing to show either. - the housekeeping now waits for Global.simulation_loaded, which says the world exists, instead of simulation::is_ready, which says the player has a cab - in a session, a player with no cab counts as ready: they are an observer in the lobby, not a game still loading, and the world has to be drawn for them - entervehicle is only taken as a crew request when the session actually lists that vehicle; anything else stays an ordinary command, so a scenario that starts its host in something the roster does not cover still works
481 lines
13 KiB
C++
481 lines
13 KiB
C++
/*
|
|
This Source Code Form is subject to the
|
|
terms of the Mozilla Public License, v.
|
|
2.0. If a copy of the MPL was not
|
|
distributed with this file, You can
|
|
obtain one at
|
|
http://mozilla.org/MPL/2.0/.
|
|
*/
|
|
|
|
#include "stdafx.h"
|
|
#include "network/session.h"
|
|
|
|
#include "input/command.h"
|
|
#include "simulation/simulation.h"
|
|
#include "utilities/Globals.h"
|
|
#include "utilities/Logs.h"
|
|
#include "utilities/Timer.h"
|
|
#include "vehicle/Driver.h"
|
|
#include "vehicle/DynObj.h"
|
|
#include "vehicle/Train.h"
|
|
|
|
namespace network
|
|
{
|
|
|
|
crew_registry Crews;
|
|
|
|
std::string describe(claim_result Result)
|
|
{
|
|
switch (Result)
|
|
{
|
|
case claim_result::granted:
|
|
return "granted";
|
|
case claim_result::already_member:
|
|
return "already a crew member";
|
|
case claim_result::unknown_vehicle:
|
|
return "no such vehicle in this session";
|
|
case claim_result::vehicle_missing:
|
|
return "the vehicle is not present in the scenario";
|
|
case claim_result::crew_full:
|
|
return "the crew of this vehicle is full";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
std::string describe(command_verdict Verdict)
|
|
{
|
|
switch (Verdict)
|
|
{
|
|
case command_verdict::accepted:
|
|
return "accepted";
|
|
case command_verdict::not_in_crew:
|
|
return "not a member of the crew of that vehicle";
|
|
case command_verdict::privileged:
|
|
return "reserved for the session host";
|
|
case command_verdict::unsupported_target:
|
|
return "command target not allowed over the network";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
command_verdict validate_command(PeerId Peer, user_command Command, uint32_t Recipient)
|
|
{
|
|
if (Peer == PEER_HOST)
|
|
{
|
|
// the host runs the session and holds every right in it
|
|
return command_verdict::accepted;
|
|
}
|
|
|
|
auto const target = (command_target)(Recipient & ~0xffff);
|
|
|
|
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;
|
|
}
|
|
|
|
if (target == command_target::simulation)
|
|
{
|
|
// setweather, setdatetime, spawntrainset, destroytrainset, queueevent, the pause
|
|
// and the debug commands all reach the whole world, so they stay with the authority.
|
|
// TODO: open a subset of them once dispatcher and admin roles exist
|
|
(void)Command;
|
|
return command_verdict::privileged;
|
|
}
|
|
|
|
return command_verdict::unsupported_target;
|
|
}
|
|
|
|
claim_result claim_by_name(PeerId Peer, std::string const &Vehicle, NetworkEntityId &Entity)
|
|
{
|
|
Entity = Entities.id_of(Vehicle);
|
|
if (Entity == ENTITY_NONE)
|
|
return claim_result::unknown_vehicle;
|
|
|
|
auto const previous = Crews.vehicle_of(Peer);
|
|
auto const result = Crews.claim(Peer, Entity);
|
|
|
|
if ((result == claim_result::granted) && (previous != ENTITY_NONE) && (previous != Entity))
|
|
{
|
|
// nobody works two vehicles at once
|
|
Crews.leave(Peer, previous);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void filter_commands(PeerId Peer, command_queue::commands_map &Commands)
|
|
{
|
|
for (auto it = Commands.begin(); it != Commands.end();)
|
|
{
|
|
auto &sequence = it->second;
|
|
|
|
for (auto command = sequence.begin(); command != sequence.end();)
|
|
{
|
|
if (command->source == PEER_NONE)
|
|
{
|
|
// posted by the authority itself rather than by a person; it is already
|
|
// the decision, not a request for one
|
|
++command;
|
|
continue;
|
|
}
|
|
|
|
if ((command->command == user_command::entervehicle) && (Entities.id_of(command->payload) != ENTITY_NONE))
|
|
{
|
|
// the local player walked into a cab. that is a crew request; the cab
|
|
// itself is built by the authority once the seat is granted.
|
|
// a vehicle the session does not list falls through to the ordinary path,
|
|
// so a scenario that starts its host in something unusual still works
|
|
NetworkEntityId entity{ENTITY_NONE};
|
|
auto const result = claim_by_name(Peer, command->payload, entity);
|
|
if (result != claim_result::granted && result != claim_result::already_member)
|
|
WriteLog("net: cannot take " + command->payload + ": " + describe(result), logtype::net);
|
|
|
|
command = sequence.erase(command);
|
|
continue;
|
|
}
|
|
|
|
if (validate_command(Peer, command->command, it->first) != command_verdict::accepted)
|
|
{
|
|
command = sequence.erase(command);
|
|
continue;
|
|
}
|
|
|
|
command->source = Peer;
|
|
++command;
|
|
}
|
|
|
|
if (sequence.empty())
|
|
it = Commands.erase(it);
|
|
else
|
|
++it;
|
|
}
|
|
}
|
|
|
|
vehicle_crew &crew_registry::entry(NetworkEntityId Id)
|
|
{
|
|
auto lookup = m_vehicles.find(Id);
|
|
if (lookup == m_vehicles.end())
|
|
{
|
|
vehicle_crew fresh;
|
|
fresh.entity_id = Id;
|
|
lookup = m_vehicles.emplace(Id, fresh).first;
|
|
}
|
|
return lookup->second;
|
|
}
|
|
|
|
claim_result crew_registry::claim(PeerId Peer, NetworkEntityId Id)
|
|
{
|
|
vehicle_entry const *published = Entities.find(Id);
|
|
if (published == nullptr)
|
|
return claim_result::unknown_vehicle;
|
|
|
|
if (Entities.resolve(Id) == nullptr)
|
|
return claim_result::vehicle_missing;
|
|
|
|
vehicle_crew &vehicle = entry(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)
|
|
return claim_result::crew_full;
|
|
|
|
if (vehicle.crew.empty())
|
|
{
|
|
// remember whether this vehicle was actually being driven by the AI, so that it is
|
|
// handed back only to a driver that was really there before
|
|
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);
|
|
|
|
WriteLog("net: peer " + std::to_string(Peer) + " joined the crew of " + published->name + " (" + std::to_string(vehicle.crew.size()) + "/" +
|
|
std::to_string((int)published->crew_capacity) + ")",
|
|
logtype::net);
|
|
|
|
return claim_result::granted;
|
|
}
|
|
|
|
bool crew_registry::leave(PeerId Peer, NetworkEntityId Id)
|
|
{
|
|
auto lookup = m_vehicles.find(Id);
|
|
if (lookup == m_vehicles.end())
|
|
return false;
|
|
|
|
auto &crew = lookup->second.crew;
|
|
auto member = std::find(crew.begin(), crew.end(), Peer);
|
|
if (member == crew.end())
|
|
return false;
|
|
|
|
crew.erase(member);
|
|
m_pending.emplace_back(Id);
|
|
|
|
WriteLog("net: peer " + std::to_string(Peer) + " left the crew of " + Entities.name_of(Id) + " (" + std::to_string(crew.size()) + " left)", logtype::net);
|
|
|
|
if (crew.empty() && lookup->second.ai_before_claim)
|
|
{
|
|
// the vehicle came with an AI driver, so it gets one back
|
|
lookup->second.ai_restore_pending = true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
int64_t pack_session_config()
|
|
{
|
|
int64_t config{0};
|
|
|
|
if (Global.FullPhysics)
|
|
config |= CONFIG_FULLPHYSICS;
|
|
if (Global.RealisticControlMode)
|
|
config |= CONFIG_REALISTICCONTROL;
|
|
|
|
return config;
|
|
}
|
|
|
|
void apply_session_config(int64_t Config)
|
|
{
|
|
bool const fullphysics{(Config & CONFIG_FULLPHYSICS) != 0};
|
|
bool const realisticcontrol{(Config & CONFIG_REALISTICCONTROL) != 0};
|
|
|
|
if (Global.FullPhysics != fullphysics)
|
|
WriteLog(std::string("net: the session runs with full physics ") + (fullphysics ? "on" : "off"), logtype::net);
|
|
if (Global.RealisticControlMode != realisticcontrol)
|
|
WriteLog(std::string("net: the session runs with realistic control ") + (realisticcontrol ? "on" : "off"), logtype::net);
|
|
|
|
Global.FullPhysics = fullphysics;
|
|
Global.RealisticControlMode = realisticcontrol;
|
|
}
|
|
|
|
void enforce_session_settings()
|
|
{
|
|
if (Global.trainThreads != 0)
|
|
{
|
|
// the order vehicles are stepped in must not depend on how threads happen to be
|
|
// scheduled; a session cannot afford that kind of drift
|
|
WriteLog("net: multiplayer session, running the vehicle physics on one thread", logtype::net);
|
|
Global.trainThreads = 0;
|
|
}
|
|
}
|
|
|
|
PeerId resolve_peer_identity(uint64_t &Token)
|
|
{
|
|
static std::unordered_map<uint64_t, PeerId> known;
|
|
|
|
if (Token != 0)
|
|
{
|
|
auto const lookup = known.find(Token);
|
|
if (lookup != known.end())
|
|
{
|
|
WriteLog("net: peer " + std::to_string(lookup->second) + " recognised, reconnecting", logtype::net);
|
|
Crews.resume_peer(lookup->second);
|
|
return lookup->second;
|
|
}
|
|
}
|
|
|
|
auto const peer = allocate_peer_id();
|
|
|
|
// only has to be hard to collide with, not hard to guess: it decides who is who
|
|
// across a dropped connection, nothing more
|
|
Token = ((uint64_t)Global.local_random_engine() << 32) ^ (uint64_t)Global.local_random_engine() ^ ((uint64_t)peer << 8);
|
|
if (Token == 0)
|
|
Token = peer;
|
|
|
|
known.emplace(Token, peer);
|
|
|
|
return peer;
|
|
}
|
|
|
|
void crew_registry::suspend_peer(PeerId Peer)
|
|
{
|
|
if (vehicle_of(Peer) == ENTITY_NONE)
|
|
return;
|
|
|
|
m_absent[Peer] = Timer::GetTime() + RECONNECT_GRACE_SECONDS;
|
|
|
|
WriteLog("net: peer " + std::to_string(Peer) + " lost its connection, holding its seat for " + std::to_string((int)RECONNECT_GRACE_SECONDS) + " s", logtype::net);
|
|
}
|
|
|
|
void crew_registry::resume_peer(PeerId Peer)
|
|
{
|
|
m_absent.erase(Peer);
|
|
}
|
|
|
|
bool crew_registry::is_absent(PeerId Peer) const
|
|
{
|
|
return m_absent.find(Peer) != m_absent.end();
|
|
}
|
|
|
|
void crew_registry::expire_absences()
|
|
{
|
|
auto const now = Timer::GetTime();
|
|
|
|
for (auto it = m_absent.begin(); it != m_absent.end();)
|
|
{
|
|
if (now < it->second)
|
|
{
|
|
++it;
|
|
continue;
|
|
}
|
|
|
|
WriteLog("net: peer " + std::to_string(it->first) + " did not come back, giving up its seat", logtype::net);
|
|
auto const peer = it->first;
|
|
it = m_absent.erase(it);
|
|
drop_peer(peer);
|
|
}
|
|
}
|
|
|
|
void crew_registry::drop_peer(PeerId Peer)
|
|
{
|
|
for (auto &pair : m_vehicles)
|
|
{
|
|
if (std::find(pair.second.crew.begin(), pair.second.crew.end(), Peer) != pair.second.crew.end())
|
|
leave(Peer, pair.first);
|
|
}
|
|
}
|
|
|
|
std::vector<PeerId> crew_registry::crew_of(NetworkEntityId Id) const
|
|
{
|
|
auto const lookup = m_vehicles.find(Id);
|
|
return lookup != m_vehicles.end() ? lookup->second.crew : std::vector<PeerId>();
|
|
}
|
|
|
|
bool crew_registry::is_member(PeerId Peer, NetworkEntityId Id) const
|
|
{
|
|
auto const lookup = m_vehicles.find(Id);
|
|
if (lookup == m_vehicles.end())
|
|
return false;
|
|
return std::find(lookup->second.crew.begin(), lookup->second.crew.end(), Peer) != lookup->second.crew.end();
|
|
}
|
|
|
|
uint8_t crew_registry::count(NetworkEntityId Id) const
|
|
{
|
|
auto const lookup = m_vehicles.find(Id);
|
|
return lookup != m_vehicles.end() ? (uint8_t)lookup->second.crew.size() : (uint8_t)0;
|
|
}
|
|
|
|
NetworkEntityId crew_registry::vehicle_of(PeerId Peer) const
|
|
{
|
|
for (auto const &pair : m_vehicles)
|
|
{
|
|
if (std::find(pair.second.crew.begin(), pair.second.crew.end(), Peer) != pair.second.crew.end())
|
|
return pair.first;
|
|
}
|
|
return ENTITY_NONE;
|
|
}
|
|
|
|
void crew_registry::mirror(NetworkEntityId Id, std::vector<PeerId> const &Crew)
|
|
{
|
|
entry(Id).crew = Crew;
|
|
}
|
|
|
|
std::vector<NetworkEntityId> crew_registry::take_pending_updates()
|
|
{
|
|
std::vector<NetworkEntityId> pending;
|
|
pending.swap(m_pending);
|
|
|
|
// the same vehicle can change more than once between two publications
|
|
std::sort(pending.begin(), pending.end());
|
|
pending.erase(std::unique(pending.begin(), pending.end()), pending.end());
|
|
|
|
return pending;
|
|
}
|
|
|
|
void crew_registry::mark_all_pending()
|
|
{
|
|
for (auto const &pair : m_vehicles)
|
|
{
|
|
if (!pair.second.crew.empty())
|
|
m_pending.emplace_back(pair.first);
|
|
}
|
|
}
|
|
|
|
void crew_registry::clear()
|
|
{
|
|
m_vehicles.clear();
|
|
m_pending.clear();
|
|
}
|
|
|
|
namespace
|
|
{
|
|
|
|
// posts a command the way an input source would, but without going through command_relay:
|
|
// the authority acts on behalf of the session, not on behalf of the local camera, so it
|
|
// must not be filtered by the local free fly state
|
|
void post_authority_command(user_command Command, uint32_t Recipient, glm::vec3 const &Location, std::string const &Payload)
|
|
{
|
|
command_data data;
|
|
data.command = Command;
|
|
data.action = GLFW_PRESS;
|
|
data.param1 = 0.0;
|
|
data.param2 = 0.0;
|
|
data.time_delta = Timer::GetDeltaTime();
|
|
data.freefly = true;
|
|
data.location = Location;
|
|
data.payload = Payload;
|
|
|
|
simulation::Commands.push(data, Recipient);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// frames to wait before repeating an authority request that has not taken effect yet
|
|
static int const ACTION_COOLDOWN_FRAMES = 30;
|
|
|
|
void crew_registry::reconcile()
|
|
{
|
|
for (auto &pair : m_vehicles)
|
|
{
|
|
vehicle_crew &vehicle = pair.second;
|
|
|
|
TDynamicObject *dynamic = Entities.resolve(vehicle.entity_id);
|
|
if (dynamic == nullptr)
|
|
continue;
|
|
|
|
if (vehicle.action_cooldown > 0)
|
|
{
|
|
--vehicle.action_cooldown;
|
|
continue;
|
|
}
|
|
|
|
TTrain *train = simulation::Trains.find(dynamic->name());
|
|
|
|
if (!vehicle.crew.empty())
|
|
{
|
|
if (train == nullptr)
|
|
{
|
|
// the cab has to exist on every peer, so the authority asks for it with the
|
|
// ordinary replicated command instead of building it locally
|
|
post_authority_command(user_command::entervehicle, (uint32_t)command_target::simulation, dynamic->GetPosition(), dynamic->name());
|
|
vehicle.action_cooldown = ACTION_COOLDOWN_FRAMES;
|
|
continue;
|
|
}
|
|
|
|
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;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (vehicle.ai_restore_pending && train != nullptr)
|
|
{
|
|
// aidriverenable resets the driver, so it must happen exactly once
|
|
vehicle.ai_restore_pending = false;
|
|
vehicle.ai_before_claim = false;
|
|
WriteLog("net: handing " + dynamic->name() + " back to the AI driver", logtype::net);
|
|
post_authority_command(user_command::aidriverenable, (uint32_t)command_target::vehicle | train->id(), dynamic->GetPosition(), std::string());
|
|
vehicle.action_cooldown = ACTION_COOLDOWN_FRAMES;
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace network
|