#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include constexpr size_t kMaxServerNameLength = 128u; // -- Cvars -- Console::Setting uServerPort{"GameServer:uPort", "Which port to host the server on", 10578u}; Console::Setting uMaxPlayerCount{"GameServer:uMaxPlayerCount", "Maximum number of players allowed on the server (going over the default of 8 is not recommended)", 8u}; Console::Setting bPremiumTickrate{"GameServer:bPremiumMode", "Use premium tick rate", true}; Console::StringSetting sServerName{"GameServer:sServerName", "Name that shows up in the server list", "Dedicated Together Server"}; Console::StringSetting sAdminPassword{"GameServer:sAdminPassword", "Admin authentication password", ""}; Console::StringSetting sPassword{"GameServer:sPassword", "Server password", ""}; // Gameplay // TODO: to make this easier for users, use game names for difficulty instead of int Console::Setting uDifficulty{"Gameplay:uDifficulty", "In game difficulty (0 to 5)", 4u}; Console::Setting bEnableGreetings{"Gameplay:bEnableGreetings", "Enables NPC greetings (disabled by default since they can be spammy with dialogue sync)", false}; Console::Setting bEnablePvp{"Gameplay:bEnablePvp", "Enables pvp", false}; Console::Setting bSyncPlayerHomes{"Gameplay:bSyncPlayerHomes", "Sync chests and displays in player homes and other NoResetZones", false}; Console::Setting bEnableDeathSystem{"Gameplay:bEnableDeathSystem", "Enables the custom multiplayer death system", true}; Console::Setting uTimeScale{"Gameplay:uTimeScale", "How many seconds pass ingame for every real second (0 to 1000). Changing this can make the game unstable", 20u}; Console::Setting bSyncPlayerCalendar{"Gameplay:bSyncPlayerCalendar", "Syncs up all player calendars to be the same day, month, and year. This uses the date of the player with the furthest ahead date at connection.", false}; Console::Setting bAutoPartyJoin{"Gameplay:bAutoPartyJoin", "Join parties automatically, as long as there is only one party in the server", true}; // ModPolicy Stuff Console::Setting bEnableModCheck{"ModPolicy:bEnableModCheck", "Bypass the checking of mods on the server", false, Console::SettingsFlags::kLocked}; Console::Setting bAllowSKSE{"ModPolicy:bAllowSKSE", "Allow clients with SKSE active to join", true, Console::SettingsFlags::kLocked}; Console::Setting bAllowMO2{"ModPolicy:bAllowMO2", "Allow clients running Mod Organizer 2 to join", true, Console::SettingsFlags::kLocked}; // -- Commands -- Console::Command<> TogglePremium( "TogglePremium", "Toggle Premium Tickrate on/off", [](Console::ArgStack&) { bPremiumTickrate = !bPremiumTickrate; spdlog::get("ConOut")->info("Premium Tickrate has been {}.", bPremiumTickrate == true ? "enabled" : "disabled"); }); Console::Command<> TogglePvp( "TogglePvp", "Toggle PvP on/off", [](Console::ArgStack&) { bEnablePvp = !bEnablePvp; spdlog::get("ConOut")->info("PvP has been {}.", bEnablePvp == true ? "enabled" : "disabled"); GameServer::Get()->UpdateSettings(); }); Console::Command SetDifficulty( "SetDifficulty", "Set server difficulty (0 being Novice and 5 being Legendary; default is 4)", [](Console::ArgStack& aStack) { auto aDiff = aStack.Pop(); if (aDiff < 0 || aDiff > 5) { spdlog::warn( "Game difficulty is invalid (should be from 0 to 5, " "current value is {}), setting difficulty to 4 (master).", aDiff); aDiff = 4; } uDifficulty = (uint32_t)aDiff; GameServer::Get()->UpdateSettings(); spdlog::get("ConOut")->info("Difficulty has been set to {}.", aDiff); }); Console::Command<> ShowVersion("version", "Show the version the server was compiled with", [](Console::ArgStack&) { spdlog::get("ConOut")->info("Server " BUILD_COMMIT); }); Console::Command<> CrashServer( "crash", "Crashes the server, don't use!", [](Console::ArgStack&) { int* i = 0; *i = 42; }); Console::Command<> ShowMoPoStatus( "ShowMOPOStats", "Shows the status of ModPolicy", [](Console::ArgStack&) { auto formatStatus = [](bool aToggle) { return aToggle ? "yes" : "no"; }; spdlog::get("ConOut")->info("Modcheck enabled: {}\nSKSE allowed: {}\nMO2 allowed: {}", formatStatus(bEnableModCheck), formatStatus(bAllowSKSE), formatStatus(bAllowMO2)); }); // -- Constants -- constexpr char kBypassMoPoWarning[]{"ModCheck is disabled. This can lead to desync and other oddities. Make sure you know what you are doing. We " "may not be able to assist you if ModCheck was disabled."}; constexpr char kMopoRecordsMissing[]{"Failed to start: ModPolicy's ModCheck is enabled, but no mods are installed. Players won't be able " "to join! Please create a Data/ directory, and put a \"loadorder.txt\" file in there." "Check the wiki, which can be found on skyrim-together.com, for more details."}; constexpr char kCalendarSyncWarning[]{"Calendar sync is enabled. We generally do not recommend that you use this feature." "Calendar sync can cause the calendar to jump ahead or behind, which might mess up the timing of quests." "If you disable this feature again (which is the default setting), the days will still progress, but the" "exact date will differ slightly between clients (which has no impact on gameplay)."}; static uint16_t GetUserTickRate() { return bPremiumTickrate ? 60 : 30; } static bool IsMoPoActive() { return bEnableModCheck; } ServerSettings GetSettings() { ServerSettings settings{}; settings.Difficulty = uDifficulty.value_as(); settings.GreetingsEnabled = bEnableGreetings; settings.PvpEnabled = bEnablePvp; settings.SyncPlayerHomes = bSyncPlayerHomes; settings.DeathSystemEnabled = bEnableDeathSystem; settings.SyncPlayerCalendar = bSyncPlayerCalendar; settings.AutoPartyJoin = bAutoPartyJoin; return settings; } GameServer::GameServer(Console::ConsoleRegistry& aConsole) noexcept : m_lastFrameTime(std::chrono::high_resolution_clock::now()) , m_startTime(std::chrono::high_resolution_clock::now()) , m_commands(aConsole) , m_requestStop(false) { BASE_ASSERT(s_pInstance == nullptr, "Server instance already exists?"); s_pInstance = this; auto port = uServerPort.value_as(); while (!Host(port, GetUserTickRate())) { spdlog::warn("Port {} is already in use, trying {}", port, port + 1); port++; } if (uDifficulty.value_as() > 5) { spdlog::warn( "Game difficulty is invalid (should be from 0 to 5, current value is {}), setting difficulty to 4 " "(master).", uDifficulty.value_as()); uDifficulty = 4; } if (!bEnableDeathSystem) { spdlog::warn("The multiplayer death system is disabled on this server. We recommend that you ONLY do this if you have" " a mod that replaces the vanilla death system. You should only disable our death system if you" " absolutely know what you are doing!"); } m_isPasswordProtected = strcmp(sPassword.value(), "") != 0; UpdateInfo(); spdlog::info("Server {} started on port {}", BUILD_COMMIT, GetPort()); UpdateTitle(); m_pWorld = MakeUnique(); BindMessageHandlers(); UpdateTimeScale(); m_pResources = MakeUnique(); } GameServer::~GameServer() { s_pInstance = nullptr; } GameServer* GameServer::Get() noexcept { return s_pInstance; } void GameServer::Initialize() { if (!CheckMoPo()) return; if (bSyncPlayerCalendar) spdlog::warn(kCalendarSyncWarning); BindServerCommands(); m_pWorld->GetScriptService().Initialize(*m_pResources); } void GameServer::Kill() { spdlog::info("Server shutdown requested"); m_requestStop = true; } bool GameServer::CheckMoPo() { if (!bEnableModCheck) { // TODO: re-enable this warning when mopo has good ui and the line endings problem is fixed // spdlog::warn(kBypassMoPoWarning); } // Server is not aware of any installed mods. else if (!m_pWorld->GetRecordCollection()) { spdlog::error(kMopoRecordsMissing); Kill(); return false; } else spdlog::info("ModPolicy is active"); return true; } void GameServer::BindMessageHandlers() { auto handlerGenerator = [this](auto& x) { using T = typename std::remove_reference_t::Type; m_messageHandlers[T::Opcode] = [this](UniquePtr& apMessage, ConnectionId_t aConnectionId) { auto* pPlayer = m_pWorld->GetPlayerManager().GetByConnectionId(aConnectionId); if (!pPlayer) { spdlog::error("Connection {:x} is not associated with a player.", aConnectionId); Kick(aConnectionId); return; } const auto pRealMessage = CastUnique(std::move(apMessage)); m_pWorld->GetDispatcher().trigger(PacketEvent(pRealMessage.get(), pPlayer)); }; return false; }; ClientMessageFactory::Visit(handlerGenerator); // Override authentication request m_messageHandlers[AuthenticationRequest::Opcode] = [this](UniquePtr& apMessage, ConnectionId_t aConnectionId) { const auto pRealMessage = CastUnique(std::move(apMessage)); HandleAuthenticationRequest(aConnectionId, pRealMessage); }; auto adminHandlerGenerator = [this](auto& x) { using T = typename std::remove_reference_t::Type; m_adminMessageHandlers[T::Opcode] = [this](UniquePtr& apMessage, ConnectionId_t aConnectionId) { const auto pRealMessage = CastUnique(std::move(apMessage)); m_pWorld->GetDispatcher().trigger(AdminPacketEvent(pRealMessage.get(), aConnectionId)); }; return false; }; ClientAdminMessageFactory::Visit(adminHandlerGenerator); } void GameServer::BindServerCommands() { m_commands.RegisterCommand<>( "uptime", "Show how long the server has been running for", [this](Console::ArgStack&) { Uptime uptime = GetUptime(); spdlog::get("ConOut")->info("Server uptime: {}w {}d {}h {}m", uptime.weeks, uptime.days, uptime.hours, uptime.minutes); }); m_commands.RegisterCommand<>( "players", "List all players on this server", [&](Console::ArgStack&) { auto out = spdlog::get("ConOut"); uint32_t count = m_pWorld->GetPlayerManager().Count(); if (count == 0) { out->warn("No players on here. Invite some friends!"); return; } out->info("<------Players-({})--->", count); for (Player* pPlayer : m_pWorld->GetPlayerManager()) { out->info("{}: {}", pPlayer->GetId(), pPlayer->GetUsername().c_str()); } }); m_commands.RegisterCommand<>( "mods", "List all installed mods on this server", [&](Console::ArgStack&) { auto out = spdlog::get("ConOut"); auto& mods = m_pWorld->ctx().at().GetServerMods(); if (mods.size() == 0) { out->warn("No mods installed"); return; } out->info("<------Mods-({})--->", mods.size()); for (auto& it : mods) { out->info(it.first); } }); m_commands.RegisterCommand<>( "resources", "List all loaded resources on the server", [&](Console::ArgStack&) { auto out = spdlog::get("ConOut"); if (!m_pResources || m_pResources->GetManifests().size() == 0) { out->warn("No resources loaded"); return; } out->info("<------Resources-({})--->", m_pResources->GetManifests().size()); m_pResources->ForEachManifest([&](const auto& aManifest) { out->info("{} -> {}", aManifest.Name.c_str(), aManifest.Description.c_str()); }); }); m_commands.RegisterCommand<>("quit", "Stop the server", [&](Console::ArgStack&) { Kill(); }); m_commands.RegisterCommand( "SetTime", "Set ingame hour and minute", [&](Console::ArgStack& aStack) { auto out = spdlog::get("ConOut"); auto hour = aStack.Pop(); auto minute = aStack.Pop(); auto timescale = m_pWorld->GetCalendarService().GetTimeScale(); bool time_set_successfully = m_pWorld->GetCalendarService().SetTime(hour, minute, timescale); if (time_set_successfully) { out->info("Time set to {:02}:{:02}", hour, minute); } else { out->error("Hour must be between 0-23 and minute must be between 0-59"); } }); m_commands.RegisterCommand( "SetDate", "Set ingame day, month, and year", [&](Console::ArgStack& aStack) { auto out = spdlog::get("ConOut"); auto day = aStack.Pop(); auto month = aStack.Pop(); auto year = aStack.Pop(); bool time_set_successfully = m_pWorld->GetCalendarService().SetDate(day, month, year); if (time_set_successfully) { out->info("Time set to {:02}/{:02}:{:02}", month, day, year); } else { out->error("Day must be between 0 and 31, month must be between 0 and 11, and year must be between 0 and 999."); } }); m_commands.RegisterCommand( "AddAdmin", "Add admin privileges to player", [&](Console::ArgStack& aStack) { auto out = spdlog::get("ConOut"); const auto& cUsername = aStack.Pop(); if (GetAdminByUsername(cUsername)) { out->info("{} is already an admin", cUsername.c_str()); return; } auto* pPlayer = PlayerManager::Get()->GetByUsername(cUsername); if (pPlayer) { AddAdminSession(pPlayer->GetConnectionId()); out->info("{} admin privileges added", cUsername.c_str()); } else { // retry after sanitizing username String backupUsername = SanitizeUsername(cUsername); pPlayer = PlayerManager::Get()->GetByUsername(backupUsername); if (pPlayer) { AddAdminSession(pPlayer->GetConnectionId()); out->info("{} admin privileges added", cUsername.c_str()); } else { out->warn("{} is not a valid player", backupUsername.c_str()); } } }); m_commands.RegisterCommand( "RemoveAdmin", "Remove admin privileges from player", [&](Console::ArgStack& aStack) { auto out = spdlog::get("ConOut"); const auto& cUsername = aStack.Pop(); auto* pPlayer = GetAdminByUsername(cUsername); if (pPlayer) { RemoveAdminSession(pPlayer->GetConnectionId()); out->info("{} admin privileges revoked", cUsername.c_str()); } else { // retry after sanitizing username String backupUsername = SanitizeUsername(cUsername); pPlayer = GetAdminByUsername(backupUsername); if (pPlayer) { RemoveAdminSession(pPlayer->GetConnectionId()); out->info("{} admin privileges revoked", cUsername.c_str()); } else { out->warn("{} is not an admin", backupUsername.c_str()); } } }); m_commands.RegisterCommand<>( "admins", "List all admins", [&](Console::ArgStack&) { auto out = spdlog::get("ConOut"); if (m_adminSessions.size() == 0) { out->warn("No admins"); return; } String output = "Admins: "; bool _first = true; for (const auto& cAdminSession : m_adminSessions) { auto* pPlayer = PlayerManager::Get()->GetByConnectionId(cAdminSession); if (!pPlayer) { out->error("Admin session not found: {}", cAdminSession); continue; } const auto& cUsername = pPlayer->GetUsername(); if (_first) { _first = false; } else { output += ", "; } output += cUsername; } out->info("{}", output.c_str()); }); } /* Update Info fields from user facing CVARS.*/ void GameServer::UpdateInfo() { const String cServerName = sServerName.c_str(); if (cServerName.length() > kMaxServerNameLength) { spdlog::error("sServerName is longer than the limit of {} characters/bytes, and has been cut short", kMaxServerNameLength); m_info.name = cServerName.substr(0U, kMaxServerNameLength); } else { m_info.name = cServerName; } m_info.desc = ""; m_info.icon_url = ""; m_info.tagList = ""; m_info.tick_rate = GetUserTickRate(); } void GameServer::UpdateTimeScale() { auto timescale = uTimeScale.value_as(); bool timescale_set_successfully = m_pWorld->GetCalendarService().SetTimeScale(timescale); if (!timescale_set_successfully) { spdlog::warn("TimeScale is invalid (should be from 0 to 1000, current value is {}), setting TimeScale to 20 (default)", timescale); uTimeScale = 20u; } } void GameServer::OnUpdate() { const auto cNow = std::chrono::high_resolution_clock::now(); const auto cDelta = cNow - m_lastFrameTime; m_lastFrameTime = cNow; const auto cDeltaSeconds = std::chrono::duration_cast>(cDelta).count(); auto& dispatcher = m_pWorld->GetDispatcher(); dispatcher.trigger(UpdateEvent{cDeltaSeconds}); if (m_requestStop) Close(); } void GameServer::OnConsume(const void* apData, const uint32_t aSize, const ConnectionId_t aConnectionId) { ViewBuffer buf((uint8_t*)apData, aSize); Buffer::Reader reader(&buf); // TODO: ClientAdminMessageFactory /*if (m_adminSessions.contains(aConnectionId)) [[unlikely]] { const ClientAdminMessageFactory factory; auto pMessage = factory.Extract(reader); if (!pMessage) { spdlog::error("Couldn't parse packet from {:x}", aConnectionId); return; } m_adminMessageHandlers[pMessage->GetOpcode()](pMessage, aConnectionId); } else {*/ const ClientMessageFactory factory; auto pMessage = factory.Extract(reader); if (!pMessage) { spdlog::error("Couldn't parse packet from {:x}", aConnectionId); return; } m_messageHandlers[pMessage->GetOpcode()](pMessage, aConnectionId); //} } void GameServer::OnConnection(const ConnectionId_t aHandle) { spdlog::info("Connection received {:x}", aHandle); UpdateTitle(); } void GameServer::OnDisconnection(const ConnectionId_t aConnectionId, EDisconnectReason aReason) { m_adminSessions.erase(aConnectionId); auto* pPlayer = m_pWorld->GetPlayerManager().GetByConnectionId(aConnectionId); spdlog::info("Connection ended {:x} - '{}' disconnected", aConnectionId, (pPlayer != NULL ? pPlayer->GetUsername().c_str() : "NULL")); m_pWorld->GetScriptService().HandlePlayerQuit(aConnectionId, aReason); if (pPlayer) { if (const auto& cell = pPlayer->GetCellComponent()) { const auto oldCell = cell.Cell; pPlayer->SetCellComponent(CellIdComponent{{}, {}, {}}); m_pWorld->GetDispatcher().trigger(PlayerLeaveCellEvent(oldCell)); } m_pWorld->GetDispatcher().trigger(PlayerLeaveEvent(pPlayer)); NotifyPlayerLeft notify{}; notify.PlayerId = pPlayer->GetId(); notify.Username = pPlayer->GetUsername(); SendToPlayers(notify); entt::entity playerCharacter = pPlayer->GetCharacter().value_or(static_cast(0)); // Cleanup all entities that we own auto ownerView = m_pWorld->view(); for (auto entity : ownerView) { if (entity == playerCharacter) { m_pWorld->GetDispatcher().enqueue(CharacterRemoveEvent(World::ToInteger(entity))); continue; } const auto& [ownerComponent] = ownerView.get(entity); if (ownerComponent.GetOwner() == pPlayer) { m_pWorld->GetDispatcher().enqueue(OwnershipTransferEvent(entity)); } } m_pWorld->GetDispatcher().update(); m_pWorld->GetPlayerManager().Remove(pPlayer); } UpdateTitle(); } void GameServer::Send(const ConnectionId_t aConnectionId, const ServerMessage& acServerMessage) const { static thread_local TiltedPhoques::ScratchAllocator s_allocator{1 << 18}; Buffer buffer(1 << 20); Buffer::Writer writer(&buffer); writer.WriteBits(0, 8); // Skip the first byte as it is used by packet acServerMessage.Serialize(writer); TiltedPhoques::PacketView packet(reinterpret_cast(buffer.GetWriteData()), static_cast(writer.Size())); Server::Send(aConnectionId, &packet); s_allocator.Reset(); } void GameServer::Send(ConnectionId_t aConnectionId, const ServerAdminMessage& acServerMessage) const { static thread_local TiltedPhoques::ScratchAllocator s_allocator{1 << 18}; Buffer buffer(1 << 20); Buffer::Writer writer(&buffer); writer.WriteBits(0, 8); // Skip the first byte as it is used by packet acServerMessage.Serialize(writer); TiltedPhoques::PacketView packet(reinterpret_cast(buffer.GetWriteData()), static_cast(writer.Size())); Server::Send(aConnectionId, &packet); s_allocator.Reset(); } void GameServer::SendToLoaded(const ServerMessage& acServerMessage) const { for (Player* pPlayer : m_pWorld->GetPlayerManager()) { if (pPlayer->GetCellComponent()) pPlayer->Send(acServerMessage); } } void GameServer::SendToPlayers(const ServerMessage& acServerMessage, const Player* apExcludedPlayer) const { for (Player* pPlayer : m_pWorld->GetPlayerManager()) { if (pPlayer != apExcludedPlayer) pPlayer->Send(acServerMessage); } } // NOTE: this doesn't check objects in range, only characters in range. bool GameServer::SendToPlayersInRange(const ServerMessage& acServerMessage, const entt::entity acOrigin, const Player* apExcludedPlayer) const { if (!m_pWorld->valid(acOrigin)) { spdlog::error("Entity is invalid: {:X}", World::ToInteger(acOrigin)); return false; } const auto view = m_pWorld->view(); const auto it = view.find(acOrigin); if (it == view.end()) { spdlog::warn("Cell component not found for entity {:X}", World::ToInteger(acOrigin)); return false; } const auto& cellComponent = view.get(*it); bool isDragon = false; if (const auto* characterComponent = m_pWorld->try_get(acOrigin)) isDragon = characterComponent->IsDragon(); for (Player* pPlayer : m_pWorld->GetPlayerManager()) { if (cellComponent.IsInRange(pPlayer->GetCellComponent(), isDragon) && pPlayer != apExcludedPlayer) pPlayer->Send(acServerMessage); } return true; } void GameServer::SendToParty(const ServerMessage& acServerMessage, const PartyComponent& acPartyComponent, const Player* apExcludeSender) const { if (!acPartyComponent.JoinedPartyId.has_value()) { spdlog::warn("Party does not exist, canceling broadcast."); return; } for (Player* pPlayer : m_pWorld->GetPlayerManager()) { if (pPlayer == apExcludeSender) continue; const auto& partyComponent = pPlayer->GetParty(); if (partyComponent.JoinedPartyId == acPartyComponent.JoinedPartyId) { pPlayer->Send(acServerMessage); } } } void GameServer::SendToPartyInRange(const ServerMessage& acServerMessage, const PartyComponent& acPartyComponent, const entt::entity acOrigin, const Player* apExcludeSender) const { if (!acPartyComponent.JoinedPartyId.has_value()) { spdlog::warn("Party does not exist, canceling broadcast."); return; } const auto view = m_pWorld->view(); const auto it = view.find(acOrigin); if (it == view.end()) { spdlog::warn("Cell component not found for entity {:X}", World::ToInteger(acOrigin)); return; } const auto& cellComponent = view.get(*it); for (Player* pPlayer : m_pWorld->GetPlayerManager()) { if (pPlayer == apExcludeSender) continue; if (!cellComponent.IsInRange(pPlayer->GetCellComponent(), false)) continue; if (pPlayer->GetParty().JoinedPartyId != acPartyComponent.JoinedPartyId) continue; pPlayer->Send(acServerMessage); } } static String PrettyPrintModList(const Vector& acMods) { String text; for (size_t i = 0; i < acMods.size(); i++) { text += acMods[i].Filename; if (i != (acMods.size() - 1)) text += ", "; } return text; } bool GameServer::ValidateAuthParams(ConnectionId_t aConnectionId, const UniquePtr& acRequest) { return false; } void GameServer::HandleAuthenticationRequest(const ConnectionId_t aConnectionId, const UniquePtr& acRequest) { const auto info = GetConnectionInfo(aConnectionId); char remoteAddress[48]{}; info.m_addrRemote.ToString(remoteAddress, 48, false); AuthenticationResponse serverResponse; serverResponse.Version = BUILD_COMMIT; using RT = AuthenticationResponse::ResponseType; auto sendKick = [&](const RT type) { serverResponse.Type = type; Send(aConnectionId, serverResponse); // the previous message is a lingering kick, it still gets delivered. Kick(aConnectionId); }; #if 1 // to make our testing life a bit easier. if (acRequest->Version != BUILD_COMMIT) { spdlog::info("New player {:x} '{}' tried to connect with client {} - Version mismatch", aConnectionId, remoteAddress, acRequest->Version.c_str()); sendKick(RT::kWrongVersion); return; } #endif if (m_pWorld->GetPlayerManager().Count() >= uMaxPlayerCount.value_as()) { sendKick(RT::kServerFull); return; } bool skseProblem = !bAllowSKSE && acRequest->SKSEActive; bool mo2Problem = !bAllowMO2 && acRequest->MO2Active; if (skseProblem || mo2Problem) { TiltedPhoques::String response; if (skseProblem) response += "SKSE "; if (mo2Problem) response += "MO2 "; spdlog::info("New player {:x} '{}' tried to connect, but {}{} disallowed - Kicked.", aConnectionId, remoteAddress, response.c_str(), skseProblem && mo2Problem ? "are" : "is"); serverResponse.SKSEActive = acRequest->SKSEActive; serverResponse.MO2Active = acRequest->MO2Active; sendKick(RT::kClientModsDisallowed); return; } bool adminPasswordUsed = acRequest->Token == sAdminPassword.value() && !sAdminPassword.empty(); // check if the proper server password was supplied. if (acRequest->Token == sPassword.value() || adminPasswordUsed) { if (adminPasswordUsed) { m_adminSessions.insert(aConnectionId); spdlog::warn("New admin session for {:x} '{}'", aConnectionId, remoteAddress); } Mods& responseList = serverResponse.UserMods; auto& modsComponent = m_pWorld->ctx().at(); if (IsMoPoActive()) { // mods that exist on the client, but not on the server // modscomponent contains a list filled in by the recordcollection Mods modsToRemove; const auto& userMods = acRequest->UserMods.ModList; for (const Mods::Entry& mod : userMods) { // if the client has more mods than the server.. if (!modsComponent.IsInstalled(mod.Filename)) { modsToRemove.ModList.push_back(mod); } } // TODO(Vince): if you have a better to do this than two for loops // let me know! // Also, for the future, lets think about a mode that allows more than the server installed mods // but requires essential mods? // mods that may exist on the server, but not on the client for (const auto& entry : modsComponent.GetServerMods()) { const auto it = std::find_if(userMods.begin(), userMods.end(), [&](const Mods::Entry& it) { return it.Filename == entry.first; }); if (it == userMods.end()) { Mods::Entry removeEntry; removeEntry.Filename = entry.first; removeEntry.Id = 0; modsToRemove.ModList.push_back(removeEntry); } } if (modsToRemove.ModList.size() > 0) { String text = PrettyPrintModList(modsToRemove.ModList); // "ModPolicy: refusing connection {:x} because essential mods are missing: {}" // for future reference ^ spdlog::info("ModPolicy: refusing connection {:x} because the following mods are installed on the client: {}", aConnectionId, text.c_str()); serverResponse.UserMods.ModList = std::move(modsToRemove.ModList); sendKick(RT::kModsMismatch); return; } } // Note: to lower traffic we only send the mod ids the user can fix in order as other ids will lead to a // null form id anyway Vector playerMods; Vector playerModsIds; size_t i = 0; for (auto& mod : acRequest->UserMods.ModList) { const uint32_t id = mod.IsLite ? modsComponent.AddLite(mod.Filename) : modsComponent.AddStandard(mod.Filename); Mods::Entry entry; entry.Filename = mod.Filename; entry.Id = static_cast(id); entry.IsLite = mod.IsLite; playerMods.push_back(mod.Filename); playerModsIds.push_back(entry.Id); responseList.ModList.push_back(entry); } Player* pPlayer = m_pWorld->GetPlayerManager().Create(aConnectionId); pPlayer->SetEndpoint(remoteAddress); pPlayer->SetDiscordId(acRequest->DiscordId); pPlayer->SetUsername(std::move(acRequest->Username)); pPlayer->SetMods(playerMods); pPlayer->SetModIds(playerModsIds); pPlayer->SetLevel(acRequest->Level); // this event is shit, needs to be fixed, i know auto [canceled, reason] = m_pWorld->GetScriptService().HandlePlayerJoin(aConnectionId); if (canceled) { spdlog::info("New player {:x} has a been rejected because \"{}\".", aConnectionId, reason.c_str()); Kick(aConnectionId); m_pWorld->GetPlayerManager().Remove(pPlayer); return; } serverResponse.PlayerId = pPlayer->GetId(); auto modList = PrettyPrintModList(acRequest->UserMods.ModList); spdlog::info("New player '{}' [{:x}] connected with {} mods\n\t: {}", pPlayer->GetUsername().c_str(), aConnectionId, acRequest->UserMods.ModList.size(), modList.c_str()); serverResponse.Settings = GetSettings(); serverResponse.Type = AuthenticationResponse::ResponseType::kAccepted; Send(aConnectionId, serverResponse); uint32_t startId = 0; auto initStringCache = StringCache::Get().Serialize(startId); pPlayer->SetStringCacheId(startId); Send(aConnectionId, initStringCache); for (auto* pOtherPlayer : m_pWorld->GetPlayerManager()) { if (pOtherPlayer == pPlayer) continue; NotifyPlayerJoined notify{}; notify.PlayerId = pOtherPlayer->GetId(); notify.Username = pOtherPlayer->GetUsername(); auto& cellComponent = pOtherPlayer->GetCellComponent(); notify.WorldSpaceId = cellComponent.WorldSpaceId; notify.CellId = cellComponent.Cell; notify.Level = pOtherPlayer->GetLevel(); spdlog::debug("[GameServer] New notify player {:x} {}", notify.PlayerId, notify.Username.c_str()); Send(pPlayer->GetConnectionId(), notify); } m_pWorld->GetDispatcher().trigger(PlayerJoinEvent(pPlayer, acRequest->WorldSpaceId, acRequest->CellId, acRequest->PlayerTime)); } /*else if (acRequest->Token == sAdminPassword.value() && !sAdminPassword.empty()) { AdminSessionOpen response; Send(aConnectionId, response); m_adminSessions.insert(aConnectionId); spdlog::warn("New admin session for {:x} '{}'", aConnectionId, remoteAddress); } */ else { spdlog::info("New player {:x} '{}' has a bad password, kicking.", aConnectionId, remoteAddress); sendKick(RT::kWrongPassword); } } void GameServer::UpdateSettings() { NotifySettingsChange notify{}; notify.Settings = GetSettings(); SendToPlayers(notify); } GameServer::Uptime GameServer::GetUptime() const noexcept { auto duration = std::chrono::high_resolution_clock::now() - m_startTime; auto weeks = std::chrono::duration_cast(duration); duration -= weeks; auto days = std::chrono::duration_cast(duration); duration -= days; auto hours = std::chrono::duration_cast(duration); duration -= hours; auto minutes = std::chrono::duration_cast(duration); return {weeks.count(), days.count(), hours.count(), minutes.count()}; } void GameServer::UpdateTitle() const { const auto name = m_info.name.empty() ? "Private server" : m_info.name; const char* playerText = GetClientCount() <= 1 ? " player" : " players"; const auto title = fmt::format("{} - {} {} - {} Ticks - " BUILD_BRANCH "@" BUILD_COMMIT, name.c_str(), GetClientCount(), playerText, GetTickRate()); #if TP_PLATFORM_WINDOWS SetConsoleTitleA(title.c_str()); #else std::cout << "\033]0;" << title << "\007"; #endif } Player* GameServer::GetAdminByUsername(const String& acUsername) const noexcept { for (auto session : m_adminSessions) { if (auto* pPlayer = PlayerManager::Get()->GetByConnectionId(session)) { if (pPlayer->GetUsername() == acUsername) return pPlayer; } } return nullptr; } Player const* GameServer::GetAdminByUsername(const String& acUsername) noexcept { for (auto session : m_adminSessions) { if (auto const* pPlayer = PlayerManager::Get()->GetByConnectionId(session)) { if (pPlayer->GetUsername() == acUsername) return pPlayer; } } return nullptr; } String GameServer::SanitizeUsername(const String& acUsername) const noexcept { String username = acUsername; // space in username handling | "_" -> space std::ranges::replace(username, '_', ' '); return username; }