diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 29b876e9f..9dcceea44 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -18,7 +18,7 @@ jobs: if: "!contains(github.event.head_commit.message, 'ci skip')" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Checkout submodules run: | @@ -28,32 +28,27 @@ jobs: # Install dependencies - name: Update apt repositories run: sudo apt-get update - - - name: Install GCC12 - shell: bash - run: | - sudo apt update - sudo apt install gcc-12 g++-12 - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 110 --slave /usr/bin/g++ g++ /usr/bin/g++-12 --slave /usr/bin/gcov gcov /usr/bin/gcov-12 - + # Install xmake - name: Setup xmake uses: xmake-io/github-action-setup-xmake@v1 with: - xmake-version: '2.9.2' + xmake-version: '2.9.5' # Update xmake repository (in order to have the file that will be cached) - name: Update xmake repository run: xmake repo --update # Setup compilation mode and install project dependencies + # (continue-on-error + timeout is a temporary solution until sentry-native is fixed; shouldn't affect the building step) - name: Configure xmake and install dependencies - run: xmake config --arch=${{ matrix.arch }} --mode=${{ matrix.mode }} --yes + continue-on-error: true + run: timeout 15m xmake config --arch=${{ matrix.arch }} --mode=${{ matrix.mode }} --yes - # Build the game + # Build the server - name: Build - run: xmake - + run: xmake -y + # Create install #- name: Install # run: xmake install -o packaged diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 50220055f..5723e146c 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -30,7 +30,7 @@ jobs: - name: Setup xmake uses: xmake-io/github-action-setup-xmake@v1 with: - xmake-version: '2.9.2' + xmake-version: '2.9.5' # Install node #- name: Setup nodejs diff --git a/Code/client/Events/ActivateEvent.h b/Code/client/Events/ActivateEvent.h index 3c5fee605..b3b64a1fe 100644 --- a/Code/client/Events/ActivateEvent.h +++ b/Code/client/Events/ActivateEvent.h @@ -22,13 +22,14 @@ struct ActivateEvent { } #elif TP_SKYRIM64 - ActivateEvent(TESObjectREFR* apObject, Actor* apActivator, TESBoundObject* apObjectToGet, int32_t aCount, bool aDefaultProcessing, uint8_t aUnk1, bool aActivateFlag = false) + ActivateEvent(TESObjectREFR* apObject, Actor* apActivator, TESBoundObject* apObjectToGet, int32_t aCount, bool aDefaultProcessing, uint8_t aUnk1, TESObjectREFR::OpenState aPreActivationOpenState, bool aActivateFlag = false) : pObject(apObject) , pActivator(apActivator) , pObjectToGet(apObjectToGet) , Count(aCount) , DefaultProcessing(aDefaultProcessing) , Unk1(aUnk1) + , PreActivationOpenState(aPreActivationOpenState) , ActivateFlag(aActivateFlag) { } @@ -46,5 +47,6 @@ struct ActivateEvent uint8_t Unk1; #endif + TESObjectREFR::OpenState PreActivationOpenState; bool ActivateFlag; }; diff --git a/Code/client/Events/SetTimeCommandEvent.h b/Code/client/Events/SetTimeCommandEvent.h new file mode 100644 index 000000000..c6385d90a --- /dev/null +++ b/Code/client/Events/SetTimeCommandEvent.h @@ -0,0 +1,16 @@ +#pragma once + +struct SetTimeCommandEvent +{ + SetTimeCommandEvent() = delete; + SetTimeCommandEvent(uint8_t aHours, uint8_t aMinutes, uint32_t aPlayerId) + : Hours(aHours) + , Minutes(aMinutes) + , PlayerId(aPlayerId) + { + } + + uint8_t Hours; + uint8_t Minutes; + uint32_t PlayerId; +}; diff --git a/Code/client/Games/Fallout4/TESObjectREFR.h b/Code/client/Games/Fallout4/TESObjectREFR.h index 007a9c244..481dcd50d 100644 --- a/Code/client/Games/Fallout4/TESObjectREFR.h +++ b/Code/client/Games/Fallout4/TESObjectREFR.h @@ -31,6 +31,15 @@ struct TESObjectREFR : TESForm IRR_STORE_IN_TEAMMATE = 0x5, }; + enum OpenState : uint8_t + { + kNone = 0, + kOpen, + kOpening, + kClosed, + kClosing, + }; + struct RemoveItemData { uint8_t stackData[0x20]{}; @@ -194,6 +203,7 @@ struct TESObjectREFR : TESForm void LockChange() noexcept; const float GetHeight() noexcept; + OpenState GetOpenState() noexcept; Inventory GetInventory() const noexcept; void SetInventory(const Inventory& acContainer) noexcept; diff --git a/Code/client/Games/References.cpp b/Code/client/Games/References.cpp index c143d901c..b0b836a17 100644 --- a/Code/client/Games/References.cpp +++ b/Code/client/Games/References.cpp @@ -690,6 +690,15 @@ const float TESObjectREFR::GetHeight() noexcept return boundMax.z - GetBoundMin().z; } +TESObjectREFR::OpenState TESObjectREFR::GetOpenState() noexcept +{ + using ObjectReference = TESObjectREFR; + + PAPYRUS_FUNCTION(TESObjectREFR::OpenState, ObjectReference, GetOpenState); + + return s_pGetOpenState(this); +} + bool ActorState::SetWeaponDrawn(bool aDraw) noexcept { TP_THIS_FUNCTION(TSetWeaponState, bool, ActorState, bool aDraw); diff --git a/Code/client/Games/Skyrim/Forms/MagicItem.cpp b/Code/client/Games/Skyrim/Forms/MagicItem.cpp index 4dcf67669..a91e33121 100644 --- a/Code/client/Games/Skyrim/Forms/MagicItem.cpp +++ b/Code/client/Games/Skyrim/Forms/MagicItem.cpp @@ -31,6 +31,17 @@ bool MagicItem::IsBuffSpell() const noexcept } } +bool MagicItem::IsBoundWeaponSpell() noexcept +{ + for (EffectItem* pEffect : listOfEffects) + { + if (pEffect->pEffectSetting && pEffect->pEffectSetting->eArchetype == EffectArchetypes::ArchetypeID::kBoundWeapon) + return true; + } + + return false; +} + EffectItem* MagicItem::GetEffect(const uint32_t aEffectId) noexcept { for (EffectItem* pEffect : listOfEffects) diff --git a/Code/client/Games/Skyrim/Forms/MagicItem.h b/Code/client/Games/Skyrim/Forms/MagicItem.h index ae86b2947..8428bd739 100644 --- a/Code/client/Games/Skyrim/Forms/MagicItem.h +++ b/Code/client/Games/Skyrim/Forms/MagicItem.h @@ -13,6 +13,7 @@ struct MagicItem : TESBoundObject bool IsInvisibilitySpell() const noexcept; bool IsHealingSpell() const noexcept; bool IsBuffSpell() const noexcept; + bool IsBoundWeaponSpell() noexcept; EffectItem* GetEffect(const uint32_t aEffectId) noexcept; diff --git a/Code/client/Games/Skyrim/TESObjectREFR.cpp b/Code/client/Games/Skyrim/TESObjectREFR.cpp index eb56bd92f..425318227 100644 --- a/Code/client/Games/Skyrim/TESObjectREFR.cpp +++ b/Code/client/Games/Skyrim/TESObjectREFR.cpp @@ -611,7 +611,15 @@ bool TP_MAKE_THISCALL(HookActivate, TESObjectREFR, TESObjectREFR* apActivator, u // Exclude books from activation since only reading them removes them from the cell // Note: Books are now unsynced if (pActivator && apThis->baseForm->formType != FormType::Book) - World::Get().GetRunner().Trigger(ActivateEvent(apThis, pActivator, apObjectToGet, aUnk1, aCount, aDefaultProcessing)); + { + auto openState = TESObjectREFR::kNone; + if (apThis->baseForm->formType == FormType::Door) + openState = apThis->GetOpenState(); + + World::Get().GetRunner().Trigger( + ActivateEvent(apThis, pActivator, apObjectToGet, aCount, aDefaultProcessing, aUnk1, openState) + ); + } return TiltedPhoques::ThisCall(RealActivate, apThis, apActivator, aUnk1, apObjectToGet, aCount, aDefaultProcessing); } diff --git a/Code/client/Games/Skyrim/TESObjectREFR.h b/Code/client/Games/Skyrim/TESObjectREFR.h index d6d2aec91..6aec10c1a 100644 --- a/Code/client/Games/Skyrim/TESObjectREFR.h +++ b/Code/client/Games/Skyrim/TESObjectREFR.h @@ -47,6 +47,15 @@ struct TESObjectREFR : TESForm CHANGE_REFR_EXTRA_GAME_ONLY = 1u << 31, }; + enum OpenState : uint8_t + { + kNone = 0, + kOpen, + kOpening, + kClosed, + kClosing, + }; + static TESObjectREFR* GetByHandle(uint32_t aHandle) noexcept; static uint32_t* GetNullHandle() noexcept; @@ -188,6 +197,7 @@ struct TESObjectREFR : TESForm const float GetHeight() noexcept; void EnableImpl() noexcept; + OpenState GetOpenState() noexcept; Inventory GetInventory() const noexcept; Inventory GetInventory(std::function aFilter) const noexcept; diff --git a/Code/client/NvidiaUtil.cpp b/Code/client/NvidiaUtil.cpp new file mode 100644 index 000000000..27dd513c7 --- /dev/null +++ b/Code/client/NvidiaUtil.cpp @@ -0,0 +1,16 @@ +#include "NvidiaUtil.h" +#include + +bool IsNvidiaOverlayLoaded() +{ + return GetModuleHandleW(L"nvspcap64.dll"); +} + +// This makes the Nvidia overlay happy. +// The call to D3D11CreateDevice probably causes some of their +// internal hooks to be called and do the required init work before the game window opens. +HRESULT CreateEarlyDxDevice(ID3D11Device* apOutDevice, D3D_FEATURE_LEVEL* apOutFeatureLevel) +{ + return D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0, D3D11_SDK_VERSION, &apOutDevice, + apOutFeatureLevel, nullptr); +} diff --git a/Code/client/NvidiaUtil.h b/Code/client/NvidiaUtil.h new file mode 100644 index 000000000..6a5666510 --- /dev/null +++ b/Code/client/NvidiaUtil.h @@ -0,0 +1,8 @@ +#pragma once + +#include +#include + +bool IsNvidiaOverlayLoaded(); + +HRESULT CreateEarlyDxDevice(ID3D11Device* apOutDevice, D3D_FEATURE_LEVEL* apOutFeatureLevel); diff --git a/Code/client/Services/CommandService.h b/Code/client/Services/CommandService.h index 56b959c00..1aa81eb8c 100644 --- a/Code/client/Services/CommandService.h +++ b/Code/client/Services/CommandService.h @@ -2,6 +2,7 @@ struct World; struct TransportService; +struct SetTimeCommandEvent; struct TeleportCommandResponse; /** @@ -19,6 +20,7 @@ struct CommandService TP_NOCOPYMOVE(CommandService); protected: + void OnSetTimeCommand(const SetTimeCommandEvent&) const noexcept; /** * @brief Processes result of teleport command. */ @@ -28,5 +30,6 @@ struct CommandService World& m_world; TransportService& m_transport; + entt::scoped_connection m_setTimeConnection; entt::scoped_connection m_teleportConnection; }; diff --git a/Code/client/Services/Generic/CommandService.cpp b/Code/client/Services/Generic/CommandService.cpp index 1d9b093c6..ce1f48e3f 100644 --- a/Code/client/Services/Generic/CommandService.cpp +++ b/Code/client/Services/Generic/CommandService.cpp @@ -1,3 +1,4 @@ + #include #include @@ -8,8 +9,11 @@ #include #include +#include + #include #include +#include "Messages/SetTimeCommandRequest.h" #include @@ -17,9 +21,19 @@ CommandService::CommandService(World& aWorld, TransportService& aTransport, entt : m_world(aWorld) , m_transport(aTransport) { + m_setTimeConnection = aDispatcher.sink().connect<&CommandService::OnSetTimeCommand>(this); m_teleportConnection = aDispatcher.sink().connect<&CommandService::OnTeleportCommandResponse>(this); } +void CommandService::OnSetTimeCommand(const SetTimeCommandEvent& acEvent) const noexcept +{ + SetTimeCommandRequest request{}; + request.Hours = acEvent.Hours; + request.Minutes = acEvent.Minutes; + request.PlayerId = acEvent.PlayerId; + m_transport.Send(request); +} + void CommandService::OnTeleportCommandResponse(const TeleportCommandResponse& acMessage) noexcept { spdlog::info("TeleportCommandResponse: {:X}:{:X} at position {}, {}, {}", acMessage.CellId.ModId, acMessage.CellId.BaseId, acMessage.Position.x, acMessage.Position.y, acMessage.Position.z); diff --git a/Code/client/Services/Generic/MagicService.cpp b/Code/client/Services/Generic/MagicService.cpp index 3f1f500a0..d8f3e62a4 100644 --- a/Code/client/Services/Generic/MagicService.cpp +++ b/Code/client/Services/Generic/MagicService.cpp @@ -298,7 +298,7 @@ void MagicService::OnAddTargetEvent(const AddTargetEvent& acEvent) noexcept // These effects are applied through spell cast sync if (SpellItem* pSpellItem = Cast(TESForm::GetById(acEvent.SpellID))) { - if ((pSpellItem->eCastingType == MagicSystem::CastingType::CONCENTRATION && !pSpellItem->IsHealingSpell()) || pSpellItem->IsWardSpell() || pSpellItem->IsInvisibilitySpell()) + if ((pSpellItem->eCastingType == MagicSystem::CastingType::CONCENTRATION && !pSpellItem->IsHealingSpell()) || pSpellItem->IsWardSpell() || pSpellItem->IsInvisibilitySpell() || pSpellItem->IsBoundWeaponSpell()) { return; } diff --git a/Code/client/Services/Generic/ObjectService.cpp b/Code/client/Services/Generic/ObjectService.cpp index a1f08431c..dfeed7731 100644 --- a/Code/client/Services/Generic/ObjectService.cpp +++ b/Code/client/Services/Generic/ObjectService.cpp @@ -277,6 +277,7 @@ void ObjectService::OnActivate(const ActivateEvent& acEvent) noexcept return; request.ActivatorId = serverIdRes.value(); + request.PreActivationOpenState = acEvent.PreActivationOpenState; m_transport.Send(request); } @@ -298,6 +299,19 @@ void ObjectService::OnActivateNotify(const NotifyActivate& acMessage) noexcept return; } + if (pObject->baseForm->formType == FormType::Door) + { + auto remotePreActivationState = static_cast(acMessage.PreActivationOpenState); + TESObjectREFR::OpenState localState = pObject->GetOpenState(); + + if (remotePreActivationState != localState) + { + // The doors are unsynced at this point. If we'll Activate the one on our side + // it'll just continue to be unsynced (open remotely, closed locally and vice versa) + return; + } + } + // unsure if these flags are the best, but these are passed with the papyrus Activate fn // might be an idea to have the client send the flags through NotifyActivate #if TP_FALLOUT4 diff --git a/Code/client/Services/Generic/OverlayClient.cpp b/Code/client/Services/Generic/OverlayClient.cpp index d493ad1c8..3d18380be 100644 --- a/Code/client/Services/Generic/OverlayClient.cpp +++ b/Code/client/Services/Generic/OverlayClient.cpp @@ -8,6 +8,8 @@ #include #include +#include + #include OverlayClient::OverlayClient(TransportService& aTransport, TiltedPhoques::OverlayRenderHandler* apHandler) @@ -44,6 +46,8 @@ bool OverlayClient::OnProcessMessageReceived(CefRefPtr browser, CefR ProcessDisconnectMessage(); else if (eventName == "sendMessage") ProcessChatMessage(eventArgs); + else if (eventName == "setTime") + ProcessSetTimeCommand(eventArgs); else if (eventName == "launchParty") World::Get().GetPartyService().CreateParty(); else if (eventName == "leaveParty") @@ -111,10 +115,19 @@ void OverlayClient::ProcessChatMessage(CefRefPtr aEventArgs) messageRequest.ChatMessage = contents; spdlog::info(L"Send chat message of type {}: '{}' ", messageRequest.MessageType, aEventArgs->GetString(1).ToWString()); + m_transport.Send(messageRequest); } } +void OverlayClient::ProcessSetTimeCommand(CefRefPtr aEventArgs) +{ + const uint8_t hours = static_cast(aEventArgs->GetInt(0)); + const uint8_t minutes = static_cast(aEventArgs->GetInt(1)); + const uint32_t senderId = m_transport.GetLocalPlayerId(); + World::Get().GetDispatcher().trigger(SetTimeCommandEvent(hours, minutes, senderId)); +} + void OverlayClient::ProcessTeleportMessage(CefRefPtr aEventArgs) { TeleportRequest request{}; diff --git a/Code/client/Services/Generic/TransportService.cpp b/Code/client/Services/Generic/TransportService.cpp index 1b48c29f9..0f3f684dc 100644 --- a/Code/client/Services/Generic/TransportService.cpp +++ b/Code/client/Services/Generic/TransportService.cpp @@ -37,6 +37,8 @@ TransportService::TransportService(World& aWorld, entt::dispatcher& aDispatcher) { m_updateConnection = m_dispatcher.sink().connect<&TransportService::HandleUpdate>(this); m_settingsChangeConnection = m_dispatcher.sink().connect<&TransportService::HandleNotifySettingsChange>(this); + m_connectedConnection = m_dispatcher.sink().connect<&TransportService::HandleConnected>(this); + m_disconnectedConnection = m_dispatcher.sink().connect<&TransportService::HandleDisconnected>(this); m_connected = false; @@ -181,6 +183,16 @@ void TransportService::HandleUpdate(const UpdateEvent& acEvent) noexcept Update(); } +void TransportService::HandleConnected(const ConnectedEvent& acEvent) noexcept +{ + m_localPlayerId = acEvent.PlayerId; +} + +void TransportService::HandleDisconnected(const DisconnectedEvent& acEvent) noexcept +{ + m_localPlayerId = NULL; +} + void TransportService::HandleAuthenticationResponse(const AuthenticationResponse& acMessage) noexcept { using AR = AuthenticationResponse::ResponseType; diff --git a/Code/client/Services/OverlayClient.h b/Code/client/Services/OverlayClient.h index 4230f4843..faab826e5 100644 --- a/Code/client/Services/OverlayClient.h +++ b/Code/client/Services/OverlayClient.h @@ -25,6 +25,7 @@ struct OverlayClient : TiltedPhoques::OverlayClient void ProcessConnectMessage(CefRefPtr aEventArgs); void ProcessDisconnectMessage(); void ProcessChatMessage(CefRefPtr aEventArgs); + void ProcessSetTimeCommand(CefRefPtr aEventArgs); void ProcessTeleportMessage(CefRefPtr aEventArgs); void ProcessToggleDebugUI(); diff --git a/Code/client/Services/TransportService.h b/Code/client/Services/TransportService.h index e0257a009..8f2c3503e 100644 --- a/Code/client/Services/TransportService.h +++ b/Code/client/Services/TransportService.h @@ -1,5 +1,8 @@ #pragma once +#include "Events/ConnectedEvent.h" +#include "Events/DisconnectedEvent.h" + #include #include @@ -32,10 +35,13 @@ struct TransportService : Client [[nodiscard]] bool IsOnline() const noexcept { return m_connected; } void SetServerPassword(const std::string& acPassword) noexcept { m_serverPassword = acPassword; } + const uint32_t& GetLocalPlayerId() const noexcept { return m_localPlayerId; } protected: // Event handlers void HandleUpdate(const UpdateEvent& acEvent) noexcept; + void HandleConnected(const ConnectedEvent& acEvent) noexcept; + void HandleDisconnected(const DisconnectedEvent& acEvent) noexcept; // Packet handlers void HandleAuthenticationResponse(const AuthenticationResponse& acMessage) noexcept; @@ -46,9 +52,12 @@ struct TransportService : Client entt::dispatcher& m_dispatcher; bool m_connected; String m_serverPassword{}; + uint32_t m_localPlayerId; entt::scoped_connection m_updateConnection; entt::scoped_connection m_sendServerMessageConnection; entt::scoped_connection m_settingsChangeConnection; + entt::scoped_connection m_connectedConnection; + entt::scoped_connection m_disconnectedConnection; std::function&)> m_messageHandlers[kServerOpcodeMax]; }; diff --git a/Code/client/TiltedOnlineApp.cpp b/Code/client/TiltedOnlineApp.cpp index 53ff356d6..a6da26aa8 100644 --- a/Code/client/TiltedOnlineApp.cpp +++ b/Code/client/TiltedOnlineApp.cpp @@ -7,6 +7,7 @@ #include +#include #include #include @@ -17,6 +18,7 @@ #include #include +#include using TiltedPhoques::Debug; @@ -56,12 +58,18 @@ bool TiltedOnlineApp::BeginMain() World::Get().ctx().emplace(World::Get().ctx().at(), World::Get().ctx().at()); LoadScriptExender(); + + if (IsNvidiaOverlayLoaded()) + ApplyNvidiaFix(); + return true; } bool TiltedOnlineApp::EndMain() { UninstallHooks(); + if (m_pDevice) + m_pDevice->Release(); return true; } @@ -107,3 +115,14 @@ void TiltedOnlineApp::InstallHooks2() void TiltedOnlineApp::UninstallHooks() { } + +void TiltedOnlineApp::ApplyNvidiaFix() noexcept +{ + auto d3dFeatureLevel = D3D_FEATURE_LEVEL_11_0; + HRESULT hr = CreateEarlyDxDevice(m_pDevice, &d3dFeatureLevel); + if (FAILED(hr)) + spdlog::error("D3D11CreateDevice failed. Detected an NVIDIA GPU, error code={0:x}", hr); + + if (d3dFeatureLevel < D3D_FEATURE_LEVEL_11_0) + spdlog::warn("Unexpected D3D11 feature level detected (< 11.0), may cause issues"); +} diff --git a/Code/client/TiltedOnlineApp.h b/Code/client/TiltedOnlineApp.h index f860966e7..19f17649b 100644 --- a/Code/client/TiltedOnlineApp.h +++ b/Code/client/TiltedOnlineApp.h @@ -1,6 +1,7 @@ #pragma once #include +#include #if (!IS_MASTER) #include "CrashHandler.h" @@ -35,9 +36,11 @@ struct TiltedOnlineApp final : App void UninstallHooks(); private: + void ApplyNvidiaFix() noexcept; #if (!IS_MASTER) CrashHandler m_crashHandler; #else //ScopedCrashHandler m_crashHandler; #endif + ID3D11Device* m_pDevice = nullptr; }; diff --git a/Code/client/main.cpp b/Code/client/main.cpp index 580dcaf64..9b7341a51 100644 --- a/Code/client/main.cpp +++ b/Code/client/main.cpp @@ -16,7 +16,7 @@ static void ShowAddressLibraryError(const wchar_t* apGamePath) #if TP_SKYRIM64 auto errorDetail = fmt::format(L"Looking for it here: {}\\Data\\SKSE\\Plugins", apGamePath); - Base::TaskDialog dia(g_SharedWindowIcon, L"Error", L"Failed to load Skyrim Address Library", L"Make sure to use the All in one (Anniversary Edition) even if you don't have the Anniversary Edition upgrade", errorDetail.c_str()); + Base::TaskDialog dia(g_SharedWindowIcon, L"Error", L"Failed to load Skyrim Address Library", L"Make sure to use \"All in one (1.6.X)\"", errorDetail.c_str()); #elif TP_FALLOUT4 auto errorDetail = fmt::format(L"Looking for it here: {}\\Data\\F4SE\\Plugins", apGamePath); diff --git a/Code/components/console/ConsoleRegistry.cpp b/Code/components/console/ConsoleRegistry.cpp index f9ed4b732..1e3e4fb3d 100644 --- a/Code/components/console/ConsoleRegistry.cpp +++ b/Code/components/console/ConsoleRegistry.cpp @@ -258,7 +258,7 @@ void ConsoleRegistry::StoreCommandInHistory(const TiltedPhoques::String& acLine) // Do some housekeeping. if (m_commandHistory.size() == 10) { - m_commandHistory.erase(m_commandHistory.end()); + m_commandHistory.pop_back(); } } diff --git a/Code/encoding/Messages/ActivateRequest.cpp b/Code/encoding/Messages/ActivateRequest.cpp index ef4e44b5b..f6e09e92b 100644 --- a/Code/encoding/Messages/ActivateRequest.cpp +++ b/Code/encoding/Messages/ActivateRequest.cpp @@ -5,6 +5,7 @@ void ActivateRequest::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const Id.Serialize(aWriter); CellId.Serialize(aWriter); Serialization::WriteVarInt(aWriter, ActivatorId); + aWriter.WriteBits(PreActivationOpenState, 8); } void ActivateRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept @@ -14,4 +15,7 @@ void ActivateRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noe Id.Deserialize(aReader); CellId.Deserialize(aReader); ActivatorId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; + uint64_t preActivationOpenState = 0; + aReader.ReadBits(preActivationOpenState, 8); + PreActivationOpenState = preActivationOpenState & 0xFF; } diff --git a/Code/encoding/Messages/ActivateRequest.h b/Code/encoding/Messages/ActivateRequest.h index 1c2d21d0b..beffe6441 100644 --- a/Code/encoding/Messages/ActivateRequest.h +++ b/Code/encoding/Messages/ActivateRequest.h @@ -16,9 +16,10 @@ struct ActivateRequest final : ClientMessage void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; - bool operator==(const ActivateRequest& acRhs) const noexcept { return Id == acRhs.Id && ActivatorId == acRhs.ActivatorId && CellId == acRhs.CellId && GetOpcode() == acRhs.GetOpcode(); } + bool operator==(const ActivateRequest& acRhs) const noexcept { return Id == acRhs.Id && ActivatorId == acRhs.ActivatorId && CellId == acRhs.CellId && PreActivationOpenState == acRhs.PreActivationOpenState && GetOpcode() == acRhs.GetOpcode(); } GameId Id; GameId CellId; uint32_t ActivatorId; + uint8_t PreActivationOpenState; }; diff --git a/Code/encoding/Messages/ClientMessageFactory.h b/Code/encoding/Messages/ClientMessageFactory.h index 8a34d91d0..9c90ad351 100644 --- a/Code/encoding/Messages/ClientMessageFactory.h +++ b/Code/encoding/Messages/ClientMessageFactory.h @@ -43,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -65,10 +66,10 @@ struct ClientMessageFactory { auto s_visitor = CreateMessageVisitor< AuthenticationRequest, AssignCharacterRequest, CancelAssignmentRequest, ClientReferencesMoveRequest, EnterInteriorCellRequest, RequestInventoryChanges, RequestFactionsChanges, RequestQuestUpdate, PartyInviteRequest, PartyAcceptInviteRequest, PartyLeaveRequest, PartyCreateRequest, - PartyChangeLeaderRequest, PartyKickRequest, RequestActorValueChanges, RequestActorMaxValueChanges, EnterExteriorCellRequest, RequestHealthChangeBroadcast, ActivateRequest, LockChangeRequest, AssignObjectsRequest, RequestDeathStateChange, ShiftGridCellRequest, - RequestOwnershipTransfer, RequestOwnershipClaim, RequestObjectInventoryChanges, SpellCastRequest, ProjectileLaunchRequest, InterruptCastRequest, AddTargetRequest, ScriptAnimationRequest, DrawWeaponRequest, MountRequest, NewPackageRequest, RequestRespawn, SyncExperienceRequest, - RequestEquipmentChanges, SendChatMessageRequest, TeleportCommandRequest, PlayerRespawnRequest, DialogueRequest, SubtitleRequest, PlayerDialogueRequest, PlayerLevelRequest, TeleportRequest, RequestPlayerHealthUpdate, RequestWeatherChange, RequestCurrentWeather, RequestSetWaypoint, - RequestRemoveWaypoint>; + PartyChangeLeaderRequest, PartyKickRequest, RequestActorValueChanges, RequestActorMaxValueChanges, EnterExteriorCellRequest, RequestHealthChangeBroadcast, ActivateRequest, LockChangeRequest, AssignObjectsRequest, RequestDeathStateChange, ShiftGridCellRequest, RequestOwnershipTransfer, + RequestOwnershipClaim, RequestObjectInventoryChanges, SpellCastRequest, ProjectileLaunchRequest, InterruptCastRequest, AddTargetRequest, ScriptAnimationRequest, DrawWeaponRequest, MountRequest, NewPackageRequest, RequestRespawn, SyncExperienceRequest, RequestEquipmentChanges, + SendChatMessageRequest, TeleportCommandRequest, PlayerRespawnRequest, DialogueRequest, SubtitleRequest, PlayerDialogueRequest, PlayerLevelRequest, TeleportRequest, RequestPlayerHealthUpdate, RequestWeatherChange, RequestCurrentWeather, RequestSetWaypoint, RequestRemoveWaypoint, + SetTimeCommandRequest>; return s_visitor(std::forward(func)); } diff --git a/Code/encoding/Messages/NotifyActivate.cpp b/Code/encoding/Messages/NotifyActivate.cpp index ba910515a..c2603c59c 100644 --- a/Code/encoding/Messages/NotifyActivate.cpp +++ b/Code/encoding/Messages/NotifyActivate.cpp @@ -4,6 +4,7 @@ void NotifyActivate::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const { Id.Serialize(aWriter); Serialization::WriteVarInt(aWriter, ActivatorId); + aWriter.WriteBits(PreActivationOpenState, 8); } void NotifyActivate::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept @@ -12,4 +13,7 @@ void NotifyActivate::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noex Id.Deserialize(aReader); ActivatorId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; + uint64_t preActivationOpenState = 0; + aReader.ReadBits(preActivationOpenState, 8); + PreActivationOpenState = preActivationOpenState & 0xFF; } diff --git a/Code/encoding/Messages/NotifyActivate.h b/Code/encoding/Messages/NotifyActivate.h index cd72b6ac2..7c90fda85 100644 --- a/Code/encoding/Messages/NotifyActivate.h +++ b/Code/encoding/Messages/NotifyActivate.h @@ -15,8 +15,9 @@ struct NotifyActivate final : ServerMessage void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; - bool operator==(const NotifyActivate& acRhs) const noexcept { return GetOpcode() == acRhs.GetOpcode() && Id == acRhs.Id && ActivatorId == acRhs.ActivatorId; } + bool operator==(const NotifyActivate& acRhs) const noexcept { return GetOpcode() == acRhs.GetOpcode() && Id == acRhs.Id && ActivatorId == acRhs.ActivatorId && PreActivationOpenState == acRhs.PreActivationOpenState; } GameId Id; uint32_t ActivatorId; + uint8_t PreActivationOpenState; }; diff --git a/Code/encoding/Messages/NotifySetTimeResult.cpp b/Code/encoding/Messages/NotifySetTimeResult.cpp new file mode 100644 index 000000000..abcdd184f --- /dev/null +++ b/Code/encoding/Messages/NotifySetTimeResult.cpp @@ -0,0 +1,15 @@ +#include + +void NotifySetTimeResult::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + aWriter.WriteBits(static_cast(Result), 8); +} + +void NotifySetTimeResult::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ServerMessage::DeserializeRaw(aReader); + + uint64_t dest = 0; + aReader.ReadBits(dest, 8); + Result = static_cast(dest & 0xFF); +} diff --git a/Code/encoding/Messages/NotifySetTimeResult.h b/Code/encoding/Messages/NotifySetTimeResult.h new file mode 100644 index 000000000..5c27e059a --- /dev/null +++ b/Code/encoding/Messages/NotifySetTimeResult.h @@ -0,0 +1,26 @@ +#pragma once + +#include "Message.h" + +struct NotifySetTimeResult final : ServerMessage +{ + static constexpr ServerOpcode Opcode = kNotifySetTimeResult; + + NotifySetTimeResult() + : ServerMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const NotifySetTimeResult& achRhs) const noexcept { return GetOpcode() == achRhs.GetOpcode() && Result == achRhs.Result; } + + enum class SetTimeResult : uint8_t + { + kSuccess, + kNoPermission + }; + + SetTimeResult Result{}; +}; diff --git a/Code/encoding/Messages/ServerMessageFactory.h b/Code/encoding/Messages/ServerMessageFactory.h index 5c22480a4..a1db0d4eb 100644 --- a/Code/encoding/Messages/ServerMessageFactory.h +++ b/Code/encoding/Messages/ServerMessageFactory.h @@ -58,6 +58,7 @@ #include #include #include +#include using TiltedPhoques::UniquePtr; @@ -71,8 +72,8 @@ struct ServerMessageFactory AuthenticationResponse, AssignCharacterResponse, ServerReferencesMoveRequest, ServerTimeSettings, CharacterSpawnRequest, NotifyInventoryChanges, StringCacheUpdate, NotifyFactionsChanges, NotifyRemoveCharacter, NotifyQuestUpdate, NotifyPlayerList, NotifyPartyInfo, NotifyPartyInvite, NotifyActorValueChanges, NotifyPartyJoined, NotifyPartyLeft, NotifyActorMaxValueChanges, NotifyHealthChangeBroadcast, NotifySpawnData, NotifyActivate, NotifyLockChange, AssignObjectsResponse, NotifyDeathStateChange, NotifyOwnershipTransfer, NotifyObjectInventoryChanges, NotifySpellCast, NotifyProjectileLaunch, NotifyInterruptCast, NotifyAddTarget, NotifyScriptAnimation, NotifyDrawWeapon, NotifyMount, NotifyNewPackage, NotifyRespawn, NotifySyncExperience, NotifyEquipmentChanges, NotifyChatMessageBroadcast, TeleportCommandResponse, NotifyPlayerRespawn, NotifyDialogue, - NotifySubtitle, NotifyPlayerDialogue, NotifyActorTeleport, NotifyRelinquishControl, NotifyPlayerLeft, NotifyPlayerJoined, NotifyDialogue, NotifySubtitle, NotifyPlayerDialogue, NotifyPlayerLevel, NotifyPlayerCellChanged, NotifyTeleport, NotifyPlayerHealthUpdate, NotifySettingsChange, NotifyWeatherChange, NotifySetWaypoint, - NotifyRemoveWaypoint>; + NotifySubtitle, NotifyPlayerDialogue, NotifyActorTeleport, NotifyRelinquishControl, NotifyPlayerLeft, NotifyPlayerJoined, NotifyDialogue, NotifySubtitle, NotifyPlayerDialogue, NotifyPlayerLevel, NotifyPlayerCellChanged, NotifyTeleport, NotifyPlayerHealthUpdate, NotifySettingsChange, + NotifyWeatherChange, NotifySetWaypoint, NotifyRemoveWaypoint, NotifySetTimeResult>; return s_visitor(std::forward(func)); } diff --git a/Code/encoding/Messages/SetTimeCommandRequest.cpp b/Code/encoding/Messages/SetTimeCommandRequest.cpp new file mode 100644 index 000000000..860dd2397 --- /dev/null +++ b/Code/encoding/Messages/SetTimeCommandRequest.cpp @@ -0,0 +1,21 @@ +#include "SetTimeCommandRequest.h" + +void SetTimeCommandRequest::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + aWriter.WriteBits(Hours, 8); + aWriter.WriteBits(Minutes, 8); + aWriter.WriteBits(PlayerId, 32); +} + +void SetTimeCommandRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ClientMessage::DeserializeRaw(aReader); + + uint64_t dest = 0; + aReader.ReadBits(dest, 8); + Hours = dest & 0xFF; + aReader.ReadBits(dest, 8); + Minutes = dest & 0xFF; + aReader.ReadBits(dest, 32); + PlayerId = dest & 0xFFFFFFFF; +} diff --git a/Code/encoding/Messages/SetTimeCommandRequest.h b/Code/encoding/Messages/SetTimeCommandRequest.h new file mode 100644 index 000000000..071dfad8e --- /dev/null +++ b/Code/encoding/Messages/SetTimeCommandRequest.h @@ -0,0 +1,22 @@ +#pragma once + +#include "Message.h" + +struct SetTimeCommandRequest final : ClientMessage +{ + static constexpr ClientOpcode Opcode = kSetTimeCommandRequest; + + SetTimeCommandRequest() + : ClientMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const SetTimeCommandRequest& acRhs) const noexcept { return GetOpcode() == acRhs.GetOpcode() && Hours == acRhs.Hours && Minutes == acRhs.Minutes && PlayerId == acRhs.PlayerId; } + + uint8_t Hours{}; + uint8_t Minutes{}; + uint32_t PlayerId{}; +}; diff --git a/Code/encoding/Opcodes.h b/Code/encoding/Opcodes.h index 318b0d125..f2c69bf71 100644 --- a/Code/encoding/Opcodes.h +++ b/Code/encoding/Opcodes.h @@ -52,6 +52,7 @@ enum ClientOpcode : unsigned char kRequestCurrentWeather, kRequestSetWaypoint, kRequestRemoveWaypoint, + kSetTimeCommandRequest, kClientOpcodeMax }; @@ -111,5 +112,6 @@ enum ServerOpcode : unsigned char kNotifyWeatherChange, kNotifySetWaypoint, kNotifyRemoveWaypoint, + kNotifySetTimeResult, kServerOpcodeMax }; diff --git a/Code/immersive_launcher/Launcher.cpp b/Code/immersive_launcher/Launcher.cpp index 0547a00de..185a7d9fd 100644 --- a/Code/immersive_launcher/Launcher.cpp +++ b/Code/immersive_launcher/Launcher.cpp @@ -10,10 +10,12 @@ #include "Utils/FileVersion.inl" #include "oobe/PathSelection.h" +#include "oobe/PathArgument.h" #include "oobe/SupportChecks.h" #include "steam/SteamLoader.h" #include "base/dialogues/win/TaskDialog.h" +#include "utils/Registry.h" #include @@ -64,11 +66,8 @@ void SetMaxstdio() int StartUp(int argc, char** argv) { bool askSelect = (GetAsyncKeyState(VK_SPACE) & 0x8000); - for (int i = 1; i < argc; i++) - { - if (std::strcmp(argv[i], "-r") == 0) - askSelect = true; - } + if (!HandleArguments(argc, argv, askSelect)) + return -1; // TODO(Force): Make some InitSharedResources func. g_SharedWindowIcon = LoadIconW(GetModuleHandleW(nullptr), MAKEINTRESOURCEW(102)); @@ -141,6 +140,33 @@ void InitClient() // Jump into client code. RunTiltedApp(); } + +bool HandleArguments(int aArgc, char** aArgv, bool& aAskSelect) +{ + for (int i = 1; i < aArgc; i++) + { + if (std::strcmp(aArgv[i], "-r") == 0) + aAskSelect = true; + else if (std::strcmp(aArgv[i], "--exePath") == 0) + { + if (i + 1 >= aArgc) + { + SetLastError(ERROR_BAD_PATHNAME); + Die(L"No exe path specified", true); + return false; + } + + if (!oobe::PathArgument(aArgv[i + 1])) + { + SetLastError(ERROR_BAD_ARGUMENTS); + Die(L"Failed to parse path argument", true); + return false; + } + } + } + + return true; +} } // namespace launcher // CreateProcess in suspended mode. diff --git a/Code/immersive_launcher/Launcher.h b/Code/immersive_launcher/Launcher.h index 4c1571d83..185cb3616 100644 --- a/Code/immersive_launcher/Launcher.h +++ b/Code/immersive_launcher/Launcher.h @@ -30,4 +30,7 @@ bool LoadProgram(LaunchContext&); int StartUp(int argc, char** argv); void InitClient(); + +bool HandleArguments(int, char**, bool&); + } // namespace launcher diff --git a/Code/immersive_launcher/Main.cpp b/Code/immersive_launcher/Main.cpp index a83c6a9aa..dd5a9b9db 100644 --- a/Code/immersive_launcher/Main.cpp +++ b/Code/immersive_launcher/Main.cpp @@ -20,9 +20,10 @@ extern "C" } auto kSystemPreloadDlls = { - L"\\dinput8.dll", // < Skyrim early init hook - L"\\dsound.dll", // < breaks DSound init in game code - // < X360CE v3 is buggy with COM hooks + L"\\dinput8.dll", // < Skyrim early init hook + L"\\dsound.dll", // < breaks DSound init in game code + L"\\nvspcap64.dll", // < Nvidia overlay, needs to be loaded before d3d11 device creation + // < X360CE v3 is buggy with COM hooks L"\\xinput9_1_0.dll", L"\\xinput1_1.dll", L"\\xinput1_2.dll", L"\\xinput1_3.dll", L"\\xinput1_4.dll", L"\\version.dll"}; static void PreloadSystemDlls() diff --git a/Code/immersive_launcher/oobe/PathArgument.cpp b/Code/immersive_launcher/oobe/PathArgument.cpp new file mode 100644 index 000000000..38daaaee6 --- /dev/null +++ b/Code/immersive_launcher/oobe/PathArgument.cpp @@ -0,0 +1,82 @@ +#include "PathArgument.h" + +#include "TargetConfig.h" + +#include "Utils/Error.h" +#include "utils/Registry.h" + +#include + +namespace oobe +{ +using namespace TiltedPhoques; + +namespace +{ +constexpr wchar_t kTiltedRegistryPath[] = LR"(Software\TiltedPhoques\TiltedEvolution\)" SHORT_NAME; + +#define DIE_NOW(err) \ + { \ + Die(err, true); \ + return false; \ + } + +bool ValidatePath(const std::wstring& acPath) +{ + const std::wstring cTitlePath = acPath.substr(0, acPath.find_last_of('\\')); + std::wstring errorText{}; + + if (acPath.find_last_of('\\') == std::string::npos || acPath.ends_with(*"\\")) + { + SetLastError(ERROR_BAD_PATHNAME); + errorText += L"Invalid path\n"; + } + + if (!acPath.ends_with(L".exe")) + { + SetLastError(ERROR_BAD_ARGUMENTS); + errorText += acPath.substr(acPath.find_last_of('\\') + 1, acPath.back()) + L" is not an executable file\n"; + } + else if (!acPath.ends_with(TARGET_NAME L".exe")) + { + SetLastError(ERROR_FILE_NOT_FOUND); + errorText += TARGET_NAME L".exe not found\n"; + } + + if (!std::filesystem::exists(acPath) || !std::filesystem::exists(cTitlePath)) + { + SetLastError(ERROR_BAD_PATHNAME); + errorText += L"Path does not exist\n"; + } + + if (!errorText.empty()) + { + errorText += L"\nPath: " + acPath; + DIE_NOW(errorText.c_str()) + } + + return true; +} +} // namespace + +bool PathArgument(const std::string& acPath) +{ + const std::wstring cExePath = std::wstring(acPath.begin(), acPath.end()); + const std::wstring cTitlePath = cExePath.substr(0, cExePath.find_last_of('\\')); + + if (!ValidatePath(cExePath)) + { + DIE_NOW(L"Failed to validate path") + } + + // Write to registry so oobe::SelectInstall can handle the rest + const bool result = Registry::WriteString(HKEY_CURRENT_USER, kTiltedRegistryPath, L"TitlePath", cTitlePath) && Registry::WriteString(HKEY_CURRENT_USER, kTiltedRegistryPath, L"TitleExe", cExePath); + + if (!result) + { + DIE_NOW(L"Failed to write to registry") + } + + return true; +} +} // namespace oobe diff --git a/Code/immersive_launcher/oobe/PathArgument.h b/Code/immersive_launcher/oobe/PathArgument.h new file mode 100644 index 000000000..8c2b8f1ba --- /dev/null +++ b/Code/immersive_launcher/oobe/PathArgument.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +namespace oobe +{ +bool PathArgument(const std::string& acPath); +} diff --git a/Code/server/Game/PlayerManager.cpp b/Code/server/Game/PlayerManager.cpp index 7ab3f6f1b..a2db42bb2 100644 --- a/Code/server/Game/PlayerManager.cpp +++ b/Code/server/Game/PlayerManager.cpp @@ -90,6 +90,30 @@ Player const* PlayerManager::GetById(uint32_t aId) const noexcept return nullptr; } +Player* PlayerManager::GetByUsername(const String& acUsername) const noexcept +{ + auto itor = std::begin(m_players); + const auto end = std::end(m_players); + + for (; itor != end; ++itor) + if (itor.value()->GetUsername() == acUsername) + return itor.value().get(); + + return nullptr; +} + +Player const* PlayerManager::GetByUsername(const String& acUsername) noexcept +{ + auto itor = std::begin(m_players); + const auto end = std::end(m_players); + + for (; itor != end; ++itor) + if (itor.value()->GetUsername() == acUsername) + return itor.value().get(); + + return nullptr; +} + uint32_t PlayerManager::Count() const noexcept { return static_cast(m_players.size()); diff --git a/Code/server/Game/PlayerManager.h b/Code/server/Game/PlayerManager.h index d5f50efe7..0fb988b1a 100644 --- a/Code/server/Game/PlayerManager.h +++ b/Code/server/Game/PlayerManager.h @@ -44,6 +44,9 @@ struct PlayerManager Player* GetById(uint32_t aId) noexcept; Player const* GetById(uint32_t aId) const noexcept; + Player* GetByUsername(const String& acUsername) const noexcept; + Player const* GetByUsername(const String& acUsername) noexcept; + uint32_t Count() const noexcept; template void ForEach(const T& acFunctor) noexcept diff --git a/Code/server/GameServer.cpp b/Code/server/GameServer.cpp index f2350c581..ba4176b83 100644 --- a/Code/server/GameServer.cpp +++ b/Code/server/GameServer.cpp @@ -26,105 +26,102 @@ 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 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 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 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 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}; +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}; +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<> 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<> 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(); +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); + 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; - } + aDiff = 4; + } - uDifficulty = (uint32_t)aDiff; + uDifficulty = (uint32_t)aDiff; - GameServer::Get()->UpdateSettings(); - spdlog::get("ConOut")->info("Difficulty has been set to {}.", 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<> 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<> 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"; }; +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)); -}); + 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 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 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)."}; +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() { @@ -150,8 +147,10 @@ ServerSettings GetSettings() } 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) + : 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; @@ -165,19 +164,19 @@ GameServer::GameServer(Console::ConsoleRegistry& aConsole) noexcept 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()); + 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!"); + 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; @@ -243,10 +242,12 @@ bool GameServer::CheckMoPo() void GameServer::BindMessageHandlers() { - auto handlerGenerator = [this](auto& x) { + auto handlerGenerator = [this](auto& x) + { using T = typename std::remove_reference_t::Type; - m_messageHandlers[T::Opcode] = [this](UniquePtr& apMessage, ConnectionId_t aConnectionId) { + m_messageHandlers[T::Opcode] = [this](UniquePtr& apMessage, ConnectionId_t aConnectionId) + { auto* pPlayer = m_pWorld->GetPlayerManager().GetByConnectionId(aConnectionId); if (!pPlayer) @@ -267,17 +268,18 @@ void GameServer::BindMessageHandlers() ClientMessageFactory::Visit(handlerGenerator); // Override authentication request - m_messageHandlers[AuthenticationRequest::Opcode] = [this](UniquePtr& apMessage, - ConnectionId_t aConnectionId) { + 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) { + auto adminHandlerGenerator = [this](auto& x) + { using T = typename std::remove_reference_t::Type; - m_adminMessageHandlers[T::Opcode] = [this](UniquePtr& apMessage, - ConnectionId_t aConnectionId) { + 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)); }; @@ -290,62 +292,73 @@ void GameServer::BindMessageHandlers() 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) + m_commands.RegisterCommand<>( + "uptime", "Show how long the server has been running for", + [this](Console::ArgStack&) { - out->warn("No players on here. Invite some friends!"); - return; - } + Uptime uptime = GetUptime(); + spdlog::get("ConOut")->info("Server uptime: {}w {}d {}h {}m", uptime.weeks, uptime.days, uptime.hours, uptime.minutes); + }); - out->info("<------Players-({})--->", count); - for (Player* pPlayer : m_pWorld->GetPlayerManager()) + m_commands.RegisterCommand<>( + "players", "List all players on this server", + [&](Console::ArgStack&) { - out->info("{}: {}", pPlayer->GetId(), pPlayer->GetUsername().c_str()); - } - }); + 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; + } - 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("<------Players-({})--->", count); + for (Player* pPlayer : m_pWorld->GetPlayerManager()) + { + out->info("{}: {}", pPlayer->GetId(), pPlayer->GetUsername().c_str()); + } + }); - out->info("<------Mods-({})--->", mods.size()); - for (auto& it : mods) + m_commands.RegisterCommand<>( + "mods", "List all installed mods on this server", + [&](Console::ArgStack&) { - out->info(it.first); - } - }); + auto out = spdlog::get("ConOut"); + auto& mods = m_pWorld->ctx().at().GetServerMods(); + if (mods.size() == 0) + { + out->warn("No mods installed"); + return; + } - 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->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&) { - out->warn("No resources loaded"); - return; - } + 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()); + 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) { + "SetTime", "Set ingame hour and minute", + [&](Console::ArgStack& aStack) + { auto out = spdlog::get("ConOut"); auto hour = aStack.Pop(); @@ -365,7 +378,9 @@ void GameServer::BindServerCommands() }); m_commands.RegisterCommand( - "SetDate", "Set ingame day, month, and year", [&](Console::ArgStack& aStack) { + "SetDate", "Set ingame day, month, and year", + [&](Console::ArgStack& aStack) + { auto out = spdlog::get("ConOut"); auto day = aStack.Pop(); @@ -383,6 +398,113 @@ void GameServer::BindServerCommands() 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.*/ @@ -392,8 +514,7 @@ void GameServer::UpdateInfo() if (cServerName.length() > kMaxServerNameLength) { - spdlog::error("sServerName is longer than the limit of {} characters/bytes, and has been cut short", - 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 @@ -415,9 +536,7 @@ void GameServer::UpdateTimeScale() if (!timescale_set_successfully) { - spdlog::warn( - "TimeScale is invalid (should be from 0 to 1000, current value is {}), setting TimeScale to 20 (default)", - timescale); + spdlog::warn("TimeScale is invalid (should be from 0 to 1000, current value is {}), setting TimeScale to 20 (default)", timescale); uTimeScale = 20u; } @@ -444,7 +563,8 @@ void GameServer::OnConsume(const void* apData, const uint32_t aSize, const Conne ViewBuffer buf((uint8_t*)apData, aSize); Buffer::Reader reader(&buf); - if (m_adminSessions.contains(aConnectionId)) [[unlikely]] + // TODO: ClientAdminMessageFactory + /*if (m_adminSessions.contains(aConnectionId)) [[unlikely]] { const ClientAdminMessageFactory factory; auto pMessage = factory.Extract(reader); @@ -457,17 +577,17 @@ void GameServer::OnConsume(const void* apData, const uint32_t aSize, const Conne m_adminMessageHandlers[pMessage->GetOpcode()](pMessage, aConnectionId); } else + {*/ + const ClientMessageFactory factory; + auto pMessage = factory.Extract(reader); + if (!pMessage) { - 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); + spdlog::error("Couldn't parse packet from {:x}", aConnectionId); + return; } + + m_messageHandlers[pMessage->GetOpcode()](pMessage, aConnectionId); + //} } void GameServer::OnConnection(const ConnectionId_t aHandle) @@ -482,8 +602,7 @@ void GameServer::OnDisconnection(const ConnectionId_t aConnectionId, EDisconnect auto* pPlayer = m_pWorld->GetPlayerManager().GetByConnectionId(aConnectionId); - spdlog::info("Connection ended {:x} - '{}' disconnected", aConnectionId, - (pPlayer != NULL ? pPlayer->GetUsername().c_str() : "NULL")); + spdlog::info("Connection ended {:x} - '{}' disconnected", aConnectionId, (pPlayer != NULL ? pPlayer->GetUsername().c_str() : "NULL")); m_pWorld->GetScriptService().HandlePlayerQuit(aConnectionId, aReason); @@ -540,8 +659,7 @@ void GameServer::Send(const ConnectionId_t aConnectionId, const ServerMessage& a acServerMessage.Serialize(writer); - TiltedPhoques::PacketView packet(reinterpret_cast(buffer.GetWriteData()), - static_cast(writer.Size())); + TiltedPhoques::PacketView packet(reinterpret_cast(buffer.GetWriteData()), static_cast(writer.Size())); Server::Send(aConnectionId, &packet); s_allocator.Reset(); @@ -557,8 +675,7 @@ void GameServer::Send(ConnectionId_t aConnectionId, const ServerAdminMessage& ac acServerMessage.Serialize(writer); - TiltedPhoques::PacketView packet(reinterpret_cast(buffer.GetWriteData()), - static_cast(writer.Size())); + TiltedPhoques::PacketView packet(reinterpret_cast(buffer.GetWriteData()), static_cast(writer.Size())); Server::Send(aConnectionId, &packet); s_allocator.Reset(); @@ -583,8 +700,7 @@ void GameServer::SendToPlayers(const ServerMessage& acServerMessage, const Playe } // 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 +bool GameServer::SendToPlayersInRange(const ServerMessage& acServerMessage, const entt::entity acOrigin, const Player* apExcludedPlayer) const { if (!m_pWorld->valid(acOrigin)) { @@ -616,8 +732,7 @@ bool GameServer::SendToPlayersInRange(const ServerMessage& acServerMessage, cons return true; } -void GameServer::SendToParty(const ServerMessage& acServerMessage, const PartyComponent& acPartyComponent, - const Player* apExcludeSender) const +void GameServer::SendToParty(const ServerMessage& acServerMessage, const PartyComponent& acPartyComponent, const Player* apExcludeSender) const { if (!acPartyComponent.JoinedPartyId.has_value()) { @@ -638,8 +753,7 @@ void GameServer::SendToParty(const ServerMessage& acServerMessage, const PartyCo } } -void GameServer::SendToPartyInRange(const ServerMessage& acServerMessage, const PartyComponent& acPartyComponent, - const entt::entity acOrigin, const Player* apExcludeSender) const +void GameServer::SendToPartyInRange(const ServerMessage& acServerMessage, const PartyComponent& acPartyComponent, const entt::entity acOrigin, const Player* apExcludeSender) const { if (!acPartyComponent.JoinedPartyId.has_value()) { @@ -691,8 +805,7 @@ bool GameServer::ValidateAuthParams(ConnectionId_t aConnectionId, const UniquePt return false; } -void GameServer::HandleAuthenticationRequest(const ConnectionId_t aConnectionId, - const UniquePtr& acRequest) +void GameServer::HandleAuthenticationRequest(const ConnectionId_t aConnectionId, const UniquePtr& acRequest) { const auto info = GetConnectionInfo(aConnectionId); @@ -703,7 +816,8 @@ void GameServer::HandleAuthenticationRequest(const ConnectionId_t aConnectionId, serverResponse.Version = BUILD_COMMIT; using RT = AuthenticationResponse::ResponseType; - auto sendKick = [&](const RT type) { + auto sendKick = [&](const RT type) + { serverResponse.Type = type; Send(aConnectionId, serverResponse); // the previous message is a lingering kick, it still gets delivered. @@ -713,8 +827,7 @@ void GameServer::HandleAuthenticationRequest(const ConnectionId_t aConnectionId, // 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()); + spdlog::info("New player {:x} '{}' tried to connect with client {} - Version mismatch", aConnectionId, remoteAddress, acRequest->Version.c_str()); sendKick(RT::kWrongVersion); return; } @@ -737,8 +850,7 @@ void GameServer::HandleAuthenticationRequest(const ConnectionId_t aConnectionId, if (mo2Problem) response += "MO2 "; - spdlog::info("New player {:x} '{}' tried to connect, but {}{} disallowed - Kicked.", aConnectionId, - remoteAddress, response.c_str(), skseProblem && mo2Problem ? "are" : "is"); + 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; @@ -746,9 +858,17 @@ void GameServer::HandleAuthenticationRequest(const ConnectionId_t aConnectionId, return; } + bool adminPasswordUsed = acRequest->Token == sAdminPassword.value() && !sAdminPassword.empty(); + // check if the proper server password was supplied. - if (acRequest->Token == sPassword.value()) + 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(); @@ -776,8 +896,7 @@ void GameServer::HandleAuthenticationRequest(const ConnectionId_t aConnectionId, // 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; }); + const auto it = std::find_if(userMods.begin(), userMods.end(), [&](const Mods::Entry& it) { return it.Filename == entry.first; }); if (it == userMods.end()) { @@ -793,9 +912,7 @@ void GameServer::HandleAuthenticationRequest(const ConnectionId_t aConnectionId, 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()); + 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); @@ -811,8 +928,7 @@ void GameServer::HandleAuthenticationRequest(const ConnectionId_t aConnectionId, size_t i = 0; for (auto& mod : acRequest->UserMods.ModList) { - const uint32_t id = - mod.IsLite ? modsComponent.AddLite(mod.Filename) : modsComponent.AddStandard(mod.Filename); + const uint32_t id = mod.IsLite ? modsComponent.AddLite(mod.Filename) : modsComponent.AddStandard(mod.Filename); Mods::Entry entry; entry.Filename = mod.Filename; @@ -845,8 +961,7 @@ void GameServer::HandleAuthenticationRequest(const ConnectionId_t aConnectionId, 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()); + 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(); @@ -882,16 +997,14 @@ void GameServer::HandleAuthenticationRequest(const ConnectionId_t aConnectionId, 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); + /*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); - } - */ + 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); @@ -925,8 +1038,7 @@ 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()); + const auto title = fmt::format("{} - {} {} - {} Ticks - " BUILD_BRANCH "@" BUILD_COMMIT, name.c_str(), GetClientCount(), playerText, GetTickRate()); #if TP_PLATFORM_WINDOWS SetConsoleTitleA(title.c_str()); @@ -934,3 +1046,41 @@ void GameServer::UpdateTitle() const 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; +} diff --git a/Code/server/GameServer.h b/Code/server/GameServer.h index 5e79b1ac0..f7256d7a8 100644 --- a/Code/server/GameServer.h +++ b/Code/server/GameServer.h @@ -58,26 +58,14 @@ struct GameServer final : Server void Send(ConnectionId_t aConnectionId, const ServerAdminMessage& acServerMessage) const; void SendToLoaded(const ServerMessage& acServerMessage) const; void SendToPlayers(const ServerMessage& acServerMessage, const Player* apExcludeSender = nullptr) const; - bool SendToPlayersInRange(const ServerMessage& acServerMessage, const entt::entity acOrigin, - const Player* apExcludeSender = nullptr) const; - void SendToParty(const ServerMessage& acServerMessage, const PartyComponent& acPartyComponent, - const Player* apExcludeSender = nullptr) const; - void SendToPartyInRange(const ServerMessage& acServerMessage, const PartyComponent& acPartyComponent, - const entt::entity acOrigin, const Player* apExcludeSender = nullptr) const; - - const Info& GetInfo() const noexcept - { - return m_info; - } + bool SendToPlayersInRange(const ServerMessage& acServerMessage, const entt::entity acOrigin, const Player* apExcludeSender = nullptr) const; + void SendToParty(const ServerMessage& acServerMessage, const PartyComponent& acPartyComponent, const Player* apExcludeSender = nullptr) const; + void SendToPartyInRange(const ServerMessage& acServerMessage, const PartyComponent& acPartyComponent, const entt::entity acOrigin, const Player* apExcludeSender = nullptr) const; - bool IsRunning() const noexcept - { - return !m_requestStop; - } - bool IsPasswordProtected() const noexcept - { - return m_isPasswordProtected; - } + const Info& GetInfo() const noexcept { return m_info; } + + bool IsRunning() const noexcept { return !m_requestStop; } + bool IsPasswordProtected() const noexcept { return m_isPasswordProtected; } template void ForEachAdmin(const T& aFunctor) { @@ -94,12 +82,18 @@ struct GameServer final : Server }; Uptime GetUptime() const noexcept; - World& GetWorld() const noexcept - { - return *m_pWorld; - } + World& GetWorld() const noexcept { return *m_pWorld; } + + [[nodiscard]] const TiltedPhoques::Set& GetAdminSessions() const noexcept { return m_adminSessions; } + + void AddAdminSession(ConnectionId_t acSession) noexcept { m_adminSessions.insert(acSession); } + + void RemoveAdminSession(ConnectionId_t acSession) noexcept { m_adminSessions.erase(acSession); } + + Player* GetAdminByUsername(const String& acUsername) const noexcept; + Player const* GetAdminByUsername(const String& acUsername) noexcept; - protected: +protected: bool ValidateAuthParams(ConnectionId_t aConnectionId, const UniquePtr& acRequest); void HandleAuthenticationRequest(ConnectionId_t aConnectionId, const UniquePtr& acRequest); @@ -109,10 +103,11 @@ struct GameServer final : Server void OnConnection(ConnectionId_t aHandle) override; void OnDisconnection(ConnectionId_t aConnectionId, EDisconnectReason aReason) override; - private: +private: void UpdateTitle() const; + String SanitizeUsername(const String& acUsername) const noexcept; - private: +private: std::chrono::high_resolution_clock::time_point m_startTime; std::chrono::high_resolution_clock::time_point m_lastFrameTime; std::function&, ConnectionId_t)> m_messageHandlers[kClientOpcodeMax]; diff --git a/Code/server/Scripting/GameServer_Bindings.cpp b/Code/server/Scripting/GameServer_Bindings.cpp index 03729fd05..11f7797a7 100644 --- a/Code/server/Scripting/GameServer_Bindings.cpp +++ b/Code/server/Scripting/GameServer_Bindings.cpp @@ -1,5 +1,6 @@ #include "GameServer.h" + #include #include @@ -22,6 +23,23 @@ void CreateGameServerBindings(sol::state_view aState) notifyMessage.ChatMessage = std::regex_replace(acMessage, escapeHtml, ""); GameServer::Get()->Send(aConnectionId, notifyMessage); }; + type["SendGlobalChatMessage"] = [](GameServer& aSelf, const std::string& acMessage) { + NotifyChatMessageBroadcast notifyMessage{}; + + std::regex escapeHtml{"<[^>]+>\\s+(?=<)|<[^>]+>"}; + notifyMessage.MessageType = ChatMessageType::kGlobalChat; + notifyMessage.PlayerName = "[Server]"; + notifyMessage.ChatMessage = std::regex_replace(acMessage, escapeHtml, ""); + GameServer::Get()->SendToPlayers(notifyMessage); + }; + type["SetTime"] = [](GameServer& aSelf, int aHours, int aMinutes, + float aScale = GameServer::Get()->GetWorld().GetCalendarService().GetTimeScale()) -> bool { + aHours = std::max(0, std::min(aHours, 23)); + aMinutes = std::max(0, std::min(aMinutes, 59)); + aScale = std::max(1.0f, std::min(aScale, 100.0f)); + + return GameServer::Get()->GetWorld().GetCalendarService().SetTime(aHours, aMinutes, aScale); + }; // type["SendPacket"] } } // namespace Script diff --git a/Code/server/Scripting/Player_Bindings.cpp b/Code/server/Scripting/Player_Bindings.cpp index 11400d61e..999fea81d 100644 --- a/Code/server/Scripting/Player_Bindings.cpp +++ b/Code/server/Scripting/Player_Bindings.cpp @@ -32,6 +32,10 @@ void BindPlayer(sol::state_view aState) playerType["SetLevel"] = &Player::SetLevel; playerType["SetCellComponent"] = &Player::SetCellComponent; playerType["Send"] = &Player::Send; + playerType["IsPartyLeader"] = [](Player& aSelf) { + return GameServer::Get()->GetWorld().GetPartyService().IsPlayerLeader( + PlayerManager::Get()->GetByConnectionId(aSelf.GetConnectionId())); + }; } void BindPlayerManager(sol::state_view aState) diff --git a/Code/server/Scripting/ScriptBindings.cpp b/Code/server/Scripting/ScriptBindings.cpp index c9342f17c..e0dbc9eac 100644 --- a/Code/server/Scripting/ScriptBindings.cpp +++ b/Code/server/Scripting/ScriptBindings.cpp @@ -44,6 +44,13 @@ void CreateMathBindings(sol::state_view); void CreatePlayerBindings(sol::state_view); void CreateComponentBindings(sol::state_view); void CreateGameServerBindings(sol::state_view); +void CreateCalendarServiceBindings(sol::state_view); +void CreatePartyServiceBindings(sol::state_view); +void CreateCharacterServiceBindings(sol::state_view); +void CreatePlayerServiceBindings(sol::state_view); +void CreateQuestServiceBindings(sol::state_view); +void CreateScriptServiceBindings(sol::state_view); +void CreateWorldBindings(sol::state_view); sol::table BindModsComponent(sol::state_view aState) { @@ -98,6 +105,16 @@ void CreateScriptBindings(sol::state& aState) CreateComponentBindings(aState); CreateGameServerBindings(aState); + + // services + CreateCalendarServiceBindings(aState); + CreatePartyServiceBindings(aState); + CreateCharacterServiceBindings(aState); + CreatePlayerServiceBindings(aState); + CreateQuestServiceBindings(aState); + CreateScriptServiceBindings(aState); + + CreateWorldBindings(aState); BindModsComponent(aState); } } // namespace Script diff --git a/Code/server/Scripting/Services/CalendarService_Bindings.cpp b/Code/server/Scripting/Services/CalendarService_Bindings.cpp new file mode 100644 index 000000000..af1c30f01 --- /dev/null +++ b/Code/server/Scripting/Services/CalendarService_Bindings.cpp @@ -0,0 +1,13 @@ +#include "GameServer.h" + +namespace Script +{ +void CreateCalendarServiceBindings(sol::state_view aState) +{ + auto calendarType = + aState.new_usertype("CalendarService", sol::meta_function::construct, sol::no_constructor); + + calendarType["get"] = []() -> CalendarService& { return GameServer::Get()->GetWorld().GetCalendarService(); }; + calendarType["GetTimeScale"] = []() { return GameServer::Get()->GetWorld().GetCalendarService().GetTimeScale(); }; +} +} // namespace Script diff --git a/Code/server/Scripting/Services/CharacterService_Bindings.cpp b/Code/server/Scripting/Services/CharacterService_Bindings.cpp new file mode 100644 index 000000000..148ca5ea3 --- /dev/null +++ b/Code/server/Scripting/Services/CharacterService_Bindings.cpp @@ -0,0 +1,12 @@ +#include "GameServer.h" + +namespace Script +{ +void CreateCharacterServiceBindings(sol::state_view aState) +{ + auto characterType = + aState.new_usertype("CharacterService", sol::meta_function::construct, sol::no_constructor); + + characterType["get"] = []() -> CharacterService& { return GameServer::Get()->GetWorld().GetCharacterService(); }; +} +} // namespace Script diff --git a/Code/server/Scripting/Services/PartyService_Bindings.cpp b/Code/server/Scripting/Services/PartyService_Bindings.cpp new file mode 100644 index 000000000..7eb8e269a --- /dev/null +++ b/Code/server/Scripting/Services/PartyService_Bindings.cpp @@ -0,0 +1,18 @@ +#include "GameServer.h" + +namespace Script +{ +void CreatePartyServiceBindings(sol::state_view aState) +{ + auto partyType = + aState.new_usertype("PartyService", sol::meta_function::construct, sol::no_constructor); + + partyType["get"] = []() -> PartyService& { return GameServer::Get()->GetWorld().GetPartyService(); }; + partyType["IsPlayerInParty"] = [](PartyService& aService, uint32_t aConnID) -> bool { + Player* player = PlayerManager::Get()->GetByConnectionId(aConnID); + if (player == nullptr) + return false; + return aService.IsPlayerInParty(player); + }; +} +} // namespace Script diff --git a/Code/server/Scripting/Services/PlayerService_Bindings.cpp b/Code/server/Scripting/Services/PlayerService_Bindings.cpp new file mode 100644 index 000000000..52d540ca9 --- /dev/null +++ b/Code/server/Scripting/Services/PlayerService_Bindings.cpp @@ -0,0 +1,12 @@ +#include "GameServer.h" + +namespace Script +{ +void CreatePlayerServiceBindings(sol::state_view aState) +{ + auto playerType = + aState.new_usertype("PlayerService", sol::meta_function::construct, sol::no_constructor); + + playerType["get"] = []() -> PlayerService& { return GameServer::Get()->GetWorld().GetPlayerService(); }; +} +} // namespace Script diff --git a/Code/server/Scripting/Services/QuestService_Bindings.cpp b/Code/server/Scripting/Services/QuestService_Bindings.cpp new file mode 100644 index 000000000..7a80dbf91 --- /dev/null +++ b/Code/server/Scripting/Services/QuestService_Bindings.cpp @@ -0,0 +1,12 @@ +#include "GameServer.h" + +namespace Script +{ +void CreateQuestServiceBindings(sol::state_view aState) +{ + auto questType = + aState.new_usertype("QuestService", sol::meta_function::construct, sol::no_constructor); + + questType["get"] = []() -> QuestService& { return GameServer::Get()->GetWorld().GetQuestService(); }; +} +} // namespace Script diff --git a/Code/server/Scripting/Services/ScriptService_Bindings.cpp b/Code/server/Scripting/Services/ScriptService_Bindings.cpp new file mode 100644 index 000000000..a0d883a03 --- /dev/null +++ b/Code/server/Scripting/Services/ScriptService_Bindings.cpp @@ -0,0 +1,12 @@ +#include "GameServer.h" + +namespace Script +{ +void CreateScriptServiceBindings(sol::state_view aState) +{ + auto scriptType = + aState.new_usertype("ScriptService", sol::meta_function::construct, sol::no_constructor); + + scriptType["get"] = []() -> ScriptService& { return GameServer::Get()->GetWorld().GetScriptService(); }; +} +} // namespace Script diff --git a/Code/server/Scripting/World_Bindings.cpp b/Code/server/Scripting/World_Bindings.cpp new file mode 100644 index 000000000..7ffa517c0 --- /dev/null +++ b/Code/server/Scripting/World_Bindings.cpp @@ -0,0 +1,13 @@ +#include "GameServer.h" +#include "World.h" + +namespace Script +{ +void CreateWorldBindings(sol::state_view aState) +{ + auto type = + aState.new_usertype("World", sol::meta_function::construct, sol::no_constructor); + + type["get"] = []() -> World& { return GameServer::Get()->GetWorld(); }; +} +} // namespace Script diff --git a/Code/server/Services/CalendarService.cpp b/Code/server/Services/CalendarService.cpp index dc2ef40e0..7014c2853 100644 --- a/Code/server/Services/CalendarService.cpp +++ b/Code/server/Services/CalendarService.cpp @@ -10,7 +10,8 @@ #include "Game/Player.h" -CalendarService::CalendarService(World& aWorld, entt::dispatcher& aDispatcher) : m_world(aWorld) +CalendarService::CalendarService(World& aWorld, entt::dispatcher& aDispatcher) + : m_world(aWorld) { m_updateConnection = aDispatcher.sink().connect<&CalendarService::OnUpdate>(this); m_joinConnection = aDispatcher.sink().connect<&CalendarService::OnPlayerJoin>(this); @@ -74,6 +75,8 @@ bool CalendarService::SetTime(int aHours, int aMinutes, float aScale) noexcept m_dateTime.m_timeModel.Time = static_cast(aHours) + minutes; SendTimeResync(); + + GameServer::Get()->GetWorld().GetScriptService().HandleSetTime(aHours, aMinutes, aScale); return true; } return false; diff --git a/Code/server/Services/CommandService.cpp b/Code/server/Services/CommandService.cpp index e07b15552..7bb66ce86 100644 --- a/Code/server/Services/CommandService.cpp +++ b/Code/server/Services/CommandService.cpp @@ -4,15 +4,45 @@ #include #include +#include +#include #include #include CommandService::CommandService(World& aWorld, entt::dispatcher& aDispatcher) noexcept : m_world(aWorld) { + m_setTimeConnection = aDispatcher.sink>().connect<&CommandService::OnSetTimeCommand>(this); m_teleportConnection = aDispatcher.sink>().connect<&CommandService::OnTeleportCommandRequest>(this); } +void CommandService::OnSetTimeCommand(const PacketEvent& acMessage) const noexcept +{ + NotifySetTimeResult response{}; + + const auto cPlayerId = static_cast(acMessage.Packet.PlayerId); + + // Only set time if player is an admin + for (const auto session : GameServer::Get()->GetAdminSessions()) + { + if (PlayerManager::Get()->GetByConnectionId(session)->GetId() == cPlayerId) + { + const auto cHours = static_cast(acMessage.Packet.Hours); + const auto cMinutes = static_cast(acMessage.Packet.Minutes); + + m_world.GetCalendarService().SetTime(cHours, cMinutes, m_world.GetCalendarService().GetTimeScale()); + + response.Result = NotifySetTimeResult::SetTimeResult::kSuccess; + acMessage.pPlayer->Send(response); + + return; + } + } + + response.Result = NotifySetTimeResult::SetTimeResult::kNoPermission; + acMessage.pPlayer->Send(response); +} + void CommandService::OnTeleportCommandRequest(const PacketEvent& acMessage) const noexcept { Player* pTargetPlayer = nullptr; diff --git a/Code/server/Services/CommandService.h b/Code/server/Services/CommandService.h index 50fa799f6..79c72d8d0 100644 --- a/Code/server/Services/CommandService.h +++ b/Code/server/Services/CommandService.h @@ -4,6 +4,7 @@ struct World; struct TeleportCommandRequest; +struct SetTimeCommandRequest; /** * @brief Processes incoming commands. @@ -16,6 +17,7 @@ struct CommandService TP_NOCOPYMOVE(CommandService); protected: + void OnSetTimeCommand(const PacketEvent& acMessage) const noexcept; /** * @brief Returns the location of the target player of the teleport command. */ @@ -24,5 +26,6 @@ struct CommandService private: World& m_world; + entt::scoped_connection m_setTimeConnection; entt::scoped_connection m_teleportConnection; }; diff --git a/Code/server/Services/ObjectService.cpp b/Code/server/Services/ObjectService.cpp index f00c3324d..2bd7d154f 100644 --- a/Code/server/Services/ObjectService.cpp +++ b/Code/server/Services/ObjectService.cpp @@ -122,6 +122,7 @@ void ObjectService::OnActivate(const PacketEvent& acMessage) co NotifyActivate notifyActivate; notifyActivate.Id = acMessage.Packet.Id; notifyActivate.ActivatorId = acMessage.Packet.ActivatorId; + notifyActivate.PreActivationOpenState = acMessage.Packet.PreActivationOpenState; for (auto pPlayer : m_world.GetPlayerManager()) { diff --git a/Code/server/Services/OverlayService.cpp b/Code/server/Services/OverlayService.cpp index 526d40311..df5343290 100644 --- a/Code/server/Services/OverlayService.cpp +++ b/Code/server/Services/OverlayService.cpp @@ -55,7 +55,6 @@ void sendPlayerMessage(const ChatMessageType acType, const String acContent, Pla spdlog::error("{}: SendToPlayersInRange failed", __FUNCTION__); } break; - default: spdlog::error("{} is not a known MessageType", static_cast(notifyMessage.MessageType)); break; } } diff --git a/Code/server/Services/ScriptService.cpp b/Code/server/Services/ScriptService.cpp index 1fcacdf79..fdf45fa26 100644 --- a/Code/server/Services/ScriptService.cpp +++ b/Code/server/Services/ScriptService.cpp @@ -196,6 +196,11 @@ void ScriptService::HandlePlayerQuit(ConnectionId_t aConnectionId, Server::EDisc CallEvent("onPlayerQuit", aConnectionId, reason); } +std::tuple ScriptService::HandleSetTime(int aHours, int aMinutes, float aTimeScale) noexcept +{ + return CallCancelableEvent("onSetTime", aHours, aMinutes, aTimeScale); +} + #if 0 void ScriptService::RegisterExtensions(ScriptContext& aContext) { diff --git a/Code/server/Services/ScriptService.h b/Code/server/Services/ScriptService.h index ffe53aef7..c5f7a14cd 100644 --- a/Code/server/Services/ScriptService.h +++ b/Code/server/Services/ScriptService.h @@ -33,6 +33,8 @@ struct ScriptService void HandlePlayerQuit(ConnectionId_t aConnectionId, Server::EDisconnectReason aReason) noexcept; + std::tuple HandleSetTime(int aHours, int aMinutes, float aTimeScale) noexcept; + protected: // void RegisterExtensions(ScriptContext& aContext) override; diff --git a/Code/skyrim_ui/src/app/services/chat/commands.ts b/Code/skyrim_ui/src/app/services/chat/commands.ts index 4a3b4c822..f2101dafa 100644 --- a/Code/skyrim_ui/src/app/services/chat/commands.ts +++ b/Code/skyrim_ui/src/app/services/chat/commands.ts @@ -1,4 +1,4 @@ -import { ChatService, MessageTypes } from '../chat.service'; +import { ChatService } from '../chat.service'; export interface Command { readonly name: string; @@ -15,12 +15,39 @@ export class CommandHandler { { cmds }, ); }, - }; + } + + private SetTime: Command = { + name: 'settime', + executor: async (args) => { + const cmds = [...this.commands.keys()].join(', '); + if (args.length != 2) { + this.chatService.pushSystemMessage( + 'COMPONENT.CHAT.SET_TIME_ARGUMENT_COUNT', + { cmds }, + ); + return; + } + const hours = parseInt(args[0]); + const minutes = parseInt(args[1]); + if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59 || Number.isNaN(hours) || Number.isNaN(minutes)) { + this.chatService.pushSystemMessage( + 'COMPONENT.CHAT.SET_TIME_INVALID_ARGUMENTS', + { cmds }, + ); + return; + } + skyrimtogether.setTime(hours, minutes); + // TODO (Toe Knee): Ideally send a localizable response string here, + // currently relies on user making it themselves with serverside scripting + }, + } private readonly commands = new Map(); public constructor(private readonly chatService: ChatService) { this.register(this.Help); + this.register(this.SetTime); } public readonly COMMAND_PREFIX = '/'; @@ -39,7 +66,7 @@ export class CommandHandler { command.executor(args); } else { this.chatService.pushSystemMessage('SERVICE.COMMANDS.COMMAND_NOT_FOUND', { - cmd: inputWithoutPrefix, + cmd: commandName, }); } } diff --git a/Code/skyrim_ui/src/app/services/error.service.ts b/Code/skyrim_ui/src/app/services/error.service.ts index fbd331daa..422b8f955 100644 --- a/Code/skyrim_ui/src/app/services/error.service.ts +++ b/Code/skyrim_ui/src/app/services/error.service.ts @@ -12,7 +12,8 @@ export interface ErrorEvent { | 'server_full' | 'no_reason' | 'bad_uGridsToLoad' - | 'non_default_install'; + | 'non_default_install' + | 'set_time_public_server'; data?: Record; } diff --git a/Code/skyrim_ui/src/assets/i18n/en.json b/Code/skyrim_ui/src/assets/i18n/en.json index 68e7594ea..9f9338095 100644 --- a/Code/skyrim_ui/src/assets/i18n/en.json +++ b/Code/skyrim_ui/src/assets/i18n/en.json @@ -3,7 +3,9 @@ "CHAT": { "SEND": "Send", "MESSAGE": "Message", - "MESSAGE_TOO_LONG": "You cannot send a message longer than {{chatMessageLengthLimit}} characters." + "MESSAGE_TOO_LONG": "You cannot send a message longer than {{chatMessageLengthLimit}} characters.", + "SET_TIME_ARGUMENT_COUNT": "Wrong arguments, usage: /settime HH MM, example: /settime 15 32.", + "SET_TIME_INVALID_ARGUMENTS": "The hours must be between 0 and 23, and the minutes between 0 and 59." }, "CONNECT": { "INFO": { @@ -148,7 +150,8 @@ }, "COMMANDS": { "AVAILABLE_COMMANDS": "Available chat commands: {{cmds}}", - "COMMAND_NOT_FOUND": "'{{cmd}}' is not recognized as a command." + "COMMAND_NOT_FOUND": "'{{cmd}}' is not a recognized command.", + "NOT_ADMIN": "You must be an admin to use that command." }, "GROUP": { "LEVEL_UP": "{{name}} has reached level {{level}}.", diff --git a/Code/skyrim_ui/src/assets/i18n/tr.json b/Code/skyrim_ui/src/assets/i18n/tr.json index fa45ee874..10ea14342 100644 --- a/Code/skyrim_ui/src/assets/i18n/tr.json +++ b/Code/skyrim_ui/src/assets/i18n/tr.json @@ -4,6 +4,8 @@ "SEND": "Gönder", "MESSAGE": "Mesaj", "MESSAGE_TOO_LONG": "{{chatMessageLengthLimit}} harften daha uzun bir mesaj gönderemezsin." + "SET_TIME_ARGUMENT_COUNT": "Yanlış argümanlar, kullanımı: /settime SS DD, örnek: /settime 15 32.", + "SET_TIME_INVALID_ARGUMENTS": "Saat 0 ile 23 ve dakika 0 ile 59 arasında olmalıdır." }, "CONNECT": { "INFO": { @@ -149,6 +151,7 @@ "COMMANDS": { "AVAILABLE_COMMANDS": "Mevcut chat komutları: {{cmds}}", "COMMAND_NOT_FOUND": "'{{cmd}}' geçerli bir komut değil." + "NOT_ADMIN": "Bu komutu kullanmak için Admin olmanız gerek." }, "GROUP": { "LEVEL_UP": "{{name}} {{level}} oldu.", @@ -166,6 +169,8 @@ "WRONG_PASSWORD": "Girdiğiniz parola yanlış.", "NO_REASON": "Sunucu sebep vermeden bağlantıyı reddetti.", "SERVER_FULL": "Sunucu Dolu." + "BAD_UGRIDSTOLOAD": "Görünüşe göre Skyrim.ini dosyanız 'uGridsToLoad' için varsayılan değerde değil. BU ÇOK KÖTÜ BİR FİKİR, ve muhtemelen modu bozar. Lütfen bu ayarı varsayılana çekiniz (5), ya da Skyrim.ini dosyasını silip Skyrim'ı vanilla olarak başlatıp yenisini oluşturun. 'uExterior Cell Buffer' ve 'uInterior Cell Buffer' ayarları varsayılan olarak kalmalı. Bunu nasıl yapacağınızı bilmiyorsanız, Lütfen wiki sayfamıza bakın veya Discord sunucusu/Reddit üzerinden yardım isteyiniz.", + "NON_DEFAULT_INSTALL": "Görünüşe göre yüklemeniz tamamen vanilla değil, örneğin, Creation Club içeriği (Anniversary Güncellemesi ya da diğer türlüsü) ya da diğer modlar.\nModlar ile oynamanızı ÖNERMİYORUZ.\nBu modları silip deaktif etmenizi öneriyoruz (talimatlar wiki sayfasında bulunabilir).\n\nEn iyi deneyim için, bu mod listesine sahip olmanız gerek:\nSkyrim.esm\nUpdate.esm\nDawnguard.esm\nHearthFires.esm\nDragonborn.esm\n_ResourcePack.esl\nSkyrimTogether.esp" } }, "PLAYER_LIST": { diff --git a/Code/skyrim_ui/src/typings.d.ts b/Code/skyrim_ui/src/typings.d.ts index 5ab28b245..80f62ef9a 100644 --- a/Code/skyrim_ui/src/typings.d.ts +++ b/Code/skyrim_ui/src/typings.d.ts @@ -368,6 +368,11 @@ interface SkyrimTogether { */ sendMessage(type: number, message: string): void; + /** + * Send a request to the server for changing the in-game time. + */ + setTime(hours: number, minutes: number): void; + /** * Deactivate UI and release control. */ diff --git a/Code/tp_process/ProcessHandler.cpp b/Code/tp_process/ProcessHandler.cpp index dc655e9e1..fec503a3b 100644 --- a/Code/tp_process/ProcessHandler.cpp +++ b/Code/tp_process/ProcessHandler.cpp @@ -16,6 +16,7 @@ void ProcessHandler::OnContextCreated(CefRefPtr browser, CefRefPtrSetValue("connect", CefV8Value::CreateFunction("connect", m_pOverlayHandler), V8_PROPERTY_ATTRIBUTE_NONE); m_pCoreObject->SetValue("disconnect", CefV8Value::CreateFunction("disconnect", m_pOverlayHandler), V8_PROPERTY_ATTRIBUTE_NONE); m_pCoreObject->SetValue("sendMessage", CefV8Value::CreateFunction("sendMessage", m_pOverlayHandler), V8_PROPERTY_ATTRIBUTE_NONE); + m_pCoreObject->SetValue("setTime", CefV8Value::CreateFunction("setTime", m_pOverlayHandler),V8_PROPERTY_ATTRIBUTE_NONE); m_pCoreObject->SetValue("deactivate", CefV8Value::CreateFunction("deactivate", m_pOverlayHandler), V8_PROPERTY_ATTRIBUTE_NONE); m_pCoreObject->SetValue("launchParty", CefV8Value::CreateFunction("launchParty", m_pOverlayHandler), V8_PROPERTY_ATTRIBUTE_NONE); m_pCoreObject->SetValue("leaveParty", CefV8Value::CreateFunction("leaveParty", m_pOverlayHandler), V8_PROPERTY_ATTRIBUTE_NONE); diff --git a/GameFiles/Skyrim/meshes/Interface/logo/logo01ae.nif b/GameFiles/Skyrim/meshes/Interface/logo/logo01ae.nif new file mode 100644 index 000000000..45a37cfa3 Binary files /dev/null and b/GameFiles/Skyrim/meshes/Interface/logo/logo01ae.nif differ diff --git a/Libraries/TiltedConnect b/Libraries/TiltedConnect index dd0a55bf1..6b21f676d 160000 --- a/Libraries/TiltedConnect +++ b/Libraries/TiltedConnect @@ -1 +1 @@ -Subproject commit dd0a55bf1935eba85949df5bfa1fb32ae2e26d1e +Subproject commit 6b21f676dd5210b3ec1bab4d51f5f07afa83c535 diff --git a/Libraries/TiltedHooks b/Libraries/TiltedHooks index e8f603e99..523bdfe2c 160000 --- a/Libraries/TiltedHooks +++ b/Libraries/TiltedHooks @@ -1 +1 @@ -Subproject commit e8f603e9997cc2861d48a8552dffc28a1f4e450c +Subproject commit 523bdfe2c6e1ddcf50da58055c353c2ef3c4a451 diff --git a/Libraries/TiltedReverse b/Libraries/TiltedReverse index 9d2dfa42c..136014238 160000 --- a/Libraries/TiltedReverse +++ b/Libraries/TiltedReverse @@ -1 +1 @@ -Subproject commit 9d2dfa42c5cbcc163e5017336701a7319571f53e +Subproject commit 1360142382fb59148cafa3d8f2257f15ce937729 diff --git a/Libraries/TiltedUI b/Libraries/TiltedUI index 2c00af54e..dc37bcf75 160000 --- a/Libraries/TiltedUI +++ b/Libraries/TiltedUI @@ -1 +1 @@ -Subproject commit 2c00af54e93310597a5b750ddb8cf7893d741e47 +Subproject commit dc37bcf75aea9153204588bcd099bbe007208649 diff --git a/xmake.lua b/xmake.lua index 7d917ee0d..a959996df 100644 --- a/xmake.lua +++ b/xmake.lua @@ -30,6 +30,7 @@ if has_config("unitybuild") then add_rules("c++.unity_build", {batchsize = 12}) end +-- direct dependencies version pinning add_requires( "entt v3.10.0", "recastnavigation v1.6.0", @@ -41,7 +42,23 @@ add_requires( "mem 1.0.0", "glm 0.9.9+8", "sentry-native 0.7.1", - "zlib v1.3.1") + "zlib v1.3.1" +) +if is_plat("windows") then + add_requires( + "discord 3.2.1", + "imgui v1.89.7" + ) +end + +-- dependencies' dependencies version pinning +add_requireconfs("*.mimalloc", { version = "2.1.7", override = true }) +add_requireconfs("*.cmake", { version = "3.30.2", override = true }) +add_requireconfs("*.openssl", { version = "1.1.1-w", override = true }) +add_requireconfs("*.zlib", { version = "v1.3.1", override = true }) +if is_plat("linux") then + add_requireconfs("*.libcurl", { version = "8.7.1", override = true }) +end add_requireconfs("cpp-httplib", {configs = {ssl = true}}) add_requireconfs("sentry-native", { configs = { backend = "crashpad" } }) @@ -52,13 +69,6 @@ add_requireconfs("magnum-integration.magnum", { configs = { sdl2 = true }}) add_requireconfs("magnum-integration.imgui", { override = true }) --]] -if is_plat("windows") then - add_requires( - "discord 3.2.1", - "imgui v1.89.7" - ) -end - before_build(function (target) import("modules.version") local branch, commitHash = version()