diff --git a/.gitignore b/.gitignore index 9161be0ee..d3a38b2e3 100644 --- a/.gitignore +++ b/.gitignore @@ -88,3 +88,5 @@ distrib_releasedbg/ new_addresses.txt packaged/* +/TiltedEvolution-PR202 +/TiltedEvolutionOLD diff --git a/Code/admin/AdminApp.cpp b/Code/admin/AdminApp.cpp deleted file mode 100644 index 5d5fdcfe1..000000000 --- a/Code/admin/AdminApp.cpp +++ /dev/null @@ -1,168 +0,0 @@ -#include "AdminApp.h" - -#include - -#include -#include - -AdminApp::AdminApp(const Arguments& arguments) - : Platform::Application{arguments, Configuration{}.setTitle("TiltedOnline Server Admin").setWindowFlags(Configuration::WindowFlag::Resizable)} -{ - m_password.resize(1024); - m_imgui = ImGuiIntegration::Context(Vector2{windowSize()} / dpiScaling(), windowSize(), framebufferSize()); - - /* Set up proper blending to be used by ImGui. There's a great chance - you'll need this exact behavior for the rest of your scene. If not, set - this only for the drawFrame() call. */ - GL::Renderer::setBlendEquation(GL::Renderer::BlendEquation::Add, GL::Renderer::BlendEquation::Add); - GL::Renderer::setBlendFunction(GL::Renderer::BlendFunction::SourceAlpha, GL::Renderer::BlendFunction::OneMinusSourceAlpha); - - auto handlerGenerator = [this](auto& x) - { - using T = typename std::remove_reference_t::Type; - - m_messageHandlers[T::Opcode] = [this](UniquePtr& apMessage) - { - const auto pRealMessage = TiltedPhoques::CastUnique(std::move(apMessage)); - HandleMessage(*pRealMessage); - }; - - return false; - }; - - ServerAdminMessageFactory::Visit(handlerGenerator); - -#if !defined(MAGNUM_TARGET_WEBGL) && !defined(CORRADE_TARGET_ANDROID) - /* Have some sane speed, please */ - setMinimalLoopPeriod(16); -#endif -} - -void AdminApp::drawEvent() -{ - GL::defaultFramebuffer.clear(GL::FramebufferClear::Color); - - m_imgui.newFrame(); - - /* Enable text input, if needed */ - if (ImGui::GetIO().WantTextInput && !isTextInputActive()) - startTextInput(); - else if (!ImGui::GetIO().WantTextInput && isTextInputActive()) - stopTextInput(); - - /* 1. Show a simple window. - Tip: if we don't call ImGui::Begin()/ImGui::End() the widgets appear in - a window called "Debug" automatically */ - if (m_state != ConnectionState::kConnected) - { - ImGui::SetNextWindowSize(ImVec2(600, 150)); - ImGui::SetNextWindowPos(ImVec2(windowSize().x() / 2, 200), 0, ImVec2(0.5f, 0.f)); - ImGui::Begin("Online", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_MenuBar); - - if (m_state == ConnectionState::kConnecting) - ImGui::Text("Please wait..."); - else if (m_state == ConnectionState::kNone) - { - static char s_endpoint[1024] = "127.0.0.1:10578"; - - ImGui::InputText("Endpoint", s_endpoint, std::size(s_endpoint)); - ImGui::InputText("Password", m_password.data(), std::size(m_password), ImGuiInputTextFlags_Password); - - if (ImGui::Button("Connect")) - { - Connect(s_endpoint); - m_state = ConnectionState::kConnecting; - } - } - - ImGui::End(); - } - - if (m_state == ConnectionState::kConnected) - drawServerUi(); - - /* Update application cursor */ - m_imgui.updateApplicationCursor(*this); - - /* Set appropriate states. If you only draw ImGui, it is sufficient to - just enable blending and scissor test in the constructor. */ - GL::Renderer::enable(GL::Renderer::Feature::Blending); - GL::Renderer::enable(GL::Renderer::Feature::ScissorTest); - GL::Renderer::disable(GL::Renderer::Feature::FaceCulling); - GL::Renderer::disable(GL::Renderer::Feature::DepthTest); - - m_imgui.drawFrame(); - - /* Reset state. Only needed if you want to draw something else with - different state after. */ - GL::Renderer::enable(GL::Renderer::Feature::DepthTest); - GL::Renderer::enable(GL::Renderer::Feature::FaceCulling); - GL::Renderer::disable(GL::Renderer::Feature::ScissorTest); - GL::Renderer::disable(GL::Renderer::Feature::Blending); - - swapBuffers(); - redraw(); -} - -void AdminApp::tickEvent() -{ - Update(); -} - -void AdminApp::viewportEvent(ViewportEvent& event) -{ - GL::defaultFramebuffer.setViewport({{}, event.framebufferSize()}); - - m_imgui.relayout(Vector2{event.windowSize()} / event.dpiScaling(), event.windowSize(), event.framebufferSize()); -} - -void AdminApp::keyPressEvent(KeyEvent& event) -{ - if (m_imgui.handleKeyPressEvent(event)) - return; -} - -void AdminApp::keyReleaseEvent(KeyEvent& event) -{ - if (m_imgui.handleKeyReleaseEvent(event)) - return; -} - -void AdminApp::mousePressEvent(MouseEvent& event) -{ - if (m_imgui.handleMousePressEvent(event)) - return; -} - -void AdminApp::mouseReleaseEvent(MouseEvent& event) -{ - if (m_imgui.handleMouseReleaseEvent(event)) - return; -} - -void AdminApp::mouseMoveEvent(MouseMoveEvent& event) -{ - if (m_imgui.handleMouseMoveEvent(event)) - return; -} - -void AdminApp::mouseScrollEvent(MouseScrollEvent& event) -{ - if (m_imgui.handleMouseScrollEvent(event)) - { - /* Prevent scrolling the page */ - event.setAccepted(); - return; - } -} - -void AdminApp::textInputEvent(TextInputEvent& event) -{ - if (m_imgui.handleTextInputEvent(event)) - return; -} - -void AdminApp::drawServerUi() -{ - m_overlay.Update(*this); -} diff --git a/Code/admin/AdminApp.h b/Code/admin/AdminApp.h deleted file mode 100644 index 04ed7bb7f..000000000 --- a/Code/admin/AdminApp.h +++ /dev/null @@ -1,69 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include "Overlay.h" - -#include - -using namespace Magnum; -using namespace Math::Literals; - -enum class ConnectionState -{ - kNone, - kConnecting, - kConnected -}; - -struct AdminSessionOpen; -struct ServerLogs; - -struct AdminApp : Platform::Application, TiltedPhoques::Client -{ - explicit AdminApp(const Arguments& arguments); - - void drawEvent() override; - void tickEvent() override; - - void viewportEvent(ViewportEvent& event) override; - - void keyPressEvent(KeyEvent& event) override; - void keyReleaseEvent(KeyEvent& event) override; - - void mousePressEvent(MouseEvent& event) override; - void mouseReleaseEvent(MouseEvent& event) override; - void mouseMoveEvent(MouseMoveEvent& event) override; - void mouseScrollEvent(MouseScrollEvent& event) override; - void textInputEvent(TextInputEvent& event) override; - - void OnConsume(const void* apData, uint32_t aSize) override; - void OnConnected() override; - void OnDisconnected(EDisconnectReason aReason) override; - void OnUpdate() override; - - void SendShutdownRequest(); - -protected: - void drawServerUi(); - - bool Send(const ClientAdminMessage& acMessage) const noexcept; - bool Send(const ClientMessage& acMessage) const noexcept; - - void HandleMessage(const AdminSessionOpen& acMessage); - void HandleMessage(const ServerLogs& acMessage); - -private: - ImGuiIntegration::Context m_imgui{NoCreate}; - - bool m_authenticated; - Color4 m_clearColor = 0x72909aff_rgbaf; - Float m_floatValue = 0.0f; - ConnectionState m_state = ConnectionState::kNone; - String m_password; - std::function&)> m_messageHandlers[kServerAdminOpcodeMax]; - Overlay m_overlay; -}; diff --git a/Code/admin/Main.cpp b/Code/admin/Main.cpp deleted file mode 100644 index 4a42d08a9..000000000 --- a/Code/admin/Main.cpp +++ /dev/null @@ -1,3 +0,0 @@ -#include "AdminApp.h" - -MAGNUM_APPLICATION_MAIN(AdminApp) diff --git a/Code/admin/Overlay.cpp b/Code/admin/Overlay.cpp deleted file mode 100644 index 66e612053..000000000 --- a/Code/admin/Overlay.cpp +++ /dev/null @@ -1,112 +0,0 @@ -#include "Overlay.h" -#include -#include "AdminApp.h" - -Console& Overlay::GetConsole() -{ - return m_console; -} - -void Overlay::Toggle() -{ - if (!m_toggled) - m_toggled = true; -} - -bool Overlay::IsEnabled() const noexcept -{ - return m_initialized && m_enabled; -} - -void Overlay::Update(AdminApp& aApp) -{ - if (m_toggled) - { - if (m_enabled) - { - if (m_widgets[static_cast(m_activeWidgetID)]->OnDisable()) - { - m_toggled = false; - m_enabled = false; - } - } - else - { - if (m_widgets[static_cast(m_activeWidgetID)]->OnEnable()) - { - m_toggled = false; - m_enabled = true; - } - } - } - - if (!m_enabled) - return; - - const auto resolution = aApp.windowSize(); - - ImGui::SetNextWindowPos(ImVec2(resolution.x() * 0.2f, resolution.y() * 0.2f), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowSize(ImVec2(resolution.x() * 0.6f, resolution.y() * 0.6f), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowSizeConstraints(ImVec2(420, 315), ImVec2(FLT_MAX, FLT_MAX)); - if (ImGui::Begin("Tools")) - { - const ImVec2 cZeroVec = {0, 0}; - - SetActiveWidget(Toolbar()); - - if (m_activeWidgetID == WidgetID::CONSOLE) - { - if (ImGui::BeginChild("Console", cZeroVec, true)) - m_console.Update(aApp); - ImGui::EndChild(); - } - else if (m_activeWidgetID == WidgetID::ADMIN) - { - if (ImGui::BeginChild("Admin", cZeroVec, true)) - m_admin.Update(aApp); - ImGui::EndChild(); - } - } - ImGui::End(); -} - -Overlay::Overlay() - : m_enabled(true) -{ - m_widgets[static_cast(WidgetID::CONSOLE)] = &m_console; - m_widgets[static_cast(WidgetID::ADMIN)] = &m_admin; -} - -Overlay::~Overlay() -{ -} - -void Overlay::SetActiveWidget(WidgetID aNewActive) -{ - if (aNewActive < WidgetID::COUNT) - m_nextActiveWidgetID = aNewActive; - - if (m_activeWidgetID != m_nextActiveWidgetID) - { - assert(m_activeWidgetID < WidgetID::COUNT); - if (m_widgets[static_cast(m_activeWidgetID)]->OnDisable()) - { - assert(m_nextActiveWidgetID < WidgetID::COUNT); - if (m_widgets[static_cast(m_nextActiveWidgetID)]->OnEnable()) - m_activeWidgetID = m_nextActiveWidgetID; - } - } -} - -WidgetID Overlay::Toolbar() -{ - WidgetID activeID = WidgetID::COUNT; - ImGui::SameLine(); - if (ImGui::Button("Console")) - activeID = WidgetID::CONSOLE; - ImGui::SameLine(); - if (ImGui::Button("Admin")) - activeID = WidgetID::ADMIN; - - return activeID; -} diff --git a/Code/admin/Overlay.h b/Code/admin/Overlay.h deleted file mode 100644 index 01b57b7e4..000000000 --- a/Code/admin/Overlay.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include "widgets/Console.h" -#include "widgets/Admin.h" -#include "widgets/Widget.h" -#include - -struct Overlay -{ - Overlay(); - ~Overlay(); - - Console& GetConsole(); - - void Toggle(); - [[nodiscard]] bool IsEnabled() const noexcept; - - void Update(AdminApp& aApp); - -private: - void SetActiveWidget(WidgetID aNewActive); - - WidgetID Toolbar(); - - Console m_console; - Admin m_admin; - std::array m_widgets{}; - - WidgetID m_activeWidgetID{WidgetID::CONSOLE}; - WidgetID m_nextActiveWidgetID{WidgetID::CONSOLE}; - - std::atomic_bool m_enabled{false}; - std::atomic_bool m_toggled{false}; - bool m_initialized{false}; -}; diff --git a/Code/admin/Transport.cpp b/Code/admin/Transport.cpp deleted file mode 100644 index dc2994c8a..000000000 --- a/Code/admin/Transport.cpp +++ /dev/null @@ -1,116 +0,0 @@ -#include "AdminApp.h" -#include "Packet.hpp" -#include "AdminMessages/AdminShutdownRequest.h" -#include "AdminMessages/ServerLogs.h" -#include "AdminMessages/ServerAdminMessageFactory.h" - -#include - -#include -#include - -void AdminApp::OnConsume(const void* apData, uint32_t aSize) -{ - ServerAdminMessageFactory factory; - TiltedPhoques::ViewBuffer buf((uint8_t*)apData, aSize); - TiltedPhoques::Buffer::Reader reader(&buf); - - auto pMessage = factory.Extract(reader); - if (!pMessage) - { - return; - } - - m_messageHandlers[pMessage->GetOpcode()](pMessage); -} - -void AdminApp::OnConnected() -{ - AuthenticationRequest request; - request.Token = String(m_password.c_str()); - m_password = String(); - m_password.resize(1024); - - Send(request); -} - -void AdminApp::OnDisconnected(EDisconnectReason aReason) -{ - m_state = ConnectionState::kNone; -} - -void AdminApp::OnUpdate() -{ -} - -bool AdminApp::Send(const ClientAdminMessage& acMessage) const noexcept -{ - static thread_local TiltedPhoques::ScratchAllocator s_allocator(1 << 18); - - struct ScopedReset - { - ~ScopedReset() { s_allocator.Reset(); } - } allocatorGuard; - - if (IsConnected()) - { - TiltedPhoques::ScopedAllocator _{s_allocator}; - - TiltedPhoques::Buffer buffer(1 << 16); - TiltedPhoques::Buffer::Writer writer(&buffer); - writer.WriteBits(0, 8); // Write first byte as packet needs it - - acMessage.Serialize(writer); - TiltedPhoques::PacketView packet(reinterpret_cast(buffer.GetWriteData()), writer.Size()); - - Client::Send(&packet); - - return true; - } - - return false; -} - -bool AdminApp::Send(const ClientMessage& acMessage) const noexcept -{ - static thread_local TiltedPhoques::ScratchAllocator s_allocator(1 << 18); - - struct ScopedReset - { - ~ScopedReset() { s_allocator.Reset(); } - } allocatorGuard; - - if (IsConnected()) - { - TiltedPhoques::ScopedAllocator _{s_allocator}; - - TiltedPhoques::Buffer buffer(1 << 16); - TiltedPhoques::Buffer::Writer writer(&buffer); - writer.WriteBits(0, 8); // Write first byte as packet needs it - - acMessage.Serialize(writer); - TiltedPhoques::PacketView packet(reinterpret_cast(buffer.GetWriteData()), writer.Size()); - - Client::Send(&packet); - - return true; - } - - return false; -} - -void AdminApp::SendShutdownRequest() -{ - AdminShutdownRequest request; - Send(request); -} - -void AdminApp::HandleMessage(const AdminSessionOpen& acMessage) -{ - m_state = ConnectionState::kConnected; -} - -void AdminApp::HandleMessage(const ServerLogs& acMessage) -{ - m_overlay.GetConsole().Log(acMessage.Logs); -} diff --git a/Code/admin/Widgets/Admin.cpp b/Code/admin/Widgets/Admin.cpp deleted file mode 100644 index 7d02f9941..000000000 --- a/Code/admin/Widgets/Admin.cpp +++ /dev/null @@ -1,41 +0,0 @@ -#include "Admin.h" -#include -#include - -Admin::Admin() -{ -} - -bool Admin::OnEnable() -{ - return true; -} - -bool Admin::OnDisable() -{ - return true; -} - -void Admin::Update(AdminApp& aApp) -{ - if (ImGui::Button("Shutdown Server")) - ImGui::OpenPopup("Shutdown Server Dialog"); - - if (ImGui::BeginPopupModal("Shutdown Server Dialog")) - { - ImGui::Text("Are you sure you want to shutdown the server?"); - - if (ImGui::Button("Yes")) - { - aApp.SendShutdownRequest(); - ImGui::CloseCurrentPopup(); - } - - ImGui::SameLine(); - - if (ImGui::Button("No")) - ImGui::CloseCurrentPopup(); - - ImGui::EndPopup(); - } -} diff --git a/Code/admin/Widgets/Admin.h b/Code/admin/Widgets/Admin.h deleted file mode 100644 index 24387853d..000000000 --- a/Code/admin/Widgets/Admin.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include "Widget.h" - -struct Admin : Widget -{ - Admin(); - ~Admin() override = default; - - bool OnEnable() override; - bool OnDisable() override; - void Update(AdminApp& aApp) override; - -private: -}; diff --git a/Code/admin/Widgets/Console.cpp b/Code/admin/Widgets/Console.cpp deleted file mode 100644 index ef521330c..000000000 --- a/Code/admin/Widgets/Console.cpp +++ /dev/null @@ -1,148 +0,0 @@ -#include "Console.h" -#include - -Console::Console() -{ -} - -bool Console::OnEnable() -{ - m_focusConsoleInput = true; - return true; -} - -bool Console::OnDisable() -{ - return true; -} - -int Console::HandleConsoleHistory(ImGuiInputTextCallbackData* apData) -{ - auto* pConsole = static_cast(apData->UserData); - - TiltedPhoques::String* pStr = nullptr; - - if (pConsole->m_newConsoleHistory) - { - pStr = &pConsole->m_consoleHistory[pConsole->m_consoleHistoryIndex]; - } - else if (apData->EventKey == ImGuiKey_UpArrow && pConsole->m_consoleHistoryIndex > 0) - { - pConsole->m_consoleHistoryIndex--; - - pStr = &pConsole->m_consoleHistory[pConsole->m_consoleHistoryIndex]; - } - else if (apData->EventKey == ImGuiKey_DownArrow && pConsole->m_consoleHistoryIndex + 1 < pConsole->m_consoleHistory.size()) - { - pConsole->m_consoleHistoryIndex++; - - pStr = &pConsole->m_consoleHistory[pConsole->m_consoleHistoryIndex]; - } - - pConsole->m_newConsoleHistory = false; - - if (pStr) - { - std::memcpy(apData->Buf, pStr->c_str(), pStr->length() + 1); - apData->BufDirty = true; - apData->BufTextLen = pStr->length(); - apData->CursorPos = apData->BufTextLen; - } - - return 0; -} - -void Console::Update(AdminApp& aApp) -{ - ImGui::Checkbox("Clear Input", &m_inputClear); - ImGui::SameLine(); - if (ImGui::Button("Clear Output")) - { - m_outputLines.clear(); - } - ImGui::SameLine(); - ImGui::Checkbox("Scroll Output", &m_outputShouldScroll); - - auto& style = ImGui::GetStyle(); - auto inputLineHeight = ImGui::GetTextLineHeight() + style.ItemInnerSpacing.y * 2; - - if (ImGui::ListBoxHeader("##ConsoleHeader", ImVec2(-1, -(inputLineHeight + style.ItemSpacing.y)))) - { - ImGuiListClipper clipper; - clipper.Begin(m_outputLines.size()); - while (clipper.Step()) - for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; ++i) - { - auto& item = m_outputLines[i]; - ImGui::PushID(i); - if (ImGui::Selectable(item.c_str())) - { - auto str = item; - if (item[0] == '>' && item[1] == ' ') - str = str.substr(2); - - std::strncpy(m_Command, str.c_str(), sizeof(m_Command) - 1); - m_focusConsoleInput = true; - } - ImGui::PopID(); - } - - if (m_outputScroll) - { - if (m_outputShouldScroll) - ImGui::SetScrollHereY(); - m_outputScroll = false; - } - - ImGui::ListBoxFooter(); - } - - if (m_focusConsoleInput) - { - ImGui::SetKeyboardFocusHere(); - m_focusConsoleInput = false; - } - ImGui::SetNextItemWidth(-FLT_MIN); - const auto execute = ImGui::InputText("##InputCommand", m_Command, std::size(m_Command), ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackHistory, &HandleConsoleHistory, this); - ImGui::SetItemDefaultFocus(); - if (execute) - { - m_consoleHistoryIndex = m_consoleHistory.size(); - m_consoleHistory.push_back(m_Command); - m_newConsoleHistory = true; - - if (m_inputClear) - { - std::memset(m_Command, 0, sizeof(m_Command)); - } - - m_focusConsoleInput = true; - } -} - -void Console::Log(const TiltedPhoques::String& acpText) -{ - size_t first = 0; - while (first < acpText.size()) - { - const auto second = acpText.find_first_of('\n', first); - - if (second == std::string_view::npos) - { - m_outputLines.emplace_back(acpText.substr(first)); - break; - } - - if (first != second) - m_outputLines.emplace_back(acpText.substr(first, second - first)); - - first = second + 1; - } - - m_outputScroll = true; -} - -bool Console::GameLogEnabled() const -{ - return !m_disabledGameLog; -} diff --git a/Code/admin/Widgets/Console.h b/Code/admin/Widgets/Console.h deleted file mode 100644 index 18faff96f..000000000 --- a/Code/admin/Widgets/Console.h +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include "Widget.h" - -#include - -struct ImGuiInputTextCallbackData; - -struct Console : Widget -{ - Console(); - ~Console() override = default; - - bool OnEnable() override; - bool OnDisable() override; - void Update(AdminApp& aApp) override; - - void Log(const TiltedPhoques::String& acpText); - bool GameLogEnabled() const; - -private: - static int HandleConsoleHistory(ImGuiInputTextCallbackData* apData); - - TiltedPhoques::Vector m_outputLines{}; - TiltedPhoques::Vector m_consoleHistory{}; - int64_t m_consoleHistoryIndex{0}; - bool m_newConsoleHistory{true}; - bool m_outputShouldScroll{true}; - bool m_outputScroll{false}; - bool m_inputClear{true}; - bool m_disabledGameLog{true}; - bool m_focusConsoleInput{false}; - char m_Command[0x10000]{0}; -}; diff --git a/Code/admin/Widgets/Widget.h b/Code/admin/Widgets/Widget.h deleted file mode 100644 index f8395a2a5..000000000 --- a/Code/admin/Widgets/Widget.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -enum class WidgetID -{ - CONSOLE, - ADMIN, - COUNT -}; - -struct AdminApp; - -struct Widget -{ - virtual ~Widget() = default; - - virtual bool OnEnable() = 0; - virtual bool OnDisable() = 0; - virtual void Update(AdminApp& aApp) = 0; -}; diff --git a/Code/admin/admin.ico b/Code/admin/admin.ico deleted file mode 100644 index 617dfb95b..000000000 Binary files a/Code/admin/admin.ico and /dev/null differ diff --git a/Code/admin/admin.rc b/Code/admin/admin.rc deleted file mode 100644 index db498f62c..000000000 --- a/Code/admin/admin.rc +++ /dev/null @@ -1,32 +0,0 @@ -#include "winres.h" -#include "BuildInfo.h" - -VS_VERSION_INFO VERSIONINFO -FILEFLAGSMASK 0x17L -#ifdef _DEBUG -FILEFLAGS 0x1L -#else -FILEFLAGS 0x0L -#endif -FILEOS 0x4L -FILETYPE 0x1L -FILESUBTYPE 0x0L -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "040904b0" - BEGIN - VALUE "CompanyName", "Tilted Phoques SRL" - VALUE "ProductName", "TiltedAdmin" - VALUE "FileDescription", "Admin panel for TiltedOnline." - VALUE "OriginalFilename", "TiltedAdmin.exe" - VALUE "ProductVersion", BUILD_BRANCH "@" BUILD_COMMIT - END - END - BLOCK "VarFileInfo" - BEGIN - VALUE "Translation", 0x409, 1200 - END -END - -102 ICON "admin.ico" diff --git a/Code/admin/xmake.lua b/Code/admin/xmake.lua deleted file mode 100644 index b72cf751c..000000000 --- a/Code/admin/xmake.lua +++ /dev/null @@ -1,31 +0,0 @@ - - -target("Admin") - set_kind("binary") - set_group("Client") - set_basename("TiltedAdmin") - set_symbols("debug", "hidden") - add_includedirs( - ".", - "../", - "../../Libraries/") - add_headerfiles("**.h") - add_files( - "**.cpp", - "admin.rc") - add_deps("CommonLib", "AdminProtocol", "TiltedConnect") - - add_deps("SkyrimEncoding") - - if is_plat("windows") then - add_syslinks("opengl32", "Shell32", "Gdi32", "Winmm", "Ole32", "version", "OleAut32", "Setupapi") - end - - add_packages( - "tiltedcore", - "spdlog", - "hopscotch-map", - "glm", - "magnum", - "magnum-integration", - "gamenetworkingsockets") diff --git a/Code/admin_protocol/AdminMessages/AdminSessionOpen.cpp b/Code/admin_protocol/AdminMessages/AdminSessionOpen.cpp deleted file mode 100644 index b87bc2484..000000000 --- a/Code/admin_protocol/AdminMessages/AdminSessionOpen.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include "AdminSessionOpen.h" - -void AdminSessionOpen::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept -{ -} - -void AdminSessionOpen::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept -{ -} diff --git a/Code/admin_protocol/AdminMessages/AdminSessionOpen.h b/Code/admin_protocol/AdminMessages/AdminSessionOpen.h deleted file mode 100644 index 5263a5adb..000000000 --- a/Code/admin_protocol/AdminMessages/AdminSessionOpen.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "Message.h" - -struct AdminSessionOpen : ServerAdminMessage -{ - static constexpr ServerAdminOpcode Opcode = kAdminSessionOpen; - - AdminSessionOpen() - : ServerAdminMessage(Opcode) - { - } - - virtual ~AdminSessionOpen() = default; - - void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; - void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; - - bool operator==(const AdminSessionOpen& achRhs) const noexcept { return GetOpcode() == achRhs.GetOpcode(); } -}; diff --git a/Code/admin_protocol/AdminMessages/AdminShutdownRequest.cpp b/Code/admin_protocol/AdminMessages/AdminShutdownRequest.cpp deleted file mode 100644 index 330e92916..000000000 --- a/Code/admin_protocol/AdminMessages/AdminShutdownRequest.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include "AdminShutdownRequest.h" - -void AdminShutdownRequest::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept -{ -} - -void AdminShutdownRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept -{ -} diff --git a/Code/admin_protocol/AdminMessages/AdminShutdownRequest.h b/Code/admin_protocol/AdminMessages/AdminShutdownRequest.h deleted file mode 100644 index 558db82e9..000000000 --- a/Code/admin_protocol/AdminMessages/AdminShutdownRequest.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "Message.h" - -struct AdminShutdownRequest : ClientAdminMessage -{ - static constexpr ClientAdminOpcode Opcode = kAdminShutdown; - - AdminShutdownRequest() - : ClientAdminMessage(Opcode) - { - } - - virtual ~AdminShutdownRequest() = default; - - void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; - void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; - - bool operator==(const AdminShutdownRequest& achRhs) const noexcept { return GetOpcode() == achRhs.GetOpcode(); } -}; diff --git a/Code/admin_protocol/AdminMessages/ClientAdminMessageFactory.cpp b/Code/admin_protocol/AdminMessages/ClientAdminMessageFactory.cpp deleted file mode 100644 index 2b87a92eb..000000000 --- a/Code/admin_protocol/AdminMessages/ClientAdminMessageFactory.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once - -#include -#include -#include "ClientAdminMessageFactory.h" - -static std::function(TiltedPhoques::Buffer::Reader& aReader)> s_clientAdminMessageExtractor[kClientAdminOpcodeMax]; - -namespace details -{ -static struct ClientAdminMessageFactoryInit -{ - ClientAdminMessageFactoryInit() - { - auto extractor = [](auto& x) - { - using T = typename std::remove_reference_t::Type; - - s_clientAdminMessageExtractor[T::Opcode] = [](TiltedPhoques::Buffer::Reader& aReader) - { - auto ptr = TiltedPhoques::MakeUnique(); - ptr->DeserializeRaw(aReader); - return TiltedPhoques::CastUnique(std::move(ptr)); - }; - - return false; - }; - - ClientAdminMessageFactory::Visit(extractor); - } -} s_ClientAdminMessageFactoryInit; -} // namespace details - -UniquePtr ClientAdminMessageFactory::Extract(TiltedPhoques::Buffer::Reader& aReader) const noexcept -{ - uint64_t data; - aReader.ReadBits(data, sizeof(ClientAdminOpcode) * 8); - - if (data >= kClientAdminOpcodeMax) [[unlikely]] - return {nullptr}; - - const auto opcode = static_cast(data); - return s_clientAdminMessageExtractor[opcode](aReader); -} diff --git a/Code/admin_protocol/AdminMessages/ClientAdminMessageFactory.h b/Code/admin_protocol/AdminMessages/ClientAdminMessageFactory.h deleted file mode 100644 index 69ea0cca1..000000000 --- a/Code/admin_protocol/AdminMessages/ClientAdminMessageFactory.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include -#include "Message.h" -#include "MetaMessage.h" - -#include "AdminShutdownRequest.h" - -using TiltedPhoques::UniquePtr; - -struct ClientAdminMessageFactory -{ - UniquePtr Extract(TiltedPhoques::Buffer::Reader& aReader) const noexcept; - - template static auto Visit(T&& func) - { - auto s_visitor = CreateMessageVisitor; - - return s_visitor(std::forward(func)); - } -}; diff --git a/Code/admin_protocol/AdminMessages/Message.cpp b/Code/admin_protocol/AdminMessages/Message.cpp deleted file mode 100644 index 23041600d..000000000 --- a/Code/admin_protocol/AdminMessages/Message.cpp +++ /dev/null @@ -1,61 +0,0 @@ -#include "Message.h" - -void ClientAdminMessage::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept -{ - // We don't read the opcode, the factory will do it -} - -void ClientAdminMessage::DeserializeDifferential(TiltedPhoques::Buffer::Reader& aReader) noexcept -{ -} - -ClientAdminOpcode ClientAdminMessage::GetOpcode() const noexcept -{ - return m_opcode; -} - -void ClientAdminMessage::Serialize(TiltedPhoques::Buffer::Writer& aWriter) const noexcept -{ - ClientAdminMessage::SerializeRaw(aWriter); - SerializeRaw(aWriter); - SerializeDifferential(aWriter); -} - -void ClientAdminMessage::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept -{ - aWriter.WriteBits(m_opcode, sizeof(m_opcode) * 8); -} - -void ClientAdminMessage::SerializeDifferential(TiltedPhoques::Buffer::Writer& aWriter) const noexcept -{ -} - -void ServerAdminMessage::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept -{ - // We don't read the opcode, the factory will do it -} - -void ServerAdminMessage::DeserializeDifferential(TiltedPhoques::Buffer::Reader& aReader) noexcept -{ -} - -ServerAdminOpcode ServerAdminMessage::GetOpcode() const noexcept -{ - return m_opcode; -} - -void ServerAdminMessage::Serialize(TiltedPhoques::Buffer::Writer& aWriter) const noexcept -{ - ServerAdminMessage::SerializeRaw(aWriter); - SerializeRaw(aWriter); - SerializeDifferential(aWriter); -} - -void ServerAdminMessage::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept -{ - aWriter.WriteBits(m_opcode, sizeof(m_opcode) * 8); -} - -void ServerAdminMessage::SerializeDifferential(TiltedPhoques::Buffer::Writer& aWriter) const noexcept -{ -} diff --git a/Code/admin_protocol/AdminMessages/Message.h b/Code/admin_protocol/AdminMessages/Message.h deleted file mode 100644 index d8d2f222b..000000000 --- a/Code/admin_protocol/AdminMessages/Message.h +++ /dev/null @@ -1,54 +0,0 @@ -#pragma once - -#include "../Opcodes.h" -#include -#include -#include - -using TiltedPhoques::Serialization; -using TiltedPhoques::String; - -struct ClientAdminMessage : TiltedPhoques::AllocatorCompatible -{ - ClientAdminMessage(ClientAdminOpcode aOpcode) - : m_opcode(aOpcode) - { - } - - virtual ~ClientAdminMessage() = default; - - void Serialize(TiltedPhoques::Buffer::Writer& aWriter) const noexcept; - // Serialize values that are not dependent on previous states - virtual void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept; - // Serialize values that are dependent on previous states - virtual void SerializeDifferential(TiltedPhoques::Buffer::Writer& aWriter) const noexcept; - // Deserialize values that are dependent on previous states, this function will already be called - virtual void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept; - virtual void DeserializeDifferential(TiltedPhoques::Buffer::Reader& aReader) noexcept; - - [[nodiscard]] ClientAdminOpcode GetOpcode() const noexcept; - -private: - ClientAdminOpcode m_opcode; -}; - -struct ServerAdminMessage : TiltedPhoques::AllocatorCompatible -{ - ServerAdminMessage(ServerAdminOpcode aOpcode) - : m_opcode(aOpcode) - { - } - - virtual ~ServerAdminMessage() = default; - - void Serialize(TiltedPhoques::Buffer::Writer& aWriter) const noexcept; - virtual void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept; - virtual void SerializeDifferential(TiltedPhoques::Buffer::Writer& aWriter) const noexcept; - virtual void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept; - virtual void DeserializeDifferential(TiltedPhoques::Buffer::Reader& aReader) noexcept; - - [[nodiscard]] ServerAdminOpcode GetOpcode() const noexcept; - -private: - ServerAdminOpcode m_opcode; -}; diff --git a/Code/admin_protocol/AdminMessages/ServerAdminMessageFactory.cpp b/Code/admin_protocol/AdminMessages/ServerAdminMessageFactory.cpp deleted file mode 100644 index c809a5d10..000000000 --- a/Code/admin_protocol/AdminMessages/ServerAdminMessageFactory.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once - -#include -#include -#include "ServerAdminMessageFactory.h" - -static std::function(TiltedPhoques::Buffer::Reader& aReader)> s_serverAdminMessageExtractor[kServerAdminOpcodeMax]; - -namespace details -{ -static struct ServerAdminMessageFactoryInit -{ - ServerAdminMessageFactoryInit() - { - auto extractor = [](auto& x) - { - using T = typename std::remove_reference_t::Type; - - s_serverAdminMessageExtractor[T::Opcode] = [](TiltedPhoques::Buffer::Reader& aReader) - { - auto ptr = TiltedPhoques::MakeUnique(); - ptr->DeserializeRaw(aReader); - return TiltedPhoques::CastUnique(std::move(ptr)); - }; - - return false; - }; - - ServerAdminMessageFactory::Visit(extractor); - } -} s_ServerAdminMessageFactoryInit; -} // namespace details - -UniquePtr ServerAdminMessageFactory::Extract(TiltedPhoques::Buffer::Reader& aReader) const noexcept -{ - uint64_t data; - aReader.ReadBits(data, sizeof(ServerAdminOpcode) * 8); - - if (data >= kServerAdminOpcodeMax) [[unlikely]] - return {nullptr}; - - const auto opcode = static_cast(data); - return s_serverAdminMessageExtractor[opcode](aReader); -} diff --git a/Code/admin_protocol/AdminMessages/ServerAdminMessageFactory.h b/Code/admin_protocol/AdminMessages/ServerAdminMessageFactory.h deleted file mode 100644 index c68d84a47..000000000 --- a/Code/admin_protocol/AdminMessages/ServerAdminMessageFactory.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include -#include "Message.h" -#include "MetaMessage.h" - -#include "ServerLogs.h" -#include "AdminSessionOpen.h" - -using TiltedPhoques::UniquePtr; - -struct ServerAdminMessageFactory -{ - UniquePtr Extract(TiltedPhoques::Buffer::Reader& aReader) const noexcept; - - template static auto Visit(T&& func) - { - auto s_visitor = CreateMessageVisitor; - - return s_visitor(std::forward(func)); - } -}; diff --git a/Code/admin_protocol/AdminMessages/ServerLogs.cpp b/Code/admin_protocol/AdminMessages/ServerLogs.cpp deleted file mode 100644 index 9aedeb8dd..000000000 --- a/Code/admin_protocol/AdminMessages/ServerLogs.cpp +++ /dev/null @@ -1,11 +0,0 @@ -#include "ServerLogs.h" - -void ServerLogs::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept -{ - Serialization::WriteString(aWriter, Logs); -} - -void ServerLogs::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept -{ - Logs = Serialization::ReadString(aReader); -} diff --git a/Code/admin_protocol/AdminMessages/ServerLogs.h b/Code/admin_protocol/AdminMessages/ServerLogs.h deleted file mode 100644 index 2cbb9c89a..000000000 --- a/Code/admin_protocol/AdminMessages/ServerLogs.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include "Message.h" - -struct ServerLogs : ServerAdminMessage -{ - static constexpr ServerAdminOpcode Opcode = kServerLogs; - - ServerLogs() - : ServerAdminMessage(Opcode) - { - } - - virtual ~ServerLogs() = default; - - void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; - void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; - - bool operator==(const ServerLogs& achRhs) const noexcept { return GetOpcode() == achRhs.GetOpcode() && Logs == achRhs.Logs; } - - String Logs; -}; diff --git a/Code/admin_protocol/MetaMessage.h b/Code/admin_protocol/MetaMessage.h deleted file mode 100644 index dc15442bf..000000000 --- a/Code/admin_protocol/MetaMessage.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -namespace details -{ -template struct MetaMessage -{ - using Type = T; -}; - -} // namespace details - -template -auto CreateMessageVisitor = [](auto&& f) mutable -{ - auto expender = [f](auto&&... xs) - { - (... && !f(xs)); - }; - - expender(::details::MetaMessage{}...); -}; diff --git a/Code/admin_protocol/Opcodes.h b/Code/admin_protocol/Opcodes.h deleted file mode 100644 index b66a2b697..000000000 --- a/Code/admin_protocol/Opcodes.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -enum ClientAdminOpcode : unsigned char -{ - kAdminShutdown = 0, - - kClientAdminOpcodeMax -}; - -enum ServerAdminOpcode : unsigned char -{ - kAdminSessionOpen = 0, - kServerLogs, - - kServerAdminOpcodeMax -}; diff --git a/Code/admin_protocol/xmake.lua b/Code/admin_protocol/xmake.lua deleted file mode 100644 index 4f0f8c28b..000000000 --- a/Code/admin_protocol/xmake.lua +++ /dev/null @@ -1,11 +0,0 @@ - -target("AdminProtocol") - set_kind("static") - set_group("common") - if is_plat("linux") then - add_cxflags("-fPIC") - end - add_includedirs(".", "../", {public = true}) - add_headerfiles("**.h", {prefixdir = "AdminProtocol"}) - add_files("**.cpp") - add_packages("tiltedcore", "hopscotch-map") diff --git a/Code/client/Components.h b/Code/client/Components.h index 0de09050e..0e95faa36 100644 --- a/Code/client/Components.h +++ b/Code/client/Components.h @@ -15,9 +15,12 @@ #include #include #include +#include +#include #include #include #include #include +#include #undef TP_INTERNAL_COMPONENTS_GUARD diff --git a/Code/client/Components/GhostComponent.h b/Code/client/Components/GhostComponent.h new file mode 100644 index 000000000..3a53a6af1 --- /dev/null +++ b/Code/client/Components/GhostComponent.h @@ -0,0 +1,15 @@ +#pragma once + +#ifndef TP_INTERNAL_COMPONENTS_GUARD +#error Include Components.h instead +#endif + +struct GhostComponent +{ + explicit GhostComponent(bool aIsGhost = false) + : IsGhost(aIsGhost) + { + } + + bool IsGhost; +}; diff --git a/Code/client/Components/PendingEquipmentComponent.h b/Code/client/Components/PendingEquipmentComponent.h new file mode 100644 index 000000000..0ccf8c3bc --- /dev/null +++ b/Code/client/Components/PendingEquipmentComponent.h @@ -0,0 +1,13 @@ +#pragma once + +#ifndef TP_INTERNAL_COMPONENTS_GUARD +#error Include Components.h instead +#endif + +#include +#include + +struct PendingEquipmentComponent +{ + TiltedPhoques::Vector PendingChanges; +}; diff --git a/Code/client/Components/PendingInventoryComponent.h b/Code/client/Components/PendingInventoryComponent.h new file mode 100644 index 000000000..8a77aa949 --- /dev/null +++ b/Code/client/Components/PendingInventoryComponent.h @@ -0,0 +1,23 @@ +#pragma once + +#ifndef TP_INTERNAL_COMPONENTS_GUARD +#error Include Components.h instead +#endif + +#include + +struct PendingInventoryComponent +{ + PendingInventoryComponent() = default; + + PendingInventoryComponent(Inventory aInventory, bool aIsDead, bool aIsWeaponDrawn) + : InventoryContent(std::move(aInventory)) + , IsDead(aIsDead) + , IsWeaponDrawn(aIsWeaponDrawn) + { + } + + Inventory InventoryContent{}; + bool IsDead{false}; + bool IsWeaponDrawn{false}; +}; diff --git a/Code/client/Events/DropItemEvent.h b/Code/client/Events/DropItemEvent.h new file mode 100644 index 000000000..ad30fbf93 --- /dev/null +++ b/Code/client/Events/DropItemEvent.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include + +struct DropItemEvent +{ + DropItemEvent(uint32_t aActorFormId, Inventory::Entry aItem, Guid aClientDropId, NiPoint3 aLocation, NiPoint3 aRotation, uint32_t aHandleBits, GameId aCellId, GameId aWorldSpaceId, GameId aReferenceId) + : ActorFormId(aActorFormId) + , Item(std::move(aItem)) + , ClientDropId(aClientDropId) + , Location(aLocation) + , Rotation(aRotation) + , HandleBits(aHandleBits) + , CellId(aCellId) + , WorldSpaceId(aWorldSpaceId) + , ReferenceId(aReferenceId) + { + } + + uint32_t ActorFormId{}; + Inventory::Entry Item{}; + Guid ClientDropId{}; + NiPoint3 Location{}; + NiPoint3 Rotation{}; + uint32_t HandleBits{}; + GameId CellId{}; + GameId WorldSpaceId{}; + GameId ReferenceId{}; +}; diff --git a/Code/client/Events/InventoryChangeEvent.h b/Code/client/Events/InventoryChangeEvent.h index 02e8b953c..4ca6841b2 100644 --- a/Code/client/Events/InventoryChangeEvent.h +++ b/Code/client/Events/InventoryChangeEvent.h @@ -1,12 +1,11 @@ #pragma once -#include #include +#include +#include /** * @brief Dispatched when the contents of an object or actor inventory changes locally. - * - * The event has a Drop member variable, since dropped items need to be handled differently. */ struct InventoryChangeEvent { @@ -16,23 +15,14 @@ struct InventoryChangeEvent { } - InventoryChangeEvent(const uint32_t aFormId, Inventory::Entry arItem, bool aDrop) - : FormId(aFormId) - , Item(std::move(arItem)) - , Drop(aDrop) - { - } - - InventoryChangeEvent(const uint32_t aFormId, Inventory::Entry arItem, bool aDrop, bool aUpdateClients) + InventoryChangeEvent(const uint32_t aFormId, Inventory::Entry arItem, bool aUpdateClients) : FormId(aFormId) , Item(std::move(arItem)) - , Drop(aDrop) , UpdateClients(aUpdateClients) { } uint32_t FormId{}; Inventory::Entry Item{}; - bool Drop = false; bool UpdateClients = true; }; diff --git a/Code/client/Events/PickupDroppedItemEvent.h b/Code/client/Events/PickupDroppedItemEvent.h new file mode 100644 index 000000000..4e295f362 --- /dev/null +++ b/Code/client/Events/PickupDroppedItemEvent.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include + +struct PickupDroppedItemEvent +{ + PickupDroppedItemEvent(uint32_t aActorFormId, uint64_t aDropId) + : ActorFormId(aActorFormId) + , DropId(aDropId) + { + } + + uint32_t ActorFormId{}; + uint64_t DropId{}; + uint32_t ReferenceFormId{}; + bool HasItemData{false}; + Inventory::Entry Item{}; + bool HasLocation{false}; + NiPoint3 Location{}; + bool HasRotation{false}; + NiPoint3 Rotation{}; + GameId CellId{}; + GameId WorldSpaceId{}; + GameId ReferenceId{}; +}; diff --git a/Code/client/Games/Animation.cpp b/Code/client/Games/Animation.cpp index 178caef13..098fba7ae 100644 --- a/Code/client/Games/Animation.cpp +++ b/Code/client/Games/Animation.cpp @@ -19,6 +19,7 @@ static TPerformAction* RealPerformAction; // TODO: make scoped override thread_local bool g_forceAnimation = false; +thread_local bool g_forceAnimationNetwork = false; uint8_t TP_MAKE_THISCALL(HookPerformAction, ActorMediator, TESActionData* apAction) { @@ -43,7 +44,7 @@ uint8_t TP_MAKE_THISCALL(HookPerformAction, ActorMediator, TESActionData* apActi // spdlog::debug("Action event name: {}, target name: {}", apAction->eventName.AsAscii(), apAction->targetEventName.AsAscii()); // This is a weird case where it gets spammed and doesn't do much, not sure if it still needs to be sent over the network - if (apAction->someFlag == 1 || g_forceAnimation) + if (apAction->someFlag == 1 || (g_forceAnimation && !g_forceAnimationNetwork)) return res; action.EventName = apAction->eventName.AsAscii(); diff --git a/Code/client/Games/ModManager.cpp b/Code/client/Games/ModManager.cpp index a8f0c4562..69f76781a 100644 --- a/Code/client/Games/ModManager.cpp +++ b/Code/client/Games/ModManager.cpp @@ -28,7 +28,8 @@ uint32_t Mod::GetFormId(uint32_t aBaseId) const noexcept return aBaseId; } -TP_THIS_FUNCTION(TSpawnNewREFR, uint32_t&, ModManager, uint32_t& aRefHandleOut, TESForm* apBaseForm, NiPoint3* apPosition, NiPoint3* apRotation, TESObjectCELL* apParentCell, TESWorldSpace* apWorldSpace, Actor* apActor, uintptr_t a9, uintptr_t a10, char aForcePersist, char a12); +TP_THIS_FUNCTION(TSpawnNewREFR, uint32_t&, ModManager, uint32_t& aRefHandleOut, TESForm* apBaseForm, NiPoint3* apPosition, NiPoint3* apRotation, TESObjectCELL* apParentCell, TESWorldSpace* apWorldSpace, + TESObjectREFR* apAlreadyCreatedRef, uintptr_t a9, uintptr_t a10, char aForcePersist, char a12); TSpawnNewREFR* RealSpawnNewREFR; uint32_t& TP_MAKE_THISCALL(SpawnNewREFR, ModManager, uint32_t& aRefHandleOut, TESForm* apBaseForm, NiPoint3* apPosition, NiPoint3* apRotation, TESObjectCELL* apParentCell, TESWorldSpace* apWorldSpace, Actor* apActor, uintptr_t a9, uintptr_t a10, char aForcePersist, char a12) @@ -47,6 +48,17 @@ uint32_t ModManager::Spawn(NiPoint3& aPosition, NiPoint3& aRotation, TESObjectCE return refrHandle; } +uint32_t ModManager::SpawnReference(TESForm* apBaseForm, NiPoint3& aPosition, NiPoint3& aRotation, TESObjectCELL* apParentCell, TESWorldSpace* apWorldSpace, TESObjectREFR* apAlreadyCreatedRef, + bool aForcePersist) noexcept +{ + uint32_t refrHandle = 0; + + TiltedPhoques::ThisCall(RealSpawnNewREFR, this, refrHandle, apBaseForm, &aPosition, &aRotation, apParentCell, apWorldSpace, apAlreadyCreatedRef, 0, 0, static_cast(aForcePersist ? 1 : 0), + static_cast(1)); + + return refrHandle; +} + Mod* ModManager::GetByName(const char* acpName) const noexcept { auto pEntry = &mods.entry; diff --git a/Code/client/Games/SaveGameUtils.cpp b/Code/client/Games/SaveGameUtils.cpp new file mode 100644 index 000000000..4dd9b6472 --- /dev/null +++ b/Code/client/Games/SaveGameUtils.cpp @@ -0,0 +1,91 @@ +#include + +#include + +#include +#include + +#include + +namespace +{ +struct BGSSaveLoadManagerLite +{ + uint8_t pad138[0x138]; + char lastFileFullName[0x104]; + uint32_t pad23C; + BSFixedString lastFileName; +}; + +static_assert(offsetof(BGSSaveLoadManagerLite, lastFileFullName) == 0x138); +static_assert(offsetof(BGSSaveLoadManagerLite, lastFileName) == 0x240); + +BGSSaveLoadManagerLite* GetSaveLoadManager() noexcept +{ + POINTER_SKYRIMSE(BGSSaveLoadManagerLite*, s_singleton, 403340); + auto** ppManager = s_singleton.Get(); + if (!ppManager) + return nullptr; + return *ppManager; +} + +BSWin32SaveDataSystemUtility* GetSaveDataSystemUtility() noexcept +{ + return BSWin32SaveDataSystemUtility::GetSingleton(); +} +} // namespace + +namespace SaveGameUtils +{ +std::string GetCurrentSaveName() noexcept +{ + auto* pManager = GetSaveLoadManager(); + if (!pManager) + return {}; + + if (pManager->lastFileName.data && pManager->lastFileName.data[0] != '\0') + return pManager->lastFileName.data; + + if (pManager->lastFileFullName[0] == '\0') + return {}; + + std::filesystem::path path(pManager->lastFileFullName); + return path.stem().string(); +} + +std::filesystem::path GetCurrentSavePath() noexcept +{ + auto* pManager = GetSaveLoadManager(); + if (!pManager) + return {}; + + if (pManager->lastFileFullName[0] != '\0') + { + std::filesystem::path fullPath(pManager->lastFileFullName); + if (fullPath.has_parent_path() || fullPath.has_root_path()) + return fullPath; + } + + const char* fileName = nullptr; + if (pManager->lastFileName.data && pManager->lastFileName.data[0] != '\0') + fileName = pManager->lastFileName.data; + else if (pManager->lastFileFullName[0] != '\0') + fileName = pManager->lastFileFullName; + + if (!fileName || fileName[0] == '\0') + return {}; + + auto* pSaveUtil = GetSaveDataSystemUtility(); + if (!pSaveUtil) + return {}; + + char resolvedPath[0x104]{}; + if (pSaveUtil->PrepareFileSavePath(fileName, resolvedPath, false, false) != 0) + return {}; + + if (resolvedPath[0] == '\0') + return {}; + + return std::filesystem::path(resolvedPath); +} +} // namespace SaveGameUtils diff --git a/Code/client/Games/SaveGameUtils.h b/Code/client/Games/SaveGameUtils.h new file mode 100644 index 000000000..31d490ca8 --- /dev/null +++ b/Code/client/Games/SaveGameUtils.h @@ -0,0 +1,10 @@ +#pragma once + +#include +#include + +namespace SaveGameUtils +{ +std::string GetCurrentSaveName() noexcept; +std::filesystem::path GetCurrentSavePath() noexcept; +} diff --git a/Code/client/Games/Skyrim/Actor.cpp b/Code/client/Games/Skyrim/Actor.cpp index 931c2bdf1..eee232cc5 100644 --- a/Code/client/Games/Skyrim/Actor.cpp +++ b/Code/client/Games/Skyrim/Actor.cpp @@ -1,3 +1,5 @@ +#include + #include #include #include @@ -14,6 +16,8 @@ #include #include +#include +#include #include #include #include @@ -21,6 +25,7 @@ #include #include +#include #include #include @@ -31,6 +36,14 @@ #include #include #include +#include + +#include +#include + +#include +#include +#include #include #include @@ -43,6 +56,7 @@ #include #include #include +#include #include @@ -54,6 +68,159 @@ #include #include +#include + +namespace +{ +bool IsRemoteGhostActor(Actor* apActor) +{ + if (!apActor || !entt::locator::has_value()) + return false; + + auto* pExt = apActor->GetExtension(); + if (!pExt || !pExt->IsRemotePlayer()) + return false; + + auto& world = World::Get(); + + if (world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost) + return true; + + auto view = world.view(); + const auto it = std::find_if(view.begin(), view.end(), + [view, formId = apActor->formID](entt::entity e) + { return view.get(e).Id == formId && view.get(e).IsGhost; }); + + return it != view.end(); +} +} // namespace + +namespace +{ +constexpr float kDropSearchRadiusSquared = 200.0f * 200.0f; + +TESObjectREFR* FindDroppedReferenceNear(const TESBoundObject* apObject, const NiPoint3& acCenter) +{ + if (!apObject) + return nullptr; + + TES* pTes = TES::Get(); + if (!pTes || !pTes->cells || !pTes->cells->arr) + return nullptr; + + const int dimension = pTes->cells->dimension; + if (dimension <= 0) + return nullptr; + + auto evaluateCell = [&](TESObjectCELL* apCell, TESObjectREFR*& apBestMatch, float& aBestDistanceSq) { + if (!apCell || !apCell->IsValid()) + return; + + auto* pReferences = apCell->refData.refArray; + if (!pReferences) + return; + + const uint32_t referenceCount = apCell->refData.Count(); + for (uint32_t j = 0; j < referenceCount; ++j) + { + TESObjectREFR* pCandidate = pReferences[j].Get(); + if (!pCandidate || pCandidate->baseForm != apObject || pCandidate->formType == Actor::Type) + continue; + + const float diffX = pCandidate->position.x - acCenter.x; + const float diffY = pCandidate->position.y - acCenter.y; + const float diffZ = pCandidate->position.z - acCenter.z; + const float distanceSq = diffX * diffX + diffY * diffY + diffZ * diffZ; + + if (distanceSq < aBestDistanceSq) + { + aBestDistanceSq = distanceSq; + apBestMatch = pCandidate; + } + } + }; + + TESObjectREFR* pClosest = nullptr; + float closestDistanceSq = kDropSearchRadiusSquared; + + const int cellCount = dimension * dimension; + for (int i = 0; i < cellCount; ++i) + { + TESObjectCELL* pCell = pTes->cells->arr[i]; + evaluateCell(pCell, pClosest, closestDistanceSq); + } + + if (!pClosest) + evaluateCell(pTes->interiorCell, pClosest, closestDistanceSq); + + return pClosest; +} + +GameId ResolveReferenceId(TESObjectREFR* apReference) noexcept +{ + GameId reference{}; + if (!apReference) + return reference; + + World::Get().GetModSystem().GetServerModId(apReference->formID, reference); + return reference; +} + +std::pair ResolveReferenceCellMetadata(TESObjectREFR* apReference) noexcept +{ + GameId cell{}; + GameId world{}; + + if (!apReference) + return {cell, world}; + + TESObjectCELL* pCell = apReference->parentCell ? apReference->parentCell : apReference->GetParentCell(); + if (pCell) + { + auto& modSystem = World::Get().GetModSystem(); + modSystem.GetServerModId(pCell->formID, cell); + if (pCell->worldspace) + modSystem.GetServerModId(pCell->worldspace->formID, world); + } + + return {cell, world}; +} + +void PopulatePickupEventFromDrop(uint64_t aDropId, PickupDroppedItemEvent& aEvent) noexcept +{ + if (const auto dropOpt = DropManager::GetServerDrop(aDropId); dropOpt) + { + aEvent.HasItemData = true; + aEvent.Item = dropOpt->Item; + aEvent.HasLocation = true; + aEvent.Location = dropOpt->Location; + aEvent.HasRotation = true; + aEvent.Rotation = dropOpt->Rotation; + aEvent.CellId = dropOpt->CellId; + aEvent.WorldSpaceId = dropOpt->WorldSpaceId; + aEvent.ReferenceId = dropOpt->ReferenceId; + } +} + +void PopulatePickupEventFromReference(TESObjectREFR* apReference, const Inventory::Entry& acItem, PickupDroppedItemEvent& aEvent) noexcept +{ + if (!apReference) + return; + + aEvent.ReferenceFormId = apReference->formID; + aEvent.HasItemData = true; + aEvent.Item = acItem; + aEvent.HasLocation = true; + aEvent.Location = apReference->position; + aEvent.HasRotation = true; + aEvent.Rotation = apReference->rotation; + + const auto cellMeta = ResolveReferenceCellMetadata(apReference); + aEvent.CellId = cellMeta.first; + aEvent.WorldSpaceId = cellMeta.second; + aEvent.ReferenceId = ResolveReferenceId(apReference); +} +} // namespace #ifdef SAVE_STUFF @@ -330,6 +497,9 @@ Actor* Actor::GetCombatTarget() const noexcept // The internal targeting system should be disabled instead. void Actor::StartCombatEx(Actor* apTarget) noexcept { + if (IsRemoteGhostActor(apTarget)) + return; + if (GetCombatTarget() != apTarget) { StopCombat(); @@ -339,12 +509,22 @@ void Actor::StartCombatEx(Actor* apTarget) noexcept void Actor::SetCombatTargetEx(Actor* apTarget) noexcept { + if (IsRemoteGhostActor(apTarget)) + { + if (pCombatController) + pCombatController->SetTarget(nullptr); + return; + } + if (pCombatController) pCombatController->SetTarget(apTarget); } void Actor::StartCombat(Actor* apTarget) noexcept { + if (IsRemoteGhostActor(apTarget)) + return; + PAPYRUS_FUNCTION(void, Actor, StartCombat, Actor*); s_pStartCombat(this, apTarget); } @@ -562,6 +742,7 @@ Factions Actor::GetFactions() const noexcept auto& modSystem = World::Get().GetModSystem(); + auto* pNpc = Cast(baseForm); if (pNpc) { @@ -690,12 +871,20 @@ void Actor::SetActorInventory(const Inventory& acInventory) noexcept Inventory currentInventory = GetActorInventory(); - if (!this->GetExtension()->IsPlayer() && currentInventory.ContainsQuestItems()) + const bool hasQuestItems = currentInventory.ContainsQuestItems(); + const bool isPlayer = this->GetExtension()->IsPlayer(); + + if (!isPlayer && hasQuestItems) + { SetInventoryRetainingQuestItems(currentInventory, acInventory); + SetMagicEquipment(acInventory.CurrentMagicEquipment); + } else + { SetInventory(acInventory); - - SetMagicEquipment(acInventory.CurrentMagicEquipment); + if (isPlayer || !hasQuestItems) + SetMagicEquipment(acInventory.CurrentMagicEquipment); + } } void Actor::SetMagicEquipment(const MagicEquipment& acEquipment) noexcept @@ -777,6 +966,16 @@ void Actor::SetPlayerTeammate(bool aSet) noexcept return TiltedPhoques::ThisCall(setPlayerTeammate, this, aSet, true); } +bool Actor::HasLineOfSight(TESObjectREFR* apTarget) noexcept +{ + if (!apTarget) + return false; + + TP_THIS_FUNCTION(THasLineOfSight, bool, Actor, TESObjectREFR*); + POINTER_SKYRIMSE(THasLineOfSight, hasLineOfSight, 37716); + return TiltedPhoques::ThisCall(hasLineOfSight, this, apTarget); +} + void Actor::UnEquipAll() noexcept { EquipManager::Get()->UnequipAll(this); @@ -910,8 +1109,11 @@ char TP_MAKE_THISCALL(HookSetPosition, Actor, NiPoint3& aPosition) { const auto pExtension = apThis ? apThis->GetExtension() : nullptr; const auto bIsRemote = pExtension && pExtension->IsRemote(); + bool bAllowRemoteUpdate = ScopedReferencesOverride::IsOverriden(); + if (!bAllowRemoteUpdate && bIsRemote && apThis) + bAllowRemoteUpdate = apThis->IsDead(); - if (bIsRemote && !ScopedReferencesOverride::IsOverriden()) + if (bIsRemote && !bAllowRemoteUpdate) return 1; // Don't interfere with non actor references, or the player, or if we are calling our self @@ -956,7 +1158,17 @@ bool TP_MAKE_THISCALL(HookSpawnActorInWorld, Actor) spdlog::info("Spawn Actor: {:X}, and NPC {}", apThis->formID, pNpc->fullName.value); } - return TiltedPhoques::ThisCall(RealSpawnActorInWorld, apThis); + const bool result = TiltedPhoques::ThisCall(RealSpawnActorInWorld, apThis); + + // Re-apply ghost visuals after 3D rebuilds/cell transitions without doing it from the per-frame update loop. + if (entt::locator::has_value()) + { + const auto* pExtension = apThis ? apThis->GetExtension() : nullptr; + if (pExtension && pExtension->IsRemotePlayer()) + World::Get().GetSyncModeService().OnActor3DUpdated(apThis); + } + + return result; } TP_THIS_FUNCTION(TDamageActor, bool, Actor, float aDamage, Actor* apHitter, bool aKillMove); @@ -965,9 +1177,42 @@ static TDamageActor* RealDamageActor = nullptr; // TODO: this is flawed, since it does not account for invulnerable actors bool TP_MAKE_THISCALL(HookDamageActor, Actor, float aDamage, Actor* apHitter, bool aKillMove) { - if (apHitter) + // Remote ghosts should never generate hits that can aggro NPCs locally. + if (apHitter && entt::locator::has_value()) + { + auto* pHitterExt = apHitter->GetExtension(); + if (pHitterExt && pHitterExt->IsRemotePlayer()) + { + auto& world = World::Get(); + bool hitterGhosted = (world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost); + + if (!hitterGhosted) + { + auto view = world.view(); + const auto it = std::find_if(view.begin(), view.end(), + [view, hitterId = apHitter->formID](entt::entity e) { return view.get(e).Id == hitterId && view.get(e).IsGhost; }); + hitterGhosted = (it != view.end()); + } + + if (hitterGhosted) + return false; + } + } + + if (apHitter && entt::locator::has_value()) World::Get().GetRunner().Trigger(HitEvent(apHitter->formID, apThis->formID)); + // Ghosted remote players are visual-only and must not be hittable. + if (apThis->GetExtension() && apThis->GetExtension()->IsRemotePlayer()) + { + const bool locallyGated = entt::locator::has_value() && World::Get().GetSyncModeService().GetLocalMode() == SyncMode::Ghost; + const auto* pNpc = Cast(apThis->baseForm); + const bool isGhostFlagged = pNpc && (pNpc->actorData.flags & (1u << 29)); + + if (locallyGated || isGhostFlagged) + return false; + } + float realDamage = GameplayFormulas::CalculateRealDamage(apThis, aDamage, aKillMove); float currentHealth = apThis->GetActorValue(ActorValueInfo::kHealth); @@ -1083,25 +1328,82 @@ void TP_MAKE_THISCALL(HookAddInventoryItem, Actor, TESBoundObject* apItem, Extra void* TP_MAKE_THISCALL(HookPickUpObject, Actor, TESObjectREFR* apObject, int32_t aCount, bool aUnk1, float aUnk2) { - if (!ScopedInventoryOverride::IsOverriden()) + const bool isRemotePickup = DropExecution::GetCurrentMode() == DropExecution::Mode::RemotePickup; + const bool isLocalPlayer = apThis->GetExtension() && apThis->GetExtension()->IsLocalPlayer(); + const bool isConnected = World::Get().GetTransport().IsConnected(); + const bool isGhosted = (World::Get().GetSyncModeService().GetLocalMode() == SyncMode::Ghost); + std::optional dropId{}; + + if (apObject) { - auto& modSystem = World::Get().GetModSystem(); + auto handle = apObject->GetHandle(); + if (handle && handle.handle.iBits) + dropId = DropManager::GetDropIdForHandle(handle.handle.iBits); - Inventory::Entry item{}; - modSystem.GetServerModId(apObject->baseForm->formID, item.BaseId); - item.Count = aCount; + if (!dropId) + { + GameId objectId{}; + World::Get().GetModSystem().GetServerModId(apObject->baseForm->formID, objectId); + if (objectId.ModId || objectId.BaseId) + dropId = DropManager::FindDropBySignature(objectId, apObject->position, kDropSearchRadiusSquared); + } + } + + Inventory::Entry fallbackItem{}; + const bool hasReferenceObject = apObject != nullptr; + if (!dropId && apObject) + { + auto& modSystem = World::Get().GetModSystem(); + modSystem.GetServerModId(apObject->baseForm->formID, fallbackItem.BaseId); + fallbackItem.Count = aCount; if (apObject->GetExtraDataList()) - apThis->GetItemFromExtraData(item, apObject->GetExtraDataList()); + { + const int32_t engineCount = fallbackItem.Count; + apThis->GetItemFromExtraData(fallbackItem, apObject->GetExtraDataList()); + fallbackItem.Count = engineCount; + } + } + + if (!isRemotePickup && isLocalPlayer && isConnected) + { + std::optional pickupEvent{}; + + if (dropId) + { + pickupEvent.emplace(apThis->formID, *dropId); + PopulatePickupEventFromDrop(*dropId, *pickupEvent); + } + else if (hasReferenceObject) + { + pickupEvent.emplace(apThis->formID, 0); + PopulatePickupEventFromReference(apObject, fallbackItem, *pickupEvent); + } - // This is here so that objects that are picked up on both clients, aka non temps, are synced through activation sync. - // The inventory change event should always be sent to the server, otherwise the server inventory won't be updated. - bool shouldUpdateClients = apObject->IsTemporary() && !ScopedActivateOverride::IsOverriden(); + if (pickupEvent && apObject) + pickupEvent->ReferenceFormId = apObject->formID; - World::Get().GetRunner().Trigger(InventoryChangeEvent(apThis->formID, std::move(item), false, shouldUpdateClients)); + if (pickupEvent) + World::Get().GetRunner().Trigger(*pickupEvent); } - return TiltedPhoques::ThisCall(RealPickUpObject, apThis, apObject, aCount, aUnk1, aUnk2); + void* pResult = nullptr; + if (!isRemotePickup && isLocalPlayer && isConnected) + { + DropExecution::Scope scope(DropExecution::Mode::LocalPickup, apThis->formID, dropId.value_or(0)); + pResult = TiltedPhoques::ThisCall(RealPickUpObject, apThis, apObject, aCount, aUnk1, aUnk2); + } + else + { + pResult = TiltedPhoques::ThisCall(RealPickUpObject, apThis, apObject, aCount, aUnk1, aUnk2); + } + + if (isRemotePickup) + { + DropManager::RemoveServerDrop(DropExecution::GetCurrentDrop()); + } + + return pResult; } void Actor::PickUpObject(TESObjectREFR* apObject, int32_t aCount, bool aUnk1, float aUnk2) noexcept @@ -1118,13 +1420,109 @@ void* TP_MAKE_THISCALL(HookDropObject, Actor, void* apResult, TESBoundObject* ap item.Count = -aCount; if (apExtraData) + { + const int32_t engineCount = item.Count; apThis->GetItemFromExtraData(item, apExtraData); + item.Count = engineCount; + } - World::Get().GetRunner().Trigger(InventoryChangeEvent(apThis->formID, std::move(item), true)); + const bool shouldSend = !ScopedInventoryOverride::IsOverriden(); + const bool isLocalPlayer = apThis->GetExtension() && apThis->GetExtension()->IsLocalPlayer(); + const bool isConnected = World::Get().GetTransport().IsConnected(); + const bool isGhosted = (World::Get().GetSyncModeService().GetLocalMode() == SyncMode::Ghost); + const bool isRemoteDrop = DropExecution::GetCurrentMode() == DropExecution::Mode::RemoteDrop; + std::optional localDropScope{}; + if (!isRemoteDrop) + localDropScope.emplace(DropExecution::Mode::LocalDrop, apThis->formID, 0); - ScopedInventoryOverride _; + void* pReturn = nullptr; + { + ScopedInventoryOverride _; + pReturn = TiltedPhoques::ThisCall(RealDropObject, apThis, apResult, apObject, apExtraData, aCount, apLocation, apRotation); + } - return TiltedPhoques::ThisCall(RealDropObject, apThis, apResult, apObject, apExtraData, aCount, apLocation, apRotation); + uint32_t handleBits = 0; + TESObjectREFR* pDroppedRef = nullptr; + if (auto* pHandle = static_cast*>(apResult); pHandle && *pHandle) + { + handleBits = pHandle->handle.iBits; + if (handleBits) + pDroppedRef = TESObjectREFR::GetByHandle(handleBits); + } + + NiPoint3 dropLocation = apLocation ? *apLocation : apThis->position; + NiPoint3 dropRotation = apRotation ? *apRotation : apThis->rotation; + + if (pDroppedRef) + { + dropLocation = pDroppedRef->position; + dropRotation = pDroppedRef->rotation; + } + + if (isRemoteDrop) + { + if (handleBits) + { + DropManager::BindHandleToServerDrop(DropExecution::GetCurrentDrop(), DropExecution::GetCurrentActor(), handleBits); + if (pDroppedRef) + { + auto& modSystem = World::Get().GetModSystem(); + GameId referenceId{}; + modSystem.GetServerModId(pDroppedRef->formID, referenceId); + DropManager::SetReferenceForDrop(DropExecution::GetCurrentDrop(), referenceId); + } + } + return pReturn; + } + + if (shouldSend && isLocalPlayer && isConnected && !isGhosted) + { + GameId cellId{}; + GameId worldId{}; + + if (pDroppedRef) + { + if (auto* pCell = pDroppedRef->GetParentCellEx()) + modSystem.GetServerModId(pCell->formID, cellId); + + if (auto* pWorld = pDroppedRef->GetWorldSpace()) + modSystem.GetServerModId(pWorld->formID, worldId); + } + else if (apThis->parentCell) + { + modSystem.GetServerModId(apThis->parentCell->formID, cellId); + if (apThis->parentCell->worldspace) + modSystem.GetServerModId(apThis->parentCell->worldspace->formID, worldId); + } + + DropManager::LocalDropData dropData{}; + dropData.ActorFormId = apThis->formID; + dropData.Item = item; + dropData.HandleBits = handleBits; + dropData.Location = dropLocation; + dropData.Rotation = dropRotation; + dropData.CellId = cellId; + dropData.WorldSpaceId = worldId; + if (pDroppedRef) + { + GameId referenceId{}; + modSystem.GetServerModId(pDroppedRef->formID, referenceId); + dropData.ReferenceId = referenceId; + } + + const Guid clientDropId = DropManager::RegisterLocalDrop(dropData); + World::Get().GetRunner().Trigger(DropItemEvent(apThis->formID, item, clientDropId, dropLocation, dropRotation, handleBits, cellId, worldId, dropData.ReferenceId)); + + if (pDroppedRef) + { + if (pDroppedRef->IsTemporary()) + pDroppedRef->Delete(); + else + pDroppedRef->Disable(); + } + } + + return pReturn; } void Actor::DropOrPickUpObject(const Inventory::Entry& arEntry, NiPoint3* apLocation, NiPoint3* apRotation) noexcept @@ -1143,7 +1541,26 @@ void Actor::DropOrPickUpObject(const Inventory::Entry& arEntry, NiPoint3* apLoca if (arEntry.Count < 0) DropObject(pObject, pExtraData, -arEntry.Count, apLocation, apRotation); - // TODO: pick up + else if (arEntry.Count > 0) + { + NiPoint3 searchLocation = apLocation ? *apLocation : position; + TESObjectREFR* pDroppedRef = FindDroppedReferenceNear(pObject, searchLocation); + + if (!pDroppedRef && apLocation) + pDroppedRef = FindDroppedReferenceNear(pObject, position); + + if (!pDroppedRef) + { + spdlog::warn("Object to pick up not found near target location, {:X}:{:X}. Falling back to inventory add.", arEntry.BaseId.ModId, arEntry.BaseId.BaseId); + + ScopedInventoryOverride _; + AddOrRemoveItem(arEntry, true); + return; + } + + spdlog::debug("Picking up object, form id: {:X}, count: {}, actor: {:X}", pObject->formID, arEntry.Count, formID); + PickUpObject(pDroppedRef, arEntry.Count, false, 0.0f); + } } void Actor::DropObject(TESBoundObject* apObject, ExtraDataList* apExtraData, int32_t aCount, NiPoint3* apLocation, NiPoint3* apRotation) noexcept @@ -1167,6 +1584,23 @@ void TP_MAKE_THISCALL(HookUpdateDetectionState, ActorKnowledge, void* apState) auto pTargetActor = Cast(pTarget); if (pOwnerActor && pTargetActor) { + const bool ownerGhosted = IsRemoteGhostActor(pOwnerActor); + const bool targetGhosted = IsRemoteGhostActor(pTargetActor); + + // Skip detection processing when ghosted remote players are involved; they should be invisible to AI. + if (ownerGhosted || targetGhosted) + { + apThis->hTarget = 0; // Clear detection target so AI loses interest. + + if (targetGhosted && !ownerGhosted && pOwnerActor && !pOwnerActor->GetExtension()->IsRemotePlayer()) + { + // Ensure local AI drops ghost targets so they don't chase or face them. + if (IsRemoteGhostActor(pOwnerActor->GetCombatTarget())) + pOwnerActor->SetCombatTargetEx(nullptr); + } + return; + } + if (pOwnerActor->GetExtension()->IsRemotePlayer() && pTargetActor->GetExtension()->IsLocalPlayer()) { spdlog::debug("Cancelling detection from remote player to local player, owner: {:X}, target: {:X}", pOwner->formID, pTarget->formID); diff --git a/Code/client/Games/Skyrim/Actor.h b/Code/client/Games/Skyrim/Actor.h index 9625c75e1..92e236862 100644 --- a/Code/client/Games/Skyrim/Actor.h +++ b/Code/client/Games/Skyrim/Actor.h @@ -3,6 +3,8 @@ #include #include +#include + #include #include #include @@ -22,6 +24,7 @@ struct ActorExtension; struct AIProcess; struct CombatController; struct TESIdleForm; +struct SpellItem; struct Actor : TESObjectREFR { @@ -229,6 +232,7 @@ struct Actor : TESObjectREFR void SetNoBleedoutRecovery(bool aSet) noexcept; void SetPlayerRespawnMode(bool aSet = true) noexcept; void SetPlayerTeammate(bool aSet) noexcept; + bool HasLineOfSight(TESObjectREFR* apTarget) noexcept; // Actions void UnEquipAll() noexcept; @@ -236,13 +240,14 @@ struct Actor : TESObjectREFR void QueueUpdate() noexcept; bool InitiateMountPackage(Actor* apMount) noexcept; void GenerateMagicCasters() noexcept; + void DispelAllSpells(bool aNow = false) noexcept; void Reset() noexcept; void Kill() noexcept; void Respawn() noexcept; void PickUpObject(TESObjectREFR* apObject, int32_t aCount, bool aUnk1, float aUnk2) noexcept; - void DropObject(TESBoundObject* apObject, ExtraDataList* apExtraData, int32_t aCount, NiPoint3* apLocation, NiPoint3* apRotation) noexcept; void DropOrPickUpObject(const Inventory::Entry& arEntry, NiPoint3* apPoint, NiPoint3* apRotate) noexcept; + void DropObject(TESBoundObject* apObject, ExtraDataList* apExtraData, int32_t aCount, NiPoint3* apLocation, NiPoint3* apRotation) noexcept; void SpeakSound(const char* pFile); void StartCombatEx(Actor* apTarget) noexcept; void SetCombatTargetEx(Actor* apTarget) noexcept; @@ -366,7 +371,6 @@ struct Actor : TESObjectREFR }; static_assert(offsetof(Actor, currentProcess) == 0xF8); -static_assert(offsetof(Actor, flags1) == 0xE8); static_assert(offsetof(Actor, actorValueOwner) == 0xB8); static_assert(offsetof(Actor, actorState) == 0xC0); static_assert(offsetof(Actor, flags2) == 0x204); diff --git a/Code/client/Games/Skyrim/Combat/CombatController.cpp b/Code/client/Games/Skyrim/Combat/CombatController.cpp index 0907a0907..98af9c310 100644 --- a/Code/client/Games/Skyrim/Combat/CombatController.cpp +++ b/Code/client/Games/Skyrim/Combat/CombatController.cpp @@ -1,6 +1,37 @@ #include "CombatController.h" #include "CombatTargetSelector.h" +#include +#include +#include +#include +#include + +namespace +{ +bool IsRemoteGhostActor(Actor* apActor) +{ + if (!apActor || !entt::locator::has_value()) + return false; + + auto* pExt = apActor->GetExtension(); + if (!pExt || !pExt->IsRemotePlayer()) + return false; + + auto& world = World::Get(); + + if (world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost) + return true; + + auto view = world.view(); + const auto it = std::find_if(view.begin(), view.end(), + [view, formId = apActor->formID](entt::entity e) + { return view.get(e).Id == formId && view.get(e).IsGhost; }); + + return it != view.end(); +} +} // namespace + void ArrayQuickSortRecursiveCombatTargets(GameArray* apArray, uint32_t aiLowIndex, uint32_t aiHighIndex) { @@ -64,7 +95,31 @@ static TUpdateTarget* RealUpdateTarget = nullptr; void TP_MAKE_THISCALL(HookUpdateTarget, CombatController) { - apThis->UpdateTarget(); + // Drop ghosted remote targets before running target selection. + if (Actor* pCurrentTarget = Cast(TESObjectREFR::GetByHandle(apThis->targetHandle))) + { + if (IsRemoteGhostActor(pCurrentTarget)) + { + apThis->SetTarget(nullptr); + + if (Actor* pAttacker = Cast(TESObjectREFR::GetByHandle(apThis->attackerHandle))) + pAttacker->SetCombatTargetEx(nullptr); + } + } + + TiltedPhoques::ThisCall(RealUpdateTarget, apThis); + + // Ensure any newly chosen target isn't a ghosted remote player. + if (Actor* pNewTarget = Cast(TESObjectREFR::GetByHandle(apThis->targetHandle))) + { + if (IsRemoteGhostActor(pNewTarget)) + { + apThis->SetTarget(nullptr); + + if (Actor* pAttacker = Cast(TESObjectREFR::GetByHandle(apThis->attackerHandle))) + pAttacker->SetCombatTargetEx(nullptr); + } + } } void CombatController::SetTarget(Actor* apTarget) @@ -77,12 +132,9 @@ void CombatController::SetTarget(Actor* apTarget) static TiltedPhoques::Initializer s_combatControllerHooks( []() { -#if 0 POINTER_SKYRIMSE(TUpdateTarget, s_updateTarget, 33236); RealUpdateTarget = s_updateTarget.Get(); TP_HOOK(&RealUpdateTarget, HookUpdateTarget); -#endif }); - diff --git a/Code/client/Games/Skyrim/EquipManager.cpp b/Code/client/Games/Skyrim/EquipManager.cpp index f1b53f16f..d753bac1b 100644 --- a/Code/client/Games/Skyrim/EquipManager.cpp +++ b/Code/client/Games/Skyrim/EquipManager.cpp @@ -157,7 +157,7 @@ void* TP_MAKE_THISCALL(EquipHook, EquipManager, Actor* apActor, TESForm* apItem, // Consumables are "equipped" as well. We don't want this to sync, for several reasons. // The right hand item on the server would be overridden by the consumable. // Furthermore, the equip action on the other clients would doubly subtract the consumables. - if (pExtension->IsLocal() && !apItem->IsConsumable() && !apData->bQueueEquip) + if (pExtension->IsLocal() && !apItem->IsConsumable() && !ScopedEquipOverride::IsOverriden()) { EquipmentChangeEvent evt{}; evt.ActorId = apActor->formID; @@ -189,7 +189,7 @@ void* TP_MAKE_THISCALL(UnEquipHook, EquipManager, Actor* apActor, TESForm* apIte return nullptr; } - if (pExtension->IsLocal() && !ScopedUnequipOverride::IsOverriden() && !apData->bQueueEquip) + if (pExtension->IsLocal() && !ScopedUnequipOverride::IsOverriden() && !ScopedEquipOverride::IsOverriden()) { EquipmentChangeEvent evt{}; evt.ActorId = apActor->formID; @@ -216,7 +216,7 @@ void* TP_MAKE_THISCALL(EquipSpellHook, EquipManager, Actor* apActor, TESForm* ap if (pExtension->IsRemote() && !ScopedEquipOverride::IsOverriden()) return nullptr; - if (pExtension->IsLocal() && !apData->bQueueEquip) + if (pExtension->IsLocal() && !ScopedEquipOverride::IsOverriden()) { EquipmentChangeEvent evt{}; evt.ActorId = apActor->formID; @@ -241,7 +241,7 @@ void* TP_MAKE_THISCALL(UnEquipSpellHook, EquipManager, Actor* apActor, TESForm* if (pExtension->IsRemote() && !ScopedEquipOverride::IsOverriden()) return nullptr; - if (pExtension->IsLocal() && !ScopedUnequipOverride::IsOverriden() && !apData->bQueueEquip) + if (pExtension->IsLocal() && !ScopedUnequipOverride::IsOverriden() && !ScopedEquipOverride::IsOverriden()) { EquipmentChangeEvent evt{}; evt.ActorId = apActor->formID; @@ -266,7 +266,7 @@ void* TP_MAKE_THISCALL(EquipShoutHook, EquipManager, Actor* apActor, TESForm* ap return nullptr; // TODO: queue check? - if (pExtension->IsLocal()) + if (pExtension->IsLocal() && !ScopedEquipOverride::IsOverriden()) { EquipmentChangeEvent evt{}; evt.ActorId = apActor->formID; @@ -291,7 +291,7 @@ void* TP_MAKE_THISCALL(UnEquipShoutHook, EquipManager, Actor* apActor, TESForm* return nullptr; // TODO: queue check? - if (pExtension->IsLocal() && !ScopedUnequipOverride::IsOverriden()) + if (pExtension->IsLocal() && !ScopedUnequipOverride::IsOverriden() && !ScopedEquipOverride::IsOverriden()) { EquipmentChangeEvent evt{}; evt.ActorId = apActor->formID; diff --git a/Code/client/Games/Skyrim/Events/EventDispatcher.h b/Code/client/Games/Skyrim/Events/EventDispatcher.h index e7a9a8d96..d6ccfa022 100644 --- a/Code/client/Games/Skyrim/Events/EventDispatcher.h +++ b/Code/client/Games/Skyrim/Events/EventDispatcher.h @@ -18,7 +18,11 @@ template struct EventDispatcher void UnRegisterSink(BSTEventSink* apSink) noexcept { details::InternalUnRegisterSink(reinterpret_cast(this), reinterpret_cast(apSink)); } - void PushEvent(const T* apEvent) noexcept { details::InternalPushEvent(reinterpret_cast(this), reinterpret_cast(apEvent)); } + void PushEvent(const T* apEvent) noexcept + { + // Engine dispatch expects a non-const event pointer; we only read from it on the engine side. + details::InternalPushEvent(reinterpret_cast(this), const_cast(reinterpret_cast(apEvent))); + } uint8_t pad0[0x58]; }; @@ -100,6 +104,9 @@ struct TESFurnitureEvent struct TESGrabReleaseEvent { + TESObjectREFR* reference; + bool grabbed; + uint8_t pad9[7]{}; }; struct TESHitEvent diff --git a/Code/client/Games/Skyrim/ExtraData/ExtraData.h b/Code/client/Games/Skyrim/ExtraData/ExtraData.h index 6365d1b98..51aeb3a56 100644 --- a/Code/client/Games/Skyrim/ExtraData/ExtraData.h +++ b/Code/client/Games/Skyrim/ExtraData/ExtraData.h @@ -12,10 +12,12 @@ enum class ExtraDataType : uint32_t Worn = 0x16, WornLeft = 0x17, ReferenceHandle = 0x1C, + Ghost = 0x1F, Count = 0x24, Health = 0x25, Charge = 0x28, Teleport = 0x2B, + MapMarker = 0x2C, LeveledCreature = 0x2D, CannotWear = 0x3D, Poison = 0x3E, diff --git a/Code/client/Games/Skyrim/ExtraData/ExtraDataList.cpp b/Code/client/Games/Skyrim/ExtraData/ExtraDataList.cpp index 1dbbd2dbd..21f5b9821 100644 --- a/Code/client/Games/Skyrim/ExtraData/ExtraDataList.cpp +++ b/Code/client/Games/Skyrim/ExtraData/ExtraDataList.cpp @@ -2,6 +2,8 @@ #include +BSExtraData::~BSExtraData() = default; + ExtraDataList* ExtraDataList::New() noexcept { ExtraDataList* pExtraDataList = Memory::Allocate(); @@ -66,6 +68,27 @@ bool ExtraDataList::Add(ExtraDataType aType, BSExtraData* apNewData) return true; } +bool ExtraDataList::Remove(ExtraDataType aType, BSExtraData* apData) +{ + BSScopedLock _(lock); + + BSExtraData** ppEntry = &data; + while (*ppEntry) + { + if ((*ppEntry)->GetType() == aType && (!apData || *ppEntry == apData)) + { + BSExtraData* pToRemove = *ppEntry; + *ppEntry = pToRemove->next; + SetType(aType, true); + return true; + } + + ppEntry = &((*ppEntry)->next); + } + + return false; +} + uint32_t ExtraDataList::GetCount() const { uint32_t count = 0; @@ -98,6 +121,13 @@ void ExtraDataList::SetSoulData(SOUL_LEVEL aSoulLevel) noexcept TiltedPhoques::ThisCall(setSoulData, this, aSoulLevel); } +void ExtraDataList::SetCount(uint16_t aCount) noexcept +{ + TP_THIS_FUNCTION(TSetCount, void, ExtraDataList, uint16_t aCount); + POINTER_SKYRIMSE(TSetCount, setCount, 11617); + TiltedPhoques::ThisCall(setCount, this, aCount); +} + void ExtraDataList::SetChargeData(float aCharge) noexcept { TP_THIS_FUNCTION(TSetChargeData, void, ExtraDataList, float aCharge); diff --git a/Code/client/Games/Skyrim/ExtraData/ExtraDataList.h b/Code/client/Games/Skyrim/ExtraData/ExtraDataList.h index 2150ad8ea..6b0bfb76f 100644 --- a/Code/client/Games/Skyrim/ExtraData/ExtraDataList.h +++ b/Code/client/Games/Skyrim/ExtraData/ExtraDataList.h @@ -23,6 +23,7 @@ struct ExtraDataList BSExtraData* GetByType(ExtraDataType type) const; void SetSoulData(SOUL_LEVEL aSoulLevel) noexcept; + void SetCount(uint16_t aCount) noexcept; void SetChargeData(float aCharge) noexcept; void SetWorn(bool aWornLeft) noexcept; void SetPoison(AlchemyItem* apItem, uint32_t aCount) noexcept; diff --git a/Code/client/Games/Skyrim/ExtraData/ExtraGhost.h b/Code/client/Games/Skyrim/ExtraData/ExtraGhost.h new file mode 100644 index 000000000..9ae772f50 --- /dev/null +++ b/Code/client/Games/Skyrim/ExtraData/ExtraGhost.h @@ -0,0 +1,21 @@ +#pragma once + +#include "ExtraData.h" + +struct ExtraGhost : BSExtraData +{ + inline static constexpr auto eExtraData = ExtraDataType::Ghost; + + ~ExtraGhost() override = default; + + ExtraDataType GetType() const noexcept override { return eExtraData; } + + // Matches CommonLibSSE ExtraGhost layout: bool at 0x10, total size 0x18 + bool ghost{true}; + uint8_t pad11{0}; + uint16_t pad12{0}; + uint32_t pad14{0}; +}; + +static_assert(sizeof(ExtraGhost) == 0x18); +static_assert(offsetof(ExtraGhost, ghost) == 0x10); diff --git a/Code/client/Games/Skyrim/ExtraData/ExtraMapMarker.h b/Code/client/Games/Skyrim/ExtraData/ExtraMapMarker.h new file mode 100644 index 000000000..a4541ae38 --- /dev/null +++ b/Code/client/Games/Skyrim/ExtraData/ExtraMapMarker.h @@ -0,0 +1,26 @@ +#pragma once + +#include "ExtraData.h" + +// Minimal map marker data used for fast travel discovery syncing. +struct MapMarkerData +{ + uint8_t pad0[0x10]; + uint8_t flags; + uint8_t pad11; + uint16_t type; + uint32_t pad14; +}; + +static_assert(sizeof(MapMarkerData) == 0x18); + +struct ExtraMapMarker : BSExtraData +{ + inline static constexpr auto eExtraData = ExtraDataType::MapMarker; + + MapMarkerData* mapData; +}; + +static_assert(sizeof(ExtraMapMarker) == 0x18); +static_assert(offsetof(ExtraMapMarker, mapData) == 0x10); + diff --git a/Code/client/Games/Skyrim/Forms/TESForm.h b/Code/client/Games/Skyrim/Forms/TESForm.h index 856367b4b..75322b1a4 100644 --- a/Code/client/Games/Skyrim/Forms/TESForm.h +++ b/Code/client/Games/Skyrim/Forms/TESForm.h @@ -5,20 +5,36 @@ enum class FormType : uint8_t { - Armor = 26, - Book = 27, - Container = 28, - Door = 29, - Ingredient = 30, - Weapon = 41, - Ammo = 42, - Npc = 43, - LeveledCharacter = 44, - Alchemy = 46, - LeveledItem = 53, - Character = 62, - QuestItem = 77, - Count = 0x87 + Scroll = 0x17, + Activator = 0x18, + TalkingActivator = 0x19, + Armor = 0x1A, + Book = 0x1B, + Container = 0x1C, + Door = 0x1D, + Ingredient = 0x1E, + Light = 0x1F, + Misc = 0x20, + Apparatus = 0x21, + Static = 0x22, + StaticCollection = 0x23, + MovableStatic = 0x24, + Weapon = 0x29, + Ammo = 0x2A, + Npc = 0x2B, + LeveledNPC = 0x2C, + LeveledCharacter = LeveledNPC, + KeyMaster = 0x2D, + AlchemyItem = 0x2E, + Alchemy = AlchemyItem, + Note = 0x30, + SoulGem = 0x34, + LeveledItem = 0x35, + ActorCharacter = 0x3E, + Character = ActorCharacter, + Quest = 0x4D, + QuestItem = Quest, + Count = 0x8A }; struct BGSSaveFormBuffer; diff --git a/Code/client/Games/Skyrim/Interface/UI.cpp b/Code/client/Games/Skyrim/Interface/UI.cpp index 2d76cc1e0..4d45eb8b0 100644 --- a/Code/client/Games/Skyrim/Interface/UI.cpp +++ b/Code/client/Games/Skyrim/Interface/UI.cpp @@ -88,12 +88,12 @@ static void* UI_AddToActiveQueue_Hook(UI* apSelf, IMenu* apMenu, void* apFoundIt if (!apMenu || !World::Get().GetTransport().IsConnected() || stubs::g_IsSoulsREActive) return UI_AddToActiveQueue(apSelf, apMenu, apFoundItem); -#if 0 - if (auto* pName = apSelf->LookupMenuNameByInstance(apEntry)) - { - spdlog::info("Menu requested {}", pName->AsAscii()); - } -#endif + static const BSFixedString s_consoleMenu("Console"); + if (auto* pName = apSelf->LookupMenuNameByInstance(apMenu)) + { + if (*pName == s_consoleMenu) + return apFoundItem; + } // NOTE(Force): could also compare by RTTI later on... for (const char* item : kAllowList) diff --git a/Code/client/Games/Skyrim/Misc/BSSaveDataSystemUtility.cpp b/Code/client/Games/Skyrim/Misc/BSSaveDataSystemUtility.cpp new file mode 100644 index 000000000..071dfa863 --- /dev/null +++ b/Code/client/Games/Skyrim/Misc/BSSaveDataSystemUtility.cpp @@ -0,0 +1,13 @@ +#include + +#include + +#include + +BSWin32SaveDataSystemUtility* BSWin32SaveDataSystemUtility::GetSingleton() noexcept +{ + using TGetSingleton = BSWin32SaveDataSystemUtility* (*)(); + const VersionDbPtr getSingleton(109278); + auto* func = reinterpret_cast(getSingleton.GetPtr()); + return func ? func() : nullptr; +} diff --git a/Code/client/Games/Skyrim/Misc/BSSaveDataSystemUtility.h b/Code/client/Games/Skyrim/Misc/BSSaveDataSystemUtility.h new file mode 100644 index 000000000..e57f15ecf --- /dev/null +++ b/Code/client/Games/Skyrim/Misc/BSSaveDataSystemUtility.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +struct BSSaveDataSystemUtility +{ + virtual ~BSSaveDataSystemUtility() = default; + + virtual bool CreateSaveDirectory(const char* a_pathName, bool a_ignoreINI) = 0; + virtual errno_t PrepareFileSavePath(const char* a_fileName, char* a_dst, bool a_tmpSave, bool a_ignoreINI) = 0; + virtual void Unk_03(void) = 0; + virtual void Unk_04(void) = 0; + virtual void Unk_05(void) = 0; + virtual void Unk_06(void) = 0; + virtual void Unk_07(void) = 0; + virtual void Unk_08(void) = 0; + virtual void Unk_09(void) = 0; + virtual void Unk_0A(void) = 0; + virtual void Unk_0B(void) = 0; + virtual void Unk_0C(void) = 0; + virtual void Unk_0D(void) = 0; + virtual void Unk_0E(void) = 0; + virtual void Unk_0F(void) = 0; + virtual void Unk_10(void) = 0; + virtual void Unk_11(void) = 0; +}; + +struct BSWin32SaveDataSystemUtility : BSSaveDataSystemUtility +{ + static BSWin32SaveDataSystemUtility* GetSingleton() noexcept; +}; diff --git a/Code/client/Games/Skyrim/PlayerCharacter.cpp b/Code/client/Games/Skyrim/PlayerCharacter.cpp index 0da41ca11..84f2f881b 100644 --- a/Code/client/Games/Skyrim/PlayerCharacter.cpp +++ b/Code/client/Games/Skyrim/PlayerCharacter.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -23,6 +24,13 @@ #include #include +#include +#include +#include +#include +#include +#include +#include int32_t PlayerCharacter::LastUsedCombatSkill = -1; @@ -40,6 +48,156 @@ static TCalculateExperience* RealCalculateExperience = nullptr; static TSetWaypoint* RealSetWaypoint = nullptr; static TRemoveWaypoint* RealRemoveWaypoint = nullptr; +namespace +{ +constexpr float kDropSearchRadiusSquared = 200.f * 200.f; + +GameId ResolveReferenceId(TESObjectREFR* apReference) noexcept +{ + GameId reference{}; + if (!apReference) + return reference; + + World::Get().GetModSystem().GetServerModId(apReference->formID, reference); + return reference; +} + +std::pair ResolveReferenceCellMetadata(TESObjectREFR* apReference) noexcept +{ + GameId cell{}; + GameId world{}; + + if (!apReference) + return {cell, world}; + + auto* pCell = apReference->parentCell ? apReference->parentCell : apReference->GetParentCell(); + if (pCell) + { + auto& modSystem = World::Get().GetModSystem(); + modSystem.GetServerModId(pCell->formID, cell); + if (pCell->worldspace) + modSystem.GetServerModId(pCell->worldspace->formID, world); + } + + return {cell, world}; +} + +void PopulatePickupEventFromDrop(uint64_t aDropId, PickupDroppedItemEvent& aEvent) noexcept +{ + if (const auto dropOpt = DropManager::GetServerDrop(aDropId); dropOpt) + { + aEvent.HasItemData = true; + aEvent.Item = dropOpt->Item; + aEvent.HasLocation = true; + aEvent.Location = dropOpt->Location; + aEvent.HasRotation = true; + aEvent.Rotation = dropOpt->Rotation; + aEvent.CellId = dropOpt->CellId; + aEvent.WorldSpaceId = dropOpt->WorldSpaceId; + } +} + +void PopulatePickupEventFromReference(TESObjectREFR* apReference, const Inventory::Entry& acItem, PickupDroppedItemEvent& aEvent) noexcept +{ + if (!apReference) + return; + + aEvent.HasItemData = true; + aEvent.Item = acItem; + aEvent.HasLocation = true; + aEvent.Location = apReference->position; + aEvent.HasRotation = true; + aEvent.Rotation = apReference->rotation; + + const auto cellMeta = ResolveReferenceCellMetadata(apReference); + aEvent.CellId = cellMeta.first; + aEvent.WorldSpaceId = cellMeta.second; + aEvent.ReferenceId = ResolveReferenceId(apReference); +} + +constexpr uint32_t kMaxCurrentMapMarkers = 4096; + +constexpr std::array kCurrentMapMarkerOffsets = { + 0x4F8, + 0x500, +}; + +std::atomic s_currentMapMarkersOffset{0}; + +const GameArray>* GetCurrentMapMarkersAt(const PlayerCharacter* apPlayer, const std::ptrdiff_t aOffset) noexcept +{ + return reinterpret_cast>*>( + reinterpret_cast(apPlayer) + aOffset); +} + +bool IsPlausibleCurrentMapMarkersArray(const GameArray>& acMarkers) noexcept +{ + if (acMarkers.length > acMarkers.capacity) + return false; + + if (acMarkers.capacity > kMaxCurrentMapMarkers) + return false; + + if (acMarkers.length > kMaxCurrentMapMarkers) + return false; + + if (acMarkers.length == 0) + return true; + + if (!acMarkers.data) + return false; + + const auto dataPtr = reinterpret_cast(acMarkers.data); + if (dataPtr < 0x10000) + return false; + + if (dataPtr % alignof(BSPointerHandle) != 0) + return false; + + return true; +} + +const GameArray>* ResolveCurrentMapMarkersArray(const PlayerCharacter* apPlayer) noexcept +{ + if (!apPlayer) + return nullptr; + + if (const auto cached = s_currentMapMarkersOffset.load(std::memory_order_relaxed); cached != 0) + { + const auto* pMarkers = GetCurrentMapMarkersAt(apPlayer, cached); + if (IsPlausibleCurrentMapMarkersArray(*pMarkers)) + return pMarkers; + + s_currentMapMarkersOffset.store(0, std::memory_order_relaxed); + } + + for (const auto offset : kCurrentMapMarkerOffsets) + { + const auto* pMarkers = GetCurrentMapMarkersAt(apPlayer, offset); + if (!IsPlausibleCurrentMapMarkersArray(*pMarkers)) + continue; + + const bool shouldCache = pMarkers->length != 0 || pMarkers->capacity != 0 || pMarkers->data != nullptr; + if (shouldCache) + { + s_currentMapMarkersOffset.store(offset, std::memory_order_relaxed); + + static std::atomic_bool s_logged{false}; + if (!s_logged.exchange(true)) + spdlog::info("Using currentMapMarkers offset 0x{:X} (len={}, cap={})", offset, pMarkers->length, pMarkers->capacity); + } + + return pMarkers; + } + + static std::atomic_bool s_loggedFailure{false}; + if (!s_loggedFailure.exchange(true)) + spdlog::warn("Failed to resolve currentMapMarkers array (fast travel sync will be limited)"); + + return nullptr; +} +} + PlayerCharacter* PlayerCharacter::Get() noexcept { POINTER_SKYRIMSE(PlayerCharacter*, s_character, 401069); @@ -150,28 +308,85 @@ void PlayerCharacter::RemoveWaypoint() noexcept return TiltedPhoques::ThisCall(RealRemoveWaypoint, this); } +GameArray>* PlayerCharacter::GetCurrentMapMarkers() noexcept +{ + return const_cast>*>( + static_cast(this)->GetCurrentMapMarkers()); +} + +const GameArray>* PlayerCharacter::GetCurrentMapMarkers() const noexcept +{ + return ResolveCurrentMapMarkersArray(this); +} + char TP_MAKE_THISCALL(HookPickUpObject, PlayerCharacter, TESObjectREFR* apObject, int32_t aCount, bool aUnk1, bool aUnk2) { - auto& modSystem = World::Get().GetModSystem(); + const bool isRemotePickup = DropExecution::GetCurrentMode() == DropExecution::Mode::RemotePickup; + const bool isConnected = World::Get().GetTransport().IsConnected(); + std::optional dropId{}; - Inventory::Entry item{}; - modSystem.GetServerModId(apObject->baseForm->formID, item.BaseId); - item.Count = aCount; + if (apObject) + { + auto handle = apObject->GetHandle(); + if (handle && handle.handle.iBits) + dropId = DropManager::GetDropIdForHandle(handle.handle.iBits); - if (apObject->GetExtraDataList() && !ScopedExtraDataOverride::IsOverriden()) + if (!dropId) + { + GameId objectId{}; + World::Get().GetModSystem().GetServerModId(apObject->baseForm->formID, objectId); + if (objectId.ModId || objectId.BaseId) + dropId = DropManager::FindDropBySignature(objectId, apObject->position, kDropSearchRadiusSquared); + } + } + + Inventory::Entry fallbackItem{}; + const bool hasReferenceObject = apObject != nullptr; + if (!dropId && apObject) { - ScopedExtraDataOverride _; - apThis->GetItemFromExtraData(item, apObject->GetExtraDataList()); + auto& modSystem = World::Get().GetModSystem(); + modSystem.GetServerModId(apObject->baseForm->formID, fallbackItem.BaseId); + fallbackItem.Count = aCount; + + if (apObject->GetExtraDataList() && !ScopedExtraDataOverride::IsOverriden()) + { + ScopedExtraDataOverride _; + const int32_t engineCount = fallbackItem.Count; + apThis->GetItemFromExtraData(fallbackItem, apObject->GetExtraDataList()); + fallbackItem.Count = engineCount; + } } - // This is here so that objects that are picked up on both clients, aka non temps, are synced through activation sync. - // The inventory change event should always be sent to the server, otherwise the server inventory won't be updated. - bool shouldUpdateClients = apObject->IsTemporary() && !ScopedActivateOverride::IsOverriden(); + if (!isRemotePickup && isConnected) + { + std::optional pickupEvent{}; - World::Get().GetRunner().Trigger(InventoryChangeEvent(apThis->formID, std::move(item), false, shouldUpdateClients)); + if (dropId) + { + pickupEvent.emplace(apThis->formID, *dropId); + PopulatePickupEventFromDrop(*dropId, *pickupEvent); + } + else if (hasReferenceObject) + { + pickupEvent.emplace(apThis->formID, 0); + PopulatePickupEventFromReference(apObject, fallbackItem, *pickupEvent); + } + + if (pickupEvent && apObject) + pickupEvent->ReferenceFormId = apObject->formID; + + if (pickupEvent) + World::Get().GetRunner().Trigger(*pickupEvent); + } ScopedInventoryOverride _; + if (!isRemotePickup && isConnected) + { + DropExecution::Scope scope(DropExecution::Mode::LocalPickup, apThis->formID, dropId.value_or(0)); + return TiltedPhoques::ThisCall(RealPickUpObject, apThis, apObject, aCount, aUnk1, aUnk2); + } + return TiltedPhoques::ThisCall(RealPickUpObject, apThis, apObject, aCount, aUnk1, aUnk2); } diff --git a/Code/client/Games/Skyrim/PlayerCharacter.h b/Code/client/Games/Skyrim/PlayerCharacter.h index a5965998a..1cbb9f968 100644 --- a/Code/client/Games/Skyrim/PlayerCharacter.h +++ b/Code/client/Games/Skyrim/PlayerCharacter.h @@ -120,6 +120,8 @@ struct PlayerCharacter : Actor void SetWaypoint(NiPoint3* apPosition, TESWorldSpace* apWorldSpace) noexcept; void RemoveWaypoint() noexcept; + GameArray>* GetCurrentMapMarkers() noexcept; + const GameArray>* GetCurrentMapMarkers() const noexcept; struct Objective { diff --git a/Code/client/Games/Skyrim/SaveLoad.cpp b/Code/client/Games/Skyrim/SaveLoad.cpp index 59b5f97ae..7f24c3d74 100644 --- a/Code/client/Games/Skyrim/SaveLoad.cpp +++ b/Code/client/Games/Skyrim/SaveLoad.cpp @@ -1,6 +1,8 @@ #include #include +#include +#include void BGSSaveLoadManager::Save(SaveData* apData) { @@ -10,3 +12,37 @@ void BGSSaveLoadManager::Save(SaveData* apData) if (apData->saveName) cSaveName = apData->saveName; } + +TP_THIS_FUNCTION(TBGSSaveLoadManager_SaveImpl, bool, BGSSaveLoadManager, int32_t, uint32_t, const char*); +static TBGSSaveLoadManager_SaveImpl* RealSaveImpl = nullptr; + +bool TP_MAKE_THISCALL(HookSaveImpl, BGSSaveLoadManager, int32_t aDeviceId, uint32_t aOutputStats, const char* apFileName) +{ + spdlog::info("SaveLoad: Save_Impl called (file='{}')", apFileName ? apFileName : ""); + const bool result = TiltedPhoques::ThisCall(RealSaveImpl, apThis, aDeviceId, aOutputStats, apFileName); + + if (result) + { + auto& world = World::Get(); + if (world.ctx().contains()) + world.ctx().at().OnSaveGame(apFileName); + } + + return result; +} + +static TiltedPhoques::Initializer s_saveLoadManagerHooks( + []() + { + POINTER_SKYRIMSE(TBGSSaveLoadManager_SaveImpl, s_save, 35727); + RealSaveImpl = s_save.Get(); + if (RealSaveImpl) + { + spdlog::info("SaveLoad: Save_Impl hook installed"); + TP_HOOK(&RealSaveImpl, HookSaveImpl); + } + else + { + spdlog::warn("SaveLoad: Save_Impl hook missing"); + } + }); diff --git a/Code/client/Games/Skyrim/TESObjectREFR.cpp b/Code/client/Games/Skyrim/TESObjectREFR.cpp index 3ef4afa92..a208560dc 100644 --- a/Code/client/Games/Skyrim/TESObjectREFR.cpp +++ b/Code/client/Games/Skyrim/TESObjectREFR.cpp @@ -5,10 +5,14 @@ #include #include +#include +#include +#include #include #include #include #include +#include #include #include @@ -45,6 +49,7 @@ TP_THIS_FUNCTION(TPlayAnimationAndWait, bool, void, uint32_t auiStackID, TESObje TP_THIS_FUNCTION(TPlayAnimation, bool, void, uint32_t auiStackID, TESObjectREFR* apSelf, BSFixedString* apEventName); TP_THIS_FUNCTION(TRotate, void, TESObjectREFR, float aAngle); TP_THIS_FUNCTION(TLockChange, void, TESObjectREFR); +TP_THIS_FUNCTION(TApplyEffectShader, ShaderReferenceEffect*, TESObjectREFR, TESEffectShader* apEffectShader, float aDuration, TESObjectREFR* apFacingRef, bool aFaceTarget, bool aAttachToCamera, NiAVObject* apAttachNode, bool aInterfaceEffect); static TActivate* RealActivate = nullptr; static TAddInventoryItem* RealAddInventoryItem = nullptr; @@ -135,6 +140,26 @@ uint32_t* TESObjectREFR::GetNullHandle() noexcept return s_nullHandle.Get(); } +ShaderReferenceEffect* TESObjectREFR::ApplyEffectShader(TESEffectShader* apEffectShader, float aDuration, TESObjectREFR* apFacingRef, bool aFaceTarget, bool aAttachToCamera, NiAVObject* apAttachNode, bool aInterfaceEffect) +{ + POINTER_SKYRIMSE(TApplyEffectShader, s_applyEffectShader, 19446); + return TiltedPhoques::ThisCall(s_applyEffectShader, this, apEffectShader, aDuration, apFacingRef, aFaceTarget, aAttachToCamera, apAttachNode, aInterfaceEffect); +} + +void TESObjectREFR::SetPosition(const NiPoint3& acPosition) noexcept +{ + TP_THIS_FUNCTION(TSetPosition, void, TESObjectREFR, const NiPoint3&); + POINTER_SKYRIMSE(TSetPosition, s_setPosition, 19790); + TiltedPhoques::ThisCall(s_setPosition, this, acPosition); +} + +void TESObjectREFR::SetAngle(const NiPoint3& acAngle) noexcept +{ + TP_THIS_FUNCTION(TSetAngle, void, TESObjectREFR, const NiPoint3&); + POINTER_SKYRIMSE(TSetAngle, s_setAngle, 19786); + TiltedPhoques::ThisCall(s_setAngle, this, acAngle); +} + void TESObjectREFR::SetRotation(float aX, float aY, float aZ) noexcept { TiltedPhoques::ThisCall(RealRotateX, this, aX); @@ -142,6 +167,11 @@ void TESObjectREFR::SetRotation(float aX, float aY, float aZ) noexcept TiltedPhoques::ThisCall(RealRotateZ, this, aZ); } +void TESObjectREFR::SetRotation(const NiPoint3& acRotation) noexcept +{ + SetAngle(acRotation); +} + using TiltedPhoques::Serialization; void TESObjectREFR::SaveAnimationVariables(AnimationVariables& aVariables) const noexcept @@ -772,13 +802,48 @@ void TESObjectREFR::SetInventory(const Inventory& aInventory) noexcept ScopedInventoryOverride _; - RemoveAllItems(); + Inventory currentInventory = GetInventory(); + Inventory desiredInventory = aInventory; + + // Remove or adjust existing entries + for (auto& currentEntry : currentInventory.Entries) + { + auto matchIt = std::find_if( + desiredInventory.Entries.begin(), desiredInventory.Entries.end(), + [¤tEntry](const Inventory::Entry& entry) { return entry.BaseId == currentEntry.BaseId && entry.IsExtraDataEquals(currentEntry); }); + + const int32_t desiredCount = matchIt != desiredInventory.Entries.end() ? matchIt->Count : 0; + const int32_t diff = desiredCount - currentEntry.Count; + + if (diff < 0) + { + Inventory::Entry removal = currentEntry; + removal.Count = diff; // negative count removes items + AddOrRemoveItem(removal, true); + } + + if (matchIt != desiredInventory.Entries.end()) + { + if (diff > 0) + { + Inventory::Entry addition = *matchIt; + addition.Count = diff; + AddOrRemoveItem(addition, true); + } + + desiredInventory.Entries.erase(matchIt); + } + } - for (const Inventory::Entry& entry : aInventory.Entries) + // Add any remaining desired entries that were not present before + for (auto& entry : desiredInventory.Entries) { if (entry.Count != 0) AddOrRemoveItem(entry, true); } + + if (auto* pActor = Cast(this)) + pActor->SetMagicEquipment(aInventory.CurrentMagicEquipment); } Vector TESObjectREFR::RemoveNonQuestItems(Inventory& aCurrentInventory) noexcept @@ -829,6 +894,9 @@ void TESObjectREFR::SetInventoryRetainingQuestItems(Inventory& aCurrentInventory AddOrRemoveItem(entry, true); } } + + if (auto* pActor = Cast(this)) + pActor->SetMagicEquipment(acSourceInventory.CurrentMagicEquipment); } void TESObjectREFR::AddOrRemoveItem(const Inventory::Entry& arEntry, bool aIsSettingInventory) noexcept @@ -980,11 +1048,38 @@ bool TP_MAKE_THISCALL(HookPlayAnimation, void, uint32_t auiStackID, TESObjectREF bool TP_MAKE_THISCALL(HookActivate, TESObjectREFR, TESObjectREFR* apActivator, uint8_t aUnk1, TESBoundObject* apObjectToGet, int32_t aCount, char aDefaultProcessing) { + // Ghosted remote player actors must be non-interactive. + if (auto* pTargetActor = Cast(apThis)) + { + const auto* pTargetEx = pTargetActor->GetExtension(); + const bool isRemotePlayer = pTargetEx && pTargetEx->IsRemotePlayer(); + + const bool locallyGated = entt::locator::has_value() && World::Get().GetSyncModeService().GetLocalMode() == SyncMode::Ghost; + const bool isGhostFlagged = pTargetActor->baseForm && pTargetActor->baseForm->formType == TESNPC::Type && + (Cast(pTargetActor->baseForm)->actorData.flags & (1u << 29)); + + if (isRemotePlayer && (locallyGated || isGhostFlagged)) + return false; + } + Actor* pActivator = Cast(apActivator); + if (pActivator && apThis->baseForm->formType == FormType::Door && entt::locator::has_value()) + { + const auto* pActivatorEx = pActivator->GetExtension(); + if (pActivatorEx && pActivatorEx->IsLocalPlayer()) + { + auto& partyService = World::Get().GetPartyService(); + if (partyService.IsCellLockActiveForLocal()) + { + partyService.NotifyCellLockBlocked(); + return false; + } + } + } // 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) + if (pActivator && apThis->baseForm->formType != FormType::Book && entt::locator::has_value()) { auto openState = TESObjectREFR::kNone; if (apThis->baseForm->formType == FormType::Door) @@ -1000,7 +1095,10 @@ bool TP_MAKE_THISCALL(HookActivate, TESObjectREFR, TESObjectREFR* apActivator, u void TP_MAKE_THISCALL(HookAddInventoryItem, TESObjectREFR, TESBoundObject* apItem, ExtraDataList* apExtraData, int32_t aCount, TESObjectREFR* apOldOwner) { - if (!ScopedInventoryOverride::IsOverriden()) + const auto dropMode = DropExecution::GetCurrentMode(); + const bool isPickupContext = dropMode == DropExecution::Mode::RemotePickup || dropMode == DropExecution::Mode::LocalPickup; + + if (!ScopedInventoryOverride::IsOverriden() && !isPickupContext) { auto& modSystem = World::Get().GetModSystem(); @@ -1022,7 +1120,11 @@ void TP_MAKE_THISCALL(HookAddInventoryItem, TESObjectREFR, TESBoundObject* apIte BSPointerHandle* TP_MAKE_THISCALL(HookRemoveInventoryItem, TESObjectREFR, BSPointerHandle* apResult, TESBoundObject* apItem, int32_t aCount, ITEM_REMOVE_REASON aReason, ExtraDataList* apExtraList, TESObjectREFR* apMoveToRef, const NiPoint3* apDropLoc, const NiPoint3* apRotate) { - if (!ScopedInventoryOverride::IsOverriden()) + const auto dropMode = DropExecution::GetCurrentMode(); + const bool isSyncSuppressedContext = + dropMode == DropExecution::Mode::LocalDrop || dropMode == DropExecution::Mode::RemoteDrop || dropMode == DropExecution::Mode::RemotePickup || dropMode == DropExecution::Mode::LocalPickup; + + if (!ScopedInventoryOverride::IsOverriden() && !isSyncSuppressedContext) { auto& modSystem = World::Get().GetModSystem(); @@ -1052,8 +1154,11 @@ void TP_MAKE_THISCALL(HookRotateX, TESObjectREFR, float aAngle) if (apThis->formType == Actor::Type) { const auto pActor = static_cast(apThis); + bool bAllowRemoteUpdate = ScopedReferencesOverride::IsOverriden(); + if (!bAllowRemoteUpdate && pActor->GetExtension()->IsRemote()) + bAllowRemoteUpdate = pActor->IsDead(); // We don't allow remotes to move - if (pActor->GetExtension()->IsRemote()) + if (pActor->GetExtension()->IsRemote() && !bAllowRemoteUpdate) return; } @@ -1065,8 +1170,11 @@ void TP_MAKE_THISCALL(HookRotateY, TESObjectREFR, float aAngle) if (apThis->formType == Actor::Type) { const auto pActor = static_cast(apThis); + bool bAllowRemoteUpdate = ScopedReferencesOverride::IsOverriden(); + if (!bAllowRemoteUpdate && pActor->GetExtension()->IsRemote()) + bAllowRemoteUpdate = pActor->IsDead(); // We don't allow remotes to move - if (pActor->GetExtension()->IsRemote()) + if (pActor->GetExtension()->IsRemote() && !bAllowRemoteUpdate) return; } @@ -1078,8 +1186,11 @@ void TP_MAKE_THISCALL(HookRotateZ, TESObjectREFR, float aAngle) if (apThis->formType == Actor::Type) { const auto pActor = static_cast(apThis); + bool bAllowRemoteUpdate = ScopedReferencesOverride::IsOverriden(); + if (!bAllowRemoteUpdate && pActor->GetExtension()->IsRemote()) + bAllowRemoteUpdate = pActor->IsDead(); // We don't allow remotes to move - if (pActor->GetExtension()->IsRemote()) + if (pActor->GetExtension()->IsRemote() && !bAllowRemoteUpdate) return; } diff --git a/Code/client/Games/Skyrim/TESObjectREFR.h b/Code/client/Games/Skyrim/TESObjectREFR.h index 504350ae0..f8db32ac7 100644 --- a/Code/client/Games/Skyrim/TESObjectREFR.h +++ b/Code/client/Games/Skyrim/TESObjectREFR.h @@ -17,6 +17,9 @@ struct AnimationVariables; struct TESWorldSpace; struct TESBoundObject; struct TESContainer; +struct TESEffectShader; +struct ShaderReferenceEffect; +struct NiAVObject; enum class ITEM_REMOVE_REASON { @@ -161,7 +164,10 @@ struct TESObjectREFR : TESForm virtual void sub_9A(); virtual void sub_9B(); + void SetPosition(const NiPoint3& acPosition) noexcept; + void SetAngle(const NiPoint3& acAngle) noexcept; void SetRotation(float aX, float aY, float aZ) noexcept; + void SetRotation(const NiPoint3& acRotation) noexcept; BSPointerHandle GetHandle() const noexcept; uint32_t GetCellId() const noexcept; @@ -186,6 +192,7 @@ struct TESObjectREFR : TESForm void PayGold(int32_t aAmount) noexcept; void PayGoldToContainer(TESObjectREFR* pContainer, int32_t aAmount) noexcept; bool SendAnimationEvent(BSFixedString* apEventName) noexcept; + ShaderReferenceEffect* ApplyEffectShader(TESEffectShader* apEffectShader, float aDuration = -1.0f, TESObjectREFR* apFacingRef = nullptr, bool aFaceTarget = false, bool aAttachToCamera = false, NiAVObject* apAttachNode = nullptr, bool aInterfaceEffect = false); bool Activate(TESObjectREFR* apActivator, uint8_t aUnk1, TESBoundObject* apObjectToGet, int32_t aCount, char aDefaultProcessing) noexcept; diff --git a/Code/client/Games/Skyrim/WorldMapProjector.cpp b/Code/client/Games/Skyrim/WorldMapProjector.cpp new file mode 100644 index 000000000..a8c8dac10 --- /dev/null +++ b/Code/client/Games/Skyrim/WorldMapProjector.cpp @@ -0,0 +1,174 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace { +struct MapLink { + uint32_t parentId{0}; + float xOff{0.f}; + float yOff{0.f}; + float zOff{0.f}; +}; + +static TiltedPhoques::Map g_links; // worldId -> mapping to its parent +static std::atomic g_linksReady{false}; +static std::atomic g_linksLoading{false}; + +static void StartAsyncLoad() noexcept +{ + std::thread([]() noexcept { + TiltedPhoques::Map links; + bool success = false; + + try + { + ESLoader::ESLoader loader; + auto records = loader.BuildRecordCollection(); + if (records && records->HasAnyRecords()) + { + for (const auto& [fid, wrld] : records->GetWorlds()) + { + MapLink link{}; + if (wrld.m_parentId) + link.parentId = *wrld.m_parentId; + if (wrld.m_onam) + { + link.xOff = wrld.m_onam->m_cellOffsetX4096; + link.yOff = wrld.m_onam->m_cellOffsetY4096; + link.zOff = wrld.m_onam->m_cellOffsetZ4096; + } + if (link.parentId != 0) + links[fid] = link; + } + success = true; + } + } + catch (const std::exception& e) + { + spdlog::warn("WorldMapProjector: failed to build world links ({})", e.what()); + } + catch (...) + { + spdlog::warn("WorldMapProjector: failed to build world links (unknown error)"); + } + + if (success) + { + g_links = std::move(links); + g_linksReady.store(true, std::memory_order_release); + } + + g_linksLoading.store(false, std::memory_order_release); + }).detach(); +} + +static void EnsureLoaded() noexcept +{ + if (g_linksReady.load(std::memory_order_acquire)) + return; + + bool expected = false; + if (g_linksLoading.compare_exchange_strong(expected, true)) + StartAsyncLoad(); +} + +// Walk up from child world to target parent, accumulating offsets. +static bool ToAncestor(uint32_t fromId, uint32_t targetAncestor, glm::vec3 srcPos, glm::vec3& outPos) noexcept { + EnsureLoaded(); + if (!g_linksReady.load(std::memory_order_acquire)) + return false; + if (fromId == 0 || targetAncestor == 0) + return false; + if (fromId == targetAncestor) + { + outPos = srcPos; + return true; + } + + glm::vec3 p = srcPos; + uint32_t cur = fromId; + // Guard against cycles / very deep chains + for (int i = 0; i < 16; ++i) + { + auto it = g_links.find(cur); + if (it == g_links.end()) + return false; + const auto& link = it->second; + // Apply offset to go into parent coordinates + p.x += link.xOff; + p.y += link.yOff; + p.z += link.zOff; + if (link.parentId == targetAncestor) + { + outPos = p; + return true; + } + cur = link.parentId; + } + return false; +} + +static uint32_t RootAncestorId(uint32_t wsId) noexcept +{ + EnsureLoaded(); + if (!g_linksReady.load(std::memory_order_acquire)) + return wsId; + uint32_t cur = wsId; + for (int i = 0; i < 16; ++i) + { + auto it = g_links.find(cur); + if (it == g_links.end() || it->second.parentId == 0) + return cur; + cur = it->second.parentId; + } + return cur; +} +} // namespace + +void WorldMapProjector::WarmupAsync() noexcept +{ + EnsureLoaded(); +} + +bool WorldMapProjector::Convert(TESWorldSpace* apFromWs, + const glm::vec3& aFromPos, + TESWorldSpace* apToWs, + glm::vec3& aOutToPos) noexcept +{ + if (!apFromWs || !apToWs) + return false; + + const uint32_t fromId = apFromWs->formID; + const uint32_t toId = apToWs->formID; + + if (fromId == toId) + { + aOutToPos = aFromPos; + return true; + } + + // Only support mapping up the parent chain (child -> parent -> ...) + if (ToAncestor(fromId, toId, aFromPos, aOutToPos)) + return true; + + return false; +} + +TESWorldSpace* WorldMapProjector::GetDisplayWorld(TESWorldSpace* apWs) noexcept +{ + if (!apWs) + return nullptr; + const uint32_t rootId = RootAncestorId(apWs->formID); + if (rootId == apWs->formID) + return apWs; + auto* pForm = TESForm::GetById(rootId); + return pForm ? static_cast(pForm) : apWs; +} diff --git a/Code/client/Games/Skyrim/WorldMapProjector.h b/Code/client/Games/Skyrim/WorldMapProjector.h new file mode 100644 index 000000000..63b5a2b1b --- /dev/null +++ b/Code/client/Games/Skyrim/WorldMapProjector.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +struct TESWorldSpace; + +// WorldMapProjector +// +// Provides conversion of a 3D point from one TESWorldSpace to another (typically to Tamriel) +// for use with the world map projection. Uses ESM WRLD ONAM parent links; no runtime addresses. +struct WorldMapProjector +{ + // Kick off asynchronous preload of WRLD ONAM data. + static void WarmupAsync() noexcept; + + // Convert a position from source worldspace to destination worldspace. + // Returns true on success and writes out position in destination worldspace coordinates. + static bool Convert(TESWorldSpace* apFromWs, + const glm::vec3& aFromPos, + TESWorldSpace* apToWs, + glm::vec3& aOutToPos) noexcept; + + // Returns the root ancestor worldspace to use for world map display (e.g., Tamriel). + // If no parent chain is known, returns the input worldspace. + static TESWorldSpace* GetDisplayWorld(TESWorldSpace* apWs) noexcept; + +private: + // Tries to use runtime addresses (currently disabled). + static bool TryExactViaAddressLib(TESWorldSpace* apFromWs, + const glm::vec3& aFromPos, + TESWorldSpace* apToWs, + glm::vec3& aOutToPos) noexcept; +}; diff --git a/Code/client/Games/TES.h b/Code/client/Games/TES.h index d423befbb..c6553bfe4 100644 --- a/Code/client/Games/TES.h +++ b/Code/client/Games/TES.h @@ -5,6 +5,7 @@ struct TESObjectCELL; struct TESWorldSpace; struct NiPoint3; struct TESForm; +struct TESObjectREFR; struct Actor; struct GridCellArray @@ -99,6 +100,8 @@ struct ModManager static ModManager* Get() noexcept; uint32_t Spawn(NiPoint3& aPosition, NiPoint3& aRotation, TESObjectCELL* apParentCell, TESWorldSpace* apWorldSpace, Actor* apCharacter) noexcept; + uint32_t SpawnReference(TESForm* apBaseForm, NiPoint3& aPosition, NiPoint3& aRotation, TESObjectCELL* apParentCell, TESWorldSpace* apWorldSpace, TESObjectREFR* apAlreadyCreatedRef = nullptr, + bool aForcePersist = false) noexcept; Mod* GetByName(const char* acpName) const noexcept; TESObjectCELL* GetCellFromCoordinates(int32_t aX, int32_t aY, TESWorldSpace* aWorldSpace, bool aSpawnCell) noexcept; diff --git a/Code/client/Services/BrandingService.h b/Code/client/Services/BrandingService.h new file mode 100644 index 000000000..afdffed97 --- /dev/null +++ b/Code/client/Services/BrandingService.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +struct World; + +// Draws a simple message on the Skyrim main menu indicating the mod is loaded. +struct BrandingService +{ + BrandingService(World& aWorld, entt::dispatcher& aDispatcher) noexcept; + ~BrandingService() = default; + + TP_NOCOPYMOVE(BrandingService); + +private: + void OnDraw() noexcept; + + World& m_world; + + // connections + entt::scoped_connection m_drawImGuiConnection; +}; + diff --git a/Code/client/Services/CharacterService.h b/Code/client/Services/CharacterService.h index 7ae13a0e6..174ef6ade 100644 --- a/Code/client/Services/CharacterService.h +++ b/Code/client/Services/CharacterService.h @@ -1,7 +1,11 @@ #pragma once #include "Structs/Inventory.h" #include "Structs/ActorData.h" +#include "Structs/SyncMode.h" +#include +#include +struct TESLoadGameEvent; struct ActorAddedEvent; struct ActorRemovedEvent; struct UpdateEvent; @@ -40,6 +44,7 @@ struct SubtitleEvent; struct NotifySubtitle; struct NotifyActorTeleport; struct NotifyRelinquishControl; +struct NotifyDrawWeapon; struct PartyJoinedEvent; struct Actor; @@ -49,7 +54,7 @@ struct TransportService; /** * @brief Handles actors and players. */ -struct CharacterService +struct CharacterService : BSTEventSink { CharacterService(World& aWorld, entt::dispatcher& aDispatcher, TransportService& aTransport) noexcept; ~CharacterService() noexcept = default; @@ -87,11 +92,16 @@ struct CharacterService void OnNotifySubtitle(const NotifySubtitle& acMessage) noexcept; void OnNotifyActorTeleport(const NotifyActorTeleport& acMessage) noexcept; void OnNotifyRelinquishControl(const NotifyRelinquishControl& acMessage) noexcept; + void OnNotifyDrawWeapon(const NotifyDrawWeapon& acMessage) noexcept; void OnPartyJoinedEvent(const PartyJoinedEvent& acEvent) noexcept; + void OnSyncModeChanged(SyncMode aPreviousMode, SyncMode aCurrentMode) noexcept; + void RefreshRemotePlayer(uint32_t aServerId) noexcept; + BSTEventResult OnEvent(const TESLoadGameEvent*, const EventDispatcher*) override; void ProcessNewEntity(entt::entity aEntity) const noexcept; private: + void CleanupRemoteActorsAndOwnership(bool aFromLoad = false) const noexcept; void MoveActor(const Actor* apActor, const GameId& acWorldSpaceId, const GameId& acCellId, const Vector3_NetQuantize& acPosition) const noexcept; void RequestServerAssignment(entt::entity aEntity) const noexcept; @@ -113,6 +123,7 @@ struct CharacterService TransportService& m_transport; float m_cachedExperience = 0.f; + bool m_pendingLoadCleanup{false}; // TODO: revamp this, read the local anim var like vampire lord? struct WeaponDrawData @@ -157,5 +168,6 @@ struct CharacterService entt::scoped_connection m_subtitleSyncConnection; entt::scoped_connection m_actorTeleportConnection; entt::scoped_connection m_relinquishConnection; + entt::scoped_connection m_notifyDrawWeaponConnection; entt::scoped_connection m_partyJoinedConnection; }; diff --git a/Code/client/Services/Debug/DebugService.cpp b/Code/client/Services/Debug/DebugService.cpp index 442eaed93..388413c7d 100644 --- a/Code/client/Services/Debug/DebugService.cpp +++ b/Code/client/Services/Debug/DebugService.cpp @@ -163,11 +163,6 @@ void DebugService::OnUpdate(const UpdateEvent& acUpdateEvent) noexcept static std::atomic s_f7Pressed = false; static std::atomic s_f6Pressed = false; - if (GetAsyncKeyState(VK_F3) & 0x01) - { - m_showDebugStuff = !m_showDebugStuff; - } - #if (!IS_MASTER) if (GetAsyncKeyState(VK_F6)) { diff --git a/Code/client/Services/Debug/Views/PartyView.cpp b/Code/client/Services/Debug/Views/PartyView.cpp index 70a861611..c1389033d 100644 --- a/Code/client/Services/Debug/Views/PartyView.cpp +++ b/Code/client/Services/Debug/Views/PartyView.cpp @@ -41,7 +41,7 @@ void DebugService::DrawPartyView() auto playerEntry = players.find(playerId); if (playerEntry != players.end()) { - auto playerName = playerEntry.value(); + auto playerName = playerEntry.value().Name; if (playerId == partyService.GetLeaderPlayerId()) { playerName += " (Leader)"; @@ -52,7 +52,7 @@ void DebugService::DrawPartyView() if (ImGui::Button("Teleport")) { TeleportCommandRequest request{}; - request.TargetPlayer = playerEntry.value(); + request.TargetPlayer = playerEntry.value().Name; m_transport.Send(request); } @@ -91,7 +91,7 @@ void DebugService::DrawPartyView() continue; playerCount++; - ImGui::BulletText(player.second.c_str()); + ImGui::BulletText(player.second.Name.c_str()); ImGui::SameLine(100); if (ImGui::Button("Invite")) { @@ -116,7 +116,7 @@ void DebugService::DrawPartyView() if (std::find(std::begin(members), std::end(members), player.first) != std::end(members)) continue; - ImGui::Text(player.second.c_str()); + ImGui::Text(player.second.Name.c_str()); ImGui::SameLine(100); if (ImGui::Button("Accept")) { diff --git a/Code/client/Services/Generic/ActorValueService.cpp b/Code/client/Services/Generic/ActorValueService.cpp index 3a6b3ac89..5afef6437 100644 --- a/Code/client/Services/Generic/ActorValueService.cpp +++ b/Code/client/Services/Generic/ActorValueService.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include @@ -310,6 +311,9 @@ void ActorValueService::OnHealthChangeBroadcast(const NotifyHealthChangeBroadcas void ActorValueService::OnActorValueChanges(const NotifyActorValueChanges& acMessage) const noexcept { + if (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost) + return; + auto view = m_world.view(); const auto itor = std::find_if(std::begin(view), std::end(view), [id = acMessage.Id, view](entt::entity entity) { return view.get(entity).Id == id; }); @@ -342,6 +346,9 @@ void ActorValueService::OnActorValueChanges(const NotifyActorValueChanges& acMes void ActorValueService::OnActorMaxValueChanges(const NotifyActorMaxValueChanges& acMessage) const noexcept { + if (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost) + return; + auto view = m_world.view(); const auto it = std::find_if(std::begin(view), std::end(view), [id = acMessage.Id, view](entt::entity entity) { return view.get(entity).Id == id; }); diff --git a/Code/client/Services/Generic/BrandingService.cpp b/Code/client/Services/Generic/BrandingService.cpp new file mode 100644 index 000000000..a6ffcc126 --- /dev/null +++ b/Code/client/Services/Generic/BrandingService.cpp @@ -0,0 +1,57 @@ +#include + +#include + +#include +#include + +#include +#include + +BrandingService::BrandingService(World& aWorld, entt::dispatcher& aDispatcher) noexcept + : m_world(aWorld) +{ + // Hook ImGui draw + auto& imgui = m_world.ctx().at(); + m_drawImGuiConnection = imgui.OnDraw.connect<&BrandingService::OnDraw>(this); +} + +void BrandingService::OnDraw() noexcept +{ + UI* pUI = UI::Get(); + if (!pUI) + return; + + // Main menu names vary; check a few common ones + const bool onMainMenu = pUI->GetMenuOpen(BSFixedString("Main Menu")) + || pUI->GetMenuOpen(BSFixedString("MainMenu")) + || pUI->GetMenuOpen(BSFixedString("Title Menu")); + + if (!onMainMenu) + return; + + auto& imguiSvc = m_world.ctx().at(); + ImFont* pFont = imguiSvc.GetSkyrimFont(); + if (pFont) + ImGui::PushFont(pFont); + + // Draw message at top-left with slight shadow for readability + ImDrawList* draw = ImGui::GetForegroundDrawList(); + const float line = ImGui::GetFontSize(); + const ImVec2 pos = ImVec2(20.f, 20.f); + + const char* line1 = "Skyrim Together"; + const char* line2 = "FazeUnion Edition"; + + // Line 1 shadow + text + draw->AddText(ImVec2(pos.x + 1.f, pos.y + 1.f), IM_COL32(0, 0, 0, 190), line1); + draw->AddText(pos, IM_COL32(200, 32, 32, 255), line1); + + // Line 2 below + ImVec2 pos2 = ImVec2(pos.x, pos.y + line + 2.f); + draw->AddText(ImVec2(pos2.x + 1.f, pos2.y + 1.f), IM_COL32(0, 0, 0, 190), line2); + draw->AddText(pos2, IM_COL32(200, 32, 32, 255), line2); + + if (pFont) + ImGui::PopFont(); +} diff --git a/Code/client/Services/Generic/CharacterService.cpp b/Code/client/Services/Generic/CharacterService.cpp index 7a1e83040..501d9f33a 100644 --- a/Code/client/Services/Generic/CharacterService.cpp +++ b/Code/client/Services/Generic/CharacterService.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -15,6 +16,7 @@ #include #include +#include #include #include @@ -38,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -63,6 +66,7 @@ #include #include #include +#include #include #include @@ -111,7 +115,11 @@ CharacterService::CharacterService(World& aWorld, entt::dispatcher& aDispatcher, m_relinquishConnection = m_dispatcher.sink().connect<&CharacterService::OnNotifyRelinquishControl>(this); + m_notifyDrawWeaponConnection = m_dispatcher.sink().connect<&CharacterService::OnNotifyDrawWeapon>(this); + m_partyJoinedConnection = aDispatcher.sink().connect<&CharacterService::OnPartyJoinedEvent>(this); + + EventDispatcherManager::Get()->loadGameEvent.RegisterSink(this); } void CharacterService::DeleteRemoteEntityComponents(entt::entity aEntity) const noexcept @@ -234,6 +242,12 @@ void CharacterService::OnActorRemoved(const ActorRemovedEvent& acEvent) noexcept void CharacterService::OnUpdate(const UpdateEvent& acUpdateEvent) noexcept { + if (m_pendingLoadCleanup && !m_transport.IsOnline()) + { + CleanupRemoteActorsAndOwnership(/*aFromLoad*/ true); + m_pendingLoadCleanup = false; + } + RunSpawnUpdates(); RunLocalUpdates(); RunFactionsUpdates(); @@ -254,11 +268,19 @@ void CharacterService::OnConnected(const ConnectedEvent& acConnectedEvent) const // Delete all temporary actors on connect if (formIdComponent.Id > 0xFF000000) { - Actor* pActor = Cast(TESForm::GetById(formIdComponent.Id)); - if (pActor) - pActor->Delete(); - - continue; + // If we are in Quest Isolation (Ghost Mode), do not delete temporary actors. + // These are likely local quest NPCs that we need to keep. + if (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost) + { + // Fallthrough to ProcessNewEntity, which will also likely ignore them due to Ghost Mode rules. + } + else + { + Actor* pActor = Cast(TESForm::GetById(formIdComponent.Id)); + if (pActor) + pActor->Delete(); + continue; + } } ProcessNewEntity(entity); @@ -266,23 +288,93 @@ void CharacterService::OnConnected(const ConnectedEvent& acConnectedEvent) const } void CharacterService::OnDisconnected(const DisconnectedEvent& acDisconnectedEvent) const noexcept +{ + const_cast(this)->m_pendingLoadCleanup = false; + CleanupRemoteActorsAndOwnership(/*aFromLoad*/ false); +} + +void CharacterService::CleanupRemoteActorsAndOwnership(const bool aFromLoad) const noexcept { auto remoteView = m_world.view(); - for (auto entity : remoteView) + TiltedPhoques::Vector remoteEntities(remoteView.begin(), remoteView.end()); + for (auto entity : remoteEntities) { auto& formIdComponent = remoteView.get(entity); - auto pActor = Cast(TESForm::GetById(formIdComponent.Id)); - if (!pActor) + if (aFromLoad) + { + if (m_world.valid(entity)) + { + if (m_world.all_of(entity)) + m_world.remove(entity); + if (m_world.all_of(entity)) + m_world.remove(entity); + } continue; + } - if (pActor->GetExtension()->IsRemotePlayer()) - pActor->Delete(); - else - pActor->GetExtension()->SetRemote(false); + auto pActor = Cast(TESForm::GetById(formIdComponent.Id)); + if (pActor) + { + if (pActor->GetExtension()->IsRemotePlayer()) + pActor->Delete(); + else + pActor->GetExtension()->SetRemote(false); + } + + if (m_world.valid(entity) && m_world.all_of(entity)) + m_world.remove(entity); + } + + auto localView = m_world.view(); + TiltedPhoques::Vector localEntities(localView.begin(), localView.end()); + for (auto entity : localEntities) + { + if (m_world.valid(entity)) + m_world.remove(entity); } - m_world.clear(); + m_world.clear(); +} + +void CharacterService::OnSyncModeChanged(const SyncMode aPreviousMode, const SyncMode aCurrentMode) noexcept +{ + if (aPreviousMode == aCurrentMode) + return; + + if (aCurrentMode == SyncMode::Ghost) + { + // Drop any local ownership/assignment requests (except the player) so others can safely control actors while we are isolated. + auto view = m_world.view(); + TiltedPhoques::Vector entities(view.begin(), view.end()); + for (auto entity : entities) + { + const auto& formIdComponent = view.get(entity); + if (formIdComponent.Id == 0x14) + continue; + + if (m_world.all_of(entity) || m_world.all_of(entity)) + CancelServerAssignment(entity, formIdComponent.Id); + } + } + + if (aPreviousMode == SyncMode::Ghost && aCurrentMode == SyncMode::Normal) + { + // After leaving isolation, re-evaluate assignments so ownership and replication snap back to the right owner (e.g., party leader). + auto view = m_world.view(entt::exclude); + TiltedPhoques::Vector entities(view.begin(), view.end()); + for (auto entity : entities) + ProcessNewEntity(entity); + } +} + +BSTEventResult CharacterService::OnEvent(const TESLoadGameEvent*, const EventDispatcher*) +{ + // Clear any ghost visuals from the previous world before we tear down components. + m_world.GetSyncModeService().OnLoadGameReset(); + // Defer cleanup to the next update tick to avoid doing heavy deletes inside the load event. + m_pendingLoadCleanup = true; + return BSTEventResult::kOk; } void CharacterService::OnAssignCharacter(const AssignCharacterResponse& acMessage) noexcept @@ -361,12 +453,20 @@ void CharacterService::OnAssignCharacter(const AssignCharacterResponse& acMessag #endif pActor->SetActorValues(acMessage.AllActorValues); - pActor->SetActorInventory(acMessage.CurrentInventory); - if (pActor->IsDead() != acMessage.IsDead) - acMessage.IsDead ? pActor->Kill() : pActor->Respawn(); + if (pActor->GetNiNode()) + { + pActor->SetActorInventory(acMessage.CurrentInventory); - m_weaponDrawUpdates[pActor->formID] = {acMessage.IsWeaponDrawn}; + if (pActor->IsDead() != acMessage.IsDead) + acMessage.IsDead ? pActor->Kill() : pActor->Respawn(); + + m_weaponDrawUpdates[pActor->formID] = {acMessage.IsWeaponDrawn}; + } + else + { + m_world.emplace_or_replace(cEntity, acMessage.CurrentInventory, acMessage.IsDead, acMessage.IsWeaponDrawn); + } MoveActor(pActor, acMessage.WorldSpaceId, acMessage.CellId, acMessage.Position); } @@ -374,6 +474,9 @@ void CharacterService::OnAssignCharacter(const AssignCharacterResponse& acMessag void CharacterService::OnCharacterSpawn(const CharacterSpawnRequest& acMessage) const noexcept { + if (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost && !acMessage.IsPlayer) + return; + auto remoteView = m_world.view(); const auto remoteItor = std::find_if(std::begin(remoteView), std::end(remoteView), [remoteView, Id = acMessage.ServerId](auto entity) { return remoteView.get(entity).Id == Id; }); @@ -507,6 +610,17 @@ void CharacterService::OnCharacterSpawn(const CharacterSpawnRequest& acMessage) void CharacterService::OnRemoteSpawnDataReceived(const NotifySpawnData& acMessage) noexcept { + const bool ghosting = (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost); + if (ghosting) + { + // While ghosting, only apply spawn data for remote players (to keep their visuals/equipment updated). + auto remotePlayerView = m_world.view(); + const auto it = std::find_if(remotePlayerView.begin(), remotePlayerView.end(), + [remotePlayerView, id = acMessage.Id](entt::entity e) { return remotePlayerView.get(e).Id == id; }); + if (it == remotePlayerView.end()) + return; + } + auto view = m_world.view(entt::exclude); const auto itor = std::find_if( @@ -538,12 +652,40 @@ void CharacterService::OnRemoteSpawnDataReceived(const NotifySpawnData& acMessag if (!pActor) return; - pActor->SetActorValues(acMessage.NewActorData.InitialActorValues); - pActor->SetActorInventory(acMessage.NewActorData.InitialInventory); - m_weaponDrawUpdates[pActor->formID] = {acMessage.NewActorData.IsWeaponDrawn}; + if (ghosting) + { + // Quest isolation: equipment-only update so ghosts aren't naked, without syncing other state. + Inventory equipmentOnly = acMessage.NewActorData.InitialInventory; + equipmentOnly.RemoveByFilter([](const Inventory::Entry& aEntry) { return !aEntry.IsWorn(); }); - if (pActor->IsDead() != acMessage.NewActorData.IsDead) - acMessage.NewActorData.IsDead ? pActor->Kill() : pActor->Respawn(); + if (pActor->GetNiNode()) + { + pActor->SetActorInventory(equipmentOnly); + m_weaponDrawUpdates[pActor->formID] = {acMessage.NewActorData.IsWeaponDrawn}; + } + else + { + const bool currentDead = pActor->IsDead(); + m_world.emplace_or_replace(*itor, std::move(equipmentOnly), currentDead, acMessage.NewActorData.IsWeaponDrawn); + } + } + else + { + pActor->SetActorValues(acMessage.NewActorData.InitialActorValues); + + if (pActor->GetNiNode()) + { + pActor->SetActorInventory(acMessage.NewActorData.InitialInventory); + m_weaponDrawUpdates[pActor->formID] = {acMessage.NewActorData.IsWeaponDrawn}; + + if (pActor->IsDead() != acMessage.NewActorData.IsDead) + acMessage.NewActorData.IsDead ? pActor->Kill() : pActor->Respawn(); + } + else + { + m_world.emplace_or_replace(*itor, acMessage.NewActorData.InitialInventory, acMessage.NewActorData.IsDead, acMessage.NewActorData.IsWeaponDrawn); + } + } spdlog::info("Applied remote spawn data, actor form id: {:X}", pActor->formID); } @@ -559,6 +701,9 @@ void CharacterService::OnReferencesMoveRequest(const ServerReferencesMoveRequest if (itor == std::end(view)) continue; + if (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost && !m_world.any_of(*itor)) + continue; + auto& interpolationComponent = view.get(*itor); auto& animationComponent = view.get(*itor); const auto& movement = update.UpdatedMovement; @@ -669,6 +814,42 @@ void CharacterService::OnRemoveCharacter(const NotifyRemoveCharacter& acMessage) } } +void CharacterService::RefreshRemotePlayer(const uint32_t aServerId) noexcept +{ + if (!m_transport.IsOnline()) + return; + + auto view = m_world.view(); + const auto itor = std::find_if(std::begin(view), std::end(view), [id = aServerId, view](entt::entity entity) { return view.get(entity).Id == id; }); + if (itor == std::end(view)) + return; + + const auto entity = *itor; + if (!m_world.any_of(entity)) + return; + + if (auto* pFormIdComponent = m_world.try_get(entity)) + { + CancelServerAssignment(entity, pFormIdComponent->Id); + + if (Actor* pActor = Cast(TESForm::GetById(pFormIdComponent->Id)); pActor && pActor->GetExtension()->IsRemotePlayer()) + pActor->Delete(); + } + + DeleteRemoteEntityComponents(entity); + + if (m_world.all_of(entity)) + m_world.remove(entity); + + if (m_world.orphan(entity)) + m_world.destroy(entity); + + RequestRespawn request; + request.ActorId = aServerId; + + m_transport.Send(request); +} + void CharacterService::OnNotifyRespawn(const NotifyRespawn& acMessage) const noexcept { auto view = m_world.view(); @@ -787,6 +968,9 @@ void CharacterService::OnMountEvent(const MountEvent& acEvent) const noexcept void CharacterService::OnNotifyMount(const NotifyMount& acMessage) const noexcept { + if (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost) + return; + auto remoteView = m_world.view(); const auto riderIt = std::find_if(std::begin(remoteView), std::end(remoteView), [remoteView, Id = acMessage.RiderId](auto entity) { return remoteView.get(entity).Id == Id; }); @@ -873,6 +1057,9 @@ void CharacterService::OnInitPackageEvent(const InitPackageEvent& acEvent) const void CharacterService::OnNotifyNewPackage(const NotifyNewPackage& acMessage) const noexcept { + if (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost) + return; + auto remoteView = m_world.view(); const auto remoteIt = std::find_if(std::begin(remoteView), std::end(remoteView), [remoteView, Id = acMessage.ActorId](auto entity) { return remoteView.get(entity).Id == Id; }); @@ -917,7 +1104,7 @@ void CharacterService::OnNotifySyncExperience(const NotifySyncExperience& acMess void CharacterService::OnDialogueEvent(const DialogueEvent& acEvent) noexcept { - if (!m_transport.IsConnected()) + if (!m_transport.IsConnected() || m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost) return; auto view = m_world.view(entt::exclude); @@ -942,6 +1129,9 @@ void CharacterService::OnDialogueEvent(const DialogueEvent& acEvent) noexcept void CharacterService::OnNotifyDialogue(const NotifyDialogue& acMessage) noexcept { + if (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost) + return; + auto remoteView = m_world.view(); const auto remoteIt = std::find_if(std::begin(remoteView), std::end(remoteView), [remoteView, Id = acMessage.ServerId](auto entity) { return remoteView.get(entity).Id == Id; }); @@ -958,13 +1148,16 @@ void CharacterService::OnNotifyDialogue(const NotifyDialogue& acMessage) noexcep if (!pActor) return; + if (m_world.all_of(*remoteIt)) + return; + pActor->StopCurrentDialogue(true); pActor->SpeakSound(acMessage.SoundFilename.c_str()); } void CharacterService::OnSubtitleEvent(const SubtitleEvent& acEvent) noexcept { - if (!m_transport.IsConnected()) + if (!m_transport.IsConnected() || m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost) return; auto view = m_world.view(entt::exclude); @@ -990,6 +1183,9 @@ void CharacterService::OnSubtitleEvent(const SubtitleEvent& acEvent) noexcept void CharacterService::OnNotifySubtitle(const NotifySubtitle& acMessage) noexcept { + if (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost) + return; + auto remoteView = m_world.view(); const auto remoteIt = std::find_if(std::begin(remoteView), std::end(remoteView), [remoteView, Id = acMessage.ServerId](auto entity) { return remoteView.get(entity).Id == Id; }); @@ -1006,6 +1202,9 @@ void CharacterService::OnNotifySubtitle(const NotifySubtitle& acMessage) noexcep if (!pActor) return; + if (m_world.all_of(*remoteIt)) + return; + // This is only for fallout 4 TESTopicInfo* pInfo = nullptr; pInfo = Cast(TESForm::GetById(acMessage.TopicFormId)); @@ -1064,6 +1263,9 @@ void CharacterService::OnNotifyRelinquishControl(const NotifyRelinquishControl& void CharacterService::OnNotifyActorTeleport(const NotifyActorTeleport& acMessage) noexcept { + if (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost) + return; + auto& modSystem = m_world.GetModSystem(); const uint32_t cActorId = World::Get().GetModSystem().GetGameId(acMessage.FormId); @@ -1079,6 +1281,19 @@ void CharacterService::OnNotifyActorTeleport(const NotifyActorTeleport& acMessag spdlog::info("Successfully teleported actor, form id: {:X}, world space: {:X}, cell: {:X}, position: ({}, {}, {})", pActor->formID, acMessage.WorldSpaceId.BaseId, acMessage.CellId.BaseId, acMessage.Position.x, acMessage.Position.y, acMessage.Position.z); } +void CharacterService::OnNotifyDrawWeapon(const NotifyDrawWeapon& acMessage) noexcept +{ + Actor* pActor = Utils::GetByServerId(acMessage.Id); + if (!pActor) + return; + + auto* pExt = pActor->GetExtension(); + if (pExt && pExt->IsLocalPlayer()) + return; + + m_weaponDrawUpdates[pActor->formID] = {acMessage.IsWeaponDrawn}; +} + void CharacterService::OnPartyJoinedEvent(const PartyJoinedEvent& acEvent) noexcept { // Takes ownership of all actors @@ -1127,6 +1342,7 @@ void CharacterService::ProcessNewEntity(entt::entity aEntity) const noexcept return; auto& formIdComponent = m_world.get(aEntity); + const bool ghosting = (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost); Actor* const pActor = Cast(TESForm::GetById(formIdComponent.Id)); if (!pActor) @@ -1137,6 +1353,9 @@ void CharacterService::ProcessNewEntity(entt::entity aEntity) const noexcept if (auto* pRemoteComponent = m_world.try_get(aEntity); pRemoteComponent) { + if (ghosting) + return; + // TODO(cosideci): don't just take all actors (i.e. from other parties), // maybe check it server side, add a variable to the request. if (m_world.GetPartyService().IsLeader() && !pActor->IsTemporary() && !pActor->IsMount()) @@ -1154,6 +1373,10 @@ void CharacterService::ProcessNewEntity(entt::entity aEntity) const noexcept if (m_world.any_of(aEntity)) return; + // During ghost isolation, only the local player is allowed to enter server ownership/assignment flows. + if (ghosting && formIdComponent.Id != 0x14) + return; + CacheSystem::Setup(World::Get(), aEntity, pActor); RequestServerAssignment(aEntity); @@ -1167,6 +1390,8 @@ void CharacterService::RequestServerAssignment(const entt::entity aEntity) const static uint32_t sCookieSeed = 0; const auto& formIdComponent = m_world.get(aEntity); + if (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost && formIdComponent.Id != 0x14) + return; auto* pActor = Cast(TESForm::GetById(formIdComponent.Id)); if (!pActor) @@ -1204,6 +1429,7 @@ void CharacterService::RequestServerAssignment(const entt::entity aEntity) const // Serialize the base form const auto isPlayer = (formIdComponent.Id == 0x14); + const bool ghosting = (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost); const auto isTemporary = pActor->formID >= 0xFF000000; if (isPlayer) @@ -1241,23 +1467,26 @@ void CharacterService::RequestServerAssignment(const entt::entity aEntity) const if (isPlayer) { auto& questLog = message.QuestContent.Entries; - auto& modSystem = m_world.GetModSystem(); - for (const auto& objective : PlayerCharacter::Get()->objectives) + if (!ghosting) { - auto* pQuest = objective.instance->quest; - if (!pQuest) - continue; - - if (!QuestService::IsNonSyncableQuest(pQuest)) + auto& modSystem = m_world.GetModSystem(); + for (const auto& objective : PlayerCharacter::Get()->objectives) { - GameId id{}; + auto* pQuest = objective.instance->quest; + if (!pQuest) + continue; - if (modSystem.GetServerModId(pQuest->formID, id)) + if (!QuestService::IsNonSyncableQuest(pQuest)) { - auto& entry = questLog.emplace_back(); - entry.Stage = pQuest->currentStage; - entry.Id = id; + GameId id{}; + + if (modSystem.GetServerModId(pQuest->formID, id)) + { + auto& entry = questLog.emplace_back(); + entry.Stage = pQuest->currentStage; + entry.Id = id; + } } } } @@ -1267,9 +1496,21 @@ void CharacterService::RequestServerAssignment(const entt::entity aEntity) const questLog.resize(std::distance(questLog.begin(), ip)); } - message.CurrentActorData = BuildActorData(pActor); + if (ghosting) + { + message.CurrentActorData.InitialActorValues = pActor->GetEssentialActorValues(); + // Quest isolation: still send equipment only so other clients can render this player as a properly equipped ghost. + // Keep the full inventory isolated. + message.CurrentActorData.InitialInventory = isPlayer ? pActor->GetEquipment() : Inventory{}; + message.CurrentActorData.IsDead = pActor->IsDead(); + message.CurrentActorData.IsWeaponDrawn = pActor->actorState.IsWeaponFullyDrawn(); + } + else + { + message.CurrentActorData = BuildActorData(pActor); + } - message.FactionsContent = pActor->GetFactions(); + message.FactionsContent = ghosting ? Factions{} : pActor->GetFactions(); message.IsDragon = pActor->IsDragon(); message.IsMount = pActor->IsMount(); message.IsPlayerSummon = pActor->GetCommandingActor() && pActor->GetCommandingActor()->formID == 0x14; @@ -1304,6 +1545,8 @@ void CharacterService::RequestServerAssignment(const entt::entity aEntity) const void CharacterService::CancelServerAssignment(const entt::entity aEntity, const uint32_t aFormId) const noexcept { + const bool connected = m_transport.IsOnline(); + if (m_world.all_of(aEntity)) { Actor* pActor = Cast(TESForm::GetById(aFormId)); @@ -1331,10 +1574,13 @@ void CharacterService::CancelServerAssignment(const entt::entity aEntity, const { auto& waitingComponent = m_world.get(aEntity); - CancelAssignmentRequest message; - message.Cookie = waitingComponent.Cookie; + if (connected) + { + CancelAssignmentRequest message; + message.Cookie = waitingComponent.Cookie; - m_transport.Send(message); + m_transport.Send(message); + } m_world.remove(aEntity); } @@ -1343,37 +1589,40 @@ void CharacterService::CancelServerAssignment(const entt::entity aEntity, const { auto& localComponent = m_world.get(aEntity); - RequestOwnershipTransfer request{}; - request.ServerId = localComponent.Id; - - if (Actor* pActor = Cast(TESForm::GetById(aFormId))) + if (connected) { - if (!pActor->IsTemporary()) - { - auto& modSystem = m_world.GetModSystem(); + RequestOwnershipTransfer request{}; + request.ServerId = localComponent.Id; - if (TESWorldSpace* pWorldSpace = pActor->GetWorldSpace()) + if (Actor* pActor = Cast(TESForm::GetById(aFormId))) + { + if (!pActor->IsTemporary()) { - if (!modSystem.GetServerModId(pWorldSpace->formID, request.WorldSpaceId)) - spdlog::error("World space id not found, despite having a world space, {:X}", pWorldSpace->formID); - } + auto& modSystem = m_world.GetModSystem(); - if (TESObjectCELL* pCell = pActor->GetParentCell()) - { - if (!modSystem.GetServerModId(pCell->formID, request.CellId)) - spdlog::error("Cell id not found, despite having a cell, {:X}", pCell->formID); - } + if (TESWorldSpace* pWorldSpace = pActor->GetWorldSpace()) + { + if (!modSystem.GetServerModId(pWorldSpace->formID, request.WorldSpaceId)) + spdlog::error("World space id not found, despite having a world space, {:X}", pWorldSpace->formID); + } + + if (TESObjectCELL* pCell = pActor->GetParentCell()) + { + if (!modSystem.GetServerModId(pCell->formID, request.CellId)) + spdlog::error("Cell id not found, despite having a cell, {:X}", pCell->formID); + } - request.Position = pActor->position; + request.Position = pActor->position; + } } - } - spdlog::info( - "Transferring ownership of local actor, server id: {:X}, worldspace: {:X}, cell: {:X}, position: " - "({}, {}, {})", - request.ServerId, request.WorldSpaceId.BaseId, request.CellId.BaseId, request.Position.x, request.Position.y, request.Position.z); + spdlog::info( + "Transferring ownership of local actor, server id: {:X}, worldspace: {:X}, cell: {:X}, position: " + "({}, {}, {})", + request.ServerId, request.WorldSpaceId.BaseId, request.CellId.BaseId, request.Position.x, request.Position.y, request.Position.z); - m_transport.Send(request); + m_transport.Send(request); + } m_world.remove(aEntity); } @@ -1477,16 +1726,21 @@ void CharacterService::RunLocalUpdates() const noexcept auto animatedLocalView = m_world.view(); + const bool ghosting = (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost); for (auto entity : animatedLocalView) { auto& localComponent = animatedLocalView.get(entity); auto& animationComponent = animatedLocalView.get(entity); auto& formIdComponent = animatedLocalView.get(entity); + if (ghosting && formIdComponent.Id != 0x14) + continue; + AnimationSystem::Serialize(m_world, message, localComponent, animationComponent, formIdComponent); } - m_transport.Send(message); + if (!message.Updates.empty()) + m_transport.Send(message); } void CharacterService::RunRemoteUpdates() noexcept @@ -1551,13 +1805,29 @@ void CharacterService::RunRemoteUpdates() noexcept auto& waitingFor3D = waitingView.get(entity); Actor* pActor = Cast(TESForm::GetById(formIdComponent.Id)); - if (!pActor || !pActor->GetNiNode()) + if (!pActor) + continue; + + ExtraContainerChanges::Data* pContainerChanges = pActor->GetContainerChanges(); + if (!pActor->GetNiNode() || !pContainerChanges || !pContainerChanges->entries) continue; // By now, the actor has materialized in the world and is ready for further setup - pActor->SetActorInventory(waitingFor3D.SpawnRequest.InventoryContent); - pActor->SetFactions(waitingFor3D.SpawnRequest.FactionsContent); + const bool ghosting = (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost); + const bool remotePlayer = pActor->GetExtension() && pActor->GetExtension()->IsRemotePlayer(); + + if (ghosting && remotePlayer) + { + Inventory equipmentOnly = waitingFor3D.SpawnRequest.InventoryContent; + equipmentOnly.RemoveByFilter([](const Inventory::Entry& aEntry) { return !aEntry.IsWorn(); }); + pActor->SetActorInventory(equipmentOnly); + } + else + { + pActor->SetActorInventory(waitingFor3D.SpawnRequest.InventoryContent); + pActor->SetFactions(waitingFor3D.SpawnRequest.FactionsContent); + } if (!waitingFor3D.SpawnRequest.ActionsToReplay.Actions.empty()) { @@ -1566,8 +1836,11 @@ void CharacterService::RunRemoteUpdates() noexcept m_weaponDrawUpdates[pActor->formID] = {waitingFor3D.SpawnRequest.IsWeaponDrawn}; - if (pActor->IsDead() != waitingFor3D.SpawnRequest.IsDead) - waitingFor3D.SpawnRequest.IsDead ? pActor->Kill() : pActor->Respawn(); + if (!(ghosting && remotePlayer)) + { + if (pActor->IsDead() != waitingFor3D.SpawnRequest.IsDead) + waitingFor3D.SpawnRequest.IsDead ? pActor->Kill() : pActor->Respawn(); + } if (pActor->IsVampireLord()) pActor->FixVampireLordModel(); @@ -1579,6 +1852,49 @@ void CharacterService::RunRemoteUpdates() noexcept for (auto entity : toRemove) m_world.remove(entity); + + auto pendingInventoryView = m_world.view(); + Vector pendingToRemove; + + for (auto entity : pendingInventoryView) + { + auto& formIdComponent = pendingInventoryView.get(entity); + auto& pendingInventory = pendingInventoryView.get(entity); + + Actor* pActor = Cast(TESForm::GetById(formIdComponent.Id)); + if (!pActor) + continue; + + ExtraContainerChanges::Data* pContainerChanges = pActor->GetContainerChanges(); + if (!pActor->GetNiNode() || !pContainerChanges || !pContainerChanges->entries) + continue; + + const bool ghosting = (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost); + const bool remotePlayer = pActor->GetExtension() && pActor->GetExtension()->IsRemotePlayer(); + + if (ghosting && remotePlayer) + { + Inventory equipmentOnly = pendingInventory.InventoryContent; + equipmentOnly.RemoveByFilter([](const Inventory::Entry& aEntry) { return !aEntry.IsWorn(); }); + pActor->SetActorInventory(equipmentOnly); + } + else + { + pActor->SetActorInventory(pendingInventory.InventoryContent); + + if (pActor->IsDead() != pendingInventory.IsDead) + pendingInventory.IsDead ? pActor->Kill() : pActor->Respawn(); + } + + m_weaponDrawUpdates[pActor->formID] = {pendingInventory.IsWeaponDrawn}; + + pendingToRemove.push_back(entity); + + spdlog::info("Applied pending inventory for actor, form id: {:X}", pActor->formID); + } + + for (auto entity : pendingToRemove) + m_world.remove(entity); } void CharacterService::RunFactionsUpdates() const noexcept diff --git a/Code/client/Services/Generic/CoSaveService.cpp b/Code/client/Services/Generic/CoSaveService.cpp new file mode 100644 index 000000000..2e6c6618b --- /dev/null +++ b/Code/client/Services/Generic/CoSaveService.cpp @@ -0,0 +1,111 @@ +#include + +#include +#include +#include + +#include + +namespace +{ +std::filesystem::path ResolveCoSavePath(const std::filesystem::path& aSavePath) noexcept +{ + if (aSavePath.empty()) + return {}; + + std::filesystem::path coSavePath = aSavePath; + coSavePath.replace_extension("tilted"); + return coSavePath; +} +} // namespace + +CoSaveService::CoSaveService(World& aWorld, entt::dispatcher&, TransportService& aTransport) noexcept + : m_world(aWorld) + , m_transport(aTransport) +{ + if (auto* pDispatcher = EventDispatcherManager::Get()) + pDispatcher->loadGameEvent.RegisterSink(this); +} + +CoSaveService::~CoSaveService() +{ + if (auto* pDispatcher = EventDispatcherManager::Get()) + pDispatcher->loadGameEvent.UnRegisterSink(this); +} + +void CoSaveService::PrepareForUser(const std::string& aUsername) noexcept +{ + const std::string username = aUsername.empty() ? "default" : aUsername; + if (username != m_cachedUsername) + { + m_cachedUsername = username; + m_dropStorage.SetActiveUser(username); + } +} + +void CoSaveService::UpdateLocalPlayerLocation(const CoSaveStorage::LocalPlayerLocation& aLocation) noexcept +{ + m_dropStorage.SetLocalPlayerLocation(aLocation); +} + +std::optional CoSaveService::GetLocalPlayerLocation() const noexcept +{ + return m_dropStorage.GetLocalPlayerLocation(); +} + +BSTEventResult CoSaveService::OnEvent(const TESLoadGameEvent*, const EventDispatcher*) +{ + spdlog::info("CoSaveService: LoadGame event received"); + LoadFromCurrentSave(); + return BSTEventResult::kOk; +} + +void CoSaveService::LoadFromCurrentSave() noexcept +{ + m_dropStorage.OnLoadGameReset(); + PrepareForUser(m_transport.GetLoginUsername()); + + const auto savePath = SaveGameUtils::GetCurrentSavePath(); + if (savePath.empty()) + return; + + const auto coSavePath = ResolveCoSavePath(savePath); + if (coSavePath.empty()) + return; + + m_dropStorage.LoadFromPath(coSavePath); +} + +void CoSaveService::OnSaveGame(const char* apFileName) noexcept +{ + spdlog::info("CoSaveService: OnSaveGame called (file='{}')", apFileName ? apFileName : ""); + PrepareForUser(m_transport.GetLoginUsername()); + + if (!apFileName || apFileName[0] == '\0') + { + const auto fallbackPath = SaveGameUtils::GetCurrentSavePath(); + if (!fallbackPath.empty()) + SaveToPath(fallbackPath); + return; + } + + const std::filesystem::path savePath(apFileName); + if (savePath.has_parent_path() || savePath.has_root_path()) + { + SaveToPath(savePath); + return; + } + + const auto fallbackPath = SaveGameUtils::GetCurrentSavePath(); + if (!fallbackPath.empty()) + SaveToPath(fallbackPath); +} + +void CoSaveService::SaveToPath(const std::filesystem::path& aSavePath) noexcept +{ + const auto coSavePath = ResolveCoSavePath(aSavePath); + if (coSavePath.empty()) + return; + + m_dropStorage.SaveToPath(coSavePath); +} diff --git a/Code/client/Services/Generic/CoSaveService.h b/Code/client/Services/Generic/CoSaveService.h new file mode 100644 index 000000000..1702ad612 --- /dev/null +++ b/Code/client/Services/Generic/CoSaveService.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +#include + +#include + +struct TransportService; +struct World; + +class CoSaveService final : public BSTEventSink +{ +public: + CoSaveService(World& aWorld, entt::dispatcher& aDispatcher, TransportService& aTransport) noexcept; + ~CoSaveService() override; + + TP_NOCOPYMOVE(CoSaveService); + + DropStorage& GetDropStorage() noexcept { return m_dropStorage; } + const DropStorage& GetDropStorage() const noexcept { return m_dropStorage; } + + void UpdateLocalPlayerLocation(const CoSaveStorage::LocalPlayerLocation& aLocation) noexcept; + std::optional GetLocalPlayerLocation() const noexcept; + + void OnSaveGame(const char* apFileName) noexcept; + void PrepareForUser(const std::string& aUsername) noexcept; + + BSTEventResult OnEvent(const TESLoadGameEvent* apEvent, const EventDispatcher* apSender) override; + +private: + void LoadFromCurrentSave() noexcept; + void SaveToPath(const std::filesystem::path& aSavePath) noexcept; + + World& m_world; + TransportService& m_transport; + DropStorage m_dropStorage; + std::string m_cachedUsername; +}; diff --git a/Code/client/Services/Generic/CoSaveStorage.cpp b/Code/client/Services/Generic/CoSaveStorage.cpp new file mode 100644 index 000000000..2e36b3911 --- /dev/null +++ b/Code/client/Services/Generic/CoSaveStorage.cpp @@ -0,0 +1,336 @@ +#include "CoSaveStorage.h" + +#include + +#include + +#include +#include +#include + +namespace +{ +constexpr uint32_t kCoSaveMagic = 'T' | ('S' << 8) | ('C' << 16) | ('O' << 24); +constexpr uint32_t kCoSaveVersion = 4; +constexpr uint32_t kLocationMagic = 'L' | ('O' << 8) | ('C' << 16) | ('L' << 24); +constexpr uint32_t kLocationVersion = 2; +} // namespace + +namespace CoSaveStorage +{ +bool Load(const std::filesystem::path& aPath, TiltedPhoques::Map& aOut) noexcept +{ + return Load(aPath, aOut, nullptr); +} + +bool Load(const std::filesystem::path& aPath, TiltedPhoques::Map& aOut, LocalPlayerLocation* apOutLocation) noexcept +{ + aOut.clear(); + if (apOutLocation) + *apOutLocation = {}; + + std::ifstream input(aPath, std::ios::binary); + if (!input.is_open()) + return true; + + uint32_t magic = 0; + uint32_t version = 0; + uint32_t buildId = 0; + + input.read(reinterpret_cast(&magic), sizeof(magic)); + input.read(reinterpret_cast(&version), sizeof(version)); + input.read(reinterpret_cast(&buildId), sizeof(buildId)); + if (!input || magic != kCoSaveMagic || version == 0) + { + spdlog::warn("CoSaveStorage: invalid header for '{}', ignoring cache", aPath.string()); + return false; + } + + uint32_t count = 0; + input.read(reinterpret_cast(&count), sizeof(count)); + if (!input) + return false; + + if (version == 1) + { + for (uint32_t i = 0; i < count; ++i) + { + Entry entry{}; + input.read(reinterpret_cast(&entry.DropId), sizeof(entry.DropId)); + entry.Type = ServerItemType::Dropped; + input.read(reinterpret_cast(&entry.ServerId), sizeof(entry.ServerId)); + + input.read(reinterpret_cast(&entry.CellId.ModId), sizeof(entry.CellId.ModId)); + input.read(reinterpret_cast(&entry.CellId.BaseId), sizeof(entry.CellId.BaseId)); + input.read(reinterpret_cast(&entry.WorldSpaceId.ModId), sizeof(entry.WorldSpaceId.ModId)); + input.read(reinterpret_cast(&entry.WorldSpaceId.BaseId), sizeof(entry.WorldSpaceId.BaseId)); + + input.read(reinterpret_cast(&entry.ReferenceId.ModId), sizeof(entry.ReferenceId.ModId)); + input.read(reinterpret_cast(&entry.ReferenceId.BaseId), sizeof(entry.ReferenceId.BaseId)); + + input.read(reinterpret_cast(&entry.Location), sizeof(entry.Location)); + input.read(reinterpret_cast(&entry.Rotation), sizeof(entry.Rotation)); + input.read(reinterpret_cast(&entry.RefFormId), sizeof(entry.RefFormId)); + input.read(reinterpret_cast(&entry.LastSeenTimestamp), sizeof(entry.LastSeenTimestamp)); + + uint32_t itemSize = 0; + input.read(reinterpret_cast(&itemSize), sizeof(itemSize)); + if (!input || itemSize == 0) + continue; + + std::vector itemBuffer(itemSize); + input.read(reinterpret_cast(itemBuffer.data()), itemSize); + if (!input) + continue; + + TiltedPhoques::ViewBuffer view(itemBuffer.data(), itemBuffer.size()); + TiltedPhoques::Buffer::Reader reader(&view); + entry.Item.Deserialize(reader); + + aOut[entry.DropId] = entry; + } + + return true; + } + + for (uint32_t i = 0; i < count; ++i) + { + uint32_t entrySize = 0; + input.read(reinterpret_cast(&entrySize), sizeof(entrySize)); + if (!input || entrySize == 0) + continue; + + std::vector entryBuffer(entrySize); + input.read(reinterpret_cast(entryBuffer.data()), entrySize); + if (!input) + continue; + + size_t offset = 0; + auto readBytes = [&](void* dest, size_t size) -> bool { + if (offset + size > entryBuffer.size()) + return false; + std::memcpy(dest, entryBuffer.data() + offset, size); + offset += size; + return true; + }; + + Entry entry{}; + if (!readBytes(&entry.DropId, sizeof(entry.DropId))) + continue; + + if (version >= 3) + { + uint8_t typeValue = 0; + if (!readBytes(&typeValue, sizeof(typeValue))) + continue; + entry.Type = typeValue == static_cast(ServerItemType::CreationEngine) ? ServerItemType::CreationEngine : ServerItemType::Dropped; + } + else + { + entry.Type = ServerItemType::Dropped; + } + + if (!readBytes(&entry.ServerId, sizeof(entry.ServerId))) + continue; + if (!readBytes(&entry.CellId.ModId, sizeof(entry.CellId.ModId))) + continue; + if (!readBytes(&entry.CellId.BaseId, sizeof(entry.CellId.BaseId))) + continue; + if (!readBytes(&entry.WorldSpaceId.ModId, sizeof(entry.WorldSpaceId.ModId))) + continue; + if (!readBytes(&entry.WorldSpaceId.BaseId, sizeof(entry.WorldSpaceId.BaseId))) + continue; + if (!readBytes(&entry.ReferenceId.ModId, sizeof(entry.ReferenceId.ModId))) + continue; + if (!readBytes(&entry.ReferenceId.BaseId, sizeof(entry.ReferenceId.BaseId))) + continue; + if (!readBytes(&entry.Location, sizeof(entry.Location))) + continue; + if (!readBytes(&entry.Rotation, sizeof(entry.Rotation))) + continue; + if (!readBytes(&entry.RefFormId, sizeof(entry.RefFormId))) + continue; + if (!readBytes(&entry.LastSeenTimestamp, sizeof(entry.LastSeenTimestamp))) + continue; + + uint32_t itemSize = 0; + if (!readBytes(&itemSize, sizeof(itemSize))) + continue; + if (itemSize == 0 || offset + itemSize > entryBuffer.size()) + continue; + + TiltedPhoques::ViewBuffer view(entryBuffer.data() + offset, itemSize); + TiltedPhoques::Buffer::Reader reader(&view); + entry.Item.Deserialize(reader); + + aOut[entry.DropId] = entry; + } + + if (!input) + return true; + + uint32_t locMagic = 0; + input.read(reinterpret_cast(&locMagic), sizeof(locMagic)); + if (!input || locMagic != kLocationMagic) + return true; + + uint32_t locVersion = 0; + input.read(reinterpret_cast(&locVersion), sizeof(locVersion)); + if (!input || locVersion == 0) + return true; + + LocalPlayerLocation tmp{}; + if (locVersion == 1) + { + uint8_t hasLocation = 0; + input.read(reinterpret_cast(&hasLocation), sizeof(hasLocation)); + if (!input) + return true; + + tmp.HasLocation = hasLocation != 0; + if (tmp.HasLocation) + { + input.read(reinterpret_cast(&tmp.Position), sizeof(tmp.Position)); + input.read(reinterpret_cast(&tmp.WorldSpaceId.ModId), sizeof(tmp.WorldSpaceId.ModId)); + input.read(reinterpret_cast(&tmp.WorldSpaceId.BaseId), sizeof(tmp.WorldSpaceId.BaseId)); + input.read(reinterpret_cast(&tmp.CellId.ModId), sizeof(tmp.CellId.ModId)); + input.read(reinterpret_cast(&tmp.CellId.BaseId), sizeof(tmp.CellId.BaseId)); + input.read(reinterpret_cast(&tmp.LastSeenEpoch), sizeof(tmp.LastSeenEpoch)); + } + + tmp.HasExterior = tmp.HasLocation && tmp.WorldSpaceId; + tmp.ExteriorPosition = tmp.Position; + tmp.ExteriorWorldSpaceId = tmp.WorldSpaceId; + tmp.ExteriorCellId = tmp.CellId; + tmp.ExteriorLastSeenEpoch = tmp.LastSeenEpoch; + } + else + { + uint8_t hasLocation = 0; + uint8_t hasExterior = 0; + input.read(reinterpret_cast(&hasLocation), sizeof(hasLocation)); + input.read(reinterpret_cast(&hasExterior), sizeof(hasExterior)); + if (!input) + return true; + + tmp.HasLocation = hasLocation != 0; + tmp.HasExterior = hasExterior != 0; + input.read(reinterpret_cast(&tmp.Position), sizeof(tmp.Position)); + input.read(reinterpret_cast(&tmp.WorldSpaceId.ModId), sizeof(tmp.WorldSpaceId.ModId)); + input.read(reinterpret_cast(&tmp.WorldSpaceId.BaseId), sizeof(tmp.WorldSpaceId.BaseId)); + input.read(reinterpret_cast(&tmp.CellId.ModId), sizeof(tmp.CellId.ModId)); + input.read(reinterpret_cast(&tmp.CellId.BaseId), sizeof(tmp.CellId.BaseId)); + input.read(reinterpret_cast(&tmp.LastSeenEpoch), sizeof(tmp.LastSeenEpoch)); + input.read(reinterpret_cast(&tmp.ExteriorPosition), sizeof(tmp.ExteriorPosition)); + input.read(reinterpret_cast(&tmp.ExteriorWorldSpaceId.ModId), sizeof(tmp.ExteriorWorldSpaceId.ModId)); + input.read(reinterpret_cast(&tmp.ExteriorWorldSpaceId.BaseId), sizeof(tmp.ExteriorWorldSpaceId.BaseId)); + input.read(reinterpret_cast(&tmp.ExteriorCellId.ModId), sizeof(tmp.ExteriorCellId.ModId)); + input.read(reinterpret_cast(&tmp.ExteriorCellId.BaseId), sizeof(tmp.ExteriorCellId.BaseId)); + input.read(reinterpret_cast(&tmp.ExteriorLastSeenEpoch), sizeof(tmp.ExteriorLastSeenEpoch)); + if (!tmp.HasExterior && tmp.HasLocation && tmp.WorldSpaceId) + { + tmp.HasExterior = true; + tmp.ExteriorPosition = tmp.Position; + tmp.ExteriorWorldSpaceId = tmp.WorldSpaceId; + tmp.ExteriorCellId = tmp.CellId; + tmp.ExteriorLastSeenEpoch = tmp.LastSeenEpoch; + } + } + + if (apOutLocation && input) + *apOutLocation = tmp; + + return true; +} + +bool Save(const std::filesystem::path& aPath, const TiltedPhoques::Map& aIn) noexcept +{ + return Save(aPath, aIn, nullptr); +} + +bool Save(const std::filesystem::path& aPath, const TiltedPhoques::Map& aIn, const LocalPlayerLocation* apLocation) noexcept +{ + std::ofstream output(aPath, std::ios::binary | std::ios::trunc); + if (!output.is_open()) + { + spdlog::error("CoSaveStorage: failed to save cache '{}'", aPath.string()); + return false; + } + + const uint32_t magic = kCoSaveMagic; + const uint32_t version = kCoSaveVersion; + const uint32_t buildId = 0; + const uint32_t count = static_cast(aIn.size()); + + output.write(reinterpret_cast(&magic), sizeof(magic)); + output.write(reinterpret_cast(&version), sizeof(version)); + output.write(reinterpret_cast(&buildId), sizeof(buildId)); + output.write(reinterpret_cast(&count), sizeof(count)); + + for (const auto& [dropId, entry] : aIn) + { + TiltedPhoques::Buffer itemBuffer(1 << 12); + TiltedPhoques::Buffer::Writer itemWriter(&itemBuffer); + entry.Item.Serialize(itemWriter); + const uint32_t itemSize = static_cast(itemWriter.Size()); + + std::vector payload; + payload.reserve(256 + itemSize); + + auto append = [&](const void* data, size_t size) { + const auto* bytes = static_cast(data); + payload.insert(payload.end(), bytes, bytes + size); + }; + + append(&entry.DropId, sizeof(entry.DropId)); + const uint8_t typeValue = static_cast(entry.Type); + append(&typeValue, sizeof(typeValue)); + append(&entry.ServerId, sizeof(entry.ServerId)); + append(&entry.CellId.ModId, sizeof(entry.CellId.ModId)); + append(&entry.CellId.BaseId, sizeof(entry.CellId.BaseId)); + append(&entry.WorldSpaceId.ModId, sizeof(entry.WorldSpaceId.ModId)); + append(&entry.WorldSpaceId.BaseId, sizeof(entry.WorldSpaceId.BaseId)); + append(&entry.ReferenceId.ModId, sizeof(entry.ReferenceId.ModId)); + append(&entry.ReferenceId.BaseId, sizeof(entry.ReferenceId.BaseId)); + append(&entry.Location, sizeof(entry.Location)); + append(&entry.Rotation, sizeof(entry.Rotation)); + append(&entry.RefFormId, sizeof(entry.RefFormId)); + append(&entry.LastSeenTimestamp, sizeof(entry.LastSeenTimestamp)); + append(&itemSize, sizeof(itemSize)); + append(itemBuffer.GetWriteData(), itemSize); + + const uint32_t entrySize = static_cast(payload.size()); + output.write(reinterpret_cast(&entrySize), sizeof(entrySize)); + output.write(reinterpret_cast(payload.data()), payload.size()); + } + + if (!output) + return false; + + if (apLocation) + { + const uint32_t locMagic = kLocationMagic; + const uint32_t locVersion = kLocationVersion; + const uint8_t hasLocation = apLocation->HasLocation ? 1 : 0; + const uint8_t hasExterior = apLocation->HasExterior ? 1 : 0; + output.write(reinterpret_cast(&locMagic), sizeof(locMagic)); + output.write(reinterpret_cast(&locVersion), sizeof(locVersion)); + output.write(reinterpret_cast(&hasLocation), sizeof(hasLocation)); + output.write(reinterpret_cast(&hasExterior), sizeof(hasExterior)); + output.write(reinterpret_cast(&apLocation->Position), sizeof(apLocation->Position)); + output.write(reinterpret_cast(&apLocation->WorldSpaceId.ModId), sizeof(apLocation->WorldSpaceId.ModId)); + output.write(reinterpret_cast(&apLocation->WorldSpaceId.BaseId), sizeof(apLocation->WorldSpaceId.BaseId)); + output.write(reinterpret_cast(&apLocation->CellId.ModId), sizeof(apLocation->CellId.ModId)); + output.write(reinterpret_cast(&apLocation->CellId.BaseId), sizeof(apLocation->CellId.BaseId)); + output.write(reinterpret_cast(&apLocation->LastSeenEpoch), sizeof(apLocation->LastSeenEpoch)); + output.write(reinterpret_cast(&apLocation->ExteriorPosition), sizeof(apLocation->ExteriorPosition)); + output.write(reinterpret_cast(&apLocation->ExteriorWorldSpaceId.ModId), sizeof(apLocation->ExteriorWorldSpaceId.ModId)); + output.write(reinterpret_cast(&apLocation->ExteriorWorldSpaceId.BaseId), sizeof(apLocation->ExteriorWorldSpaceId.BaseId)); + output.write(reinterpret_cast(&apLocation->ExteriorCellId.ModId), sizeof(apLocation->ExteriorCellId.ModId)); + output.write(reinterpret_cast(&apLocation->ExteriorCellId.BaseId), sizeof(apLocation->ExteriorCellId.BaseId)); + output.write(reinterpret_cast(&apLocation->ExteriorLastSeenEpoch), sizeof(apLocation->ExteriorLastSeenEpoch)); + } + + return true; +} +} // namespace CoSaveStorage diff --git a/Code/client/Services/Generic/CoSaveStorage.h b/Code/client/Services/Generic/CoSaveStorage.h new file mode 100644 index 000000000..bff7d1e73 --- /dev/null +++ b/Code/client/Services/Generic/CoSaveStorage.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +namespace CoSaveStorage +{ +struct Entry +{ + uint64_t DropId{}; + ServerItemType Type{ServerItemType::Dropped}; + uint32_t ServerId{}; + GameId CellId{}; + GameId WorldSpaceId{}; + GameId ReferenceId{}; + Inventory::Entry Item{}; + NiPoint3 Location{}; + NiPoint3 Rotation{}; + uint32_t RefFormId{}; + uint64_t LastSeenTimestamp{}; +}; + +struct LocalPlayerLocation +{ + bool HasLocation{false}; + bool HasExterior{false}; + Vector3_NetQuantize Position{}; + GameId WorldSpaceId{}; + GameId CellId{}; + uint64_t LastSeenEpoch{0}; + Vector3_NetQuantize ExteriorPosition{}; + GameId ExteriorWorldSpaceId{}; + GameId ExteriorCellId{}; + uint64_t ExteriorLastSeenEpoch{0}; + + bool operator==(const LocalPlayerLocation& rhs) const noexcept + { + return HasLocation == rhs.HasLocation && HasExterior == rhs.HasExterior && Position == rhs.Position && + WorldSpaceId == rhs.WorldSpaceId && CellId == rhs.CellId && LastSeenEpoch == rhs.LastSeenEpoch && + ExteriorPosition == rhs.ExteriorPosition && ExteriorWorldSpaceId == rhs.ExteriorWorldSpaceId && + ExteriorCellId == rhs.ExteriorCellId && ExteriorLastSeenEpoch == rhs.ExteriorLastSeenEpoch; + } + bool operator!=(const LocalPlayerLocation& rhs) const noexcept { return !(*this == rhs); } +}; + +bool Load(const std::filesystem::path& aPath, TiltedPhoques::Map& aOut) noexcept; +bool Load(const std::filesystem::path& aPath, TiltedPhoques::Map& aOut, LocalPlayerLocation* apOutLocation) noexcept; +bool Save(const std::filesystem::path& aPath, const TiltedPhoques::Map& aIn) noexcept; +bool Save(const std::filesystem::path& aPath, const TiltedPhoques::Map& aIn, const LocalPlayerLocation* apLocation) noexcept; +} // namespace CoSaveStorage diff --git a/Code/client/Services/Generic/DropService.cpp b/Code/client/Services/Generic/DropService.cpp new file mode 100644 index 000000000..8e93fec4f --- /dev/null +++ b/Code/client/Services/Generic/DropService.cpp @@ -0,0 +1,3040 @@ +#include "DropService.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace +{ +constexpr float kDropSearchRadiusSquared = 400.f * 400.f; +constexpr float kPickupRemovalRadiusSquared = 2500.f * 2500.f; +constexpr float kMaterializeGraceSeconds = 0.5f; +constexpr float kDropPhysicsHoldSeconds = 5.0f; +constexpr float kDropMoveSyncIntervalSeconds = 0.25f; +constexpr float kDropMoveInterpolationSeconds = 0.25f; +constexpr float kDropMovePredictionSeconds = 0.1f; +constexpr float kDropRotationNetScale = 1000.0f; +constexpr double kPeriodicPlayerCellSyncSeconds = 5.0; +constexpr double kDropSyncQueueIntervalSeconds = 0.5; +constexpr double kGrabEventSuppressSeconds = 2.0; +constexpr float kDropPhysicsDisableSuppressSeconds = 1.0f; +constexpr double kLoadSuspendSeconds = 3.0; +constexpr double kCellPhysicsGraceSeconds = 1.0; +constexpr uint32_t kMaxPendingDropRetries = 600; +constexpr uint32_t kMaxPendingPickupRetries = 120; +TiltedPhoques::Map g_materializeGrace; + +bool HasDropLocation(const NiPoint3& aLocation) noexcept +{ + return std::abs(aLocation.x) > std::numeric_limits::epsilon() || + std::abs(aLocation.y) > std::numeric_limits::epsilon() || + std::abs(aLocation.z) > std::numeric_limits::epsilon(); +} + +bool IsServerItemFormType(FormType aType) noexcept +{ + switch (aType) + { + case FormType::Weapon: + case FormType::Armor: + case FormType::Ammo: + case FormType::Ingredient: + case FormType::Alchemy: + case FormType::Book: + case FormType::Scroll: + case FormType::Note: + case FormType::SoulGem: + case FormType::KeyMaster: + case FormType::Light: + case FormType::Misc: + case FormType::Apparatus: + case FormType::LeveledItem: + return true; + default: + return false; + } +} + +bool IsEligibleServerItemRef(TESObjectREFR* apReference) noexcept +{ + if (!apReference || !apReference->baseForm) + return false; + + if (apReference->IsDisabled()) + return false; + + if (ExtraDataList* pExtra = apReference->GetExtraDataList(); pExtra && pExtra->HasQuestObjectAlias()) + return false; + + return IsServerItemFormType(apReference->baseForm->formType); +} + +bool ShouldDeferPhysicsLock(double aRemainingGraceSeconds) noexcept +{ + return aRemainingGraceSeconds > 0.0; +} + +enum class MotionType : uint8_t +{ + kInvalid = 0, + kDynamic = 1, + kSphereInertia = 2, + kBoxInertia = 3, + kKeyframed = 4, + kFixed = 5, + kThinBoxInertia = 6, + kCharacter = 7 +}; + +bool SetNodeMotionType(NiAVObject* apNode, MotionType aMotionType, bool aAllowActivate) noexcept +{ + if (!apNode) + return false; + + using TSetMotionType = bool(NiAVObject*, uint8_t, bool, bool, bool); + POINTER_SKYRIMSE(TSetMotionType, s_setMotionType, 77866); + return TiltedPhoques::ThisCall(s_setMotionType.Get(), apNode, static_cast(aMotionType), true, false, aAllowActivate); +} + +bool SetReferenceMotionType(TESObjectREFR* apReference, MotionType aMotionType, bool aAllowActivate) noexcept +{ + if (!apReference) + return false; + + NiAVObject* pNode = apReference->GetNiNode(); + const bool updated = SetNodeMotionType(pNode, aMotionType, aAllowActivate); + apReference->Update3DPosition(false); + return updated; +} + +NiPoint3 ToPoint(const Vector3_NetQuantize& aVector) +{ + return NiPoint3(aVector); +} + +NiPoint3 ToDropRotation(const Vector3_NetQuantize& aVector) noexcept +{ + NiPoint3 rotation{}; + rotation.x = aVector.x / kDropRotationNetScale; + rotation.y = aVector.y / kDropRotationNetScale; + rotation.z = aVector.z / kDropRotationNetScale; + return rotation; +} + +Vector3_NetQuantize ToNetVector(const NiPoint3& aVector) +{ + Vector3_NetQuantize value; + value.x = aVector.x; + value.y = aVector.y; + value.z = aVector.z; + return value; +} + +Vector3_NetQuantize ToNetDropRotation(const NiPoint3& aRotation) noexcept +{ + Vector3_NetQuantize value; + value.x = aRotation.x * kDropRotationNetScale; + value.y = aRotation.y * kDropRotationNetScale; + value.z = aRotation.z * kDropRotationNetScale; + return value; +} + +float NormalizeAngleDelta(float aDelta) noexcept +{ + constexpr float kPi = 3.14159265358979323846f; + constexpr float kTwoPi = 6.28318530717958647692f; + aDelta = std::fmod(aDelta, kTwoPi); + if (aDelta > kPi) + aDelta -= kTwoPi; + else if (aDelta < -kPi) + aDelta += kTwoPi; + return aDelta; +} + +TESBoundObject* ResolveDroppedObject(const Inventory::Entry& acEntry) +{ + auto& modSystem = World::Get().GetModSystem(); + const uint32_t objectId = modSystem.GetGameId(acEntry.BaseId); + return Cast(TESForm::GetById(objectId)); +} + +uint16_t ClampExtraCount(int32_t aCount) noexcept +{ + int32_t count = aCount; + if (count < 0) + count = -count; + if (count <= 0) + count = 1; + + constexpr int32_t kMaxExtraCount = std::numeric_limits::max(); + if (count > kMaxExtraCount) + count = kMaxExtraCount; + + return static_cast(count); +} + +void ApplyDropExtraData(TESObjectREFR* apReference, const Inventory::Entry& acItem, uint16_t aCount) noexcept +{ + if (!apReference) + return; + + ExtraDataList* pExtraDataList = apReference->GetExtraDataList(); + if (!pExtraDataList) + return; + + pExtraDataList->SetCount(aCount); + + if (acItem.ExtraCharge > 0.f) + pExtraDataList->SetChargeData(acItem.ExtraCharge); + + if (acItem.ExtraEnchantId != 0) + { + auto& modSystem = World::Get().GetModSystem(); + + EnchantmentItem* pEnchantment = nullptr; + if (acItem.ExtraEnchantId.ModId == 0xFFFFFFFF) + { + pEnchantment = EnchantmentItem::Create(acItem.EnchantData); + } + else + { + const uint32_t enchantId = modSystem.GetGameId(acItem.ExtraEnchantId); + pEnchantment = Cast(TESForm::GetById(enchantId)); + } + + TP_ASSERT(pEnchantment, "No Enchantment created or found."); + if (pEnchantment) + pExtraDataList->SetEnchantmentData(pEnchantment, acItem.ExtraEnchantCharge, acItem.ExtraEnchantRemoveUnequip); + } + + if (acItem.ExtraPoisonId != 0) + { + auto& modSystem = World::Get().GetModSystem(); + const uint32_t poisonId = modSystem.GetGameId(acItem.ExtraPoisonId); + if (AlchemyItem* pPoison = Cast(TESForm::GetById(poisonId))) + pExtraDataList->SetPoison(pPoison, acItem.ExtraPoisonCount); + } + + if (acItem.ExtraHealth > 0.f) + pExtraDataList->SetHealth(acItem.ExtraHealth); + + if (acItem.ExtraSoulLevel > 0 && acItem.ExtraSoulLevel <= 5) + pExtraDataList->SetSoulData(static_cast(acItem.ExtraSoulLevel)); + + if (acItem.ExtraWorn) + pExtraDataList->SetWorn(false); + if (acItem.ExtraWornLeft) + pExtraDataList->SetWorn(true); +} + +TESObjectREFR* FindReferenceNear(TESBoundObject* apObject, const NiPoint3& acCenter, float aRadiusSq = kDropSearchRadiusSquared) +{ + if (!apObject) + return nullptr; + + TES* pTes = TES::Get(); + if (!pTes || !pTes->cells || !pTes->cells->arr) + return nullptr; + + const int dimension = pTes->cells->dimension; + if (dimension <= 0) + return nullptr; + + TESObjectREFR* pClosest = nullptr; + float closestDistanceSq = aRadiusSq; + + const int cellCount = dimension * dimension; + for (int i = 0; i < cellCount; ++i) + { + TESObjectCELL* pCell = pTes->cells->arr[i]; + if (!pCell || !pCell->IsValid()) + continue; + + auto* pReferences = pCell->refData.refArray; + if (!pReferences) + continue; + + const uint32_t referenceCount = pCell->refData.Count(); + for (uint32_t j = 0; j < referenceCount; ++j) + { + TESObjectREFR* pCandidate = pReferences[j].Get(); + if (!pCandidate || pCandidate->baseForm != apObject || pCandidate->formType == Actor::Type) + continue; + + const float diffX = pCandidate->position.x - acCenter.x; + const float diffY = pCandidate->position.y - acCenter.y; + const float diffZ = pCandidate->position.z - acCenter.z; + const float distanceSq = diffX * diffX + diffY * diffY + diffZ * diffZ; + + if (distanceSq < closestDistanceSq) + { + closestDistanceSq = distanceSq; + pClosest = pCandidate; + } + } + } + + return pClosest; +} + +std::pair ResolveCellMetadata(World& aWorld, Actor* apActor) +{ + GameId cell{}; + GameId world{}; + + if (!apActor) + return {cell, world}; + + TESObjectCELL* pCell = apActor->parentCell ? apActor->parentCell : apActor->GetParentCell(); + if (pCell) + { + aWorld.GetModSystem().GetServerModId(pCell->formID, cell); + if (pCell->worldspace) + aWorld.GetModSystem().GetServerModId(pCell->worldspace->formID, world); + } + + return {cell, world}; +} +} // namespace + +DropService::DropService(World& aWorld, entt::dispatcher& aDispatcher, TransportService& aTransport) noexcept + : m_world(aWorld) + , m_dispatcher(aDispatcher) + , m_transport(aTransport) + , m_coSaveService(aWorld.ctx().at()) + , m_dropStorage(m_coSaveService.GetDropStorage()) +{ + m_dropEventConnection = m_dispatcher.sink().connect<&DropService::OnDropEvent>(this); + m_pickupEventConnection = m_dispatcher.sink().connect<&DropService::OnPickupEvent>(this); + m_notifyDropConnection = m_dispatcher.sink().connect<&DropService::OnNotifyDrop>(this); + m_notifyPickupConnection = m_dispatcher.sink().connect<&DropService::OnNotifyPickup>(this); + m_notifyDroppedItemsConnection = m_dispatcher.sink().connect<&DropService::OnNotifyDroppedItems>(this); + m_notifyDropMoveConnection = m_dispatcher.sink().connect<&DropService::OnNotifyDropMove>(this); + m_notifyDropPhysicsDisabledConnection = m_dispatcher.sink().connect<&DropService::OnNotifyDropPhysicsDisabled>(this); + m_connectedEventConnection = m_dispatcher.sink().connect<&DropService::OnConnected>(this); + m_cellChangeConnection = m_dispatcher.sink().connect<&DropService::OnCellChange>(this); + m_gridCellChangeConnection = m_dispatcher.sink().connect<&DropService::OnGridCellChange>(this); + m_updateConnection = m_dispatcher.sink().connect<&DropService::OnUpdate>(this); + + DropManager::SetStorageListener(&m_dropStorage); + + if (auto* pDispatcher = EventDispatcherManager::Get()) + { + pDispatcher->grabReleaseEvent.RegisterSink(this); + pDispatcher->loadGameEvent.RegisterSink(this); + } +} + +DropService::~DropService() +{ + DropManager::SetStorageListener(nullptr); + + if (auto* pDispatcher = EventDispatcherManager::Get()) + { + pDispatcher->grabReleaseEvent.UnRegisterSink(this); + pDispatcher->loadGameEvent.UnRegisterSink(this); + } +} + + +BSTEventResult DropService::OnEvent(const TESLoadGameEvent*, const EventDispatcher*) +{ + spdlog::info("DropService: LoadGame event received"); + m_pendingActions.clear(); + m_pendingDropSyncs.clear(); + m_dropSyncQueue.clear(); + m_dropSyncQueuedCells.clear(); + m_materializingDrops.clear(); + m_localDrops.clear(); + m_knownSpawnEpochs.clear(); + m_grabbedDrops.clear(); + m_dropPhysicsCooldowns.clear(); + m_dropMoveSyncTimers.clear(); + m_dropPhysicsDisableSuppressions.clear(); + m_grabbedReferences.clear(); + m_referencePhysicsCooldowns.clear(); + m_referenceMoveSyncTimers.clear(); + m_pendingCreationEngineRemovals.clear(); + m_dropSyncWorldSpace = {}; + m_dropSyncQueueAccumulator = 0.0; + m_periodicPlayerCellSyncAccumulator = 0.0; + m_nextDropSyncRequestId = 1; + m_grabEventSuppressionRemaining = kGrabEventSuppressSeconds; + m_cachedUsername.clear(); + m_suspendProcessing = true; + m_requestResyncAfterSuspend = true; + m_suspendProcessingAccumulator = 0.0; + m_pendingDiscoveryResyncs = 0; + m_cellPhysicsGraceRemaining = kCellPhysicsGraceSeconds; + m_requestPhysicsLockAfterGrace = true; + + return BSTEventResult::kOk; +} + +void DropService::OnDropEvent(const DropItemEvent& acEvent) noexcept +{ + if (!m_transport.IsConnected()) + return; + if (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost) + return; + + auto serverIdRes = ResolveServerId(acEvent.ActorFormId); + if (!serverIdRes) + { + spdlog::warn("{}: failed to resolve server id for actor {:X}", __FUNCTION__, acEvent.ActorFormId); + return; + } + + RequestActorDrop request{}; + request.ServerId = *serverIdRes; + request.ActorFormId = acEvent.ActorFormId; + request.Item = acEvent.Item; + request.ClientDropId = acEvent.ClientDropId; + request.HasLocation = true; + request.Location = ToNetVector(acEvent.Location); + request.HasRotation = true; + request.Rotation = ToNetDropRotation(acEvent.Rotation); + request.CellId = acEvent.CellId; + request.WorldSpaceId = acEvent.WorldSpaceId; + request.ReferenceId = acEvent.ReferenceId; + + spdlog::debug("DropService: requesting drop for actor {:X}, server {:X}, item {:X}:{:X}", acEvent.ActorFormId, *serverIdRes, acEvent.Item.BaseId.ModId, acEvent.Item.BaseId.BaseId); + m_transport.Send(request); + + // Client drop payload is captured in the request; mapping cache is server-authoritative. + DropManager::ConsumeLocalDrop(acEvent.ClientDropId); +} + +void DropService::OnPickupEvent(const PickupDroppedItemEvent& acEvent) noexcept +{ + if (!m_transport.IsConnected()) + return; + if (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost) + return; + + uint64_t resolvedDropId = 0; + if (acEvent.ReferenceFormId) + { + EnsureStorageReady(); + if (const auto mapped = m_dropStorage.FindDropIdByRefFormId(acEvent.ReferenceFormId, acEvent.CellId, acEvent.WorldSpaceId); mapped) + { + resolvedDropId = *mapped; + spdlog::debug("DropService: resolved pickup ref {:X} to drop {} via cache", acEvent.ReferenceFormId, resolvedDropId); + } + } + if (resolvedDropId == 0) + resolvedDropId = acEvent.DropId; + + spdlog::debug("DropService: local pickup event actor {:X} drop {}", acEvent.ActorFormId, resolvedDropId); + + auto serverIdRes = ResolveServerId(acEvent.ActorFormId); + if (!serverIdRes) + { + spdlog::warn("{}: failed to resolve server id for actor {:X}", __FUNCTION__, acEvent.ActorFormId); + return; + } + + RequestPickupDroppedItem request{}; + request.ServerId = *serverIdRes; + request.DropId = resolvedDropId; + if (resolvedDropId) + { + if (const auto dropOpt = DropManager::GetServerDrop(resolvedDropId); dropOpt) + { + request.Item = dropOpt->Item; + request.HasLocation = true; + request.Location = ToNetVector(dropOpt->Location); + request.HasRotation = true; + request.Rotation = ToNetDropRotation(dropOpt->Rotation); + request.CellId = dropOpt->CellId; + request.WorldSpaceId = dropOpt->WorldSpaceId; + request.ReferenceId = dropOpt->ReferenceId ? dropOpt->ReferenceId : acEvent.ReferenceId; + } + else if (acEvent.HasItemData) + { + request.Item = acEvent.Item; + request.HasLocation = acEvent.HasLocation; + if (acEvent.HasLocation) + request.Location = ToNetVector(acEvent.Location); + request.HasRotation = acEvent.HasRotation; + if (acEvent.HasRotation) + request.Rotation = ToNetDropRotation(acEvent.Rotation); + request.CellId = acEvent.CellId; + request.WorldSpaceId = acEvent.WorldSpaceId; + request.ReferenceId = acEvent.ReferenceId; + } + } + else + { + if (acEvent.HasItemData) + request.Item = acEvent.Item; + + request.HasLocation = acEvent.HasLocation; + if (acEvent.HasLocation) + request.Location = ToNetVector(acEvent.Location); + + request.HasRotation = acEvent.HasRotation; + if (acEvent.HasRotation) + request.Rotation = ToNetDropRotation(acEvent.Rotation); + + request.CellId = acEvent.CellId; + request.WorldSpaceId = acEvent.WorldSpaceId; + request.ReferenceId = acEvent.ReferenceId; + } + + spdlog::debug("DropService: requesting pickup for actor {:X} (server {:X}), drop {}", acEvent.ActorFormId, *serverIdRes, resolvedDropId); + m_transport.Send(request); +} + +void DropService::OnNotifyDrop(const NotifyActorDrop& acMessage) noexcept +{ + if (m_suspendProcessing) + { + m_requestResyncAfterSuspend = true; + return; + } + + if (!ApplyDrop(acMessage)) + { + PendingAction pending{}; + pending.Type = PendingType::Drop; + pending.DropMessage = acMessage; + m_pendingActions.push_back(std::move(pending)); + spdlog::debug("DropService: queued drop {} for later", acMessage.DropId); + } + else + { + spdlog::debug("DropService: processed drop {} immediately", acMessage.DropId); + } +} + +bool DropService::ApplyDrop(const NotifyActorDrop& acMessage) noexcept +{ + if (const auto itEpoch = m_knownSpawnEpochs.find(acMessage.DropId); + itEpoch != std::end(m_knownSpawnEpochs) && acMessage.SpawnEpoch < itEpoch->second) + { + spdlog::debug("DropService: ignoring stale drop {} epoch {} (known {})", acMessage.DropId, acMessage.SpawnEpoch, itEpoch->second); + return true; + } + + if (const auto itEpoch = m_knownSpawnEpochs.find(acMessage.DropId); + itEpoch == std::end(m_knownSpawnEpochs) || acMessage.SpawnEpoch > itEpoch->second) + { + m_knownSpawnEpochs[acMessage.DropId] = acMessage.SpawnEpoch; + } + + Actor* pActor = Utils::GetByServerId(acMessage.ServerId); + PlayerCharacter* pPlayer = PlayerCharacter::Get(); + + DropManager::ServerDropData serverData{}; + serverData.ServerId = acMessage.ServerId; + serverData.ActorFormId = acMessage.ActorFormId ? acMessage.ActorFormId : (pActor ? pActor->formID : 0); + serverData.Type = ServerItemType::Dropped; + serverData.CellId = acMessage.CellId; + serverData.WorldSpaceId = acMessage.WorldSpaceId; + serverData.ReferenceId = acMessage.ReferenceId; + serverData.Item = acMessage.Item; + + auto [fallbackCellId, fallbackWorldId] = ResolveCellMetadata(m_world, pActor ? pActor : static_cast(pPlayer)); + if (!serverData.CellId) + serverData.CellId = fallbackCellId; + if (!serverData.WorldSpaceId) + serverData.WorldSpaceId = fallbackWorldId; + + if (acMessage.HasLocation) + serverData.Location = ToPoint(acMessage.Location); + else if (pActor) + serverData.Location = pActor->position; + else if (pPlayer) + serverData.Location = pPlayer->position; + + if (acMessage.HasRotation) + serverData.Rotation = ToDropRotation(acMessage.Rotation); + else if (pActor) + serverData.Rotation = pActor->rotation; + else if (pPlayer) + serverData.Rotation = pPlayer->rotation; + + DropManager::TrackServerDrop(acMessage.DropId, serverData); + + if (const auto handleOpt = DropManager::GetHandleForDrop(acMessage.DropId); handleOpt) + { + if (TESObjectREFR::GetByHandle(*handleOpt)) + { + m_localDrops.insert(acMessage.DropId); + spdlog::debug("DropService: drop {} already materialized, skipping spawn", acMessage.DropId); + return true; + } + + DropManager::ClearHandleBinding(acMessage.DropId); + } + + if (TryBindExistingReference(acMessage.DropId, serverData)) + { + spdlog::debug("DropService: bound existing reference for drop {}, skipping spawn", acMessage.DropId); + return true; + } + + if (!IsDropCellLoaded(serverData.CellId, serverData.WorldSpaceId)) + { + spdlog::debug("DropService: drop {} not in loaded cells (cell {:X}:{:X}), skipping spawn", acMessage.DropId, serverData.CellId.ModId, serverData.CellId.BaseId); + return true; + } + + if (!MaterializeDrop(acMessage.DropId, serverData, true)) + return false; + + m_localDrops.insert(acMessage.DropId); + m_grabbedDrops.erase(acMessage.DropId); + m_dropPhysicsCooldowns.erase(acMessage.DropId); + m_dropMoveSyncTimers.erase(acMessage.DropId); + spdlog::debug("DropService: applied drop {} for actor {:X}", acMessage.DropId, serverData.ActorFormId); + return true; +} + +bool CanTouchReference3D(TESObjectREFR* apReference) noexcept +{ + if (!apReference) + return false; + if (!apReference->GetNiNode()) + return false; + if (!apReference->GetParentCellEx() && !apReference->GetParentCell()) + return false; + return true; +} + +void SafeSetReferenceMotionType(TESObjectREFR* apReference, MotionType aMotionType, bool aAllowActivate) noexcept +{ + if (!apReference || !apReference->GetNiNode()) + return; + SetReferenceMotionType(apReference, aMotionType, aAllowActivate); +} + +void SafeUpdateReference3D(TESObjectREFR* apReference, bool aWarp) noexcept +{ + if (!CanTouchReference3D(apReference)) + return; + apReference->Update3DPosition(aWarp); +} + +void DropService::OnNotifyPickup(const NotifyDroppedItemPickedUp& acMessage) noexcept +{ + if (m_suspendProcessing) + { + m_requestResyncAfterSuspend = true; + return; + } + + spdlog::debug("DropService: received pickup notify drop {} from actor {:X}", acMessage.DropId, acMessage.ServerId); + if (!ApplyPickup(acMessage)) + { + PendingAction pending{}; + pending.Type = PendingType::Pickup; + pending.PickupMessage = acMessage; + m_pendingActions.push_back(std::move(pending)); + spdlog::debug("DropService: queued pickup {} for later", acMessage.DropId); + } + else + { + spdlog::debug("DropService: processed pickup {} immediately", acMessage.DropId); + } +} + +bool DropService::ApplyPickup(const NotifyDroppedItemPickedUp& acMessage) noexcept +{ + if (acMessage.DropId == 0) + return HandleUntrackedPickup(acMessage); + + EnsureStorageReady(); + + const uint64_t dropId = acMessage.DropId; + const uint32_t localPlayerId = m_transport.GetLocalPlayerId(); + const bool isLocalPicker = localPlayerId != 0 && acMessage.ServerId == localPlayerId; + + if (!IsPickupRelevant(acMessage) && !isLocalPicker) + { + DropManager::RemoveServerDrop(dropId); + ForgetLocalDrop(dropId); + spdlog::debug("DropService: ignored pickup {} (out of range)", dropId); + return true; + } + + bool removed = false; + + if (const auto handleOpt = DropManager::GetHandleForDrop(dropId); handleOpt) + { + if (TESObjectREFR* pDroppedRef = TESObjectREFR::GetByHandle(*handleOpt)) + { + if (pDroppedRef->IsTemporary()) + pDroppedRef->Delete(); + else + pDroppedRef->Disable(); + removed = true; + } + else + { + DropManager::ClearHandleBinding(dropId); + } + } + + if (!removed) + { + if (const auto refFormId = m_dropStorage.GetRefFormId(dropId); refFormId) + { + if (TESForm* pForm = TESForm::GetById(*refFormId)) + { + if (TESObjectREFR* pRef = Cast(pForm)) + { + if (pRef->IsTemporary()) + pRef->Delete(); + else + pRef->Disable(); + removed = true; + } + } + } + } + + if (!removed && acMessage.HasLocation) + removed = RemoveReferenceByLocation(acMessage.Item, acMessage.Location, "pickup notify location fallback", kPickupRemovalRadiusSquared); + if (!removed) + removed = RemoveNearbyReference(dropId, "pickup notify nearby fallback", kPickupRemovalRadiusSquared); + + DropManager::RemoveServerDrop(dropId); + ForgetLocalDrop(dropId); + + if (isLocalPicker || removed) + m_dropStorage.RemoveCachedDrop(dropId); + + spdlog::debug("DropService: applied pickup {} (removed={}, localPicker={})", dropId, removed, isLocalPicker); + return true; +} + +bool DropService::HandleUntrackedPickup(const NotifyDroppedItemPickedUp& acMessage) noexcept +{ + const GameId playerWorld = GetPlayerWorldId(); + + if (acMessage.CellId && !IsDropCellLoaded(acMessage.CellId, acMessage.WorldSpaceId)) + { + spdlog::debug("DropService: ignoring untracked pickup in non-loaded cell {:X}:{:X}", acMessage.CellId.ModId, acMessage.CellId.BaseId); + return true; + } + + if (acMessage.WorldSpaceId && playerWorld && acMessage.WorldSpaceId != playerWorld) + { + spdlog::debug("DropService: ignoring pickup in world {:X}:{:X}, player world {:X}:{:X}", acMessage.WorldSpaceId.ModId, acMessage.WorldSpaceId.BaseId, playerWorld.ModId, playerWorld.BaseId); + return true; + } + + bool removed = RemoveReferenceById(acMessage.ReferenceId, "untracked pickup via reference id"); + if (!removed && acMessage.HasLocation) + removed = RemoveReferenceByLocation(acMessage.Item, acMessage.Location, "untracked pickup via location", kPickupRemovalRadiusSquared); + if (!removed) + { + if (auto* pPlayer = PlayerCharacter::Get()) + { + Vector3_NetQuantize approximate = ToNetVector(pPlayer->position); + removed = RemoveReferenceByLocation(acMessage.Item, approximate, "untracked pickup player proximity", kPickupRemovalRadiusSquared); + } + } + + if (!removed) + spdlog::warn("DropService: pickup notify without drop id could not find reference ({:X}:{:X})", acMessage.Item.BaseId.ModId, acMessage.Item.BaseId.BaseId); + + return true; +} + +bool DropService::IsPickupRelevant(const NotifyDroppedItemPickedUp& acMessage) noexcept +{ + const GameId playerWorld = GetPlayerWorldId(); + + if (acMessage.CellId) + return IsDropCellLoaded(acMessage.CellId, acMessage.WorldSpaceId); + + if (acMessage.WorldSpaceId && playerWorld && acMessage.WorldSpaceId != playerWorld) + return false; + + return true; +} + +void DropService::OnNotifyDroppedItems(const NotifyDroppedItems& acMessage) noexcept +{ + if (m_suspendProcessing) + { + m_requestResyncAfterSuspend = true; + return; + } + + spdlog::debug("DropService: received {} drops and {} creation-engine pickups from server (request {})", acMessage.Entries.size(), acMessage.CreationEnginePickedUpReferences.size(), acMessage.RequestId); + HandleDropSyncResponse(acMessage); +} + +void DropService::OnNotifyDropMove(const NotifyDroppedItemMove& acMessage) noexcept +{ + if (!m_transport.IsConnected()) + return; + if (m_suspendProcessing) + { + m_requestResyncAfterSuspend = true; + return; + } + + uint64_t resolvedDropId = acMessage.DropId; + if (resolvedDropId == 0 && acMessage.ReferenceId) + { + if (const auto mappedDropId = DropManager::GetDropIdForReference(acMessage.ReferenceId)) + resolvedDropId = *mappedDropId; + } + + TESObjectREFR* pReference = nullptr; + NiPoint3 location{}; + NiPoint3 rotation{}; + NiPoint3 velocity{}; + + if (resolvedDropId != 0) + { + const auto dropOpt = DropManager::GetServerDrop(resolvedDropId); + if (!dropOpt) + return; + + location = acMessage.HasLocation ? ToPoint(acMessage.Location) : dropOpt->Location; + rotation = acMessage.HasRotation ? ToDropRotation(acMessage.Rotation) : dropOpt->Rotation; + if (acMessage.HasVelocity) + velocity = ToPoint(acMessage.Velocity); + + DropManager::UpdateServerDropTransform(resolvedDropId, location, rotation, acMessage.CellId, acMessage.WorldSpaceId, acMessage.ReferenceId, acMessage.HasVelocity, velocity); + + const auto updatedOpt = DropManager::GetServerDrop(resolvedDropId); + if (!updatedOpt) + return; + + if (const auto handleOpt = DropManager::GetHandleForDrop(resolvedDropId); handleOpt) + pReference = TESObjectREFR::GetByHandle(*handleOpt); + + if (!pReference && updatedOpt->ReferenceId) + { + pReference = GetReferenceById(updatedOpt->ReferenceId); + if (pReference) + { + auto handle = pReference->GetHandle(); + if (handle && handle.handle.iBits != 0) + DropManager::BindHandleToServerDrop(resolvedDropId, updatedOpt->ActorFormId, handle.handle.iBits); + } + } + + if (!pReference && updatedOpt->Type == ServerItemType::CreationEngine) + { + DropManager::ServerDropData rebound = *updatedOpt; + rebound.Location = location; + rebound.Rotation = rotation; + TryBindExistingReference(resolvedDropId, rebound); + if (const auto handleOpt = DropManager::GetHandleForDrop(resolvedDropId); handleOpt) + pReference = TESObjectREFR::GetByHandle(*handleOpt); + } + + if (!pReference) + return; + + m_localDrops.insert(resolvedDropId); + + if (IsDropLocallyActive(resolvedDropId, updatedOpt->ReferenceId)) + return; + } + else if (acMessage.ReferenceId) + { + pReference = GetReferenceById(acMessage.ReferenceId); + if (!pReference) + return; + + location = acMessage.HasLocation ? ToPoint(acMessage.Location) : pReference->position; + rotation = acMessage.HasRotation ? ToDropRotation(acMessage.Rotation) : pReference->rotation; + + if (IsDropLocallyActive(0, acMessage.ReferenceId)) + return; + } + else + { + return; + } + + if (acMessage.HasLocation || acMessage.HasRotation) + { + SafeSetReferenceMotionType(pReference, MotionType::kKeyframed, true); + + if (!CanTouchReference3D(pReference)) + return; + + MoveInterpolation interpolation{}; + interpolation.DropId = resolvedDropId; + interpolation.ReferenceId = acMessage.ReferenceId; + interpolation.HasLocation = acMessage.HasLocation; + interpolation.HasRotation = acMessage.HasRotation; + interpolation.HasVelocity = acMessage.HasVelocity; + interpolation.HasAngularVelocity = acMessage.HasAngularVelocity; + interpolation.Duration = kDropMoveInterpolationSeconds; + interpolation.StartLocation = pReference->position; + interpolation.StartRotation = pReference->rotation; + interpolation.TargetLocation = location; + interpolation.TargetRotation = rotation; + if (interpolation.HasVelocity) + { + interpolation.Velocity = velocity; + if (interpolation.HasLocation) + { + interpolation.TargetLocation.x += velocity.x * kDropMovePredictionSeconds; + interpolation.TargetLocation.y += velocity.y * kDropMovePredictionSeconds; + interpolation.TargetLocation.z += velocity.z * kDropMovePredictionSeconds; + } + } + if (interpolation.HasAngularVelocity) + { + interpolation.AngularVelocity = ToPoint(acMessage.AngularVelocity); + if (interpolation.HasRotation) + { + interpolation.TargetRotation.x += interpolation.AngularVelocity.x * kDropMovePredictionSeconds; + interpolation.TargetRotation.y += interpolation.AngularVelocity.y * kDropMovePredictionSeconds; + interpolation.TargetRotation.z += interpolation.AngularVelocity.z * kDropMovePredictionSeconds; + } + } + + if (resolvedDropId != 0) + m_dropMoveInterpolations[resolvedDropId] = interpolation; + else if (acMessage.ReferenceId) + m_referenceMoveInterpolations[acMessage.ReferenceId] = interpolation; + } +} + +void DropService::OnNotifyDropPhysicsDisabled(const NotifyDroppedItemPhysicsDisabled& acMessage) noexcept +{ + if (!m_transport.IsConnected()) + return; + if (m_suspendProcessing) + { + m_requestResyncAfterSuspend = true; + return; + } + + if (acMessage.DropId == 0) + return; + + const auto dropOpt = DropManager::GetServerDrop(acMessage.DropId); + if (!dropOpt) + return; + + const NiPoint3 location = acMessage.HasLocation ? ToPoint(acMessage.Location) : dropOpt->Location; + const NiPoint3 rotation = acMessage.HasRotation ? ToDropRotation(acMessage.Rotation) : dropOpt->Rotation; + + DropManager::UpdateServerDropTransform(acMessage.DropId, location, rotation, acMessage.CellId, acMessage.WorldSpaceId, acMessage.ReferenceId); + + const auto updatedOpt = DropManager::GetServerDrop(acMessage.DropId); + if (!updatedOpt) + return; + + const auto& data = *updatedOpt; + const bool suppress = m_dropPhysicsDisableSuppressions.erase(acMessage.DropId) > 0; + if (IsDropLocallyActive(acMessage.DropId, data.ReferenceId)) + { + m_localDrops.insert(acMessage.DropId); + return; + } + if (data.Type == ServerItemType::CreationEngine) + { + TESObjectREFR* pReference = nullptr; + if (const auto handleOpt = DropManager::GetHandleForDrop(acMessage.DropId); handleOpt) + pReference = TESObjectREFR::GetByHandle(*handleOpt); + + if (!pReference && data.ReferenceId) + { + pReference = GetReferenceById(data.ReferenceId); + if (pReference) + { + auto handle = pReference->GetHandle(); + if (handle && handle.handle.iBits != 0) + DropManager::BindHandleToServerDrop(acMessage.DropId, data.ActorFormId, handle.handle.iBits); + } + } + + if (!pReference) + { + DropManager::ServerDropData rebound = data; + rebound.Location = location; + rebound.Rotation = rotation; + TryBindExistingReference(acMessage.DropId, rebound); + if (const auto handleOpt = DropManager::GetHandleForDrop(acMessage.DropId); handleOpt) + pReference = TESObjectREFR::GetByHandle(*handleOpt); + } + + if (!pReference) + return; + + if (suppress) + { + if (ShouldDeferPhysicsLock(m_cellPhysicsGraceRemaining)) + { + m_requestPhysicsLockAfterGrace = true; + } + else + { + SafeSetReferenceMotionType(pReference, MotionType::kKeyframed, true); + SafeUpdateReference3D(pReference, true); + } + m_localDrops.insert(acMessage.DropId); + return; + } + + if (acMessage.HasLocation) + { + if (TESObjectCELL* pCell = pReference->GetParentCellEx()) + pReference->SetPosition(location); + } + + if (acMessage.HasRotation) + { + if (pReference->GetParentCellEx() || pReference->GetParentCell()) + pReference->SetRotation(rotation); + } + + const bool locallyActive = IsDropLocallyActive(acMessage.DropId, data.ReferenceId); + if (!locallyActive) + { + if (ShouldDeferPhysicsLock(m_cellPhysicsGraceRemaining)) + { + m_requestPhysicsLockAfterGrace = true; + } + else + { + SafeSetReferenceMotionType(pReference, MotionType::kKeyframed, true); + SafeUpdateReference3D(pReference, true); + } + } + + m_localDrops.insert(acMessage.DropId); + return; + } + + if (!IsDropCellLoaded(data.CellId, data.WorldSpaceId)) + return; + + if (const auto handleOpt = DropManager::GetHandleForDrop(acMessage.DropId); handleOpt) + { + if (TESObjectREFR* pExisting = TESObjectREFR::GetByHandle(*handleOpt)) + { + if (suppress) + { + SafeSetReferenceMotionType(pExisting, MotionType::kKeyframed, true); + SafeUpdateReference3D(pExisting, true); + m_localDrops.insert(acMessage.DropId); + return; + } + + if (pExisting->IsTemporary()) + pExisting->Delete(); + else + pExisting->Disable(); + } + DropManager::ClearHandleBinding(acMessage.DropId); + } + + if (!SpawnLocalDrop(data, acMessage.DropId)) + return; + + m_localDrops.insert(acMessage.DropId); + + if (const auto newHandleOpt = DropManager::GetHandleForDrop(acMessage.DropId); newHandleOpt) + { + if (TESObjectREFR* pNewRef = TESObjectREFR::GetByHandle(*newHandleOpt)) + { + SafeUpdateReference3D(pNewRef, true); + SafeSetReferenceMotionType(pNewRef, MotionType::kKeyframed, true); + SafeUpdateReference3D(pNewRef, true); + GameId newRefId{}; + m_world.GetModSystem().GetServerModId(pNewRef->formID, newRefId); + if (newRefId) + DropManager::SetReferenceForDrop(acMessage.DropId, newRefId); + } + } +} + +void DropService::OnConnected(const ConnectedEvent&) noexcept +{ + EnsureStorageReady(); + if (m_suspendProcessing) + { + m_requestResyncAfterSuspend = true; + return; + } + + m_pendingCreationEngineRemovals.clear(); + m_dropSyncQueue.clear(); + m_dropSyncQueuedCells.clear(); + m_grabbedDrops.clear(); + m_dropPhysicsCooldowns.clear(); + m_dropMoveSyncTimers.clear(); + m_dropPhysicsDisableSuppressions.clear(); + m_grabbedReferences.clear(); + m_referencePhysicsCooldowns.clear(); + m_referenceMoveSyncTimers.clear(); + m_dropSyncWorldSpace = GetPlayerWorldId(); + m_dropSyncQueueAccumulator = 0.0; + m_periodicPlayerCellSyncAccumulator = 0.0; + m_grabEventSuppressionRemaining = kGrabEventSuppressSeconds; + RequestCellSync(true); + + if (m_dropSyncWorldSpace) + QueueLoadedExteriorCells(m_dropSyncWorldSpace); +} + +void DropService::OnCellChange(const CellChangeEvent& acEvent) noexcept +{ + if (m_suspendProcessing) + { + m_requestResyncAfterSuspend = true; + return; + } + + m_pendingCreationEngineRemovals.clear(); + m_grabEventSuppressionRemaining = kGrabEventSuppressSeconds; + m_grabbedDrops.clear(); + m_dropPhysicsCooldowns.clear(); + m_dropMoveSyncTimers.clear(); + m_dropPhysicsDisableSuppressions.clear(); + m_grabbedReferences.clear(); + m_referencePhysicsCooldowns.clear(); + m_referenceMoveSyncTimers.clear(); + + if (m_dropSyncWorldSpace != acEvent.WorldSpaceId) + { + m_dropSyncWorldSpace = acEvent.WorldSpaceId; + m_dropSyncQueue.clear(); + m_dropSyncQueuedCells.clear(); + m_dropSyncQueueAccumulator = 0.0; + } + + m_periodicPlayerCellSyncAccumulator = 0.0; + m_pendingDiscoveryResyncs = 2; + m_cellPhysicsGraceRemaining = kCellPhysicsGraceSeconds; + m_requestPhysicsLockAfterGrace = true; + + QueueDropSync(acEvent.CellId, acEvent.WorldSpaceId, true); +} + +void DropService::OnGridCellChange(const GridCellChangeEvent& acEvent) noexcept +{ + if (!m_transport.IsConnected()) + return; + if (m_suspendProcessing) + { + m_requestResyncAfterSuspend = true; + return; + } + + TESWorldSpace* pWorldSpace = nullptr; + if (acEvent.WorldSpaceId != 0) + { + if (TESForm* pForm = TESForm::GetById(acEvent.WorldSpaceId)) + pWorldSpace = Cast(pForm); + } + + if (!pWorldSpace) + { + if (auto* pPlayer = PlayerCharacter::Get()) + pWorldSpace = pPlayer->GetWorldSpace(); + } + + if (!pWorldSpace) + return; + + GameId worldId{}; + m_world.GetModSystem().GetServerModId(pWorldSpace->formID, worldId); + if (!worldId) + return; + + if (m_dropSyncWorldSpace != worldId) + { + m_dropSyncWorldSpace = worldId; + m_dropSyncQueue.clear(); + m_dropSyncQueuedCells.clear(); + m_dropSyncQueueAccumulator = 0.0; + } + + // Sync newly loaded cells so drops appear as soon as the cell is loaded, not only after walking into it. + for (const auto& cellId : acEvent.Cells) + { + if (!cellId) + continue; + QueueDropSync(cellId, worldId, true); + } +} + +void DropService::OnUpdate(const UpdateEvent& acEvent) noexcept +{ + if (m_suspendProcessing) + { + m_suspendProcessingAccumulator += acEvent.Delta; + auto* pPlayer = PlayerCharacter::Get(); + const bool ready = pPlayer && (pPlayer->GetParentCell() || pPlayer->parentCell) && pPlayer->GetNiNode(); + if (ready && m_suspendProcessingAccumulator >= kLoadSuspendSeconds) + { + m_suspendProcessing = false; + m_suspendProcessingAccumulator = 0.0; + m_cellPhysicsGraceRemaining = kCellPhysicsGraceSeconds; + m_requestPhysicsLockAfterGrace = true; + + if (m_requestResyncAfterSuspend && m_transport.IsConnected()) + { + RequestCellSync(true); + const GameId worldId = GetPlayerWorldId(); + if (worldId) + QueueLoadedExteriorCells(worldId); + } + m_requestResyncAfterSuspend = false; + } + return; + } + + const float delta = static_cast(acEvent.Delta); + if (m_cellPhysicsGraceRemaining > 0.0) + { + m_cellPhysicsGraceRemaining = std::max(0.0, m_cellPhysicsGraceRemaining - acEvent.Delta); + if (m_cellPhysicsGraceRemaining <= 0.0 && m_requestPhysicsLockAfterGrace) + { + TiltedPhoques::Vector dropIds(m_localDrops.begin(), m_localDrops.end()); + for (const auto dropId : dropIds) + { + const auto dropOpt = DropManager::GetServerDrop(dropId); + if (!dropOpt) + continue; + + const auto& data = *dropOpt; + if (IsDropLocallyActive(dropId, data.ReferenceId)) + continue; + + TESObjectREFR* pReference = nullptr; + if (const auto handleOpt = DropManager::GetHandleForDrop(dropId); handleOpt) + pReference = TESObjectREFR::GetByHandle(*handleOpt); + + if (!pReference && data.ReferenceId) + pReference = GetReferenceById(data.ReferenceId); + + if (!pReference || !pReference->GetNiNode()) + continue; + + SafeSetReferenceMotionType(pReference, MotionType::kKeyframed, true); + SafeUpdateReference3D(pReference, true); + } + + m_requestPhysicsLockAfterGrace = false; + } + } + if (m_grabEventSuppressionRemaining > 0.0) + { + m_grabEventSuppressionRemaining = std::max(0.0, m_grabEventSuppressionRemaining - acEvent.Delta); + } + if (!m_dropPhysicsDisableSuppressions.empty()) + { + TiltedPhoques::Vector toErase; + toErase.reserve(m_dropPhysicsDisableSuppressions.size()); + for (const auto& entry : m_dropPhysicsDisableSuppressions) + { + const float remaining = entry.second - delta; + if (remaining <= 0.f) + toErase.push_back(entry.first); + else + m_dropPhysicsDisableSuppressions[entry.first] = remaining; + } + for (const auto id : toErase) + m_dropPhysicsDisableSuppressions.erase(id); + } + + // Decrease grace timers for materialization gating + if (!g_materializeGrace.empty()) + { + TiltedPhoques::Vector toErase; + toErase.reserve(g_materializeGrace.size()); + for (const auto& kv : g_materializeGrace) + { + const uint64_t id = kv.first; + const float newValue = kv.second - delta; + if (newValue <= 0.f) + { + toErase.push_back(id); + } + else + { + g_materializeGrace[id] = newValue; + } + } + for (const auto id : toErase) + { + g_materializeGrace.erase(id); + } + } + + + ProcessPendingCreationEngineRemovals(); + + if (m_transport.IsConnected()) + { + m_periodicPlayerCellSyncAccumulator += acEvent.Delta; + + if (m_periodicPlayerCellSyncAccumulator >= kPeriodicPlayerCellSyncSeconds) + { + m_periodicPlayerCellSyncAccumulator = 0.0; + if (m_pendingDiscoveryResyncs > 0) + { + --m_pendingDiscoveryResyncs; + RequestCellSync(true); + } + else + { + RequestCellSync(false); + } + } + + m_dropSyncQueueAccumulator += acEvent.Delta; + while (m_dropSyncQueueAccumulator >= kDropSyncQueueIntervalSeconds && !m_dropSyncQueue.empty()) + { + m_dropSyncQueueAccumulator -= kDropSyncQueueIntervalSeconds; + const auto next = m_dropSyncQueue.front(); + m_dropSyncQueue.pop_front(); + m_dropSyncQueuedCells.erase(next.CellId); + auto discoveries = next.IncludeDiscovery ? BuildDiscoveryEntries(next.CellId, next.WorldSpaceId) : TiltedPhoques::Vector{}; + SendDropSyncRequest(false, true, next.CellId, static_cast(next.WorldSpaceId), next.WorldSpaceId, std::move(discoveries)); + } + } + + if (!m_dropMoveInterpolations.empty() || !m_referenceMoveInterpolations.empty()) + { + auto updateInterpolation = [&](auto& map, auto&& resolveReference) { + using MapType = std::decay_t; + using KeyType = typename MapType::key_type; + TiltedPhoques::Vector keys; + keys.reserve(map.size()); + for (const auto& entry : map) + keys.push_back(entry.first); + + TiltedPhoques::Vector toErase; + toErase.reserve(map.size()); + + for (const auto& key : keys) + { + auto& interpolation = map[key]; + TESObjectREFR* pReference = resolveReference(interpolation); + if (!pReference) + { + toErase.push_back(key); + continue; + } + + if (!CanTouchReference3D(pReference)) + { + toErase.push_back(key); + continue; + } + + if (interpolation.DropId != 0 && IsDropLocallyActive(interpolation.DropId, interpolation.ReferenceId)) + { + toErase.push_back(key); + continue; + } + + if (interpolation.Duration <= 0.f) + { + toErase.push_back(key); + continue; + } + + interpolation.Elapsed += delta; + const float duration = interpolation.Duration > 0.f ? interpolation.Duration : kDropMoveInterpolationSeconds; + const float rawT = std::min(interpolation.Elapsed / duration, 1.f); + const float t = rawT * rawT * (3.f - 2.f * rawT); + + if (interpolation.HasLocation) + { + NiPoint3 blended{}; + blended.x = interpolation.StartLocation.x + (interpolation.TargetLocation.x - interpolation.StartLocation.x) * t; + blended.y = interpolation.StartLocation.y + (interpolation.TargetLocation.y - interpolation.StartLocation.y) * t; + blended.z = interpolation.StartLocation.z + (interpolation.TargetLocation.z - interpolation.StartLocation.z) * t; + if (std::isfinite(blended.x) && std::isfinite(blended.y) && std::isfinite(blended.z) && pReference->GetParentCellEx()) + pReference->SetPosition(blended); + } + + if (interpolation.HasRotation) + { + NiPoint3 blended{}; + const float dx = NormalizeAngleDelta(interpolation.TargetRotation.x - interpolation.StartRotation.x); + const float dy = NormalizeAngleDelta(interpolation.TargetRotation.y - interpolation.StartRotation.y); + const float dz = NormalizeAngleDelta(interpolation.TargetRotation.z - interpolation.StartRotation.z); + blended.x = interpolation.StartRotation.x + dx * t; + blended.y = interpolation.StartRotation.y + dy * t; + blended.z = interpolation.StartRotation.z + dz * t; + if (std::isfinite(blended.x) && std::isfinite(blended.y) && std::isfinite(blended.z) && + (pReference->GetParentCellEx() || pReference->GetParentCell())) + pReference->SetRotation(blended); + } + + if (rawT >= 1.f) + { + if (ShouldDeferPhysicsLock(m_cellPhysicsGraceRemaining)) + { + m_requestPhysicsLockAfterGrace = true; + } + else + { + SafeSetReferenceMotionType(pReference, MotionType::kKeyframed, true); + SafeUpdateReference3D(pReference, true); + } + + toErase.push_back(key); + } + } + + for (const auto& key : toErase) + map.erase(key); + }; + + if (!m_dropMoveInterpolations.empty()) + { + updateInterpolation(m_dropMoveInterpolations, [&](const MoveInterpolation& interpolation) -> TESObjectREFR* { + if (const auto handleOpt = DropManager::GetHandleForDrop(interpolation.DropId); handleOpt) + return TESObjectREFR::GetByHandle(*handleOpt); + if (interpolation.ReferenceId) + return GetReferenceById(interpolation.ReferenceId); + return nullptr; + }); + } + + if (!m_referenceMoveInterpolations.empty()) + { + updateInterpolation(m_referenceMoveInterpolations, [&](const MoveInterpolation& interpolation) -> TESObjectREFR* { + if (interpolation.ReferenceId) + return GetReferenceById(interpolation.ReferenceId); + return nullptr; + }); + } + } + + UpdateDropPhysics(acEvent); + + if (m_pendingActions.empty()) + return; + + TiltedPhoques::Vector remaining; + remaining.reserve(m_pendingActions.size()); + for (auto& action : m_pendingActions) + { + bool applied = false; + if (action.Type == PendingType::Drop) + { + // Respect grace wait for materialization + auto it = g_materializeGrace.find(action.DropMessage.DropId); + if (it != g_materializeGrace.end() && it->second > 0.f) + { + remaining.push_back(std::move(action)); + continue; + } + + applied = ApplyDrop(action.DropMessage); + if (!applied) + ++action.RetryCounter; + } + else + { + applied = ApplyPickup(action.PickupMessage); + if (!applied) + ++action.RetryCounter; + } + + const uint32_t maxRetries = action.Type == PendingType::Drop ? kMaxPendingDropRetries : kMaxPendingPickupRetries; + if (!applied && action.RetryCounter < maxRetries) + remaining.push_back(std::move(action)); + else if (!applied) + spdlog::warn("DropService: dropping pending {} after {} retries (limit {})", action.Type == PendingType::Drop ? "drop" : "pickup", action.RetryCounter, maxRetries); + } + + m_pendingActions = std::move(remaining); + if (!m_pendingActions.empty()) + spdlog::debug("DropService: {} pending drop actions remain", m_pendingActions.size()); +} + + +BSTEventResult DropService::OnEvent(const TESGrabReleaseEvent* apEvent, const EventDispatcher*) +{ + if (!apEvent || !m_transport.IsConnected() || m_grabEventSuppressionRemaining > 0.0) + return BSTEventResult::kOk; + if (m_suspendProcessing) + return BSTEventResult::kOk; + + TESObjectREFR* pReference = apEvent->reference; + if (!pReference) + return BSTEventResult::kOk; + + const auto handle = pReference->GetHandle(); + if (!handle) + return BSTEventResult::kOk; + + TESObjectREFR* pResolved = TESObjectREFR::GetByHandle(handle.handle.iBits); + if (!pResolved) + return BSTEventResult::kOk; + + pReference = pResolved; + + GameId referenceId{}; + auto& modSystem = m_world.GetModSystem(); + modSystem.GetServerModId(pReference->formID, referenceId); + + const uint32_t handleBits = handle.handle.iBits; + auto bindDrop = [&](uint64_t dropId) -> bool { + const auto dropOpt = DropManager::GetServerDrop(dropId); + if (!dropOpt) + return false; + + if (const auto existingHandleOpt = DropManager::GetHandleForDrop(dropId); existingHandleOpt) + { + if (*existingHandleOpt != handleBits) + { + if (TESObjectREFR::GetByHandle(*existingHandleOpt)) + return false; + + DropManager::ClearHandleBinding(dropId); + } + } + + if (!DropManager::BindHandleToServerDrop(dropId, dropOpt->ActorFormId, handleBits)) + return false; + + if (referenceId) + DropManager::SetReferenceForDrop(dropId, referenceId); + + m_localDrops.insert(dropId); + return true; + }; + + auto dropIdOpt = DropManager::GetDropIdForHandle(handleBits); + if (dropIdOpt) + { + if (referenceId) + DropManager::SetReferenceForDrop(*dropIdOpt, referenceId); + m_localDrops.insert(*dropIdOpt); + } + + if (!dropIdOpt && referenceId) + { + if (const auto mappedDropId = DropManager::GetDropIdForReference(referenceId)) + { + if (bindDrop(*mappedDropId)) + dropIdOpt = mappedDropId; + } + } + + if (!dropIdOpt && pReference->baseForm && IsServerItemFormType(pReference->baseForm->formType)) + { + GameId baseId{}; + modSystem.GetServerModId(pReference->baseForm->formID, baseId); + if (baseId) + { + pReference->Update3DPosition(false); + if (const auto matchedDropId = DropManager::FindDropBySignature(baseId, pReference->position, kDropSearchRadiusSquared)) + { + if (bindDrop(*matchedDropId)) + dropIdOpt = matchedDropId; + } + } + } + + if (!dropIdOpt) + { + if (!referenceId) + return BSTEventResult::kOk; + + if (apEvent->grabbed && IsEligibleServerItemRef(pReference)) + { + if (!DropManager::GetDropIdForReference(referenceId)) + { + auto& modSystem = m_world.GetModSystem(); + GameId cellId{}; + GameId worldId{}; + if (TESObjectCELL* pCell = pReference->GetParentCellEx()) + modSystem.GetServerModId(pCell->formID, cellId); + if (TESWorldSpace* pWorld = pReference->GetWorldSpace()) + modSystem.GetServerModId(pWorld->formID, worldId); + if (cellId && !m_dropStorage.FindDropIdByRefFormId(pReference->formID, cellId, worldId)) + { + RequestDroppedItems::DiscoveryEntry entry{}; + entry.ReferenceId = referenceId; + entry.CellId = cellId; + entry.WorldSpaceId = worldId; + entry.HasLocation = true; + entry.Location = ToNetVector(pReference->position); + entry.HasRotation = true; + entry.Rotation = ToNetDropRotation(pReference->rotation); + entry.Item.Count = 1; + m_world.GetModSystem().GetServerModId(pReference->baseForm->formID, entry.Item.BaseId); + if (entry.Item.BaseId) + { + TESObjectREFR::GetItemFromExtraData(entry.Item, pReference->GetExtraDataList()); + if (entry.Item.Count == 0) + entry.Item.Count = 1; + + TiltedPhoques::Vector discoveries; + discoveries.push_back(entry); + SendDropSyncRequest(false, true, cellId, static_cast(worldId), worldId, std::move(discoveries)); + } + } + } + } + + if (apEvent->grabbed) + { + m_grabbedReferences.insert(referenceId); + m_referencePhysicsCooldowns.erase(referenceId); + SafeSetReferenceMotionType(pReference, MotionType::kDynamic, true); + } + else + { + m_grabbedReferences.erase(referenceId); + m_referencePhysicsCooldowns[referenceId] = kDropPhysicsHoldSeconds; + SafeSetReferenceMotionType(pReference, MotionType::kDynamic, true); + } + + return BSTEventResult::kOk; + } + + if (!referenceId) + { + modSystem.GetServerModId(pReference->formID, referenceId); + } + if (referenceId) + { + m_grabbedReferences.erase(referenceId); + m_referencePhysicsCooldowns.erase(referenceId); + m_referenceMoveSyncTimers.erase(referenceId); + } + + m_localDrops.insert(*dropIdOpt); + if (apEvent->grabbed) + { + m_grabbedDrops.insert(*dropIdOpt); + m_dropPhysicsCooldowns.erase(*dropIdOpt); + SafeSetReferenceMotionType(pReference, MotionType::kDynamic, true); + } + else + { + m_grabbedDrops.erase(*dropIdOpt); + m_dropPhysicsCooldowns[*dropIdOpt] = kDropPhysicsHoldSeconds; + SafeSetReferenceMotionType(pReference, MotionType::kDynamic, true); + } + + return BSTEventResult::kOk; +} + +void DropService::UpdateDropPhysics(const UpdateEvent& acEvent) noexcept +{ + if (!m_transport.IsConnected()) + return; + + const float delta = static_cast(acEvent.Delta); + + for (const auto dropId : m_localDrops) + { + const auto handleOpt = DropManager::GetHandleForDrop(dropId); + if (!handleOpt) + continue; + + TESObjectREFR* pReference = TESObjectREFR::GetByHandle(*handleOpt); + if (!pReference) + continue; + + if (m_grabbedDrops.find(dropId) != m_grabbedDrops.end()) + { + SafeSetReferenceMotionType(pReference, MotionType::kDynamic, true); + m_dropMoveSyncTimers[dropId] += delta; + if (m_dropMoveSyncTimers[dropId] >= kDropMoveSyncIntervalSeconds) + { + const float elapsed = m_dropMoveSyncTimers[dropId]; + m_dropMoveSyncTimers[dropId] = 0.f; + SendDropMoveRequest(dropId, pReference, true, elapsed); + } + continue; + } + + if (auto cooldownIt = m_dropPhysicsCooldowns.find(dropId); cooldownIt != m_dropPhysicsCooldowns.end()) + { + const float remaining = cooldownIt->second - delta; + if (remaining <= 0.f) + { + m_dropPhysicsCooldowns.erase(cooldownIt); + float elapsed = 0.f; + if (const auto it = m_dropMoveSyncTimers.find(dropId); it != m_dropMoveSyncTimers.end()) + elapsed = it->second; + m_dropMoveSyncTimers.erase(dropId); + SendDropMoveRequest(dropId, pReference, false, elapsed); + SendDropPhysicsDisabledRequest(dropId, pReference); + m_dropMoveLastLocations.erase(dropId); + m_dropMoveLastRotations.erase(dropId); + SafeSetReferenceMotionType(pReference, MotionType::kKeyframed, true); + SafeUpdateReference3D(pReference, true); + } + else + { + m_dropPhysicsCooldowns[dropId] = remaining; + SafeSetReferenceMotionType(pReference, MotionType::kDynamic, true); + m_dropMoveSyncTimers[dropId] += delta; + if (m_dropMoveSyncTimers[dropId] >= kDropMoveSyncIntervalSeconds) + { + const float elapsed = m_dropMoveSyncTimers[dropId]; + m_dropMoveSyncTimers[dropId] = 0.f; + SendDropMoveRequest(dropId, pReference, true, elapsed); + } + } + + continue; + } + + m_dropMoveSyncTimers.erase(dropId); + m_dropMoveLastLocations.erase(dropId); + m_dropMoveLastRotations.erase(dropId); + SafeSetReferenceMotionType(pReference, MotionType::kKeyframed, true); + continue; + } + + if (!m_grabbedReferences.empty()) + { + TiltedPhoques::Vector grabbedRefs; + grabbedRefs.reserve(m_grabbedReferences.size()); + for (const auto& refId : m_grabbedReferences) + grabbedRefs.push_back(refId); + + for (const auto& referenceId : grabbedRefs) + { + if (TESObjectREFR* pReference = GetReferenceById(referenceId)) + { + SafeSetReferenceMotionType(pReference, MotionType::kDynamic, true); + m_referenceMoveSyncTimers[referenceId] += delta; + if (m_referenceMoveSyncTimers[referenceId] >= kDropMoveSyncIntervalSeconds) + { + const float elapsed = m_referenceMoveSyncTimers[referenceId]; + m_referenceMoveSyncTimers[referenceId] = 0.f; + SendReferenceMoveRequest(referenceId, pReference, elapsed); + } + } + } + } + + if (m_referencePhysicsCooldowns.empty()) + return; + + TiltedPhoques::Vector readyReferences; + readyReferences.reserve(m_referencePhysicsCooldowns.size()); + TiltedPhoques::Vector> pendingUpdates; + pendingUpdates.reserve(m_referencePhysicsCooldowns.size()); + for (const auto& entry : m_referencePhysicsCooldowns) + { + const float remaining = entry.second - delta; + if (remaining <= 0.f) + readyReferences.push_back(entry.first); + else + { + pendingUpdates.emplace_back(entry.first, remaining); + if (TESObjectREFR* pReference = GetReferenceById(entry.first)) + { + SafeSetReferenceMotionType(pReference, MotionType::kDynamic, true); + m_referenceMoveSyncTimers[entry.first] += delta; + if (m_referenceMoveSyncTimers[entry.first] >= kDropMoveSyncIntervalSeconds) + { + const float elapsed = m_referenceMoveSyncTimers[entry.first]; + m_referenceMoveSyncTimers[entry.first] = 0.f; + SendReferenceMoveRequest(entry.first, pReference, elapsed); + } + } + } + } + + for (const auto& update : pendingUpdates) + { + m_referencePhysicsCooldowns[update.first] = update.second; + } + + for (const auto& referenceId : readyReferences) + { + m_referencePhysicsCooldowns.erase(referenceId); + float elapsed = 0.f; + if (const auto it = m_referenceMoveSyncTimers.find(referenceId); it != m_referenceMoveSyncTimers.end()) + elapsed = it->second; + m_referenceMoveSyncTimers.erase(referenceId); + if (TESObjectREFR* pReference = GetReferenceById(referenceId)) + { + SendReferenceMoveRequest(referenceId, pReference, elapsed); + SafeSetReferenceMotionType(pReference, MotionType::kKeyframed, true); + SafeUpdateReference3D(pReference, true); + } + + m_referenceMoveLastLocations.erase(referenceId); + m_referenceMoveLastRotations.erase(referenceId); + } +} + +void DropService::SendDropMoveRequest(uint64_t aDropId, TESObjectREFR* apReference, bool aForce, float aElapsedSeconds) noexcept +{ + if (!m_transport.IsConnected() || !apReference) + return; + + const auto dropOpt = DropManager::GetServerDrop(aDropId); + if (!dropOpt) + return; + + auto* pPlayer = PlayerCharacter::Get(); + if (!pPlayer) + return; + + const auto serverIdRes = ResolveServerId(pPlayer->formID); + if (!serverIdRes) + return; + + SafeUpdateReference3D(apReference, false); + + auto& modSystem = m_world.GetModSystem(); + GameId cellId{}; + GameId worldId{}; + if (TESObjectCELL* pCell = apReference->GetParentCellEx()) + modSystem.GetServerModId(pCell->formID, cellId); + if (TESWorldSpace* pWorld = apReference->GetWorldSpace()) + modSystem.GetServerModId(pWorld->formID, worldId); + + GameId referenceId{}; + modSystem.GetServerModId(apReference->formID, referenceId); + + const NiPoint3 location = apReference->position; + const NiPoint3 rotation = apReference->rotation; + const float dx = location.x - dropOpt->Location.x; + const float dy = location.y - dropOpt->Location.y; + const float dz = location.z - dropOpt->Location.z; + const float distSq = dx * dx + dy * dy + dz * dz; + constexpr float kMoveEpsilonSq = 0.5f * 0.5f; + const float rotDx = rotation.x - dropOpt->Rotation.x; + const float rotDy = rotation.y - dropOpt->Rotation.y; + const float rotDz = rotation.z - dropOpt->Rotation.z; + const float rotDistSq = rotDx * rotDx + rotDy * rotDy + rotDz * rotDz; + constexpr float kRotEpsilonSq = 0.5f * 0.5f; + + if (!aForce && distSq <= kMoveEpsilonSq && rotDistSq <= kRotEpsilonSq) + return; + + bool hasVelocity = false; + NiPoint3 velocity{}; + bool hasAngularVelocity = false; + NiPoint3 angularVelocity{}; + if (aElapsedSeconds > 0.f) + { + if (const auto it = m_dropMoveLastLocations.find(aDropId); it != m_dropMoveLastLocations.end()) + { + velocity.x = (location.x - it->second.x) / aElapsedSeconds; + velocity.y = (location.y - it->second.y) / aElapsedSeconds; + velocity.z = (location.z - it->second.z) / aElapsedSeconds; + hasVelocity = true; + } + if (const auto it = m_dropMoveLastRotations.find(aDropId); it != m_dropMoveLastRotations.end()) + { + angularVelocity.x = NormalizeAngleDelta(rotation.x - it->second.x) / aElapsedSeconds; + angularVelocity.y = NormalizeAngleDelta(rotation.y - it->second.y) / aElapsedSeconds; + angularVelocity.z = NormalizeAngleDelta(rotation.z - it->second.z) / aElapsedSeconds; + hasAngularVelocity = true; + } + } + + m_dropMoveLastLocations[aDropId] = location; + m_dropMoveLastRotations[aDropId] = rotation; + + DropManager::UpdateServerDropTransform(aDropId, location, rotation, cellId, worldId, referenceId, hasVelocity, velocity); + + RequestDroppedItemMove request{}; + request.ServerId = *serverIdRes; + request.DropId = aDropId; + request.HasLocation = true; + request.Location = ToNetVector(location); + request.HasRotation = true; + request.Rotation = ToNetDropRotation(rotation); + request.HasVelocity = hasVelocity; + if (hasVelocity) + request.Velocity = ToNetVector(velocity); + request.HasAngularVelocity = hasAngularVelocity; + if (hasAngularVelocity) + request.AngularVelocity = ToNetVector(angularVelocity); + request.CellId = cellId; + request.WorldSpaceId = worldId; + request.ReferenceId = referenceId; + + m_transport.Send(request); +} + +void DropService::SendDropPhysicsDisabledRequest(uint64_t aDropId, TESObjectREFR* apReference) noexcept +{ + if (!m_transport.IsConnected() || !apReference) + return; + + const auto dropOpt = DropManager::GetServerDrop(aDropId); + if (!dropOpt) + return; + + auto* pPlayer = PlayerCharacter::Get(); + if (!pPlayer) + return; + + const auto serverIdRes = ResolveServerId(pPlayer->formID); + if (!serverIdRes) + return; + + apReference->Update3DPosition(false); + + auto& modSystem = m_world.GetModSystem(); + GameId cellId{}; + GameId worldId{}; + if (TESObjectCELL* pCell = apReference->GetParentCellEx()) + modSystem.GetServerModId(pCell->formID, cellId); + if (TESWorldSpace* pWorld = apReference->GetWorldSpace()) + modSystem.GetServerModId(pWorld->formID, worldId); + + GameId referenceId{}; + modSystem.GetServerModId(apReference->formID, referenceId); + + RequestDroppedItemPhysicsDisabled request{}; + request.ServerId = *serverIdRes; + request.DropId = aDropId; + request.HasLocation = true; + request.Location = ToNetVector(apReference->position); + request.HasRotation = true; + request.Rotation = ToNetDropRotation(apReference->rotation); + request.CellId = cellId; + request.WorldSpaceId = worldId; + request.ReferenceId = referenceId; + + m_transport.Send(request); +} + +void DropService::SendReferenceMoveRequest(const GameId& acReferenceId, TESObjectREFR* apReference, float aElapsedSeconds) noexcept +{ + if (!m_transport.IsConnected() || !apReference) + return; + + auto* pPlayer = PlayerCharacter::Get(); + if (!pPlayer) + return; + + const auto serverIdRes = ResolveServerId(pPlayer->formID); + if (!serverIdRes) + return; + + apReference->Update3DPosition(false); + + auto& modSystem = m_world.GetModSystem(); + GameId cellId{}; + GameId worldId{}; + if (TESObjectCELL* pCell = apReference->GetParentCellEx()) + modSystem.GetServerModId(pCell->formID, cellId); + if (TESWorldSpace* pWorld = apReference->GetWorldSpace()) + modSystem.GetServerModId(pWorld->formID, worldId); + + const NiPoint3 location = apReference->position; + const NiPoint3 rotation = apReference->rotation; + bool hasVelocity = false; + NiPoint3 velocity{}; + bool hasAngularVelocity = false; + NiPoint3 angularVelocity{}; + if (aElapsedSeconds > 0.f) + { + if (const auto it = m_referenceMoveLastLocations.find(acReferenceId); it != m_referenceMoveLastLocations.end()) + { + velocity.x = (location.x - it->second.x) / aElapsedSeconds; + velocity.y = (location.y - it->second.y) / aElapsedSeconds; + velocity.z = (location.z - it->second.z) / aElapsedSeconds; + hasVelocity = true; + } + if (const auto it = m_referenceMoveLastRotations.find(acReferenceId); it != m_referenceMoveLastRotations.end()) + { + angularVelocity.x = NormalizeAngleDelta(rotation.x - it->second.x) / aElapsedSeconds; + angularVelocity.y = NormalizeAngleDelta(rotation.y - it->second.y) / aElapsedSeconds; + angularVelocity.z = NormalizeAngleDelta(rotation.z - it->second.z) / aElapsedSeconds; + hasAngularVelocity = true; + } + } + + m_referenceMoveLastLocations[acReferenceId] = location; + m_referenceMoveLastRotations[acReferenceId] = rotation; + + RequestDroppedItemMove request{}; + request.ServerId = *serverIdRes; + request.DropId = 0; + request.HasLocation = true; + request.Location = ToNetVector(location); + request.HasRotation = true; + request.Rotation = ToNetDropRotation(rotation); + request.HasVelocity = hasVelocity; + if (hasVelocity) + request.Velocity = ToNetVector(velocity); + request.HasAngularVelocity = hasAngularVelocity; + if (hasAngularVelocity) + request.AngularVelocity = ToNetVector(angularVelocity); + request.CellId = cellId; + request.WorldSpaceId = worldId; + request.ReferenceId = acReferenceId; + + m_transport.Send(request); +} + +std::optional DropService::ResolveServerId(uint32_t aFormId) const noexcept +{ + auto view = m_world.view(); + + const auto it = std::find_if(std::begin(view), std::end(view), [view, formId = aFormId](auto entity) { return view.get(entity).Id == formId; }); + if (it != std::end(view)) + return Utils::GetServerId(*it); + + if (auto* pPlayer = PlayerCharacter::Get(); pPlayer && pPlayer->formID == aFormId) + { + const uint32_t playerId = m_transport.GetLocalPlayerId(); + if (playerId) + return playerId; + } + + return std::nullopt; +} + +bool DropService::EnsureActorReady(Actor* apActor, const char* apContext) const noexcept +{ + if (!apActor) + return false; + + if (!apActor->GetNiNode()) + { + spdlog::debug("{}: actor {:X} missing 3D during {}", __FUNCTION__, apActor->formID, apContext); + return false; + } + + if (ExtraContainerChanges::Data* pContainerChanges = apActor->GetContainerChanges(); + !pContainerChanges || !pContainerChanges->entries) + { + spdlog::debug("{}: actor {:X} missing container data during {}", __FUNCTION__, apActor->formID, apContext); + return false; + } + + return true; +} + +bool DropService::EnsureStorageReady() noexcept +{ + std::string username = m_transport.GetLoginUsername(); + if (username.empty()) + username = "default"; + + if (username != m_cachedUsername) + { + m_cachedUsername = username; + m_coSaveService.PrepareForUser(username); + } + + m_dropStorage.PrepareInMemory(); + spdlog::info("DropService: storage ready=true (user='{}')", username); + return true; +} + +uint32_t DropService::SendDropSyncRequest(bool aRequestAll, bool aHasCellFilter, const GameId& acCellId, bool aHasWorldFilter, const GameId& acWorldId, TiltedPhoques::Vector aDiscoveries) noexcept +{ + if (!m_transport.IsConnected()) + return 0; + if (m_suspendProcessing) + { + m_requestResyncAfterSuspend = true; + return 0; + } + + RequestDroppedItems request{}; + request.RequestId = m_nextDropSyncRequestId++; + if (request.RequestId == 0) + request.RequestId = m_nextDropSyncRequestId++; + + request.RequestAll = aRequestAll; + request.HasCellFilter = aHasCellFilter; + if (aHasCellFilter) + request.CellId = acCellId; + request.HasWorldSpaceFilter = aHasWorldFilter; + if (aHasWorldFilter) + request.WorldSpaceId = acWorldId; + request.Discoveries = std::move(aDiscoveries); + + m_transport.Send(request); + + DropSyncContext context{}; + context.IsFullSync = aRequestAll; + if (aHasCellFilter) + context.CellId = acCellId; + if (aHasWorldFilter) + context.WorldSpaceId = acWorldId; + + if (request.RequestId != 0) + m_pendingDropSyncs[request.RequestId] = context; + + spdlog::debug("DropService: requested drop sync {}, all={}, cell {:X}:{:X}", request.RequestId, aRequestAll, context.CellId.ModId, context.CellId.BaseId); + return request.RequestId; +} + +void DropService::QueueDropSync(const GameId& acCellId, const GameId& acWorldId, bool aIncludeDiscovery) noexcept +{ + if (!acCellId) + return; + + if (m_dropSyncQueuedCells.find(acCellId) != std::end(m_dropSyncQueuedCells)) + { + if (aIncludeDiscovery) + { + for (auto& queued : m_dropSyncQueue) + { + if (queued.CellId == acCellId) + { + queued.IncludeDiscovery = true; + break; + } + } + } + return; + } + + QueuedDropSync queued{}; + queued.CellId = acCellId; + queued.WorldSpaceId = acWorldId; + queued.IncludeDiscovery = aIncludeDiscovery; + + m_dropSyncQueue.push_back(queued); + m_dropSyncQueuedCells.insert(acCellId); +} + +void DropService::QueueLoadedExteriorCells(const GameId& acWorldId) noexcept +{ + if (!acWorldId) + return; + + TES* pTes = TES::Get(); + if (!pTes || !pTes->cells || !pTes->cells->arr) + return; + + const int dimension = pTes->cells->dimension; + if (dimension <= 0) + return; + + const int cellCount = dimension * dimension; + for (int i = 0; i < cellCount; ++i) + { + TESObjectCELL* pCell = pTes->cells->arr[i]; + if (!pCell) + continue; + + GameId cellId{}; + if (!m_world.GetModSystem().GetServerModId(pCell->formID, cellId) || !cellId) + continue; + + QueueDropSync(cellId, acWorldId, true); + } +} + +void DropService::HandleDropSyncResponse(const NotifyDroppedItems& acMessage) noexcept +{ + std::optional context{}; + if (acMessage.RequestId != 0) + { + auto it = m_pendingDropSyncs.find(acMessage.RequestId); + if (it != m_pendingDropSyncs.end()) + { + context = it->second; + m_pendingDropSyncs.erase(it); + } + } + + TiltedPhoques::Vector serverDropIds; + serverDropIds.reserve(acMessage.Entries.size()); + + for (const auto& entry : acMessage.Entries) + { + const bool forceMaterialize = context && !context->IsFullSync; + auto& knownEpoch = m_knownSpawnEpochs[entry.DropId]; + if (entry.SpawnEpoch > knownEpoch) + knownEpoch = entry.SpawnEpoch; + ProcessDropEntry(entry, forceMaterialize); + serverDropIds.push_back(entry.DropId); + } + + if (context && !context->IsFullSync) + { + ReconcileCachedDrops(context->CellId, context->WorldSpaceId, serverDropIds); + ApplyCreationEngineCellSync(*context, acMessage.CreationEnginePickedUpReferences); + } + else if (context && context->IsFullSync) + { + TiltedPhoques::Map liveIds; + for (auto id : serverDropIds) + liveIds[id] = true; + + auto cached = m_dropStorage.GetAllDrops(); + for (const auto& entry : cached) + { + if (liveIds.find(entry.DropId) != std::end(liveIds)) + continue; + + bool removed = false; + if (entry.ReferenceId) + removed = RemoveReferenceById(entry.ReferenceId, "full sync stale reference"); + if (entry.RefFormId && !removed) + { + if (auto* pForm = TESForm::GetById(entry.RefFormId)) + { + if (auto* pRef = Cast(pForm)) + { + pRef->Delete(); + removed = true; + } + } + } + + if (!removed) + { + if (TESBoundObject* pObject = ResolveDroppedObject(entry.Item)) + { + if (TESObjectREFR* pRef = FindReferenceNear(pObject, entry.Location)) + { + pRef->Delete(); + removed = true; + } + } + } + + if (removed) + spdlog::warn("DropService: removed stale cached drop {} during full sync", entry.DropId); + + m_dropStorage.RemoveCachedDrop(entry.DropId); + } + } +} + +void DropService::ProcessDropEntry(const NotifyDroppedItems::Entry& acEntry, bool aForceMaterialize) noexcept +{ + DropManager::ServerDropData data{}; + data.ServerId = acEntry.ServerId; + data.ActorFormId = acEntry.ActorFormId; + data.Type = acEntry.Type; + data.Item = acEntry.Item; + data.Location = acEntry.HasLocation ? ToPoint(acEntry.Location) : NiPoint3{}; + data.Rotation = acEntry.HasRotation ? ToDropRotation(acEntry.Rotation) : NiPoint3{}; + data.HandleBits = 0; + data.CellId = acEntry.CellId; + data.WorldSpaceId = acEntry.WorldSpaceId; + data.ReferenceId = acEntry.ReferenceId; + + DropManager::TrackServerDrop(acEntry.DropId, data); + + if (const auto handleOpt = DropManager::GetHandleForDrop(acEntry.DropId); handleOpt && TESObjectREFR::GetByHandle(*handleOpt)) + { + if (TESObjectREFR* pReference = TESObjectREFR::GetByHandle(*handleOpt)) + { + const bool locallyActive = IsDropLocallyActive(acEntry.DropId, data.ReferenceId); + if (!locallyActive && HasDropLocation(data.Location) && IsDropCellLoaded(data.CellId, data.WorldSpaceId)) + { + TESObjectCELL* pCell = nullptr; + if (data.CellId) + { + const uint32_t cellFormId = m_world.GetModSystem().GetGameId(data.CellId); + if (cellFormId) + pCell = Cast(TESForm::GetById(cellFormId)); + } + if (!pCell) + pCell = pReference->GetParentCellEx(); + const auto* pCurrentCell = pReference->GetParentCellEx(); + if (!pCell && !pCurrentCell) + { + spdlog::debug("DropService: skip transform for drop {} (reference missing parent cell)", acEntry.DropId); + } + else + { + if (pCell && (!pCurrentCell || pCell != pCurrentCell)) + pReference->MoveTo(pCell, data.Location); + else if (pCurrentCell) + pReference->SetPosition(data.Location); + pReference->SetRotation(data.Rotation); + if (pReference->GetNiNode()) + SafeUpdateReference3D(pReference, true); + } + } + if (!locallyActive && pReference->GetNiNode()) + { + if (ShouldDeferPhysicsLock(m_cellPhysicsGraceRemaining)) + m_requestPhysicsLockAfterGrace = true; + else + { + SafeSetReferenceMotionType(pReference, MotionType::kKeyframed, true); + SafeUpdateReference3D(pReference, true); + } + } + } + m_localDrops.insert(acEntry.DropId); + spdlog::debug("DropService: skip drop {} from sync (already present locally)", acEntry.DropId); + return; + } + + if (!MaterializeDrop(acEntry.DropId, data, aForceMaterialize)) + { + if (data.Type == ServerItemType::CreationEngine) + { + spdlog::debug("DropService: creation-engine item {} missing locally, deferring bind", acEntry.DropId); + return; + } + + if (!aForceMaterialize && g_materializeGrace.find(acEntry.DropId) == std::end(g_materializeGrace)) + g_materializeGrace[acEntry.DropId] = kMaterializeGraceSeconds; + + PendingAction pending{}; + pending.Type = PendingType::Drop; + pending.DropMessage.DropId = acEntry.DropId; + pending.DropMessage.ServerId = acEntry.ServerId; + pending.DropMessage.ActorFormId = acEntry.ActorFormId; + pending.DropMessage.Item = acEntry.Item; + pending.DropMessage.HasLocation = acEntry.HasLocation; + if (acEntry.HasLocation) + pending.DropMessage.Location = acEntry.Location; + pending.DropMessage.HasRotation = acEntry.HasRotation; + if (acEntry.HasRotation) + pending.DropMessage.Rotation = acEntry.Rotation; + pending.DropMessage.CellId = acEntry.CellId; + pending.DropMessage.WorldSpaceId = acEntry.WorldSpaceId; + pending.DropMessage.SpawnEpoch = acEntry.SpawnEpoch; + m_pendingActions.push_back(std::move(pending)); + spdlog::debug("DropService: queued drop {} for delayed materialization", acEntry.DropId); + } +} + +bool DropService::MaterializeDrop(uint64_t aDropId, const DropManager::ServerDropData& acData, bool aForce) noexcept +{ + if (const auto handleOpt = DropManager::GetHandleForDrop(aDropId); handleOpt) + { + if (TESObjectREFR::GetByHandle(*handleOpt)) + return true; + + spdlog::debug("DropService: stale handle {:X} for drop {}, clearing and re-materializing", *handleOpt, aDropId); + DropManager::ClearHandleBinding(aDropId); + m_localDrops.erase(aDropId); + } + + if (TryBindExistingReference(aDropId, acData)) + return true; + + if (acData.Type == ServerItemType::CreationEngine) + return false; + + if (m_materializingDrops.find(aDropId) != std::end(m_materializingDrops)) + { + spdlog::debug("DropService: drop {} already materializing, skipping duplicate spawn", aDropId); + return true; + } + + const bool hasLocation = HasDropLocation(acData.Location); + if (!hasLocation) + return false; + + if (!IsDropCellLoaded(acData.CellId, acData.WorldSpaceId)) + return false; + + if (!aForce) + { + auto itTimer = g_materializeGrace.find(aDropId); + if (itTimer == std::end(g_materializeGrace)) + { + g_materializeGrace[aDropId] = kMaterializeGraceSeconds; + + PendingAction pending{}; + pending.Type = PendingType::Drop; + pending.DropMessage.DropId = aDropId; + pending.DropMessage.ServerId = acData.ServerId; + pending.DropMessage.ActorFormId = acData.ActorFormId; + pending.DropMessage.Item = acData.Item; + pending.DropMessage.HasLocation = true; + pending.DropMessage.Location = ToNetVector(acData.Location); + pending.DropMessage.HasRotation = true; + pending.DropMessage.Rotation = ToNetDropRotation(acData.Rotation); + pending.DropMessage.CellId = acData.CellId; + pending.DropMessage.WorldSpaceId = acData.WorldSpaceId; + pending.DropMessage.ReferenceId = acData.ReferenceId; + pending.DropMessage.SpawnEpoch = (m_knownSpawnEpochs.find(aDropId) != std::end(m_knownSpawnEpochs)) ? m_knownSpawnEpochs[aDropId] : 0; + m_pendingActions.push_back(std::move(pending)); + + spdlog::debug("DropService: scheduled grace wait before spawning drop {}", aDropId); + return false; + } + + if (itTimer->second > 0.f) + return false; + } + + m_materializingDrops.insert(aDropId); + const bool spawned = SpawnLocalDrop(acData, aDropId); + m_materializingDrops.erase(aDropId); + + if (!spawned) + { + spdlog::warn("DropService: failed to materialize drop {} in cell {:X}:{:X}", aDropId, acData.CellId.ModId, acData.CellId.BaseId); + return false; + } + + m_localDrops.insert(aDropId); + return true; +} + +bool DropService::SpawnLocalDrop(const DropManager::ServerDropData& acData, uint64_t aDropId) noexcept +{ + PlayerCharacter* pPlayer = PlayerCharacter::Get(); + if (!pPlayer) + return false; + + if (!EnsureActorReady(pPlayer, "materialize drop")) + return false; + + TESBoundObject* pObject = ResolveDroppedObject(acData.Item); + if (!pObject) + return false; + + TESObjectCELL* pCell = nullptr; + if (acData.CellId) + { + const uint32_t cellFormId = m_world.GetModSystem().GetGameId(acData.CellId); + if (cellFormId) + { + TES* pTes = TES::Get(); + if (pTes && pTes->cells && pTes->cells->arr) + { + const int dimension = pTes->cells->dimension; + if (dimension > 0) + { + const int cellCount = dimension * dimension; + for (int i = 0; i < cellCount; ++i) + { + TESObjectCELL* pCandidate = pTes->cells->arr[i]; + if (pCandidate && pCandidate->formID == cellFormId) + { + pCell = pCandidate; + break; + } + } + } + } + + if (!pCell) + { + TESObjectCELL* pInteriorCell = pTes ? pTes->interiorCell : nullptr; + if (pInteriorCell && pInteriorCell->formID == cellFormId) + pCell = pInteriorCell; + } + } + } + + if (!pCell) + { + TESObjectCELL* pPlayerCell = pPlayer->parentCell ? pPlayer->parentCell : pPlayer->GetParentCell(); + if (pPlayerCell) + { + GameId playerCellId{}; + m_world.GetModSystem().GetServerModId(pPlayerCell->formID, playerCellId); + if (playerCellId == acData.CellId) + pCell = pPlayerCell; + } + } + + if (!pCell) + return false; + + TESWorldSpace* pWorldSpace = pPlayer->GetWorldSpace(); + if (acData.WorldSpaceId) + { + const uint32_t worldFormId = m_world.GetModSystem().GetGameId(acData.WorldSpaceId); + if (worldFormId) + { + if (TESForm* pWorldForm = TESForm::GetById(worldFormId)) + { + if (TESWorldSpace* pResolvedWorld = Cast(pWorldForm)) + pWorldSpace = pResolvedWorld; + } + } + } + NiPoint3 dropLocation = acData.Location; + NiPoint3 dropRotation = acData.Rotation; + + const uint32_t handleBits = ModManager::Get()->SpawnReference(pObject, dropLocation, dropRotation, pCell, pWorldSpace, nullptr, false); + if (handleBits == 0) + return false; + + TESObjectREFR* pReference = TESObjectREFR::GetByHandle(handleBits); + if (!pReference) + return false; + + Inventory::Entry extraEntry = acData.Item; + extraEntry.ExtraWorn = false; + extraEntry.ExtraWornLeft = false; + const uint16_t dropCount = ClampExtraCount(extraEntry.Count); + ApplyDropExtraData(pReference, extraEntry, dropCount); + if (ShouldDeferPhysicsLock(m_cellPhysicsGraceRemaining)) + { + m_requestPhysicsLockAfterGrace = true; + } + else + { + SafeSetReferenceMotionType(pReference, MotionType::kKeyframed, true); + SafeUpdateReference3D(pReference, true); + } + + DropManager::BindHandleToServerDrop(aDropId, acData.ActorFormId, handleBits); + + spdlog::debug("DropService: spawned local drop {} at ({:.2f}, {:.2f}, {:.2f}) handle {:X}", aDropId, dropLocation.x, dropLocation.y, dropLocation.z, handleBits); + return true; +} + +bool DropService::RemoveNearbyReference(uint64_t aDropId, const char* apReason, float aRadiusSq) noexcept +{ + const auto dropOpt = DropManager::GetServerDrop(aDropId); + if (!dropOpt) + return false; + + TESBoundObject* pObject = ResolveDroppedObject(dropOpt->Item); + if (!pObject) + return false; + + if (TESObjectREFR* pRef = FindReferenceNear(pObject, dropOpt->Location, aRadiusSq)) + { + if (pRef->IsTemporary()) + pRef->Delete(); + else + pRef->Disable(); + spdlog::warn("DropService: {} -> removed fallback drop {} ({:X}:{:X})", apReason ? apReason : "cleanup", aDropId, dropOpt->Item.BaseId.ModId, dropOpt->Item.BaseId.BaseId); + return true; + } + + spdlog::debug("DropService: {} fallback failed for drop {}", apReason ? apReason : "cleanup", aDropId); + return false; +} + +TESObjectREFR* DropService::GetReferenceById(const GameId& acReferenceId) noexcept +{ + if (!acReferenceId) + return nullptr; + + auto& modSystem = m_world.GetModSystem(); + const uint32_t formId = modSystem.GetGameId(acReferenceId); + if (!formId) + return nullptr; + + TESForm* pForm = TESForm::GetById(formId); + return Cast(pForm); +} + +bool DropService::RemoveReferenceById(const GameId& acReferenceId, const char* apReason) noexcept +{ + TESObjectREFR* pReference = GetReferenceById(acReferenceId); + if (!pReference) + return false; + + if (pReference->IsTemporary()) + pReference->Delete(); + else + pReference->Disable(); + spdlog::debug("DropService: {} -> removed reference {:X}:{:X}", apReason ? apReason : "cleanup", acReferenceId.ModId, acReferenceId.BaseId); + return true; +} + +bool DropService::RemoveReferenceByLocation(const Inventory::Entry& acItem, const Vector3_NetQuantize& acLocation, const char* apReason, float aRadiusSq) noexcept +{ + TESBoundObject* pObject = ResolveDroppedObject(acItem); + if (!pObject) + return false; + + const NiPoint3 location = ToPoint(acLocation); + if (TESObjectREFR* pRef = FindReferenceNear(pObject, location, aRadiusSq)) + { + if (pRef->IsTemporary()) + pRef->Delete(); + else + pRef->Disable(); + spdlog::debug("DropService: {} -> removed reference near ({:.2f}, {:.2f}, {:.2f}) for item {:X}:{:X}", apReason ? apReason : "cleanup", location.x, location.y, location.z, acItem.BaseId.ModId, + acItem.BaseId.BaseId); + return true; + } + + spdlog::debug("DropService: {} failed to remove reference by location for item {:X}:{:X}", apReason ? apReason : "cleanup", acItem.BaseId.ModId, acItem.BaseId.BaseId); + return false; +} + +bool DropService::TryBindExistingReference(uint64_t aDropId, const DropManager::ServerDropData& acData) noexcept +{ + auto tryBind = [&](TESObjectREFR* apReference) -> bool { + if (!apReference) + return false; + + auto handle = apReference->GetHandle(); + if (!handle || handle.handle.iBits == 0) + return false; + + if (!DropManager::BindHandleToServerDrop(aDropId, acData.ActorFormId, handle.handle.iBits)) + return false; + + GameId referenceId = acData.ReferenceId; + if (!referenceId) + { + auto& modSystem = m_world.GetModSystem(); + modSystem.GetServerModId(apReference->formID, referenceId); + } + + if (referenceId) + DropManager::SetReferenceForDrop(aDropId, referenceId); + + m_localDrops.insert(aDropId); + + if (referenceId) + { + if (m_grabbedReferences.erase(referenceId) > 0) + { + m_referenceMoveSyncTimers.erase(referenceId); + m_grabbedDrops.insert(aDropId); + } + + if (const auto cooldownIt = m_referencePhysicsCooldowns.find(referenceId); cooldownIt != m_referencePhysicsCooldowns.end()) + { + const float remaining = cooldownIt->second; + m_referencePhysicsCooldowns.erase(cooldownIt); + m_referenceMoveSyncTimers.erase(referenceId); + m_dropPhysicsCooldowns[aDropId] = remaining; + } + } + + const bool locallyActive = IsDropLocallyActive(aDropId, referenceId); + const bool cellLoaded = IsDropCellLoaded(acData.CellId, acData.WorldSpaceId); + if (!locallyActive && HasDropLocation(acData.Location) && cellLoaded) + { + TESObjectCELL* pCell = nullptr; + if (acData.CellId) + { + const uint32_t cellFormId = m_world.GetModSystem().GetGameId(acData.CellId); + if (cellFormId) + pCell = Cast(TESForm::GetById(cellFormId)); + } + if (!pCell) + pCell = apReference->GetParentCellEx(); + const auto* pCurrentCell = apReference->GetParentCellEx(); + if (!pCell && !pCurrentCell) + { + spdlog::debug("DropService: skip transform for drop {} (reference missing parent cell)", aDropId); + } + else + { + if (pCell && (!pCurrentCell || pCell != pCurrentCell)) + apReference->MoveTo(pCell, acData.Location); + else if (pCurrentCell) + apReference->SetPosition(acData.Location); + apReference->SetRotation(acData.Rotation); + if (apReference->GetNiNode()) + SafeUpdateReference3D(apReference, true); + } + } + + if (!locallyActive) + { + if (apReference->GetNiNode()) + { + if (ShouldDeferPhysicsLock(m_cellPhysicsGraceRemaining)) + { + m_requestPhysicsLockAfterGrace = true; + } + else + { + SafeSetReferenceMotionType(apReference, MotionType::kKeyframed, true); + SafeUpdateReference3D(apReference, true); + } + } + } + + spdlog::debug("DropService: rebound existing reference {:X}:{:X} for drop {}", referenceId.ModId, referenceId.BaseId, aDropId); + return true; + }; + + EnsureStorageReady(); + + const auto cachedRefFormId = m_dropStorage.GetRefFormId(aDropId); + TESBoundObject* pObject = ResolveDroppedObject(acData.Item); + + if (cachedRefFormId) + { + if (TESForm* pForm = TESForm::GetById(*cachedRefFormId)) + { + if (TESObjectREFR* pRef = Cast(pForm)) + { + if (pObject && pRef->baseForm != pObject) + { + spdlog::warn("DropService: cached ref {:X} base mismatch for drop {}, ignoring", *cachedRefFormId, aDropId); + } + else if (tryBind(pRef)) + { + return true; + } + } + } + } + + if (acData.ReferenceId) + { + if (TESObjectREFR* pRef = GetReferenceById(acData.ReferenceId)) + { + if (tryBind(pRef)) + return true; + } + } + + if (!pObject) + return false; + + if (TESObjectREFR* pNearby = FindReferenceNear(pObject, acData.Location, kDropSearchRadiusSquared)) + return tryBind(pNearby); + + return false; +} + +void DropService::ReconcileCachedDrops(const GameId& acCellId, const GameId& acWorldId, const TiltedPhoques::Vector& acAuthoritativeDropIds) noexcept +{ + auto cachedDrops = m_dropStorage.GetDropsForCell(acCellId, acWorldId); + TiltedPhoques::Map liveIds; + for (auto id : acAuthoritativeDropIds) + liveIds[id] = true; + + for (const auto& cached : cachedDrops) + { + if (liveIds.find(cached.DropId) != std::end(liveIds)) + continue; + + bool removed = false; + if (cached.ReferenceId) + removed = RemoveReferenceById(cached.ReferenceId, "cell sync stale reference id"); + if (cached.RefFormId && !removed) + { + if (auto* pForm = TESForm::GetById(cached.RefFormId)) + { + if (auto* pRef = Cast(pForm)) + { + pRef->Delete(); + removed = true; + } + } + } + + if (!removed) + { + if (TESBoundObject* pObject = ResolveDroppedObject(cached.Item)) + { + if (TESObjectREFR* pRef = FindReferenceNear(pObject, cached.Location)) + { + pRef->Delete(); + removed = true; + } + } + } + + if (removed) + spdlog::warn("DropService: removed stale cached drop {} in cell {:X}:{:X}", cached.DropId, acCellId.ModId, acCellId.BaseId); + else + spdlog::warn("DropService: cached drop {} missing locally for cell {:X}:{:X}", cached.DropId, acCellId.ModId, acCellId.BaseId); + + m_dropStorage.RemoveCachedDrop(cached.DropId); + } +} + +void DropService::ApplyCreationEngineCellSync(const DropSyncContext& acContext, const TiltedPhoques::Vector& acPickedUpRefs) noexcept +{ + if (acPickedUpRefs.empty()) + return; + + if (!IsDropCellLoaded(acContext.CellId, acContext.WorldSpaceId)) + return; + + for (const auto& refId : acPickedUpRefs) + { + if (!refId) + continue; + + if (RemoveReferenceById(refId, "cell sync creation-engine pickup")) + { + m_pendingCreationEngineRemovals.erase(refId); + continue; + } + + auto& pending = m_pendingCreationEngineRemovals[refId]; + pending.CellId = acContext.CellId; + pending.WorldSpaceId = acContext.WorldSpaceId; + pending.RemainingRetries = 30; + } +} + +void DropService::ProcessPendingCreationEngineRemovals() noexcept +{ + if (m_pendingCreationEngineRemovals.empty()) + return; + + TiltedPhoques::Vector toErase; + toErase.reserve(m_pendingCreationEngineRemovals.size()); + + for (auto& [refId, pending] : m_pendingCreationEngineRemovals) + { + if (!refId || pending.RemainingRetries == 0) + { + toErase.push_back(refId); + continue; + } + + if (pending.CellId && !IsDropCellLoaded(pending.CellId, pending.WorldSpaceId)) + continue; + + if (RemoveReferenceById(refId, "cell sync deferred creation-engine pickup")) + { + toErase.push_back(refId); + continue; + } + + --pending.RemainingRetries; + if (pending.RemainingRetries == 0) + toErase.push_back(refId); + } + + for (const auto& refId : toErase) + m_pendingCreationEngineRemovals.erase(refId); +} + +GameId DropService::GetPlayerCellId() noexcept +{ + GameId cell{}; + auto* pPlayer = PlayerCharacter::Get(); + if (!pPlayer) + return cell; + + if (auto* pCell = pPlayer->parentCell) + m_world.GetModSystem().GetServerModId(pCell->formID, cell); + + return cell; +} + +GameId DropService::GetPlayerWorldId() noexcept +{ + GameId world{}; + auto* pPlayer = PlayerCharacter::Get(); + if (!pPlayer) + return world; + + if (auto* pWorld = pPlayer->GetWorldSpace()) + m_world.GetModSystem().GetServerModId(pWorld->formID, world); + + return world; +} + +bool DropService::IsDropCellLoaded(const GameId& acCellId, const GameId& acWorldId) noexcept +{ + if (!acCellId) + return false; + + const GameId playerCell = GetPlayerCellId(); + if (playerCell && playerCell == acCellId) + return true; + + const GameId playerWorld = GetPlayerWorldId(); + + // Interior: only the current cell is treated as loaded for drops. + if (!playerWorld) + return false; + + if (acWorldId && acWorldId != playerWorld) + return false; + + const uint32_t cellFormId = m_world.GetModSystem().GetGameId(acCellId); + if (!cellFormId) + return false; + + TES* pTes = TES::Get(); + if (!pTes || !pTes->cells || !pTes->cells->arr) + return false; + + const int dimension = pTes->cells->dimension; + if (dimension <= 0) + return false; + + const int cellCount = dimension * dimension; + for (int i = 0; i < cellCount; ++i) + { + TESObjectCELL* pCell = pTes->cells->arr[i]; + if (pCell && pCell->formID == cellFormId) + return true; + } + + return false; +} + +bool DropService::IsDropLocallyActive(uint64_t aDropId, const GameId& acReferenceId) const noexcept +{ + if (aDropId != 0) + { + if (m_grabbedDrops.find(aDropId) != m_grabbedDrops.end()) + return true; + if (m_dropPhysicsCooldowns.find(aDropId) != m_dropPhysicsCooldowns.end()) + return true; + } + + if (acReferenceId) + { + if (m_grabbedReferences.find(acReferenceId) != m_grabbedReferences.end()) + return true; + if (m_referencePhysicsCooldowns.find(acReferenceId) != m_referencePhysicsCooldowns.end()) + return true; + } + + return false; +} + +void DropService::RequestCellSync(bool aIncludeDiscovery) noexcept +{ + const GameId cellId = GetPlayerCellId(); + if (!cellId) + return; + + const GameId worldId = GetPlayerWorldId(); + QueueDropSync(cellId, worldId, aIncludeDiscovery); +} + +TiltedPhoques::Vector DropService::BuildDiscoveryEntries(const GameId& acCellId, const GameId& acWorldId) noexcept +{ + TiltedPhoques::Vector entries; + if (!acCellId) + return entries; + + const uint32_t cellFormId = m_world.GetModSystem().GetGameId(acCellId); + if (!cellFormId) + return entries; + + TESObjectCELL* pCell = Cast(TESForm::GetById(cellFormId)); + if (!pCell) + return entries; + + TiltedPhoques::Vector formTypes = { + FormType::Weapon, + FormType::Armor, + FormType::Ammo, + FormType::Ingredient, + FormType::Alchemy, + FormType::Book, + FormType::Scroll, + FormType::Note, + FormType::SoulGem, + FormType::KeyMaster, + FormType::Light, + FormType::Misc, + FormType::Apparatus + }; + + const auto references = pCell->GetRefsByFormTypes(formTypes); + entries.reserve(references.size()); + + for (TESObjectREFR* pRef : references) + { + if (!IsEligibleServerItemRef(pRef)) + continue; + + GameId referenceId{}; + m_world.GetModSystem().GetServerModId(pRef->formID, referenceId); + if (!referenceId) + continue; + + if (DropManager::GetDropIdForReference(referenceId)) + continue; + + if (m_dropStorage.FindDropIdByRefFormId(pRef->formID, acCellId, acWorldId)) + continue; + + RequestDroppedItems::DiscoveryEntry entry{}; + entry.ReferenceId = referenceId; + entry.CellId = acCellId; + entry.WorldSpaceId = acWorldId; + entry.HasLocation = true; + entry.Location = ToNetVector(pRef->position); + entry.HasRotation = true; + entry.Rotation = ToNetDropRotation(pRef->rotation); + + entry.Item.Count = 1; + m_world.GetModSystem().GetServerModId(pRef->baseForm->formID, entry.Item.BaseId); + if (!entry.Item.BaseId) + continue; + + TESObjectREFR::GetItemFromExtraData(entry.Item, pRef->GetExtraDataList()); + if (entry.Item.Count == 0) + entry.Item.Count = 1; + + entries.push_back(std::move(entry)); + } + + if (!entries.empty()) + spdlog::debug("DropService: queued {} creation-engine discoveries for cell {:X}:{:X}", entries.size(), acCellId.ModId, acCellId.BaseId); + + return entries; +} + +void DropService::ForgetLocalDrop(uint64_t aDropId) noexcept +{ + if (aDropId == 0) + return; + + m_localDrops.erase(aDropId); + m_grabbedDrops.erase(aDropId); + m_dropPhysicsCooldowns.erase(aDropId); + m_dropMoveSyncTimers.erase(aDropId); +} diff --git a/Code/client/Services/Generic/DropService.h b/Code/client/Services/Generic/DropService.h new file mode 100644 index 000000000..056f34212 --- /dev/null +++ b/Code/client/Services/Generic/DropService.h @@ -0,0 +1,201 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +struct DropItemEvent; +struct PickupDroppedItemEvent; +struct UpdateEvent; + +struct World; +struct TransportService; +struct Actor; + +class DropService : public BSTEventSink, public BSTEventSink +{ +public: + DropService(World& aWorld, entt::dispatcher& aDispatcher, TransportService& aTransport) noexcept; + ~DropService(); + +private: + struct DropSyncContext; + + void OnDropEvent(const DropItemEvent& acEvent) noexcept; + void OnPickupEvent(const PickupDroppedItemEvent& acEvent) noexcept; + void OnNotifyDrop(const NotifyActorDrop& acMessage) noexcept; + void OnNotifyPickup(const NotifyDroppedItemPickedUp& acMessage) noexcept; + void OnNotifyDroppedItems(const NotifyDroppedItems& acMessage) noexcept; + void OnNotifyDropMove(const NotifyDroppedItemMove& acMessage) noexcept; + void OnNotifyDropPhysicsDisabled(const NotifyDroppedItemPhysicsDisabled& acMessage) noexcept; + void OnConnected(const ConnectedEvent& acEvent) noexcept; + void OnCellChange(const CellChangeEvent& acEvent) noexcept; + void OnGridCellChange(const GridCellChangeEvent& acEvent) noexcept; + void OnUpdate(const UpdateEvent& acEvent) noexcept; + BSTEventResult OnEvent(const TESGrabReleaseEvent* apEvent, const EventDispatcher* apSender) override; + BSTEventResult OnEvent(const TESLoadGameEvent* apEvent, const EventDispatcher* apSender) override; + + std::optional ResolveServerId(uint32_t aFormId) const noexcept; + bool EnsureActorReady(Actor* apActor, const char* apContext) const noexcept; + bool ApplyDrop(const NotifyActorDrop& acMessage) noexcept; + bool ApplyPickup(const NotifyDroppedItemPickedUp& acMessage) noexcept; + bool EnsureStorageReady() noexcept; + uint32_t SendDropSyncRequest(bool aRequestAll, bool aHasCellFilter, const GameId& acCellId, bool aHasWorldFilter, const GameId& acWorldId, TiltedPhoques::Vector aDiscoveries) noexcept; + void QueueDropSync(const GameId& acCellId, const GameId& acWorldId, bool aIncludeDiscovery) noexcept; + void QueueLoadedExteriorCells(const GameId& acWorldId) noexcept; + void HandleDropSyncResponse(const NotifyDroppedItems& acMessage) noexcept; + void ProcessDropEntry(const NotifyDroppedItems::Entry& acEntry, bool aForceMaterialize) noexcept; + bool MaterializeDrop(uint64_t aDropId, const DropManager::ServerDropData& acData, bool aForce) noexcept; + bool SpawnLocalDrop(const DropManager::ServerDropData& acData, uint64_t aDropId) noexcept; + bool RemoveNearbyReference(uint64_t aDropId, const char* apReason, float aRadiusSq) noexcept; + bool RemoveReferenceById(const GameId& acReferenceId, const char* apReason) noexcept; + bool RemoveReferenceByLocation(const Inventory::Entry& acItem, const Vector3_NetQuantize& acLocation, const char* apReason, float aRadiusSq) noexcept; + bool TryBindExistingReference(uint64_t aDropId, const DropManager::ServerDropData& acData) noexcept; + TESObjectREFR* GetReferenceById(const GameId& acReferenceId) noexcept; + bool HandleUntrackedPickup(const NotifyDroppedItemPickedUp& acMessage) noexcept; + void UpdateDropPhysics(const UpdateEvent& acEvent) noexcept; + void SendDropMoveRequest(uint64_t aDropId, TESObjectREFR* apReference, bool aForce, float aElapsedSeconds) noexcept; + void SendDropPhysicsDisabledRequest(uint64_t aDropId, TESObjectREFR* apReference) noexcept; + void SendReferenceMoveRequest(const GameId& acReferenceId, TESObjectREFR* apReference, float aElapsedSeconds) noexcept; + void ReconcileCachedDrops(const GameId& acCellId, const GameId& acWorldId, const TiltedPhoques::Vector& acAuthoritativeDropIds) noexcept; + void ApplyCreationEngineCellSync(const DropSyncContext& acContext, const TiltedPhoques::Vector& acPickedUpRefs) noexcept; + void ProcessPendingCreationEngineRemovals() noexcept; + GameId GetPlayerCellId() noexcept; + GameId GetPlayerWorldId() noexcept; + void RequestCellSync(bool aIncludeDiscovery) noexcept; + TiltedPhoques::Vector BuildDiscoveryEntries(const GameId& acCellId, const GameId& acWorldId) noexcept; + void ForgetLocalDrop(uint64_t aDropId) noexcept; + bool IsPickupRelevant(const NotifyDroppedItemPickedUp& acMessage) noexcept; + bool IsDropCellLoaded(const GameId& acCellId, const GameId& acWorldId) noexcept; + bool IsDropLocallyActive(uint64_t aDropId, const GameId& acReferenceId) const noexcept; + + enum class PendingType + { + Drop, + Pickup + }; + + struct PendingAction + { + PendingType Type{}; + NotifyActorDrop DropMessage{}; + NotifyDroppedItemPickedUp PickupMessage{}; + uint32_t RetryCounter{0}; + }; + + World& m_world; + entt::dispatcher& m_dispatcher; + TransportService& m_transport; + CoSaveService& m_coSaveService; + DropStorage& m_dropStorage; + + entt::scoped_connection m_dropEventConnection; + entt::scoped_connection m_pickupEventConnection; + entt::scoped_connection m_notifyDropConnection; + entt::scoped_connection m_notifyPickupConnection; + entt::scoped_connection m_notifyDroppedItemsConnection; + entt::scoped_connection m_notifyDropMoveConnection; + entt::scoped_connection m_notifyDropPhysicsDisabledConnection; + entt::scoped_connection m_connectedEventConnection; + entt::scoped_connection m_cellChangeConnection; + entt::scoped_connection m_gridCellChangeConnection; + entt::scoped_connection m_updateConnection; + TiltedPhoques::Vector m_pendingActions; + std::string m_cachedUsername; + uint32_t m_nextDropSyncRequestId{1}; + TiltedPhoques::Set m_materializingDrops; + TiltedPhoques::Set m_localDrops; + double m_periodicPlayerCellSyncAccumulator{0.0}; + + struct QueuedDropSync + { + GameId CellId{}; + GameId WorldSpaceId{}; + bool IncludeDiscovery{false}; + }; + + std::deque m_dropSyncQueue{}; + TiltedPhoques::Set m_dropSyncQueuedCells{}; + GameId m_dropSyncWorldSpace{}; + double m_dropSyncQueueAccumulator{0.0}; + + struct DropSyncContext + { + bool IsFullSync{false}; + GameId CellId{}; + GameId WorldSpaceId{}; + }; + + struct MoveInterpolation + { + uint64_t DropId{}; + GameId ReferenceId{}; + NiPoint3 StartLocation{}; + NiPoint3 StartRotation{}; + NiPoint3 TargetLocation{}; + NiPoint3 TargetRotation{}; + NiPoint3 Velocity{}; + NiPoint3 AngularVelocity{}; + bool HasLocation{false}; + bool HasRotation{false}; + bool HasVelocity{false}; + bool HasAngularVelocity{false}; + float Elapsed{0.f}; + float Duration{0.f}; + }; + + std::unordered_map m_pendingDropSyncs; + // Tracks the latest spawn epoch processed per server drop to ignore stale notifications + TiltedPhoques::Map m_knownSpawnEpochs; + TiltedPhoques::Set m_grabbedDrops; + TiltedPhoques::Map m_dropPhysicsCooldowns; + TiltedPhoques::Map m_dropMoveSyncTimers; + TiltedPhoques::Map m_dropMoveLastLocations; + TiltedPhoques::Map m_dropMoveLastRotations; + TiltedPhoques::Map m_dropMoveInterpolations; + TiltedPhoques::Map m_dropPhysicsDisableSuppressions; + TiltedPhoques::Set m_grabbedReferences; + TiltedPhoques::Map m_referencePhysicsCooldowns; + TiltedPhoques::Map m_referenceMoveSyncTimers; + TiltedPhoques::Map m_referenceMoveLastLocations; + TiltedPhoques::Map m_referenceMoveLastRotations; + TiltedPhoques::Map m_referenceMoveInterpolations; + double m_grabEventSuppressionRemaining{0.0}; + bool m_suspendProcessing{false}; + bool m_requestResyncAfterSuspend{false}; + double m_suspendProcessingAccumulator{0.0}; + uint8_t m_pendingDiscoveryResyncs{0}; + double m_cellPhysicsGraceRemaining{0.0}; + bool m_requestPhysicsLockAfterGrace{false}; + + struct PendingCreationEngineRemoval + { + GameId CellId{}; + GameId WorldSpaceId{}; + uint32_t RemainingRetries{0}; + }; + + std::unordered_map m_pendingCreationEngineRemovals; +}; diff --git a/Code/client/Services/Generic/DropStorage.cpp b/Code/client/Services/Generic/DropStorage.cpp new file mode 100644 index 000000000..2bf496a3b --- /dev/null +++ b/Code/client/Services/Generic/DropStorage.cpp @@ -0,0 +1,285 @@ +#include "DropStorage.h" + +#include +#include + +#include +#include + +namespace +{ + +uint64_t GetEpochSeconds() noexcept +{ + return static_cast(std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count()); +} +} // namespace + +DropStorage::~DropStorage() +{ + Shutdown(); +} + +void DropStorage::SetActiveUser(std::string aUsername) noexcept +{ + const auto sanitized = SanitizeUser(aUsername); + if (sanitized == m_activeUser) + return; + + Shutdown(); + m_activeUser = sanitized.empty() ? "default" : sanitized; +} + +void DropStorage::PrepareInMemory() noexcept +{ + if (m_activeUser.empty()) + m_activeUser = "default"; + m_initialized = true; +} + +void DropStorage::OnLoadGameReset() noexcept +{ + m_cachedDrops.clear(); + m_localPlayerLocation.reset(); + m_storagePath.clear(); + m_initialized = false; + m_dirty = false; +} + +void DropStorage::Shutdown() noexcept +{ + m_cachedDrops.clear(); + m_localPlayerLocation.reset(); + m_storagePath.clear(); + m_initialized = false; + m_dirty = false; +} + +std::vector DropStorage::GetDropsForCell(const GameId& aCellId, const GameId& aWorldId) const noexcept +{ + std::vector drops; + drops.reserve(m_cachedDrops.size()); + + for (const auto& [dropId, drop] : m_cachedDrops) + { + if (drop.CellId == aCellId && drop.WorldSpaceId == aWorldId) + drops.push_back(drop); + } + + return drops; +} + +std::vector DropStorage::GetAllDrops() const noexcept +{ + std::vector drops; + drops.reserve(m_cachedDrops.size()); + for (const auto& [_, drop] : m_cachedDrops) + drops.push_back(drop); + return drops; +} + +std::optional DropStorage::GetRefFormId(uint64_t aDropId) const noexcept +{ + const auto it = m_cachedDrops.find(aDropId); + if (it == m_cachedDrops.end()) + return std::nullopt; + + const auto& drop = it.value(); + if (drop.RefFormId == 0) + return std::nullopt; + + return drop.RefFormId; +} + +std::optional DropStorage::FindDropIdByRefFormId(uint32_t aRefFormId, const GameId& aCellId, const GameId& aWorldId) const noexcept +{ + if (aRefFormId == 0) + return std::nullopt; + + for (const auto& [dropId, drop] : m_cachedDrops) + { + if (drop.RefFormId != aRefFormId) + continue; + + if (aCellId && drop.CellId && drop.CellId != aCellId) + continue; + + if (aWorldId && drop.WorldSpaceId && drop.WorldSpaceId != aWorldId) + continue; + + return dropId; + } + + return std::nullopt; +} + +void DropStorage::SetLocalPlayerLocation(const CoSaveStorage::LocalPlayerLocation& aLocation) noexcept +{ + PrepareInMemory(); + if (!m_localPlayerLocation || *m_localPlayerLocation != aLocation) + { + m_localPlayerLocation = aLocation; + m_dirty = true; + } +} + +std::optional DropStorage::GetLocalPlayerLocation() const noexcept +{ + return m_localPlayerLocation; +} + +void DropStorage::RemoveCachedDrop(uint64_t aDropId) noexcept +{ + if (m_cachedDrops.erase(aDropId) > 0) + m_dirty = true; +} + +void DropStorage::OnServerDropTracked(uint64_t aDropId, const DropManager::ServerDropData& acData) noexcept +{ + PrepareInMemory(); + + CachedDrop drop{}; + drop.DropId = aDropId; + drop.Type = acData.Type; + drop.ServerId = acData.ServerId; + drop.CellId = acData.CellId; + drop.WorldSpaceId = acData.WorldSpaceId; + drop.ReferenceId = acData.ReferenceId; + drop.Item = acData.Item; + drop.Location = acData.Location; + drop.Rotation = acData.Rotation; + drop.LastSeenTimestamp = GetEpochSeconds(); + + if (auto it = m_cachedDrops.find(aDropId); it != m_cachedDrops.end()) + { + drop.RefFormId = it->second.RefFormId; + if (drop.Type == ServerItemType::Dropped) + drop.Type = it->second.Type; + if (drop.LastSeenTimestamp == 0) + drop.LastSeenTimestamp = it->second.LastSeenTimestamp; + } + else + drop.RefFormId = 0; + + m_cachedDrops[aDropId] = drop; + spdlog::info("DropStorage: cached drop {} for cell {:X}:{:X}", aDropId, drop.CellId.ModId, drop.CellId.BaseId); + m_dirty = true; +} + +void DropStorage::OnDropHandleBound(uint64_t aDropId, uint32_t aHandleBits) noexcept +{ + PrepareInMemory(); + + if (aHandleBits == 0) + return; + + auto it = m_cachedDrops.find(aDropId); + if (it == m_cachedDrops.end()) + return; + + auto& drop = it.value(); + drop.RefFormId = ResolveRefFormId(aHandleBits); + m_dirty = true; +} + +void DropStorage::OnServerDropRemoved(uint64_t aDropId) noexcept +{ + spdlog::debug("DropStorage: server drop {} removed (retaining cache for later cleanup)", aDropId); +} + +std::string DropStorage::SanitizeUser(const std::string& aUsername) +{ + std::string result; + result.reserve(aUsername.size()); + for (const char ch : aUsername) + { + if (std::isalnum(static_cast(ch)) || ch == '-' || ch == '_') + result.push_back(ch); + else + result.push_back('_'); + } + return result; +} + +void DropStorage::EnsureDirectories(const std::filesystem::path& aPath) const noexcept +{ + if (aPath.empty()) + return; + + std::error_code ec; + if (!std::filesystem::exists(aPath, ec)) + std::filesystem::create_directories(aPath, ec); + + if (ec) + spdlog::warn("DropStorage: failed to ensure directory '{}': {}", aPath.string(), ec.message()); +} + +bool DropStorage::LoadFromPath(const std::filesystem::path& aSavePath) noexcept +{ + m_cachedDrops.clear(); + m_localPlayerLocation.reset(); + m_dirty = false; + m_initialized = true; + + if (aSavePath.empty()) + return false; + + m_storagePath = aSavePath; + + CoSaveStorage::LocalPlayerLocation location{}; + if (!CoSaveStorage::Load(m_storagePath, m_cachedDrops, &location)) + { + spdlog::warn("DropStorage: failed to load cache '{}'", m_storagePath.string()); + return false; + } + else + { + spdlog::info("DropStorage: loaded cache '{}' ({} entries)", m_storagePath.string(), m_cachedDrops.size()); + } + + if (location.HasLocation) + m_localPlayerLocation = location; + + return true; +} + +bool DropStorage::SaveToPath(const std::filesystem::path& aSavePath) noexcept +{ + if (aSavePath.empty()) + return false; + + PrepareInMemory(); + m_storagePath = aSavePath; + if (!m_dirty) + return true; + + EnsureDirectories(m_storagePath.parent_path()); + + CoSaveStorage::LocalPlayerLocation location{}; + if (m_localPlayerLocation) + location = *m_localPlayerLocation; + else + location.HasLocation = false; + + if (!CoSaveStorage::Save(m_storagePath, m_cachedDrops, &location)) + { + spdlog::warn("DropStorage: failed to save cache '{}'", m_storagePath.string()); + return false; + } + + spdlog::info("DropStorage: saved cache '{}' ({} entries)", m_storagePath.string(), m_cachedDrops.size()); + m_dirty = false; + return true; +} + +uint32_t DropStorage::ResolveRefFormId(uint32_t aHandleBits) const noexcept +{ + if (!aHandleBits) + return 0; + + if (auto* pRef = TESObjectREFR::GetByHandle(aHandleBits)) + return pRef->formID; + + return 0; +} diff --git a/Code/client/Services/Generic/DropStorage.h b/Code/client/Services/Generic/DropStorage.h new file mode 100644 index 000000000..ba3303228 --- /dev/null +++ b/Code/client/Services/Generic/DropStorage.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +class DropStorage final : public DropManager::StorageListener +{ +public: + using CachedDrop = CoSaveStorage::Entry; + + DropStorage() = default; + ~DropStorage() override; + + void SetActiveUser(std::string aUsername) noexcept; + void PrepareInMemory() noexcept; + void OnLoadGameReset() noexcept; + void Shutdown() noexcept; + bool LoadFromPath(const std::filesystem::path& aSavePath) noexcept; + bool SaveToPath(const std::filesystem::path& aSavePath) noexcept; + std::vector GetDropsForCell(const GameId& aCellId, const GameId& aWorldId) const noexcept; + std::vector GetAllDrops() const noexcept; + std::optional GetRefFormId(uint64_t aDropId) const noexcept; + std::optional FindDropIdByRefFormId(uint32_t aRefFormId, const GameId& aCellId, const GameId& aWorldId) const noexcept; + void SetLocalPlayerLocation(const CoSaveStorage::LocalPlayerLocation& aLocation) noexcept; + std::optional GetLocalPlayerLocation() const noexcept; + void RemoveCachedDrop(uint64_t aDropId) noexcept; + + void OnServerDropTracked(uint64_t aDropId, const DropManager::ServerDropData& acData) noexcept override; + void OnDropHandleBound(uint64_t aDropId, uint32_t aHandleBits) noexcept override; + void OnServerDropRemoved(uint64_t aDropId) noexcept override; + +private: + static std::string SanitizeUser(const std::string& aUsername); + void EnsureDirectories(const std::filesystem::path& aPath) const noexcept; + uint32_t ResolveRefFormId(uint32_t aHandleBits) const noexcept; + + std::filesystem::path m_storagePath; + std::string m_activeUser{}; + bool m_initialized{false}; + bool m_dirty{false}; + TiltedPhoques::Map m_cachedDrops; + std::optional m_localPlayerLocation; +}; diff --git a/Code/client/Services/Generic/EquipmentSnapshot.h b/Code/client/Services/Generic/EquipmentSnapshot.h new file mode 100644 index 000000000..bf6f0c2c4 --- /dev/null +++ b/Code/client/Services/Generic/EquipmentSnapshot.h @@ -0,0 +1,150 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +struct EquipmentSnapshot +{ + uint32_t LeftSpellId = 0; + uint32_t RightSpellId = 0; + uint32_t LeftWeaponId = 0; + uint32_t RightWeaponId = 0; + uint32_t TwoHandWeaponId = 0; + uint32_t AmmoId = 0; + uint32_t ShoutId = 0; + bool WasWeaponDrawn = false; +}; + +inline EquipmentSnapshot CaptureEquipmentSnapshot(Actor* pActor) noexcept +{ + EquipmentSnapshot snapshot{}; + if (!pActor) + return snapshot; + + snapshot.WasWeaponDrawn = pActor->actorState.IsWeaponDrawn(); + + TESForm* pLeftSpell = pActor->magicItems[0]; + TESForm* pRightSpell = pActor->magicItems[1]; + + if (pLeftSpell) + snapshot.LeftSpellId = pLeftSpell->formID; + if (pRightSpell) + snapshot.RightSpellId = pRightSpell->formID; + + TESForm* pLeftWeapon = pLeftSpell ? nullptr : pActor->GetEquippedWeapon(0); + TESForm* pRightWeapon = pRightSpell ? nullptr : pActor->GetEquippedWeapon(1); + const bool isTwoHand = pLeftWeapon && pRightWeapon && pLeftWeapon == pRightWeapon; + + if (isTwoHand) + { + snapshot.TwoHandWeaponId = pLeftWeapon->formID; + } + else + { + if (pLeftWeapon) + snapshot.LeftWeaponId = pLeftWeapon->formID; + if (pRightWeapon) + snapshot.RightWeaponId = pRightWeapon->formID; + } + + if (auto* pAmmo = pActor->GetEquippedAmmo()) + snapshot.AmmoId = pAmmo->formID; + + if (auto* pShout = pActor->equippedShout) + snapshot.ShoutId = pShout->formID; + + return snapshot; +} + +inline void RestoreEquipmentSnapshot(PlayerCharacter* pPlayer, const EquipmentSnapshot& snapshot, bool restoreDrawState) noexcept +{ + if (!pPlayer) + return; + + auto* pEquipManager = EquipManager::Get(); + if (!pEquipManager) + return; + + auto& defaults = DefaultObjectManager::Get(); + + pPlayer->SetWeaponDrawnEx(false); + + if (auto* pSpell = pPlayer->magicItems[0]) + pEquipManager->UnEquipSpell(pPlayer, pSpell, 0); + if (auto* pSpell = pPlayer->magicItems[1]) + pEquipManager->UnEquipSpell(pPlayer, pSpell, 1); + + TESForm* pLeftWeapon = pPlayer->GetEquippedWeapon(0); + TESForm* pRightWeapon = pPlayer->GetEquippedWeapon(1); + TESForm* pTwoHand = (pLeftWeapon && pRightWeapon && pLeftWeapon == pRightWeapon) ? pLeftWeapon : nullptr; + + if (pTwoHand) + { + pEquipManager->UnEquip(pPlayer, pTwoHand, nullptr, 1, defaults.eitherEquipSlot, false, true, false, false, nullptr); + } + else + { + if (pLeftWeapon) + pEquipManager->UnEquip(pPlayer, pLeftWeapon, nullptr, 1, defaults.leftEquipSlot, false, true, false, false, nullptr); + if (pRightWeapon) + pEquipManager->UnEquip(pPlayer, pRightWeapon, nullptr, 1, defaults.rightEquipSlot, false, true, false, false, nullptr); + } + + if (auto* pAmmo = pPlayer->GetEquippedAmmo()) + pEquipManager->UnEquip(pPlayer, pAmmo, nullptr, 1, defaults.rightEquipSlot, false, true, false, false, nullptr); + + if (auto* pShout = pPlayer->equippedShout) + pEquipManager->UnEquipShout(pPlayer, pShout); + + const bool useTwoHand = snapshot.TwoHandWeaponId != 0; + + if (!useTwoHand) + { + if (snapshot.LeftSpellId) + { + if (auto* pSpell = TESForm::GetById(snapshot.LeftSpellId)) + pEquipManager->EquipSpell(pPlayer, pSpell, 0); + } + else if (snapshot.LeftWeaponId) + { + if (auto* pItem = TESForm::GetById(snapshot.LeftWeaponId)) + pEquipManager->Equip(pPlayer, pItem, nullptr, 1, defaults.leftEquipSlot, false, true, false, false); + } + } + + if (snapshot.RightSpellId && !useTwoHand) + { + if (auto* pSpell = TESForm::GetById(snapshot.RightSpellId)) + pEquipManager->EquipSpell(pPlayer, pSpell, 1); + } + else if (useTwoHand) + { + if (auto* pItem = TESForm::GetById(snapshot.TwoHandWeaponId)) + pEquipManager->Equip(pPlayer, pItem, nullptr, 1, defaults.eitherEquipSlot, false, true, false, false); + } + else if (snapshot.RightWeaponId) + { + if (auto* pItem = TESForm::GetById(snapshot.RightWeaponId)) + pEquipManager->Equip(pPlayer, pItem, nullptr, 1, defaults.rightEquipSlot, false, true, false, false); + } + + if (snapshot.AmmoId) + { + if (auto* pAmmo = TESForm::GetById(snapshot.AmmoId)) + pEquipManager->Equip(pPlayer, pAmmo, nullptr, 1, defaults.rightEquipSlot, false, true, false, false); + } + + if (snapshot.ShoutId) + { + if (auto* pShout = TESForm::GetById(snapshot.ShoutId)) + pEquipManager->EquipShout(pPlayer, pShout); + } + + if (restoreDrawState) + pPlayer->SetWeaponDrawnEx(snapshot.WasWeaponDrawn); +} diff --git a/Code/client/Services/Generic/ImguiService.cpp b/Code/client/Services/Generic/ImguiService.cpp index 8faba9c50..03ce836eb 100644 --- a/Code/client/Services/Generic/ImguiService.cpp +++ b/Code/client/Services/Generic/ImguiService.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +29,24 @@ void ImguiService::Create(RenderSystemD3D11* apRenderSystem, HWND aHwnd) spdlog::error("Failed to initialize Imgui-Win32"); ImGui_ImplDX11_Init(apRenderSystem->GetDevice(), apRenderSystem->GetDeviceContext()); + + // Load "Skyrim"-like font (Futura Condensed) from assets if present + ImGuiIO& io = ImGui::GetIO(); + const std::filesystem::path fontPath = + TiltedPhoques::GetPath() / "UI" / "assets" / "futura-condensed-medium.otf"; + const std::string fontPathUtf8 = fontPath.generic_string(); + + if (auto* font = io.Fonts->AddFontFromFileTTF(fontPathUtf8.c_str(), 24.0f)) + { + m_skyrimFont = font; + // Recreate device objects so the new font atlas is uploaded + ImGui_ImplDX11_InvalidateDeviceObjects(); + ImGui_ImplDX11_CreateDeviceObjects(); + } + else + { + spdlog::warn("ImguiService: failed to load font at {}", fontPathUtf8); + } } void ImguiService::Render() const diff --git a/Code/client/Services/Generic/InputService.cpp b/Code/client/Services/Generic/InputService.cpp index 558ead849..3623d706f 100644 --- a/Code/client/Services/Generic/InputService.cpp +++ b/Code/client/Services/Generic/InputService.cpp @@ -2,6 +2,7 @@ #include #include +#include #include @@ -9,11 +10,14 @@ #include #include +#include #include #include #include #include "Games/Skyrim/Interface/MenuControls.h" +#include "Games/Skyrim/Interface/UI.h" +#include static OverlayService* s_pOverlay = nullptr; static UINT s_currentACP = CP_ACP; @@ -92,11 +96,43 @@ bool IsToggleKey(int aKey) noexcept return aKey == VK_RCONTROL || aKey == VK_F2; } +static constexpr uint16_t kScanCodeB = 0x30; +static constexpr uint16_t kVirtualKeyB = 98; + +bool IsEmoteKey(uint16_t aVirtualKey, uint16_t aScanCode) noexcept +{ + return aVirtualKey == kVirtualKeyB || aScanCode == kScanCodeB; +} + bool IsDisableKey(int aKey) noexcept { return aKey == VK_ESCAPE; } +bool IsAnyMenuOpen() noexcept +{ + UI* pUI = UI::Get(); + if (!pUI) + return false; + + for (auto* pMenu : pUI->menuStack) + { + if (!pMenu) + continue; + + const BSFixedString* pName = pUI->LookupMenuNameByInstance(pMenu); + if (pName && (std::strcmp(pName->AsAscii(), "HUD Menu") == 0 || std::strcmp(pName->AsAscii(), "HUDMenu") == 0)) + continue; + + return true; + } + + return false; +} + +static bool s_emoteOpenedFromInactive = false; +static bool s_f3Pressed = false; + void SetUIActive(OverlayService& aOverlay, auto apRenderer, bool aActive) { TiltedPhoques::DInputHook::Get().SetEnabled(aActive); @@ -191,6 +227,22 @@ void ProcessKeyboard(uint16_t aKey, uint16_t aScanCode, cef_key_event_type_t aTy spdlog::debug("ProcessKey, type: {}, key: {}, active: {}", aType, aKey, active); + if (aType != KEYEVENT_CHAR && aKey == VK_F3) + { + if (aType == KEYEVENT_KEYDOWN) + { + if (!s_f3Pressed) + { + World::Get().GetDebugService().m_showDebugStuff = !World::Get().GetDebugService().m_showDebugStuff; + s_f3Pressed = true; + } + } + else if (aType == KEYEVENT_KEYUP) + { + s_f3Pressed = false; + } + } + if (aType != KEYEVENT_CHAR && (IsToggleKey(aKey) || (IsDisableKey(aKey) && active))) { if (!overlay.GetInGame()) @@ -202,6 +254,48 @@ void ProcessKeyboard(uint16_t aKey, uint16_t aScanCode, cef_key_event_type_t aTy SetUIActive(overlay, pRenderer, !active); } } + else if (IsEmoteKey(aKey, aScanCode) && (g_emoteWheelActive.load() || (!active && !IsAnyMenuOpen()))) + { + if (!overlay.GetInGame()) + { + if (auto* pApp = overlay.GetOverlayApp()) + { + pApp->InjectKey(aType, GetCefModifiers(aKey), aKey, aScanCode); + } + return; + } + + if (aType == KEYEVENT_CHAR) + { + const bool wasActive = active; + if (!wasActive) + SetUIActive(overlay, pRenderer, true); + + auto* pApp = overlay.GetOverlayApp(); + // If we just activated the overlay, fetch again in case it was spun up. + if (!pApp && !wasActive) + pApp = overlay.GetOverlayApp(); + if (pApp) + { + if (wasActive) + { + pApp->ExecuteAsync("toggleEmoteMenu"); + if (s_emoteOpenedFromInactive) + { + s_emoteOpenedFromInactive = false; + SetUIActive(overlay, pRenderer, false); + } + } + else + { + auto pArgs = CefListValue::Create(); + pArgs->SetBool(0, true); // opened from inactive overlay + pApp->ExecuteAsync("openEmoteMenu", pArgs); + s_emoteOpenedFromInactive = true; + } + } + } + } else if (active) { pApp->InjectKey(aType, GetCefModifiers(aKey), aKey, aScanCode); diff --git a/Code/client/Services/Generic/InventoryService.cpp b/Code/client/Services/Generic/InventoryService.cpp index 50a6523f3..3a374a093 100644 --- a/Code/client/Services/Generic/InventoryService.cpp +++ b/Code/client/Services/Generic/InventoryService.cpp @@ -1,5 +1,8 @@ #include +#include +#include + #include #include #include @@ -8,11 +11,13 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -26,6 +31,9 @@ #include #include #include +#include +#include +#include InventoryService::InventoryService(World& aWorld, entt::dispatcher& aDispatcher, TransportService& aTransport) noexcept : m_world(aWorld) @@ -41,6 +49,10 @@ InventoryService::InventoryService(World& aWorld, entt::dispatcher& aDispatcher, void InventoryService::OnUpdate(const UpdateEvent& acUpdateEvent) noexcept { + ProcessPendingInventoryChanges(); + ProcessPendingEquipment(); + ProcessPendingEquipmentChanges(); + ProcessPendingEquipmentRequests(); RunWeaponStateUpdates(); RunNakedNPCBugChecks(); } @@ -50,6 +62,10 @@ void InventoryService::OnInventoryChangeEvent(const InventoryChangeEvent& acEven if (!m_transport.IsConnected()) return; + // Ignore inventory sync while in ghost-only mode (quest isolation). + if (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost) + return; + auto view = m_world.view(); const auto iter = std::find_if(std::begin(view), std::end(view), [view, formId = acEvent.FormId](auto entity) { return view.get(entity).Id == formId; }); @@ -60,15 +76,26 @@ void InventoryService::OnInventoryChangeEvent(const InventoryChangeEvent& acEven std::optional serverIdRes = Utils::GetServerId(*iter); if (!serverIdRes.has_value()) { - spdlog::error(__FUNCTION__ ": failed to find server id, target form id: {:X}, item id: {:X}, count: {}", acEvent.FormId, acEvent.Item.BaseId.BaseId, acEvent.Item.Count); + spdlog::debug("{}: failed to find server id, target form id: {:X}, item id: {:X}, count: {}", __FUNCTION__, acEvent.FormId, acEvent.Item.BaseId.BaseId, acEvent.Item.Count); return; } + bool updateClients = acEvent.UpdateClients; + if (updateClients && acEvent.Item.Count < 0) + { + const auto& partyService = m_world.GetPartyService(); + if (partyService.IsInParty() && !partyService.GetPartyOptions().SyncDeadBodyLoot()) + { + Actor* pActor = Cast(TESForm::GetById(acEvent.FormId)); + if (pActor && pActor->IsDead()) + updateClients = false; + } + } + RequestInventoryChanges request; request.ServerId = serverIdRes.value(); request.Item = acEvent.Item; - request.Drop = acEvent.Drop; - request.UpdateClients = acEvent.UpdateClients; + request.UpdateClients = updateClients; m_transport.Send(request); @@ -77,85 +104,29 @@ void InventoryService::OnInventoryChangeEvent(const InventoryChangeEvent& acEven void InventoryService::OnEquipmentChangeEvent(const EquipmentChangeEvent& acEvent) noexcept { - if (!m_transport.IsConnected()) - return; - - auto view = m_world.view(); - - const auto iter = std::find_if(std::begin(view), std::end(view), [view, formId = acEvent.ActorId](auto entity) { return view.get(entity).Id == formId; }); - - if (iter == std::end(view)) - return; - - std::optional serverIdRes = Utils::GetServerId(*iter); - if (!serverIdRes.has_value()) - { - spdlog::error(__FUNCTION__ ": failed to find server id, actor id: {:X}, item id: {:X}, isAmmo: {}, unequip: {}, slot: {:X}", acEvent.ActorId, acEvent.ItemId, acEvent.IsAmmo, acEvent.Unequip, acEvent.EquipSlotId); - return; - } - - Actor* pActor = Cast(TESForm::GetById(acEvent.ActorId)); - if (!pActor) - return; - - auto& modSystem = World::Get().GetModSystem(); - - RequestEquipmentChanges request; - request.ServerId = serverIdRes.value(); - - if (!modSystem.GetServerModId(acEvent.EquipSlotId, request.EquipSlotId)) - return; - if (!modSystem.GetServerModId(acEvent.ItemId, request.ItemId)) - return; - - request.Count = acEvent.Count; - request.Unequip = acEvent.Unequip; - request.IsSpell = acEvent.IsSpell; - request.IsShout = acEvent.IsShout; - request.IsAmmo = acEvent.IsAmmo; - request.CurrentInventory = pActor->GetEquipment(); - - m_transport.Send(request); - - spdlog::info("Sending equipment request, item: {:X}, count: {}, target object: {:X}", acEvent.ItemId, acEvent.Count, acEvent.ActorId); + m_pendingEquipmentRequests.push_back(acEvent); } void InventoryService::OnNotifyInventoryChanges(const NotifyInventoryChanges& acMessage) noexcept { - if (acMessage.Drop) - { - Actor* pActor = Utils::GetByServerId(acMessage.ServerId); - if (!pActor) - { - spdlog::error("{}: could not find actor server id {:X}", __FUNCTION__, acMessage.ServerId); - return; - } - - ScopedInventoryOverride _; - - pActor->DropOrPickUpObject(acMessage.Item, nullptr, nullptr); - } - else - { - TESObjectREFR* pObject = Utils::GetByServerId(acMessage.ServerId); - if (!pObject) - return; - - ScopedInventoryOverride _; + if (TryApplyInventoryChange(acMessage)) + return; - pObject->AddOrRemoveItem(acMessage.Item); - } + m_pendingInventoryChanges.push_back(acMessage); + spdlog::debug("{}: queued inventory change for server id {:X}", __FUNCTION__, acMessage.ServerId); } void InventoryService::OnNotifyEquipmentChanges(const NotifyEquipmentChanges& acMessage) noexcept { - Actor* pActor = Utils::GetByServerId(acMessage.ServerId); - if (!pActor) - { - spdlog::error("{}: could not find actor server id {:X}", __FUNCTION__, acMessage.ServerId); + if (TryApplyEquipmentChange(acMessage)) return; - } + m_pendingEquipmentChanges.push_back(acMessage); + spdlog::debug("{}: queued equipment change for server id {:X}", __FUNCTION__, acMessage.ServerId); +} + +void InventoryService::ApplyEquipmentChange(Actor* pActor, const NotifyEquipmentChanges& acMessage) noexcept +{ auto& modSystem = World::Get().GetModSystem(); uint32_t itemId = modSystem.GetGameId(acMessage.ItemId); @@ -198,9 +169,26 @@ void InventoryService::OnNotifyEquipmentChanges(const NotifyEquipmentChanges& ac auto* pObject = Cast(pItem); // TODO: ExtraData necessary? probably + const int32_t count = acMessage.Count == 0 ? 1 : acMessage.Count; + + // Ensure the item exists locally before attempting to equip, purely for visuals. + if (!acMessage.Unequip && !acMessage.IsSpell && !acMessage.IsShout) + { + auto* pExt = pActor->GetExtension(); + if (pExt && pExt->IsRemotePlayer() && !pActor->IsItemInInventory(itemId)) + { + Inventory::Entry add{}; + add.BaseId = acMessage.ItemId; + add.Count = count; + + ScopedInventoryOverride sio; + pActor->AddOrRemoveItem(add, true); + } + } + if (acMessage.Unequip) { - pEquipManager->UnEquip(pActor, pItem, nullptr, acMessage.Count, pEquipSlot, false, true, false, false, nullptr); + pEquipManager->UnEquip(pActor, pItem, nullptr, count, pEquipSlot, false, true, false, false, nullptr); } else { @@ -218,7 +206,7 @@ void InventoryService::OnNotifyEquipmentChanges(const NotifyEquipmentChanges& ac } } - pEquipManager->Equip(pActor, pItem, nullptr, acMessage.Count, pEquipSlot, false, true, false, false); + pEquipManager->Equip(pActor, pItem, nullptr, count, pEquipSlot, false, true, false, false); for (const auto& armor : wornArmor.Entries) { @@ -230,6 +218,252 @@ void InventoryService::OnNotifyEquipmentChanges(const NotifyEquipmentChanges& ac } } +void InventoryService::ProcessPendingEquipment() noexcept +{ + auto view = m_world.view(); + TiltedPhoques::Vector toClear; + + for (auto entity : view) + { + auto& formIdComponent = view.get(entity); + auto& pending = view.get(entity); + + Actor* pActor = Cast(TESForm::GetById(formIdComponent.Id)); + const auto readiness = EvaluateActorReadiness(pActor); + if (readiness != ActorReadinessStatus::Ready) + { + if (readiness == ActorReadinessStatus::MissingActor) + toClear.push_back(entity); + continue; + } + + for (const auto& change : pending.PendingChanges) + ApplyEquipmentChange(pActor, change); + + toClear.push_back(entity); + } + + for (auto entity : toClear) + m_world.remove(entity); +} + +void InventoryService::ProcessPendingInventoryChanges() noexcept +{ + if (m_pendingInventoryChanges.empty()) + return; + + TiltedPhoques::Vector remaining; + remaining.reserve(m_pendingInventoryChanges.size()); + + for (const auto& change : m_pendingInventoryChanges) + { + if (!TryApplyInventoryChange(change)) + remaining.push_back(change); + } + + m_pendingInventoryChanges = std::move(remaining); +} + +void InventoryService::ProcessPendingEquipmentChanges() noexcept +{ + if (m_pendingEquipmentChanges.empty()) + return; + + TiltedPhoques::Vector remaining; + remaining.reserve(m_pendingEquipmentChanges.size()); + + for (const auto& change : m_pendingEquipmentChanges) + { + if (!TryApplyEquipmentChange(change)) + remaining.push_back(change); + } + + m_pendingEquipmentChanges = std::move(remaining); +} + +void InventoryService::ProcessPendingEquipmentRequests() noexcept +{ + if (m_pendingEquipmentRequests.empty()) + return; + + TiltedPhoques::Vector remaining; + remaining.reserve(m_pendingEquipmentRequests.size()); + + for (const auto& request : m_pendingEquipmentRequests) + { + if (!SendEquipmentChange(request)) + remaining.push_back(request); + } + + m_pendingEquipmentRequests = std::move(remaining); +} + +bool InventoryService::SendEquipmentChange(const EquipmentChangeEvent& acEvent) noexcept +{ + if (!m_transport.IsConnected()) + return false; + + // Quest isolation: allow equipment sync only for the local player (so others see your ghost correctly). + if (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost) + { + // Player reference form is always 0x14; keep isolation strict and avoid relying on extension flags being initialized. + if (acEvent.ActorId != 0x14) + return true; // Drop anything else to avoid retries/log spam. + } + + auto view = m_world.view(); + + const auto iter = std::find_if(std::begin(view), std::end(view), [view, formId = acEvent.ActorId](auto entity) { return view.get(entity).Id == formId; }); + + if (iter == std::end(view)) + { + spdlog::debug("{}: form id {:X} not found yet, postponing equipment sync", __FUNCTION__, acEvent.ActorId); + return false; + } + + std::optional serverIdRes = Utils::GetServerId(*iter); + if (!serverIdRes.has_value()) + return false; + + Actor* pActor = Cast(TESForm::GetById(acEvent.ActorId)); + const auto readiness = EvaluateActorReadiness(pActor); + if (readiness != ActorReadinessStatus::Ready) + { + spdlog::debug("{}: actor {:X} not ready (waiting for {}), postponing equipment sync", __FUNCTION__, acEvent.ActorId, DescribeReadiness(readiness)); + return false; + } + + auto& modSystem = World::Get().GetModSystem(); + + RequestEquipmentChanges request; + request.ServerId = serverIdRes.value(); + + if (!modSystem.GetServerModId(acEvent.EquipSlotId, request.EquipSlotId)) + return true; + if (!modSystem.GetServerModId(acEvent.ItemId, request.ItemId)) + return true; + + const int32_t cEffectiveCount = acEvent.Count == 0 ? 1 : acEvent.Count; + request.Count = cEffectiveCount; + request.Unequip = acEvent.Unequip; + request.IsSpell = acEvent.IsSpell; + request.IsShout = acEvent.IsShout; + request.IsAmmo = acEvent.IsAmmo; + request.CurrentInventory = pActor->GetEquipment(); + + m_transport.Send(request); + + spdlog::info("Sending equipment request, item: {:X}, count: {}, target object: {:X}", acEvent.ItemId, cEffectiveCount, acEvent.ActorId); + + return true; +} + +bool InventoryService::TryApplyInventoryChange(const NotifyInventoryChanges& acMessage) noexcept +{ + TESObjectREFR* pObject = Utils::GetByServerId(acMessage.ServerId); + if (!pObject) + return false; + + if (acMessage.Silent) + { + ScopedInventoryOverride _; + pObject->AddOrRemoveItem(acMessage.Item); + return true; + } + + Actor* pActor = Cast(pObject); + + ScopedInventoryOverride _; + + if (pActor) + { + const auto readiness = EvaluateActorReadiness(pActor); + if (readiness != ActorReadinessStatus::Ready) + { + spdlog::debug("{}: actor {:X} not ready (waiting for {}) while applying inventory delta", __FUNCTION__, pActor->formID, DescribeReadiness(readiness)); + return false; + } + + pActor->AddOrRemoveItem(acMessage.Item); + return true; + } + + pObject->AddOrRemoveItem(acMessage.Item); + return true; +} + +bool InventoryService::TryApplyEquipmentChange(const NotifyEquipmentChanges& acMessage) noexcept +{ + Actor* pActor = Utils::GetByServerId(acMessage.ServerId); + if (!pActor) + return false; + + // Quest isolation: only apply equipment changes to remote player ghost actors. + if (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost) + { + auto remotePlayerView = m_world.view(); + const auto it = std::find_if(remotePlayerView.begin(), remotePlayerView.end(), + [remotePlayerView, serverId = acMessage.ServerId](entt::entity e) { return remotePlayerView.get(e).Id == serverId; }); + + if (it == remotePlayerView.end()) + return true; + } + + const auto readiness = EvaluateActorReadiness(pActor); + if (readiness != ActorReadinessStatus::Ready) + { + auto view = m_world.view(); + const auto itor = std::find_if(std::begin(view), std::end(view), [formId = pActor->formID, view](entt::entity entity) { return view.get(entity).Id == formId; }); + + if (itor != std::end(view)) + { + auto* pPending = m_world.try_get(*itor); + if (!pPending) + pPending = &m_world.emplace(*itor); + + pPending->PendingChanges.push_back(acMessage); + spdlog::debug("Queued equipment change for actor {:X} (waiting for {})", pActor->formID, DescribeReadiness(readiness)); + return true; + } + + spdlog::warn("{}: could not queue equipment change, entity not found for form id {:X}", __FUNCTION__, pActor->formID); + return false; + } + + ApplyEquipmentChange(pActor, acMessage); + return true; +} + +InventoryService::ActorReadinessStatus InventoryService::EvaluateActorReadiness(Actor* pActor) const noexcept +{ + if (!pActor) + return ActorReadinessStatus::MissingActor; + + if (!pActor->GetNiNode()) + return ActorReadinessStatus::Missing3D; + + if (ExtraContainerChanges::Data* pContainerChanges = pActor->GetContainerChanges(); + !pContainerChanges || !pContainerChanges->entries) + return ActorReadinessStatus::MissingContainerData; + + return ActorReadinessStatus::Ready; +} + +const char* InventoryService::DescribeReadiness(ActorReadinessStatus aStatus) noexcept +{ + switch (aStatus) + { + case ActorReadinessStatus::MissingActor: + return "actor reference"; + case ActorReadinessStatus::Missing3D: + return "3D data"; + case ActorReadinessStatus::MissingContainerData: + return "inventory data"; + default: + return "ready state"; + } +} + void InventoryService::RunWeaponStateUpdates() noexcept { if (!m_transport.IsConnected()) diff --git a/Code/client/Services/Generic/MagicService.cpp b/Code/client/Services/Generic/MagicService.cpp index d5d5bb45f..1731fb64f 100644 --- a/Code/client/Services/Generic/MagicService.cpp +++ b/Code/client/Services/Generic/MagicService.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include @@ -15,6 +17,8 @@ #include #include +#include +#include #include #include @@ -30,6 +34,24 @@ #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +constexpr uint32_t cHealingHandsBaseId = 0x4D3F2; +constexpr float cHealingHandsRange = 300.0f; +constexpr auto cReviveChannelTimeout = std::chrono::milliseconds(2000); +constexpr uint32_t cFormIdMask = 0x00FFFFFF; +constexpr double cHealingHandsPingInterval = 0.35; +} MagicService::MagicService(World& aWorld, entt::dispatcher& aDispatcher, TransportService& aTransport) noexcept : m_world(aWorld) @@ -45,6 +67,10 @@ MagicService::MagicService(World& aWorld, entt::dispatcher& aDispatcher, Transpo m_notifyAddTargetConnection = m_dispatcher.sink().connect<&MagicService::OnNotifyAddTarget>(this); m_removeSpellEventConnection = m_dispatcher.sink().connect<&MagicService::OnRemoveSpellEvent>(this); m_notifyRemoveSpell = m_dispatcher.sink().connect<&MagicService::OnNotifyRemoveSpell>(this); + m_notifyHealingProximityConnection = m_dispatcher.sink().connect<&MagicService::OnNotifyHealingProximity>(this); + + // Listen for party member downed/revived notifications + m_notifyPartyMemberDownedConnection = m_dispatcher.sink().connect<&MagicService::OnNotifyPartyMemberDowned>(this); } void MagicService::OnUpdate(const UpdateEvent& acEvent) noexcept @@ -55,21 +81,45 @@ void MagicService::OnUpdate(const UpdateEvent& acEvent) noexcept ApplyQueuedEffects(); UpdateRevealOtherPlayersEffect(); + UpdateRevealDownedPlayersEffect(); + UpdateReviveChannels(acEvent.Delta); + UpdateHealerChannel(acEvent.Delta); + UpdateHealingHandsBroadcast(acEvent.Delta); } -void MagicService::OnSpellCastEvent(const SpellCastEvent& acEvent) const noexcept +void MagicService::OnSpellCastEvent(const SpellCastEvent& acEvent) noexcept { if (!m_transport.IsConnected()) return; + if (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost) + return; + if (!acEvent.pCaster->pCasterActor || !acEvent.pCaster->pCasterActor->GetNiNode()) { spdlog::warn("Spell cast event has no actor or actor is not loaded"); return; } + SpellItem* pSpell = Cast(TESForm::GetById(acEvent.SpellId)); + + if (pSpell && pSpell->IsHealingSpell() && IsHealingHandsSpell(acEvent.SpellId, pSpell)) + { + if (SendHealingProximityPing(acEvent.SpellId)) + { + const auto source = static_cast(acEvent.pCaster->GetCastingSource()); + if (source >= 0 && source < MagicSystem::CastingSource::CASTING_SOURCE_COUNT) + { + m_localHealingHandsSources[source] = true; + m_isLocalHealingHandsActive = true; + m_activeHealingHandsSpellId = acEvent.SpellId; + m_healingHandsPingAccumulator = cHealingHandsPingInterval; + } + } + } + // only sync concentration spells through spell cast sync, the rest through projectile sync for accuracy - if (SpellItem* pSpell = Cast(TESForm::GetById(acEvent.SpellId))) + if (pSpell) { if ((pSpell->eCastingType != MagicSystem::CastingType::CONCENTRATION || pSpell->IsHealingSpell()) && !pSpell->IsWardSpell() && !pSpell->IsInvisibilitySpell()) { @@ -111,7 +161,7 @@ void MagicService::OnSpellCastEvent(const SpellCastEvent& acEvent) const noexcep if (desiredTargetIdRes.has_value()) request.DesiredTarget = desiredTargetIdRes.value(); else - spdlog::error("{}: failed to find server id", __FUNCTION__); + spdlog::debug("{}: failed to find server id", __FUNCTION__); } } @@ -187,7 +237,7 @@ void MagicService::OnNotifySpellCast(const NotifySpellCast& acMessage) const noe std::optional serverIdRes = Utils::GetServerId(entity); if (!serverIdRes.has_value()) { - spdlog::error("{}: failed to find server id", __FUNCTION__); + spdlog::debug("{}: failed to find server id", __FUNCTION__); continue; } @@ -215,11 +265,14 @@ void MagicService::OnNotifySpellCast(const NotifySpellCast& acMessage) const noe spdlog::debug("Successfully casted remote spell"); } -void MagicService::OnInterruptCastEvent(const InterruptCastEvent& acEvent) const noexcept +void MagicService::OnInterruptCastEvent(const InterruptCastEvent& acEvent) noexcept { if (!m_transport.IsConnected()) return; + if (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost) + return; + uint32_t formId = acEvent.CasterFormID; auto view = m_world.view(); @@ -227,12 +280,14 @@ void MagicService::OnInterruptCastEvent(const InterruptCastEvent& acEvent) const if (casterEntityIt == std::end(view)) { - spdlog::warn("{}: could not find caster, form id {:X}", __FUNCTION__, formId); + spdlog::debug("{}: could not find caster, form id {:X}", __FUNCTION__, formId); return; } auto& localComponent = view.get(*casterEntityIt); + HandleHealingHandsInterrupt(static_cast(acEvent.CastingSource)); + InterruptCastRequest request; request.CasterId = localComponent.Id; request.CastingSource = acEvent.CastingSource; @@ -283,6 +338,9 @@ void MagicService::OnAddTargetEvent(const AddTargetEvent& acEvent) noexcept if (!m_transport.IsConnected()) return; + if (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost) + return; + // These effects are applied through spell cast sync if (SpellItem* pSpellItem = Cast(TESForm::GetById(acEvent.SpellID))) { @@ -337,7 +395,7 @@ void MagicService::OnAddTargetEvent(const AddTargetEvent& acEvent) noexcept if (casterIt == std::end(view)) { MagicQueue::Spdlog("{}: server entity for caster formID not found, formID: {:X}, queueing", __FUNCTION__, acEvent.CasterID); - m_queuedEffects.push(MagicAddTargetEventQueue(acEvent)); + m_queuedEffects.push(MagicAddTargetEventQueue(acEvent)); return; } @@ -408,6 +466,8 @@ void MagicService::OnNotifyAddTarget(const NotifyAddTarget& acMessage) noexcept return; } + ScopedSpellCastOverride spellOverrideGuard; + MagicTarget::AddTargetData data{}; data.pCaster = pCaster; data.pSpell = pSpell; @@ -436,6 +496,9 @@ void MagicService::OnRemoveSpellEvent(const RemoveSpellEvent& acEvent) noexcept if (!m_transport.IsConnected()) return; + if (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost) + return; + RemoveSpellRequest request{}; if (!m_world.GetModSystem().GetServerModId(acEvent.SpellId, request.SpellId.ModId, request.SpellId.BaseId)) @@ -568,9 +631,9 @@ void MagicService::ApplyQueuedEffects() noexcept } // At this point, it will succeed or fail, but not queue another one ad infinitum - MagicQueue::Spdlog("{}: retrying AddTargetEvent for caster {}({:X}), spell {:X}, effect {:X}, target {}({:X})", + MagicQueue::Spdlog("{}: retrying AddTargetEvent for caster {}({:X}), spell {:X}, effect {:X}, target {}({:X})", __FUNCTION__, pCasterName, target.CasterID, target.SpellID, target.EffectID, pTargetName, target.TargetID); - OnAddTargetEvent(target); + OnAddTargetEvent(target); } m_queuedEffects.pop(); } @@ -579,10 +642,10 @@ void MagicService::ApplyQueuedEffects() noexcept while (!m_queuedRemoteEffects.empty()) { NotifyAddTarget target = m_queuedRemoteEffects.front().Target(); - Actor* pTarget = Utils::GetByServerId(target.TargetId); - Actor* pCaster = Utils::GetByServerId(target.CasterId); + Actor* pTarget = Utils::GetByServerId(target.TargetId); + Actor* pCaster = Utils::GetByServerId(target.CasterId); auto pTargetName = !pTarget ? "" : pTarget->baseForm->GetName(); - auto pCasterName = !pCaster ? "" : pCaster->baseForm->GetName(); + auto pCasterName = !pCaster ? "" : pCaster->baseForm->GetName(); if (m_queuedRemoteEffects.front().Expired()) MagicQueue::Spdlog("{}: removing expired NotifyAddTarget event from queue: caster {}({:X}), spell {:X}, effect {:X}, target {}({:X})", @@ -600,7 +663,7 @@ void MagicService::ApplyQueuedEffects() noexcept { spdlog::debug("{}: Actor for caster serverID still not found for NotifyAddTarget: caster {}({:X}), spell {:X}, effect {:X}, target {}({:X})", __FUNCTION__, pCasterName, target.CasterId, target.SpellId, target.EffectId, pTargetName, target.TargetId); - break; + break; } MagicQueue::Spdlog("{}: retrying NotifyAddTarget for caster {}({:X}), spell {:X}, effect {:X}, target {}({:X})", @@ -682,3 +745,590 @@ void MagicService::UpdateRevealOtherPlayersEffect(bool aForceTrigger) noexcept pRemotePlayer->magicTarget.AddTarget(data, false, false); } } + +// Handler for NotifyPartyMemberDowned: updates local state, posts a party chat message, and drives glow logic +void MagicService::OnNotifyPartyMemberDowned(const NotifyPartyMemberDowned& acMessage) noexcept +{ + // Track downed set + if (acMessage.ServerId != 0) + { + if (acMessage.IsDowned) + { + DownedMemberInfo info{}; + info.PlayerId = acMessage.PlayerId; + info.PositionX = acMessage.PositionX; + info.PositionY = acMessage.PositionY; + info.PositionZ = acMessage.PositionZ; + m_downedPartyMembers[acMessage.ServerId] = info; + } + else + { + m_downedPartyMembers.erase(acMessage.ServerId); + } + } + + // Resolve a nicer display name for the player if we know it, otherwise fall back to the ID. + std::string playerName; + const auto& players = m_world.GetPartyService().GetPlayers(); + if (auto it = players.find(acMessage.PlayerId); it != players.end()) + playerName = it->second.Name.c_str(); + else + playerName = "Player " + std::to_string(acMessage.PlayerId); + + // Build a simple party message (no explicit "Party:" prefix, just colored/typed chat on UI side). + std::string text = acMessage.IsDowned + ? playerName + " has died! You can revive them using Healing Hands." + : playerName + " has been revived."; + + // Push to overlay as a system line so it doesn't look like player chat + if (auto pOverlay = m_world.GetOverlayService().GetOverlayApp()) + { + auto pArguments = CefListValue::Create(); + pArguments->SetInt(0, static_cast(kSystemMessage)); // message type + pArguments->SetString(1, text); // message text + pArguments->SetString(2, ""); // sender label (system) + pOverlay->ExecuteAsync("message", pArguments); + } +} + +// Periodically re-apply reveal effect to downed party members only +void MagicService::UpdateRevealDownedPlayersEffect() noexcept +{ + using namespace std::chrono_literals; + + if (m_downedPartyMembers.empty()) + return; + + static std::chrono::steady_clock::time_point s_lastSendTimePoint; + constexpr auto cDelayBetweenUpdates = 2s; + + const auto now = std::chrono::steady_clock::now(); + if (now - s_lastSendTimePoint < cDelayBetweenUpdates) + return; + + s_lastSendTimePoint = now; + + Mod* pSkyrimTogether = ModManager::Get()->GetByName("SkyrimTogether.esp"); + if (!pSkyrimTogether) + return; + + MagicItem* pSpell = Cast(TESForm::GetById((pSkyrimTogether->standardId << 24) | 0x1825)); + if (!pSpell) + return; + + MagicTarget::AddTargetData data{}; + data.pSpell = pSpell; + data.pEffectItem = pSpell->GetEffect((pSkyrimTogether->standardId << 24) | 0x1824); + data.fMagnitude = 1.f; + data.fUnkFloat1 = 1.f; + data.eCastingSource = MagicSystem::CastingSource::CASTING_SOURCE_COUNT; + + // Match Reveal Players targeting: all remote players, then filter by downed server id + auto view = m_world.view(); + for (const auto entity : view) + { + const auto& formIdComponent = view.get(entity); + + // Never glow the local player + if (formIdComponent.Id == 0x14) + continue; + + // Resolve server id for this actor; skip if we can't + auto serverIdOpt = Utils::GetServerId(entity); + if (!serverIdOpt.has_value()) + continue; + + const auto serverId = serverIdOpt.value(); + + // Only apply to players currently marked as downed + if (m_downedPartyMembers.find(serverId) == m_downedPartyMembers.end()) + continue; + + if (auto* pRemotePlayer = Cast(TESForm::GetById(formIdComponent.Id))) + pRemotePlayer->magicTarget.AddTarget(data, false, false); + } +} + +void MagicService::UpdateReviveChannels(double aDeltaSeconds) noexcept +{ + if (!m_victimReviveState) + return; + + PlayerCharacter* pLocalPlayer = PlayerCharacter::Get(); + if (!pLocalPlayer || !pLocalPlayer->actorState.IsBleedingOut()) + { + StopVictimReviveUi(); + return; + } + + const auto now = std::chrono::steady_clock::now(); + if (now - m_victimReviveState->LastPingAt > cReviveChannelTimeout) + { + StopVictimReviveUi(); + return; + } + + auto& state = *m_victimReviveState; + state.AccumulatedSeconds = std::min(state.RequiredSeconds, state.AccumulatedSeconds + static_cast(aDeltaSeconds)); + + UpdateVictimReviveUi(state); + + if (state.AccumulatedSeconds >= state.RequiredSeconds) + { + World::Get().ctx().at().OnHealRevive(); + StopVictimReviveUi(); + } +} + +void MagicService::UpdateHealerChannel(double aDeltaSeconds) noexcept +{ + if (!m_healerChannelState.Active) + return; + + const auto now = std::chrono::steady_clock::now(); + + if (!HasDownedPartyMemberInRange(cHealingHandsRange) || now - m_healerChannelState.LastUpdate > cReviveChannelTimeout) + { + StopHealerUi(); + return; + } + + m_healerChannelState.AccumulatedSeconds = std::min( + m_healerChannelState.RequiredSeconds, + m_healerChannelState.AccumulatedSeconds + static_cast(aDeltaSeconds)); + + UpdateHealerUi(); + + if (m_healerChannelState.AccumulatedSeconds >= m_healerChannelState.RequiredSeconds) + StopHealerUi(); +} + +float MagicService::GetRequiredReviveDuration(float aRestorationLevel) const noexcept +{ + constexpr float cMinLevel = 20.f; + constexpr float cMaxLevel = 100.f; + constexpr float cMinSeconds = 5.f; + constexpr float cMaxSeconds = 15.f; + + if (aRestorationLevel <= cMinLevel) + return cMaxSeconds; + if (aRestorationLevel >= cMaxLevel) + return cMinSeconds; + + const float normalized = (aRestorationLevel - cMinLevel) / (cMaxLevel - cMinLevel); + return cMaxSeconds - normalized * (cMaxSeconds - cMinSeconds); +} + +void MagicService::UpdateVictimReviveUi(const ReviveChannelState& aState) const noexcept +{ + if (auto* pOverlay = m_world.GetOverlayService().GetOverlayApp()) + { + auto pArgs = CefListValue::Create(); + pArgs->SetDouble(0, aState.AccumulatedSeconds); + pArgs->SetDouble(1, aState.RequiredSeconds); + pArgs->SetString(2, aState.HealerName); + pOverlay->ExecuteAsync("updateReviveVictimProgress", pArgs); + } +} + +void MagicService::StopVictimReviveUi() noexcept +{ + if (!m_victimReviveState) + return; + + if (auto* pOverlay = m_world.GetOverlayService().GetOverlayApp()) + { + auto pArgs = CefListValue::Create(); + pOverlay->ExecuteAsync("stopReviveVictimProgress", pArgs); + } + + m_victimReviveState.reset(); +} + +void MagicService::UpdateHealerUi() const noexcept +{ + if (!m_healerChannelState.Active) + return; + + if (auto* pOverlay = m_world.GetOverlayService().GetOverlayApp()) + { + auto pArgs = CefListValue::Create(); + pArgs->SetDouble(0, m_healerChannelState.AccumulatedSeconds); + pArgs->SetDouble(1, m_healerChannelState.RequiredSeconds); + pOverlay->ExecuteAsync("updateReviveHealerProgress", pArgs); + } +} + +void MagicService::StopHealerUi() noexcept +{ + if (!m_healerChannelState.Active) + return; + + if (auto* pOverlay = m_world.GetOverlayService().GetOverlayApp()) + { + auto pArgs = CefListValue::Create(); + pOverlay->ExecuteAsync("stopReviveHealerProgress", pArgs); + } + + m_healerChannelState.Active = false; + m_healerChannelState.AccumulatedSeconds = 0.f; + m_healerChannelState.RequiredSeconds = 0.f; + m_healerChannelState.LastUpdate = {}; +} + +Actor* MagicService::FindActorByServerId(uint32_t aServerId) const noexcept +{ + auto localView = m_world.view(); + for (auto entity : localView) + { + const auto& localComponent = localView.get(entity); + if (localComponent.Id != aServerId) + continue; + + const auto& formIdComponent = localView.get(entity); + return Cast(TESForm::GetById(formIdComponent.Id)); + } + + auto remoteView = m_world.view(); + for (auto entity : remoteView) + { + const auto& remoteComponent = remoteView.get(entity); + if (remoteComponent.Id != aServerId) + continue; + + const auto& formIdComponent = remoteView.get(entity); + return Cast(TESForm::GetById(formIdComponent.Id)); + } + + return nullptr; +} + +bool MagicService::HasDownedPartyMemberInRange(float aRange) noexcept +{ + if (m_downedPartyMembers.empty()) + { + // Fall back to live actor state if we missed a downed notification. + const auto& partyMembers = m_world.GetPartyService().GetPartyMembers(); + if (partyMembers.empty()) + return false; + + PlayerCharacter* pLocalPlayer = PlayerCharacter::Get(); + if (!pLocalPlayer) + return false; + + const float rangeSquared = aRange * aRange; + auto remoteView = m_world.view(); + for (auto entity : remoteView) + { + const auto& playerComponent = remoteView.get(entity); + if (std::find(partyMembers.begin(), partyMembers.end(), playerComponent.Id) == partyMembers.end()) + continue; + + const auto& formIdComponent = remoteView.get(entity); + Actor* pRemotePlayer = Cast(TESForm::GetById(formIdComponent.Id)); + if (!pRemotePlayer || !pRemotePlayer->actorState.IsBleedingOut()) + continue; + + const float dx = pLocalPlayer->position.x - pRemotePlayer->position.x; + const float dy = pLocalPlayer->position.y - pRemotePlayer->position.y; + const float dz = pLocalPlayer->position.z - pRemotePlayer->position.z; + const float distanceSquared = dx * dx + dy * dy + dz * dz; + if (distanceSquared > rangeSquared) + continue; + + DownedMemberInfo info{}; + info.PlayerId = playerComponent.Id; + info.PositionX = pRemotePlayer->position.x; + info.PositionY = pRemotePlayer->position.y; + info.PositionZ = pRemotePlayer->position.z; + m_downedPartyMembers[remoteView.get(entity).Id] = info; + return true; + } + + return false; + } + + PlayerCharacter* pLocalPlayer = PlayerCharacter::Get(); + if (!pLocalPlayer) + return false; + + const float rangeSquared = aRange * aRange; + const auto localServerId = GetLocalServerId(); + + for (auto& [serverId, info] : m_downedPartyMembers) + { + if (localServerId && serverId == *localServerId) + continue; + + if (Actor* pActor = FindActorByServerId(serverId)) + { + info.PositionX = pActor->position.x; + info.PositionY = pActor->position.y; + info.PositionZ = pActor->position.z; + } + + const float dx = pLocalPlayer->position.x - info.PositionX; + const float dy = pLocalPlayer->position.y - info.PositionY; + const float dz = pLocalPlayer->position.z - info.PositionZ; + const float distanceSquared = dx * dx + dy * dy + dz * dz; + + if (distanceSquared <= rangeSquared) + return true; + } + + // Re-check live actors in case the cached downed positions are stale. + const auto& partyMembers = m_world.GetPartyService().GetPartyMembers(); + if (!partyMembers.empty()) + { + auto remoteView = m_world.view(); + for (auto entity : remoteView) + { + const auto& playerComponent = remoteView.get(entity); + if (std::find(partyMembers.begin(), partyMembers.end(), playerComponent.Id) == partyMembers.end()) + continue; + + const auto& formIdComponent = remoteView.get(entity); + Actor* pRemotePlayer = Cast(TESForm::GetById(formIdComponent.Id)); + if (!pRemotePlayer || !pRemotePlayer->actorState.IsBleedingOut()) + continue; + + const float dx = pLocalPlayer->position.x - pRemotePlayer->position.x; + const float dy = pLocalPlayer->position.y - pRemotePlayer->position.y; + const float dz = pLocalPlayer->position.z - pRemotePlayer->position.z; + const float distanceSquared = dx * dx + dy * dy + dz * dz; + if (distanceSquared > rangeSquared) + continue; + + DownedMemberInfo info{}; + info.PlayerId = playerComponent.Id; + info.PositionX = pRemotePlayer->position.x; + info.PositionY = pRemotePlayer->position.y; + info.PositionZ = pRemotePlayer->position.z; + m_downedPartyMembers[remoteView.get(entity).Id] = info; + return true; + } + } + + return false; +} + +std::string MagicService::ResolvePlayerName(uint32_t aServerId) const +{ + const auto resolveByPlayerId = [&](uint32_t playerId) -> std::string + { + const auto& players = m_world.GetPartyService().GetPlayers(); + if (auto it = players.find(playerId); it != players.end()) + return it->second.Name.c_str(); + return {}; + }; + + auto localView = m_world.view(); + for (auto entity : localView) + { + const auto& localComponent = localView.get(entity); + if (localComponent.Id == aServerId) + return resolveByPlayerId(localView.get(entity).Id); + } + + auto remoteView = m_world.view(); + for (auto entity : remoteView) + { + const auto& remoteComponent = remoteView.get(entity); + if (remoteComponent.Id == aServerId) + return resolveByPlayerId(remoteView.get(entity).Id); + } + + return {}; +} + +std::optional MagicService::GetLocalServerId() const noexcept +{ + auto view = m_world.view(); + for (auto entity : view) + return view.get(entity).Id; + + return std::nullopt; +} + +void MagicService::UpdateHealingHandsBroadcast(double aDeltaSeconds) noexcept +{ + if (!m_isLocalHealingHandsActive || m_activeHealingHandsSpellId == 0) + return; + + m_healingHandsPingAccumulator -= aDeltaSeconds; + if (m_healingHandsPingAccumulator > 0.0) + return; + + if (!SendHealingProximityPing(m_activeHealingHandsSpellId)) + { + spdlog::warn("UpdateHealingHandsBroadcast: failed to send healing ping, aborting local channel"); + ResetLocalHealingHandsState(); + return; + } + + m_healingHandsPingAccumulator = cHealingHandsPingInterval; +} + +bool MagicService::SendHealingProximityPing(uint32_t aSpellFormId) noexcept +{ + PlayerCharacter* pCaster = PlayerCharacter::Get(); + if (!pCaster) + return false; + + auto view = m_world.view(); + const auto casterIt = std::find_if(std::begin(view), std::end(view), + [formId = pCaster->formID, view](entt::entity entity) { + return view.get(entity).Id == formId; + }); + + if (casterIt == std::end(view)) + return false; + + auto& localComponent = view.get(*casterIt); + + HealingProximityRequest healRequest{}; + healRequest.CasterId = localComponent.Id; + healRequest.CasterX = pCaster->position.x; + healRequest.CasterY = pCaster->position.y; + healRequest.CasterZ = pCaster->position.z; + + const float restorationLevel = pCaster->GetActorValue(ActorValueInfo::kRestoration); + healRequest.CasterRestorationLevel = static_cast(std::clamp(restorationLevel, 0.f, 1000.f)); + + if (!m_world.GetModSystem().GetServerModId(aSpellFormId, healRequest.SpellFormId)) + { + spdlog::error("SendHealingProximityPing: server spell id not found for spell {:X}", aSpellFormId); + return false; + } + + spdlog::debug("SendHealingProximityPing: spell {:X} at ({:.1f}, {:.1f}, {:.1f})", + aSpellFormId, healRequest.CasterX, healRequest.CasterY, healRequest.CasterZ); + m_transport.Send(healRequest); + return true; +} + +void MagicService::ResetLocalHealingHandsState() noexcept +{ + StopHealerUi(); + m_isLocalHealingHandsActive = false; + m_activeHealingHandsSpellId = 0; + m_healingHandsPingAccumulator = 0.0; + m_localHealingHandsSources.fill(false); +} + +void MagicService::HandleHealingHandsInterrupt(MagicSystem::CastingSource aSource) noexcept +{ + if (aSource < 0 || aSource >= MagicSystem::CastingSource::CASTING_SOURCE_COUNT) + return; + + if (!m_localHealingHandsSources[aSource]) + return; + + m_localHealingHandsSources[aSource] = false; + + const bool anyActive = std::any_of( + m_localHealingHandsSources.begin(), + m_localHealingHandsSources.end(), + [](bool active) { return active; }); + + if (!anyActive) + ResetLocalHealingHandsState(); +} + +bool MagicService::IsHealingHandsSpell(uint32_t aSpellFormId, const SpellItem* apSpell) const noexcept +{ + if ((aSpellFormId & cFormIdMask) == cHealingHandsBaseId) + return true; + + if (!apSpell) + return false; + + if (!apSpell->IsHealingSpell()) + return false; + + if (apSpell->eCastingType != MagicSystem::CastingType::CONCENTRATION) + return false; + + if (apSpell->eDelivery == MagicSystem::Delivery::SELF) + return false; + + for (EffectItem* pEffect : apSpell->listOfEffects) + { + if (!pEffect || !pEffect->pEffectSetting) + continue; + + const auto delivery = static_cast(pEffect->pEffectSetting->deliveryType); + if (delivery == MagicSystem::Delivery::AIMED || delivery == MagicSystem::Delivery::TARGET_ACTOR || delivery == MagicSystem::Delivery::TOUCH) + return true; + } + + return false; +} + +void MagicService::OnNotifyHealingProximity(const NotifyHealingProximity& acMessage) noexcept +{ + PlayerCharacter* pLocalPlayer = PlayerCharacter::Get(); + if (!pLocalPlayer) + return; + + if (!IsHealingHandsSpell(acMessage.SpellFormId.BaseId)) + return; + + const auto now = std::chrono::steady_clock::now(); + const auto localServerId = GetLocalServerId(); + const bool isCaster = localServerId.has_value() && acMessage.CasterId == localServerId.value(); + + if (pLocalPlayer->actorState.IsBleedingOut()) + { + const float distance = std::sqrt( + std::pow(pLocalPlayer->position.x - acMessage.CasterX, 2.0f) + + std::pow(pLocalPlayer->position.y - acMessage.CasterY, 2.0f) + + std::pow(pLocalPlayer->position.z - acMessage.CasterZ, 2.0f)); + + if (distance <= cHealingHandsRange) + { + const float requiredSeconds = GetRequiredReviveDuration(static_cast(acMessage.CasterRestorationLevel)); + + if (!m_victimReviveState || m_victimReviveState->CasterServerId != acMessage.CasterId) + { + m_victimReviveState = ReviveChannelState{}; + m_victimReviveState->CasterServerId = acMessage.CasterId; + m_victimReviveState->AccumulatedSeconds = 0.f; + m_victimReviveState->HealerName = ResolvePlayerName(acMessage.CasterId); + } + + auto& state = *m_victimReviveState; + state.RequiredSeconds = requiredSeconds; + state.LastPingAt = now; + + if (state.HealerName.empty()) + state.HealerName = ResolvePlayerName(acMessage.CasterId); + + UpdateVictimReviveUi(state); + } + else + { + StopVictimReviveUi(); + } + } + + if (isCaster) + { + if (HasDownedPartyMemberInRange(cHealingHandsRange)) + { + if (!m_healerChannelState.Active) + { + m_healerChannelState.AccumulatedSeconds = 0.f; + m_healerChannelState.Active = true; + } + + m_healerChannelState.RequiredSeconds = GetRequiredReviveDuration(static_cast(acMessage.CasterRestorationLevel)); + m_healerChannelState.LastUpdate = now; + UpdateHealerUi(); + } + else + { + StopHealerUi(); + } + } +} diff --git a/Code/client/Services/Generic/MapService.cpp b/Code/client/Services/Generic/MapService.cpp index 7d57dbf0e..937d54dd0 100644 --- a/Code/client/Services/Generic/MapService.cpp +++ b/Code/client/Services/Generic/MapService.cpp @@ -2,18 +2,30 @@ #include +#include +#include #include #include +#include #include #include +#include +#include #include #include +#include +#include + #include +#include +#include MapService::MapService(World& aWorld, entt::dispatcher& aDispatcher, TransportService& aTransport) noexcept : m_world(aWorld), m_dispatcher(aDispatcher), m_transport(aTransport) { + m_updateConnection = m_dispatcher.sink().connect<&MapService::OnUpdate>(this); + m_playerNotifySetWaypointConnection = m_dispatcher.sink().connect<&MapService::OnNotifySetWaypoint>(this); m_playerNotifyRemoveWaypointConnection = @@ -22,6 +34,177 @@ MapService::MapService(World& aWorld, entt::dispatcher& aDispatcher, TransportSe m_dispatcher.sink().connect<&MapService::OnSetWaypoint>(this); m_playerRemoveWaypointConnection = m_dispatcher.sink().connect<&MapService::OnRemoveWaypoint>(this); + + m_partyInfoConnection = + m_dispatcher.sink().connect<&MapService::OnNotifyPartyInfo>(this); + m_partyFastTravelMarkersConnection = + m_dispatcher.sink().connect<&MapService::OnNotifyPartyFastTravelMarkers>(this); + m_partyJoinedConnection = + m_dispatcher.sink().connect<&MapService::OnPartyJoined>(this); + + EventDispatcherManager::Get()->loadGameEvent.RegisterSink(this); +} + +namespace +{ +constexpr uint8_t kMapMarkerFlag_Visible = 1 << 0; +constexpr uint8_t kMapMarkerFlag_CanTravelTo = 1 << 1; + +struct LocationDiscoveryEvent +{ + MapMarkerData* mapMarkerData; + const char* worldspaceID; +}; + +inline EventDispatcher* GetLocationDiscoveryEventDispatcher() noexcept +{ + using TGetEventSource = EventDispatcher*(); + + // CommonLibSSE: RE::LocationDiscovery::GetEventSource() -> RELOCATION_ID(40056, 41067) + POINTER_SKYRIMSE(TGetEventSource, s_getEventSource, 40056); + + return s_getEventSource.Get()(); +} + +inline bool IsFastTravelMarker(TESObjectREFR* apRefr) noexcept +{ + if (!apRefr) + return false; + + auto* pExtraData = apRefr->GetExtraDataList(); + if (!pExtraData) + return false; + + auto* pExtraMapMarker = static_cast(pExtraData->GetByType(ExtraDataType::MapMarker)); + if (!pExtraMapMarker || !pExtraMapMarker->mapData) + return false; + + const uint8_t flags = pExtraMapMarker->mapData->flags; + return (flags & kMapMarkerFlag_Visible) && (flags & kMapMarkerFlag_CanTravelTo); +} + +inline bool DiscoverFastTravelMarker(TESObjectREFR* apMarkerRefr) noexcept +{ + if (!apMarkerRefr) + return false; + + auto* pExtraData = apMarkerRefr->GetExtraDataList(); + if (!pExtraData) + return false; + + auto* pExtraMapMarker = static_cast(pExtraData->GetByType(ExtraDataType::MapMarker)); + if (!pExtraMapMarker || !pExtraMapMarker->mapData) + return false; + + const uint8_t oldFlags = pExtraMapMarker->mapData->flags; + const bool wasFastTravelMarker = (oldFlags & kMapMarkerFlag_Visible) && (oldFlags & kMapMarkerFlag_CanTravelTo); + + pExtraMapMarker->mapData->flags |= (kMapMarkerFlag_Visible | kMapMarkerFlag_CanTravelTo); + + auto* pPlayer = PlayerCharacter::Get(); + if (!pPlayer) + return false; + + const auto handle = apMarkerRefr->GetHandle(); + if (!handle) + return false; + + auto* pCurrentMarkers = pPlayer->GetCurrentMapMarkers(); + if (!pCurrentMarkers) + return true; + + auto& currentMarkers = *pCurrentMarkers; + for (uint32_t i = 0; i < currentMarkers.length; ++i) + { + if (currentMarkers[i].handle.iBits == handle.handle.iBits) + return true; + } + + const uint32_t oldLen = currentMarkers.length; + currentMarkers.Resize(oldLen + 1); + currentMarkers[oldLen] = handle; + + // Fire the game's location discovery event so scripts/stat tracking/UI can react, but avoid refiring if already discovered. + if (!wasFastTravelMarker) + { + if (auto* pDispatcher = GetLocationDiscoveryEventDispatcher(); pDispatcher != nullptr) + { + const char* pWorldspaceName = ""; + if (TESWorldSpace* pWorldSpace = apMarkerRefr->GetWorldSpace(); pWorldSpace != nullptr) + { + if (pWorldSpace->fullName.value.AsAscii() != nullptr) + pWorldspaceName = pWorldSpace->fullName.value.AsAscii(); + } + + LocationDiscoveryEvent ev{}; + ev.mapMarkerData = pExtraMapMarker->mapData; + ev.worldspaceID = pWorldspaceName; + + pDispatcher->PushEvent(&ev); + } + } + + return true; +} +} + +void MapService::OnUpdate(const UpdateEvent&) noexcept +{ + ProcessPendingFastTravelMarkers(); + + if (!m_transport.IsConnected()) + return; + + if (!m_world.Get().GetPartyService().IsInParty()) + return; + + if (!m_world.Get().GetPartyService().GetPartyOptions().SyncFastTravelMarkers()) + return; + + const auto now = m_transport.GetClock().GetCurrentTick(); + if (m_nextMarkerScanTick > now) + return; + + // Scan at most twice per second. + m_nextMarkerScanTick = now + 500; + + auto* pPlayer = PlayerCharacter::Get(); + if (!pPlayer) + return; + + const auto* pCurrentMarkers = pPlayer->GetCurrentMapMarkers(); + if (!pCurrentMarkers) + return; + + const auto& currentMarkers = *pCurrentMarkers; + + TiltedPhoques::Vector newlyDiscovered{}; + newlyDiscovered.reserve(currentMarkers.length); + + ModSystem& modSystem = m_world.Get().GetModSystem(); + + for (uint32_t i = 0; i < currentMarkers.length; ++i) + { + const uint32_t handleBits = currentMarkers[i].handle.iBits; + if (handleBits == 0) + continue; + + TESObjectREFR* pMarker = TESObjectREFR::GetByHandle(handleBits); + if (!pMarker) + continue; + + if (!IsFastTravelMarker(pMarker)) + continue; + + GameId markerId{}; + if (!modSystem.GetServerModId(pMarker->formID, markerId)) + continue; + + if (m_knownFastTravelMarkers.insert(markerId).second) + newlyDiscovered.push_back(markerId); + } + + SendFastTravelMarkers(newlyDiscovered, /*aAllowEmpty*/ false, /*aFullSync*/ false); } void MapService::OnSetWaypoint(const SetWaypointEvent& acMessage) noexcept @@ -66,3 +249,213 @@ void MapService::OnNotifyRemoveWaypoint(const NotifyRemoveWaypoint& acMessage) n PlayerCharacter::Get()->RemoveWaypoint(); } +void MapService::OnNotifyPartyFastTravelMarkers(const NotifyPartyFastTravelMarkers& acMessage) noexcept +{ + if (!m_world.Get().GetPartyService().GetPartyOptions().SyncFastTravelMarkers()) + return; + + // Queue for retry to handle missing mod resolution / unloaded references. + for (const auto& marker : acMessage.Markers) + { + if (!marker) + continue; + + if (m_knownFastTravelMarkers.insert(marker).second) + m_pendingFastTravelMarkers.push_back(marker); + } + + ProcessPendingFastTravelMarkers(); +} + +void MapService::OnNotifyPartyInfo(const NotifyPartyInfo& acMessage) noexcept +{ + if (!m_transport.IsConnected()) + return; + + if (!m_world.Get().GetPartyService().IsInParty()) + return; + + if (!m_world.Get().GetPartyService().GetPartyOptions().SyncFastTravelMarkers()) + return; + + TiltedPhoques::Set newMembers{}; + newMembers.reserve(acMessage.PlayerIds.size()); + for (const auto playerId : acMessage.PlayerIds) + newMembers.insert(playerId); + + bool membersChanged = (newMembers.size() != m_lastPartyMembers.size()); + if (!membersChanged) + { + for (const auto playerId : newMembers) + { + if (!m_lastPartyMembers.contains(playerId)) + { + membersChanged = true; + break; + } + } + } + + const bool leaderChanged = (m_lastLeaderPlayerId != 0) && (m_lastLeaderPlayerId != acMessage.LeaderPlayerId); + + // Update cached party snapshot. + m_lastPartyMembers = std::move(newMembers); + m_lastLeaderPlayerId = acMessage.LeaderPlayerId; + + if (membersChanged || leaderChanged) + { + spdlog::debug("Party updated: triggering fast travel marker resync (membersChanged={}, leaderChanged={})", membersChanged, + leaderChanged); + SyncFastTravelMarkers(/*aForceSendEvenIfEmpty*/ true); + } +} + +void MapService::OnPartyJoined(const PartyJoinedEvent&) noexcept +{ + if (!m_world.Get().GetPartyService().GetPartyOptions().SyncFastTravelMarkers()) + return; + + // Full sync on join to ensure two-way union. + const auto& partyService = m_world.Get().GetPartyService(); + m_lastPartyMembers.clear(); + for (const auto playerId : partyService.GetPartyMembers()) + m_lastPartyMembers.insert(playerId); + m_lastLeaderPlayerId = partyService.GetLeaderPlayerId(); + + spdlog::debug("Party joined: syncing fast travel markers"); + SyncFastTravelMarkers(/*aForceSendEvenIfEmpty*/ true); +} + +void MapService::SyncFastTravelMarkers(bool aForceSendEvenIfEmpty) noexcept +{ + if (!m_world.Get().GetPartyService().GetPartyOptions().SyncFastTravelMarkers()) + return; + + // Force a quick scan and also send a full marker list (or an empty request) to receive the party union. + m_nextMarkerScanTick = 0; + + const auto markers = CollectLocalFastTravelMarkers(); + for (const auto& marker : markers) + m_knownFastTravelMarkers.insert(marker); + + spdlog::debug("Fast travel marker sync: sending {} markers (forceEmpty={})", markers.size(), aForceSendEvenIfEmpty); + SendFastTravelMarkers(markers, /*aAllowEmpty*/ aForceSendEvenIfEmpty, /*aFullSync*/ true); +} + +void MapService::SendFastTravelMarkers(const TiltedPhoques::Vector& aMarkers, bool aAllowEmpty, bool aFullSync) noexcept +{ + if (aMarkers.empty() && !aAllowEmpty) + return; + + if (!m_transport.IsConnected()) + return; + + if (!m_world.Get().GetPartyService().IsInParty()) + return; + + if (!m_world.Get().GetPartyService().GetPartyOptions().SyncFastTravelMarkers()) + return; + + PartyFastTravelMarkersRequest request{}; + request.FullSync = aFullSync; + request.Markers = aMarkers; + m_transport.Send(request); +} + +BSTEventResult MapService::OnEvent(const TESLoadGameEvent*, const EventDispatcher*) +{ + // Switching saves can change which markers are actually discovered; clear local caches so party sync can re-unlock correctly. + spdlog::info("Load game: resetting fast travel marker sync state"); + + m_knownFastTravelMarkers.clear(); + m_pendingFastTravelMarkers.clear(); + m_nextMarkerScanTick = 0; + + if (m_transport.IsConnected() && m_world.Get().GetPartyService().IsInParty() && + m_world.Get().GetPartyService().GetPartyOptions().SyncFastTravelMarkers()) + SyncFastTravelMarkers(/*aForceSendEvenIfEmpty*/ true); + + return BSTEventResult::kOk; +} + +TiltedPhoques::Vector MapService::CollectLocalFastTravelMarkers() const noexcept +{ + TiltedPhoques::Set unique{}; + + auto* pPlayer = PlayerCharacter::Get(); + if (!pPlayer) + return {}; + + const auto* pCurrentMarkers = pPlayer->GetCurrentMapMarkers(); + if (!pCurrentMarkers) + return {}; + + const auto& currentMarkers = *pCurrentMarkers; + unique.reserve(currentMarkers.length); + + ModSystem& modSystem = m_world.Get().GetModSystem(); + + for (uint32_t i = 0; i < currentMarkers.length; ++i) + { + const uint32_t handleBits = currentMarkers[i].handle.iBits; + if (handleBits == 0) + continue; + + TESObjectREFR* pMarker = TESObjectREFR::GetByHandle(handleBits); + if (!pMarker) + continue; + + if (!IsFastTravelMarker(pMarker)) + continue; + + GameId markerId{}; + if (modSystem.GetServerModId(pMarker->formID, markerId)) + unique.insert(markerId); + } + + TiltedPhoques::Vector out{}; + out.reserve(unique.size()); + for (const auto& marker : unique) + out.push_back(marker); + + return out; +} + +void MapService::ProcessPendingFastTravelMarkers() noexcept +{ + if (!m_world.Get().GetPartyService().GetPartyOptions().SyncFastTravelMarkers()) + return; + + if (m_pendingFastTravelMarkers.empty()) + return; + + ModSystem& modSystem = m_world.Get().GetModSystem(); + + TiltedPhoques::Vector remaining{}; + remaining.reserve(m_pendingFastTravelMarkers.size()); + + for (const auto& marker : m_pendingFastTravelMarkers) + { + const uint32_t gameFormId = modSystem.GetGameId(marker); + if (gameFormId == 0) + { + remaining.push_back(marker); + continue; + } + + TESObjectREFR* pMarker = Cast(TESForm::GetById(gameFormId)); + if (!pMarker) + { + remaining.push_back(marker); + continue; + } + + if (!DiscoverFastTravelMarker(pMarker)) + { + remaining.push_back(marker); + continue; + } + } + + m_pendingFastTravelMarkers = std::move(remaining); +} diff --git a/Code/client/Services/Generic/NameTagService.cpp b/Code/client/Services/Generic/NameTagService.cpp new file mode 100644 index 000000000..21fff05fe --- /dev/null +++ b/Code/client/Services/Generic/NameTagService.cpp @@ -0,0 +1,774 @@ +#include + +#include + +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _MSC_VER +#pragma comment(lib, "windowscodecs.lib") +#endif + +namespace +{ +float Clamp01(float aValue) noexcept +{ + return std::clamp(aValue, 0.f, 1.f); +} + +float SmoothStep(float aEdge0, float aEdge1, float aValue) noexcept +{ + if (aEdge0 == aEdge1) + return aValue >= aEdge1 ? 1.f : 0.f; + + const float t = Clamp01((aValue - aEdge0) / (aEdge1 - aEdge0)); + return t * t * (3.f - 2.f * t); +} + +constexpr std::array BuildBase64Table() noexcept +{ + std::array table{}; + table.fill(-1); + + for (int i = 0; i < 26; ++i) + { + table['A' + i] = static_cast(i); + table['a' + i] = static_cast(i + 26); + } + + for (int i = 0; i < 10; ++i) + table['0' + i] = static_cast(52 + i); + + table[static_cast('+')] = 62; + table[static_cast('/')] = 63; + + return table; +} + +constexpr std::array kBase64Table = BuildBase64Table(); + +bool DecodeBase64(std::string_view aInput, std::vector& aOutput) noexcept +{ + int value = 0; + int bits = -8; + aOutput.clear(); + aOutput.reserve((aInput.size() * 3) / 4); + + for (unsigned char c : aInput) + { + if (c == '=') + break; + + if (std::isspace(static_cast(c))) + continue; + + const int8_t decoded = kBase64Table[c]; + if (decoded < 0) + continue; + + value = (value << 6) + decoded; + bits += 6; + + if (bits >= 0) + { + aOutput.push_back(static_cast((value >> bits) & 0xFF)); + bits -= 8; + } + } + + return !aOutput.empty(); +} + +IWICImagingFactory* GetWicFactory() noexcept +{ + static Microsoft::WRL::ComPtr s_factory; + static std::once_flag s_factoryOnce; + static HRESULT s_factoryResult = E_FAIL; + + std::call_once(s_factoryOnce, []() { + HRESULT hr = CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&s_factory)); + if (hr == CO_E_NOTINITIALIZED) + { + hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + if (SUCCEEDED(hr) || hr == RPC_E_CHANGED_MODE) + hr = CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&s_factory)); + } + s_factoryResult = hr; + }); + + return SUCCEEDED(s_factoryResult) ? s_factory.Get() : nullptr; +} +} // namespace + +NameTagService::NameTagService(World& aWorld, entt::dispatcher& aDispatcher) noexcept + : m_world(aWorld) +{ + auto& imgui = m_world.ctx().at(); + m_drawConnection = imgui.OnDraw.connect<&NameTagService::OnDraw>(this); + m_updateConnection = aDispatcher.sink().connect<&NameTagService::OnUpdate>(this); + + if (m_world.ctx().contains()) + m_renderSystem = &m_world.ctx().at(); +} + +void NameTagService::OnDraw() noexcept +{ + auto* pLocalPlayer = PlayerCharacter::Get(); + if (!pLocalPlayer) + return; + + if (auto* pUI = UI::Get()) + { + auto menuOpen = [pUI](const char* acName) { + return acName && pUI->GetMenuOpen(BSFixedString(acName)); + }; + + if (menuOpen("Dialogue Menu") || menuOpen("DialogueMenu") || menuOpen("InventoryMenu") || menuOpen("StatsMenu") || + menuOpen("SkillsMenu") || menuOpen("MagicMenu") || menuOpen("MapMenu") || menuOpen("TweenMenu") || + menuOpen("FavoritesMenu")) + { + return; + } + } + + const uint64_t nowTick = m_world.GetTick(); + GarbageCollectAvatarCache(nowTick); + + auto view = m_world.view(); + if (view.begin() == view.end()) + return; + + const auto& playerDirectory = m_world.GetPartyService().GetPlayers(); + + auto& imguiSvc = m_world.ctx().at(); + ImFont* pFont = imguiSvc.GetSkyrimFont(); + + ImDrawList* drawList = ImGui::GetForegroundDrawList(); + if (!drawList) + return; + + if (pFont) + ImGui::PushFont(pFont); + + const ImVec2 viewport = ImGui::GetIO().DisplaySize; + const glm::vec3 localPos = {pLocalPlayer->position.x, pLocalPlayer->position.y, pLocalPlayer->position.z}; + ImFont* font = pFont ? pFont : ImGui::GetFont(); + if (!font) + { + if (pFont) + ImGui::PopFont(); + return; + } + + const float baseFontSize = font->FontSize; + + if (m_mode == Mode::Hidden) + { + if (pFont) + ImGui::PopFont(); + return; + } + + for (auto entity : view) + { + const auto& playerComponent = view.get(entity); + const auto& formComponent = view.get(entity); + + Actor* pActor = Cast(TESForm::GetById(formComponent.Id)); + if (!pActor) + continue; + + const ActorExtension* pExtension = pActor->GetExtension(); + if (!pExtension || pExtension->IsLocalPlayer()) + continue; + + if (!ShouldRenderTag(playerComponent.Id)) + continue; + + auto nameIt = playerDirectory.find(playerComponent.Id); + const auto* actorName = m_world.GetPartyService().GetActorName(playerComponent.Id); + std::string fallbackName; + std::string_view displayName; + + if (m_namePreference == NamePreference::Actor && actorName && !actorName->empty()) + { + displayName = actorName->c_str(); + } + else if (nameIt != playerDirectory.end() && !nameIt->second.Name.empty()) + { + displayName = nameIt->second.Name.c_str(); + } + else if (actorName && !actorName->empty()) + { + displayName = actorName->c_str(); + } + else + { + fallbackName = fmt::format("#{}", playerComponent.Id); + displayName = fallbackName; + } + + const bool useCompactLayout = m_mode == Mode::Detailed; + const bool showAvatar = (m_mode == Mode::Detailed || m_mode == Mode::Normal); + const bool showLevel = (m_mode == Mode::Detailed || m_mode == Mode::Normal); + + std::string_view avatarData; + if (showAvatar && nameIt != playerDirectory.end() && !nameIt->second.Avatar.empty()) + avatarData = std::string_view(nameIt->second.Avatar.c_str(), nameIt->second.Avatar.size()); + + const NiPoint3 anchor = BuildAnchorPoint(pActor); + + ImVec2 screenPos; + float depth = 0.f; + if (!ProjectWorldPoint(anchor, screenPos, depth, viewport)) + continue; + + const glm::vec3 remotePos = {anchor.x, anchor.y, anchor.z}; + const float distance = glm::distance(localPos, remotePos); + if (distance > m_style.MaxDistance) + continue; + + const float reference = std::max(m_style.ScaleReferenceDistance, 1.f); + const float normalizedDistance = reference / (reference + std::max(distance, 0.1f)); + const float falloffPower = std::max(m_style.ScaleFalloff, 0.05f); + float scale = m_style.MinScale + (m_style.MaxScale - m_style.MinScale) * std::pow(normalizedDistance, falloffPower); + scale = std::clamp(scale, m_style.MinScale, m_style.MaxScale); + + if (m_style.DepthFar > m_style.DepthNear) + { + const float depthFactor = 1.f - Clamp01((depth - m_style.DepthNear) / (m_style.DepthFar - m_style.DepthNear)); + scale = std::clamp(scale * (1.f + depthFactor * m_style.DepthPerspectiveBoost), m_style.MinScale, m_style.MaxScale); + } + + float alpha = 1.f; + if (distance > m_style.FadeDistance) + { + const float fadeSpan = std::max(m_style.MaxDistance - m_style.FadeDistance, 1.f); + const float fadeT = Clamp01((distance - m_style.FadeDistance) / fadeSpan); + alpha = 1.f - SmoothStep(0.f, 1.f, fadeT); + } + + alpha *= Clamp01(1.f - std::max(depth - 0.95f, 0.f) * 4.f); + if (alpha <= 0.01f) + continue; + + const uint16_t level = pActor->GetLevel(); + const std::string levelText = level > 0 ? fmt::format("Lv. {}", level) : std::string("Lv. --"); + + const float nameFontSize = baseFontSize * scale; + const float levelFontSize = nameFontSize * m_style.LevelFontScale; + const ImVec2 nameTextSize = font->CalcTextSizeA(nameFontSize, FLT_MAX, 0.f, displayName.data(), displayName.data() + displayName.size()); + const ImVec2 levelTextSize = font->CalcTextSizeA(levelFontSize, FLT_MAX, 0.f, levelText.c_str(), levelText.c_str() + levelText.size()); + + const float paddingX = m_style.PaddingX * scale; + const float paddingY = m_style.PaddingY * scale; + const float baseAvatarSize = m_style.AvatarSize * scale; + const float avatarSize = showAvatar ? baseAvatarSize * (useCompactLayout ? m_style.CompactAvatarScale : 1.f) : 0.f; + const float avatarSpacing = m_style.AvatarSpacing * scale; + const float nameLevelSpacing = m_style.NameLevelSpacing * scale; + const float accentHeight = m_style.AccentThickness * scale; + + float textColumnWidth = nameTextSize.x; + float textColumnHeight = nameTextSize.y; + if (showLevel) + { + if (useCompactLayout) + { + textColumnWidth += nameLevelSpacing + levelTextSize.x; + textColumnHeight = std::max(nameTextSize.y, levelTextSize.y); + } + else + { + textColumnWidth = std::max(nameTextSize.x, levelTextSize.x); + textColumnHeight = nameTextSize.y + nameLevelSpacing + levelTextSize.y; + } + } + + const float avatarContribution = showAvatar ? (avatarSize + avatarSpacing) : 0.f; + const float contentHeight = showAvatar ? std::max(textColumnHeight, avatarSize) : textColumnHeight; + const float contentWidth = avatarContribution + textColumnWidth; + const float totalWidth = contentWidth + paddingX * 2.f; + const float totalHeight = contentHeight + paddingY * 2.f + accentHeight; + + ImVec2 topLeft(screenPos.x - totalWidth * 0.5f, screenPos.y - totalHeight * 0.5f); + ImVec2 bottomRight(screenPos.x + totalWidth * 0.5f, screenPos.y + totalHeight * 0.5f); + + // Light 3D cue: slight horizontal offset based on how much the actor looks at the camera. + const glm::vec3 actorForward = {std::sin(pActor->rotation.z), std::cos(pActor->rotation.z), 0.f}; + glm::vec3 toCameraVec = localPos - remotePos; + if (glm::length2(toCameraVec) <= std::numeric_limits::epsilon()) + toCameraVec = {0.f, 0.f, 1.f}; + const glm::vec3 toCamera = glm::normalize(toCameraVec); + const glm::vec3 forwardNorm = glm::normalize(actorForward); + const float facing = Clamp01((glm::dot(forwardNorm, toCamera) + 1.f) * 0.5f); + const float skew = (1.f - facing) * (paddingX * 0.35f); + topLeft.x -= skew; + bottomRight.x -= skew; + screenPos.x -= skew; + + const ImU32 bgColor = ColorWithAlpha(m_style.BackgroundColor, alpha); + drawList->AddRectFilled(topLeft, bottomRight, bgColor, m_style.CornerRounding); + + const ImU32 borderColor = ColorWithAlpha(m_style.BorderColor, alpha); + drawList->AddRect(topLeft, bottomRight, borderColor, m_style.CornerRounding); + + ImVec2 avatarMin{}; + ImVec2 avatarMax{}; + ImVec2 avatarCenter{}; + if (showAvatar) + { + avatarMin = ImVec2(topLeft.x + paddingX, topLeft.y + paddingY + (contentHeight - avatarSize) * 0.5f); + avatarMax = ImVec2(avatarMin.x + avatarSize, avatarMin.y + avatarSize); + avatarCenter = ImVec2((avatarMin.x + avatarMax.x) * 0.5f, (avatarMin.y + avatarMax.y) * 0.5f); + } + + const ImVec2 textTopLeft = + ImVec2(topLeft.x + paddingX + avatarContribution, topLeft.y + paddingY + (contentHeight - textColumnHeight) * 0.5f); + ImVec2 namePos = ImVec2(textTopLeft.x, textTopLeft.y); + if (useCompactLayout) + namePos.y += (textColumnHeight - nameTextSize.y) * 0.5f; + + ImVec2 levelPos = namePos; + if (showLevel) + { + if (useCompactLayout) + { + levelPos.x = textTopLeft.x + nameTextSize.x + nameLevelSpacing; + levelPos.y = textTopLeft.y + (textColumnHeight - levelTextSize.y) * 0.5f; + } + else + { + levelPos.x = textTopLeft.x; + levelPos.y = textTopLeft.y + nameTextSize.y + nameLevelSpacing; + } + } + + const ImVec2 accentMin(topLeft.x + paddingX, bottomRight.y - paddingY - accentHeight); + const ImVec2 accentMax(bottomRight.x - paddingX, accentMin.y + accentHeight); + const ImU32 accentColor = ColorWithAlpha(m_style.AccentColor, alpha); + drawList->AddRectFilled(accentMin, accentMax, accentColor, m_style.CornerRounding); + + if (showAvatar) + { + const AvatarTexture* avatarTexture = ResolveAvatarTexture(playerComponent.Id, avatarData, nowTick); + ID3D11ShaderResourceView* avatarView = avatarTexture ? avatarTexture->Texture.Get() : nullptr; + const ImU32 ringColor = ColorWithAlpha(m_style.AvatarRingColor, alpha); + + const float avatarRounding = avatarSize * 0.5f; + if (avatarView) + { + drawList->AddImageRounded(avatarView, avatarMin, avatarMax, ImVec2(0.f, 0.f), ImVec2(1.f, 1.f), IM_COL32_WHITE, avatarRounding, + ImDrawFlags_RoundCornersAll); + } + else + { + const ImU32 placeholderColor = ColorWithAlpha(m_style.PlaceholderAvatarColor, alpha); + drawList->AddCircleFilled(avatarCenter, avatarSize * 0.5f, placeholderColor, 48); + + if (!displayName.empty()) + { + const unsigned char rawChar = static_cast(displayName.front()); + const char initial = static_cast(std::toupper(rawChar)); + char buffer[2] = {initial, '\0'}; + const ImVec2 initialSize = font->CalcTextSizeA(levelFontSize, FLT_MAX, 0.f, buffer, buffer + 1); + const ImVec2 initialPos = ImVec2(avatarCenter.x - initialSize.x * 0.5f, avatarCenter.y - initialSize.y * 0.5f); + drawList->AddText(font, levelFontSize, initialPos, ColorWithAlpha(m_style.TextColor, alpha), buffer); + } + } + + drawList->AddCircle(avatarCenter, (avatarSize * 0.5f) + 1.f, ringColor, 48, 2.f); + } + + const ImU32 textColor = ColorWithAlpha(m_style.TextColor, alpha); + drawList->AddText(font, nameFontSize, namePos, textColor, displayName.data(), displayName.data() + displayName.size()); + + if (showLevel) + { + const ImU32 levelColor = ColorWithAlpha(m_style.LevelTextColor, alpha); + drawList->AddText(font, levelFontSize, levelPos, levelColor, levelText.c_str(), levelText.c_str() + levelText.size()); + } + } + + if (pFont) + ImGui::PopFont(); +} + +void NameTagService::OnUpdate(const UpdateEvent&) noexcept +{ + auto* pLocalPlayer = PlayerCharacter::Get(); + if (!pLocalPlayer) + { + m_visibility.clear(); + return; + } + + const uint64_t tick = m_world.GetTick(); + size_t total = 0; + size_t resolved = 0; + size_t visibleCount = 0; + + auto view = m_world.view(); + for (auto entity : view) + { + ++total; + const auto& playerComponent = view.get(entity); + const auto& formComponent = view.get(entity); + Actor* pActor = Cast(TESForm::GetById(formComponent.Id)); + if (!pActor) + continue; + ++resolved; + + const float dx = pLocalPlayer->position.x - pActor->position.x; + const float dy = pLocalPlayer->position.y - pActor->position.y; + const float dz = pLocalPlayer->position.z - pActor->position.z; + const float distance = std::sqrt(dx * dx + dy * dy + dz * dz); + + bool isVisible = ComputeLineOfSight(pLocalPlayer, pActor); + if (!isVisible && distance <= m_style.NearBypassDistance) + isVisible = true; + + VisibilityInfo& info = m_visibility[playerComponent.Id]; + info.Visible = isVisible; + info.Tick = tick; + if (info.Visible) + ++visibleCount; + } + + constexpr uint64_t kMaxAgeMs = 2000; + for (auto it = m_visibility.begin(); it != m_visibility.end();) + { + if (tick - it->second.Tick > kMaxAgeMs) + it = m_visibility.erase(it); + else + ++it; + } + + static uint64_t s_lastLogTick = 0; + if (tick - s_lastLogTick > 1000) + { + spdlog::debug("[NameTagService] update candidates={} resolved={} visible={}", total, resolved, visibleCount); + s_lastLogTick = tick; + } +} + +bool NameTagService::ShouldRenderTag(uint32_t aPlayerId) const noexcept +{ + const uint64_t tick = m_world.GetTick(); + auto it = m_visibility.find(aPlayerId); + if (it == m_visibility.end()) + return true; // not evaluated yet, optimistically show tag + + constexpr uint64_t kStaleThresholdMs = 1500; + if (tick - it->second.Tick > kStaleThresholdMs) + return true; + + return it->second.Visible; +} + +bool NameTagService::ComputeLineOfSight(PlayerCharacter* apLocalPlayer, Actor* apRemoteActor) const noexcept +{ + if (!apLocalPlayer || !apRemoteActor) + return false; + + if (apRemoteActor->IsDead()) + return false; + + if (apLocalPlayer->HasLineOfSight(apRemoteActor)) + return true; + + if (apRemoteActor->HasLineOfSight(apLocalPlayer)) + return true; + + return false; +} + +bool NameTagService::ProjectWorldPoint(const NiPoint3& aWorldPoint, ImVec2& aScreenPos, float& aDepth, const ImVec2& aViewport) const noexcept +{ + NiPoint3 projected{}; + if (!HUDMenuUtils::WorldPtToScreenPt3(aWorldPoint, projected)) + return false; + + if (projected.z <= m_style.VisibilityEpsilon || projected.z >= 1.f) + return false; + + aScreenPos.x = projected.x * aViewport.x; + aScreenPos.y = (1.f - projected.y) * aViewport.y; + aDepth = projected.z; + + return true; +} + +NiPoint3 NameTagService::BuildAnchorPoint(Actor* apActor) const noexcept +{ + NiPoint3 anchor = apActor->position; + anchor.z += apActor->GetHeight() + m_style.VerticalOffset; + + const float yaw = apActor->rotation.z; + anchor.x += std::sin(yaw) * m_style.ForwardOffset; + anchor.y += std::cos(yaw) * m_style.ForwardOffset; + + return anchor; +} + +ImU32 NameTagService::ColorWithAlpha(const ImVec4& aColor, float aAlpha) noexcept +{ + ImVec4 adjusted = aColor; + adjusted.w *= Clamp01(aAlpha); + return ImGui::ColorConvertFloat4ToU32(adjusted); +} + +void NameTagService::GarbageCollectAvatarCache(uint64_t aNowTick) noexcept +{ + constexpr uint64_t kTtlMs = 30'000; + + for (auto it = m_avatarCache.begin(); it != m_avatarCache.end();) + { + if (it->second.LastSeenTick != 0 && aNowTick - it->second.LastSeenTick > kTtlMs) + it = m_avatarCache.erase(it); + else + ++it; + } +} + +const NameTagService::AvatarTexture* NameTagService::ResolveAvatarTexture(uint32_t aPlayerId, std::string_view aAvatarData, uint64_t aNowTick) +{ + if (aAvatarData.empty()) + { + m_avatarCache.erase(aPlayerId); + return nullptr; + } + + AvatarTexture& entry = m_avatarCache[aPlayerId]; + entry.LastSeenTick = aNowTick; + + if (entry.Texture && entry.Signature == aAvatarData) + return &entry; + + if (entry.Signature == aAvatarData && entry.Failed) + { + const uint64_t retryDelay = static_cast(m_style.AvatarRetryDelayMs); + if (aNowTick - entry.LastAttemptTick < retryDelay) + return entry.Texture ? &entry : nullptr; + } + + entry.Signature.assign(aAvatarData.begin(), aAvatarData.end()); + entry.LastAttemptTick = aNowTick; + + std::vector decoded; + if (!DecodeAvatarData(aAvatarData, decoded)) + { + entry.Texture.Reset(); + entry.Size = ImVec2{}; + entry.Failed = true; + return nullptr; + } + + if (!CreateAvatarTexture(decoded, entry)) + { + entry.Texture.Reset(); + entry.Size = ImVec2{}; + entry.Failed = true; + return nullptr; + } + + entry.Failed = false; + return &entry; +} + +bool NameTagService::DecodeAvatarData(std::string_view aAvatarData, std::vector& aDecodedData) const +{ + if (aAvatarData.rfind("data:", 0) != 0) + return false; + + const size_t maxBytes = static_cast(m_style.AvatarMaxBytes); + if (aAvatarData.size() > maxBytes * 3ull) + return false; + + const size_t comma = aAvatarData.find(','); + if (comma == std::string_view::npos) + return false; + + const std::string_view header = aAvatarData.substr(0, comma); + if (header.find(";base64") == std::string_view::npos) + return false; + + const std::string_view payload = aAvatarData.substr(comma + 1); + if (!DecodeBase64(payload, aDecodedData)) + return false; + + if (aDecodedData.empty() || aDecodedData.size() > maxBytes) + return false; + + return true; +} + +bool NameTagService::CreateAvatarTexture(const std::vector& aDecodedData, AvatarTexture& aEntry) noexcept +{ + if (aDecodedData.empty()) + return false; + + ID3D11Device* pDevice = AcquireD3DDevice(); + if (!pDevice) + return false; + + IWICImagingFactory* pFactory = GetWicFactory(); + if (!pFactory) + return false; + + Microsoft::WRL::ComPtr stream; + HRESULT hr = pFactory->CreateStream(stream.GetAddressOf()); + if (FAILED(hr)) + return false; + + hr = stream->InitializeFromMemory(const_cast(reinterpret_cast(aDecodedData.data())), static_cast(aDecodedData.size())); + if (FAILED(hr)) + return false; + + Microsoft::WRL::ComPtr decoder; + hr = pFactory->CreateDecoderFromStream(stream.Get(), nullptr, WICDecodeMetadataCacheOnLoad, decoder.GetAddressOf()); + if (FAILED(hr)) + return false; + + Microsoft::WRL::ComPtr frame; + hr = decoder->GetFrame(0, frame.GetAddressOf()); + if (FAILED(hr)) + return false; + + UINT width = 0; + UINT height = 0; + hr = frame->GetSize(&width, &height); + if (FAILED(hr) || width == 0 || height == 0) + return false; + + constexpr UINT kMaxDimension = 1024; + if (width > kMaxDimension || height > kMaxDimension) + return false; + + Microsoft::WRL::ComPtr converter; + hr = pFactory->CreateFormatConverter(converter.GetAddressOf()); + if (FAILED(hr)) + return false; + + hr = converter->Initialize(frame.Get(), GUID_WICPixelFormat32bppPBGRA, WICBitmapDitherTypeNone, nullptr, 0.f, WICBitmapPaletteTypeCustom); + if (FAILED(hr)) + return false; + + const UINT stride = width * 4; + std::vector pixels(static_cast(stride) * height); + hr = converter->CopyPixels(nullptr, stride, static_cast(pixels.size()), pixels.data()); + if (FAILED(hr)) + return false; + + D3D11_TEXTURE2D_DESC desc{}; + desc.Width = width; + desc.Height = height; + desc.MipLevels = 1; + desc.ArraySize = 1; + desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + desc.SampleDesc.Count = 1; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + + D3D11_SUBRESOURCE_DATA initData{}; + initData.pSysMem = pixels.data(); + initData.SysMemPitch = stride; + + Microsoft::WRL::ComPtr texture; + hr = pDevice->CreateTexture2D(&desc, &initData, texture.GetAddressOf()); + if (FAILED(hr)) + { + if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET) + m_cachedDevice.Reset(); + return false; + } + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc{}; + srvDesc.Format = desc.Format; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + srvDesc.Texture2D.MipLevels = desc.MipLevels; + + Microsoft::WRL::ComPtr view; + hr = pDevice->CreateShaderResourceView(texture.Get(), &srvDesc, view.GetAddressOf()); + if (FAILED(hr)) + return false; + + aEntry.Texture = std::move(view); + aEntry.Size = ImVec2(static_cast(width), static_cast(height)); + return true; +} + +ID3D11Device* NameTagService::AcquireD3DDevice() noexcept +{ + if (m_cachedDevice) + return m_cachedDevice.Get(); + + if (!m_renderSystem && m_world.ctx().contains()) + m_renderSystem = &m_world.ctx().at(); + + if (!m_renderSystem) + return nullptr; + + IDXGISwapChain* pSwapChain = m_renderSystem->GetSwapChain(); + if (!pSwapChain) + { + m_cachedDevice.Reset(); + return nullptr; + } + + Microsoft::WRL::ComPtr device; + if (FAILED(pSwapChain->GetDevice(__uuidof(ID3D11Device), reinterpret_cast(device.GetAddressOf())))) + return nullptr; + + m_cachedDevice = std::move(device); + return m_cachedDevice.Get(); +} + +void NameTagService::SetMode(Mode aMode) noexcept +{ + switch (aMode) + { + case Mode::Detailed: + case Mode::Basic: + case Mode::Hidden: + case Mode::Normal: + break; + default: + aMode = Mode::Normal; + break; + } + + if (m_mode == aMode) + return; + + spdlog::debug("[NameTagService] switching mode {}", static_cast(aMode)); + m_mode = aMode; +} diff --git a/Code/client/Services/Generic/ObjectService.cpp b/Code/client/Services/Generic/ObjectService.cpp index b78b68588..6de52b14d 100644 --- a/Code/client/Services/Generic/ObjectService.cpp +++ b/Code/client/Services/Generic/ObjectService.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include @@ -238,17 +239,54 @@ void ObjectService::OnActivate(const ActivateEvent& acEvent) noexcept return; } + bool hasCellId = false; TESObjectCELL* pCell = acEvent.pObject->GetParentCellEx(); - if (!pCell) + if (pCell) { - spdlog::error("Activated object has no parent cell: {:X}", acEvent.pObject->formID); - return; + hasCellId = m_world.GetModSystem().GetServerModId(pCell->formID, request.CellId); + if (!hasCellId) + { + spdlog::error("Server cell id not found for cell form id {:X}", pCell->formID); + return; + } } - - if (!m_world.GetModSystem().GetServerModId(pCell->formID, request.CellId)) + else { - spdlog::error("Server cell id not found for cell form id {:X}", acEvent.pObject->parentCell->formID); - return; + const auto resolveFromDrop = [&](uint64_t aDropId) { + if (auto dropData = DropManager::GetServerDrop(aDropId)) + { + request.CellId = dropData->CellId; + return request.CellId.ModId != 0 || request.CellId.BaseId != 0; + } + return false; + }; + + if (auto handle = acEvent.pObject->GetHandle(); handle && handle.handle.iBits) + { + if (auto dropId = DropManager::GetDropIdForHandle(handle.handle.iBits)) + hasCellId = resolveFromDrop(*dropId); + } + + if (!hasCellId) + { + GameId objectId{}; + World::Get().GetModSystem().GetServerModId(acEvent.pObject->baseForm->formID, objectId); + if (objectId.ModId || objectId.BaseId) + { + if (auto dropId = DropManager::FindDropBySignature(objectId, acEvent.pObject->position, 200.0f * 200.0f)) + { + hasCellId = resolveFromDrop(*dropId); + if (hasCellId) + spdlog::debug("ObjectService: resolved cell from drop {} for object {:X}", *dropId, acEvent.pObject->formID); + } + } + } + + if (!hasCellId) + { + spdlog::error("Activated object has no parent cell: {:X}", acEvent.pObject->formID); + return; + } } auto view = m_world.view(); diff --git a/Code/client/Services/Generic/OverlayClient.cpp b/Code/client/Services/Generic/OverlayClient.cpp index 6e6b983f5..ade104306 100644 --- a/Code/client/Services/Generic/OverlayClient.cpp +++ b/Code/client/Services/Generic/OverlayClient.cpp @@ -5,13 +5,159 @@ #include #include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include +#include +#include +#include +#include #include +#include #include +#include +#include + +extern thread_local bool g_forceAnimation; +extern thread_local bool g_forceAnimationNetwork; +std::atomic g_emoteWheelActive{false}; +std::string g_emoteEventName{}; +std::chrono::steady_clock::time_point g_emoteLastPlayed{}; +NiPoint3 g_emoteStartPos{}; +NiPoint3 g_emoteStartRot{}; +std::atomic g_emoteStartValid{false}; +std::atomic g_emoteEquipmentValid{false}; +EquipmentSnapshot g_emoteEquipmentSnapshot{}; + +namespace +{ +uint32_t ResolveFallbackActionId(Actor* apActor) noexcept +{ + if (!apActor) + return 0; + + const auto& latest = apActor->GetExtension()->LatestAnimation; + if (latest.ActionId) + return latest.ActionId; + + if (auto* pFallbackAction = DefaultObjectManager::Get().someAction) + return pFallbackAction->formID; + + return 0; +} + +void PlayEmoteInternal(const std::string& acEventName) +{ + World::Get().GetRunner().Queue([eventName = acEventName]() + { + Actor* pPlayer = PlayerCharacter::Get(); + if (!pPlayer) + return; + + g_emoteEquipmentSnapshot = CaptureEquipmentSnapshot(pPlayer); + g_emoteEquipmentValid.store(true); + + // Ensure hands/weapons are lowered before playing an emote; raised fists can block graph events. + pPlayer->SetWeaponDrawnEx(false); + + // Capture the starting transform so any keep-alive replay snaps back to the original pose. + g_emoteStartPos = pPlayer->position; + g_emoteStartRot = pPlayer->rotation; + g_emoteStartValid.store(true); + + if (g_emoteWheelActive.load()) + { + BSFixedString stopEvent("IdleForceDefaultState"); + pPlayer->SendAnimationEvent(&stopEvent); + } + + const auto actionId = ResolveFallbackActionId(pPlayer); + auto* pAction = Cast(TESForm::GetById(actionId)); + if (!pAction) + { + spdlog::warn("Unable to play emote '{}': no usable action found", eventName); + return; + } + + if (auto& transport = World::Get().GetTransport(); transport.IsConnected()) + { + auto view = World::Get().view(); + const auto it = std::find_if( + std::begin(view), std::end(view), + [view, formId = pPlayer->formID](entt::entity entity) { return view.get(entity).Id == formId; }); + + if (it != std::end(view)) + { + PlayEmoteRequest request{}; + request.ServerId = view.get(*it).Id; + request.EventName = eventName; + transport.Send(request); + } + } + + const auto& latest = pPlayer->GetExtension()->LatestAnimation; + TESIdleForm* pIdle = latest.IdleId ? Cast(TESForm::GetById(latest.IdleId)) : nullptr; + + // Use the same parameter bits the game already produced; fall back to the common idle param (2) + const uint32_t typeBits = latest.Type ? (latest.Type & 0x3) : 2; + const bool instantFlag = (latest.Type & 0x4) != 0; + + TESActionData actionData(typeBits, pPlayer, pAction, nullptr); + actionData.eventName = BSFixedString(eventName.c_str()); + actionData.idleForm = pIdle; + actionData.someFlag = instantFlag ? 1u : 0u; + + const bool ghosting = World::Get().ctx().contains() && + (World::Get().GetSyncModeService().GetLocalMode() == SyncMode::Ghost); + + g_forceAnimation = true; + g_forceAnimationNetwork = ghosting; + ActorMediator::Get()->PerformAction(&actionData); + g_forceAnimationNetwork = false; + g_forceAnimation = false; + + g_emoteWheelActive.store(true); + g_emoteEventName = eventName; + g_emoteLastPlayed = std::chrono::steady_clock::now(); + + // If the user chose a stop/clear emote, don't keep a stale transform around. + if (g_emoteEventName == "IdleForceDefaultState" || g_emoteEventName == "IdleStopInstant") + g_emoteStartValid.store(false); + else + { + if (auto* pOverlayApp = World::Get().GetOverlayService().GetOverlayApp()) + { + auto pArgs = CefListValue::Create(); + pArgs->SetString(0, "Press . to cancel emote"); + pArgs->SetInt(1, 0); // Persist until explicitly cleared + pOverlayApp->ExecuteAsync("showBanner", pArgs); + } + } + }); +} +} OverlayClient::OverlayClient(TransportService& aTransport, TiltedPhoques::OverlayRenderHandler* apHandler) : TiltedPhoques::OverlayClient(apHandler) @@ -32,10 +178,7 @@ bool OverlayClient::OnProcessMessageReceived(CefRefPtr browser, CefR auto eventName = pArguments->GetString(0).ToString(); auto eventArgs = pArguments->GetList(1); - spdlog::info(eventName); - spdlog::info(eventArgs->GetString(0).ToString()); - spdlog::info(std::to_string(eventArgs->GetInt(1))); - spdlog::info(eventArgs->GetString(2).ToString()); + spdlog::info("ui-event '{}' ({} args)", eventName, eventArgs->GetSize()); #ifndef PUBLIC_BUILD LOG(INFO) << "event=ui_event name=" << eventName; @@ -76,10 +219,83 @@ bool OverlayClient::OnProcessMessageReceived(CefRefPtr browser, CefR uint32_t aPlayerId = eventArgs->GetInt(0); World::Get().GetPartyService().ChangePartyLeader(aPlayerId); } - else if (eventName == "teleportToPlayer") - ProcessTeleportMessage(eventArgs); + else if (eventName == "setProfilePicture") + ProcessSetProfilePicture(eventArgs); + else if (eventName == "setNameTagMode") + ProcessSetNameTagMode(eventArgs); + else if (eventName == "setPlayerNamePreference") + ProcessSetPlayerNamePreference(eventArgs); + else if (eventName == "setPartyOptions") + ProcessSetPartyOptions(eventArgs); + else if (eventName == "teleportToPlayer" || eventName == "requestTeleport") + ProcessTeleportRequestMessage(eventArgs); + else if (eventName == "respondTeleportRequest") + ProcessTeleportResponseMessage(eventArgs); else if (eventName == "toggleDebugUI") ProcessToggleDebugUI(); + else if (eventName == "respawnButtonClicked") + World::Get().GetRunner().Queue([]() { World::Get().ctx().at().RequestManualRespawn(); }); + else if (eventName == "sendTradeInvite") + { + uint32_t aPlayerId = eventArgs->GetInt(0); + World::Get().GetTradeService().SendInvite(aPlayerId); + } + else if (eventName == "respondTradeInvite") + { + uint32_t inviterId = eventArgs->GetInt(0); + bool accept = eventArgs->GetBool(1); + World::Get().GetTradeService().RespondToInvite(inviterId, accept); + } + else if (eventName == "cancelTrade") + { + World::Get().GetTradeService().CancelTrade(); + } + else if (eventName == "setTradeReady") + { + bool ready = eventArgs->GetBool(0); + World::Get().GetTradeService().SetReady(ready); + } + else if (eventName == "playEmote") + ProcessPlayEmote(eventArgs); + else if (eventName == "updateTradeOffer") + { + TiltedPhoques::Vector selections; + auto pList = eventArgs->GetList(0); + if (pList) + { + const auto cCount = pList->GetSize(); + selections.reserve(cCount); + for (size_t i = 0; i < cCount; ++i) + { + TradeService::OfferSelection selection{}; + + if (pList->GetType(static_cast(i)) == VTYPE_DICTIONARY) + { + auto pEntry = pList->GetDictionary(static_cast(i)); + if (!pEntry) + continue; + + selection.Index = static_cast(pEntry->GetInt("index")); + selection.Count = pEntry->GetInt("count"); + } + else + { + auto pEntry = pList->GetList(static_cast(i)); + if (!pEntry || pEntry->GetSize() < 2) + continue; + + selection.Index = static_cast(pEntry->GetInt(0)); + selection.Count = pEntry->GetInt(1); + } + + if (selection.Count <= 0) + continue; + + selections.push_back(selection); + } + } + World::Get().GetTradeService().UpdateOffer(selections); + } return true; } @@ -95,8 +311,20 @@ void OverlayClient::ProcessConnectMessage(CefRefPtr aEventArgs) baseIp = "127.0.0.1"; } - uint16_t port = aEventArgs->GetInt(1) ? aEventArgs->GetInt(1) : 10578; - World::Get().GetTransport().SetServerPassword(aEventArgs->GetString(2)); + const uint16_t port = aEventArgs->GetInt(1) ? static_cast(aEventArgs->GetInt(1)) : 10578; + std::string username; + std::string password; + + if (aEventArgs->GetSize() >= 3) + username = aEventArgs->GetString(2); + if (aEventArgs->GetSize() >= 4) + password = aEventArgs->GetString(3); + + if (aEventArgs->GetSize() >= 5) + World::Get().GetTransport().SetServerPassword(aEventArgs->GetString(4)); + else + World::Get().GetTransport().SetServerPassword(""); + World::Get().GetTransport().SetLoginCredentials(username, password); std::string endpoint = baseIp + ":" + std::to_string(port); @@ -137,19 +365,175 @@ void OverlayClient::ProcessSetTimeCommand(CefRefPtr aEventArgs) World::Get().GetDispatcher().trigger(SetTimeCommandEvent(hours, minutes, senderId)); } -void OverlayClient::ProcessTeleportMessage(CefRefPtr aEventArgs) +void OverlayClient::ProcessTeleportRequestMessage(CefRefPtr aEventArgs) { + if (!aEventArgs || aEventArgs->GetSize() < 1) + return; + + const auto& partyService = World::Get().GetPartyService(); + if (partyService.IsCellLockActiveForLocal()) + { + World::Get().GetOverlayService().SendSystemMessage("Party is locked to the leader's cell."); + return; + } + TeleportRequest request{}; request.PlayerId = aEventArgs->GetInt(0); m_transport.Send(request); } +void OverlayClient::ProcessTeleportResponseMessage(CefRefPtr aEventArgs) +{ + if (!aEventArgs || aEventArgs->GetSize() < 2) + return; + + const auto& partyService = World::Get().GetPartyService(); + if (partyService.IsCellLockActiveForLocal()) + { + World::Get().GetOverlayService().SendSystemMessage("Party is locked to the leader's cell."); + return; + } + + TeleportResponse response{}; + response.RequesterId = static_cast(aEventArgs->GetInt(0)); + response.Accepted = aEventArgs->GetBool(1); + + m_transport.Send(response); +} + +void OverlayClient::ProcessSetProfilePicture(CefRefPtr aEventArgs) +{ + std::string payload; + if (aEventArgs->GetSize() > 0) + payload = aEventArgs->GetString(0).ToString(); + + constexpr size_t cMaxAvatarBytes = 256u * 1024u; + if (payload.size() > cMaxAvatarBytes) + { + spdlog::warn("[OverlayClient] Ignoring avatar upload larger than {} bytes", cMaxAvatarBytes); + return; + } + + PlayerProfileImageUpdateRequest request{}; + request.ImageData = payload; + m_transport.Send(request); +} + +void OverlayClient::ProcessSetNameTagMode(CefRefPtr aEventArgs) +{ + if (!aEventArgs || aEventArgs->GetSize() < 1) + return; + + const int rawMode = aEventArgs->GetInt(0); + NameTagService::Mode mode = NameTagService::Mode::Normal; + switch (rawMode) + { + case static_cast(NameTagService::Mode::Detailed): + mode = NameTagService::Mode::Detailed; + break; + case static_cast(NameTagService::Mode::Basic): + mode = NameTagService::Mode::Basic; + break; + case static_cast(NameTagService::Mode::Hidden): + mode = NameTagService::Mode::Hidden; + break; + case static_cast(NameTagService::Mode::Normal): + mode = NameTagService::Mode::Normal; + break; + default: + break; + } + + World::Get().GetRunner().Queue([mode]() { + auto& world = World::Get(); + if (!world.ctx().contains()) + return; + world.ctx().at().SetMode(mode); + }); +} + +void OverlayClient::ProcessSetPlayerNamePreference(CefRefPtr aEventArgs) +{ + if (!aEventArgs || aEventArgs->GetSize() < 1) + return; + + NameTagService::NamePreference preference = NameTagService::NamePreference::Username; + const auto rawType = aEventArgs->GetType(0); + if (rawType == VTYPE_INT) + { + const int rawValue = aEventArgs->GetInt(0); + if (rawValue == static_cast(NameTagService::NamePreference::Actor)) + preference = NameTagService::NamePreference::Actor; + } + else + { + const std::string raw = aEventArgs->GetString(0).ToString(); + if (raw == "actor") + preference = NameTagService::NamePreference::Actor; + } + + World::Get().GetRunner().Queue([preference]() { + auto& world = World::Get(); + if (!world.ctx().contains()) + return; + world.ctx().at().SetNamePreference(preference); + }); +} + +void OverlayClient::ProcessSetPartyOptions(CefRefPtr aEventArgs) +{ + if (!aEventArgs || aEventArgs->GetSize() < 1) + return; + + PartyOptions options{}; + + if (aEventArgs->GetType(0) == VTYPE_DICTIONARY) + { + auto dict = aEventArgs->GetDictionary(0); + if (!dict) + return; + + if (dict->HasKey("syncFastTravelMarkers")) + options.SetSyncFastTravelMarkers(dict->GetBool("syncFastTravelMarkers")); + if (dict->HasKey("showPartyMemberMarkers")) + options.SetShowPartyMemberMarkers(dict->GetBool("showPartyMemberMarkers")); + if (dict->HasKey("syncDeadBodyLoot")) + options.SetSyncDeadBodyLoot(dict->GetBool("syncDeadBodyLoot")); + if (dict->HasKey("lockPartyToLeaderCell")) + options.SetLockPartyToLeaderCell(dict->GetBool("lockPartyToLeaderCell")); + } + else + { + options.SetSyncFastTravelMarkers(aEventArgs->GetBool(0)); + if (aEventArgs->GetSize() > 1) + options.SetShowPartyMemberMarkers(aEventArgs->GetBool(1)); + if (aEventArgs->GetSize() > 2) + options.SetSyncDeadBodyLoot(aEventArgs->GetBool(2)); + if (aEventArgs->GetSize() > 3) + options.SetLockPartyToLeaderCell(aEventArgs->GetBool(3)); + } + + World::Get().GetPartyService().UpdatePartyOptions(options); +} + void OverlayClient::ProcessToggleDebugUI() { World::Get().GetDebugService().m_showDebugStuff = !World::Get().GetDebugService().m_showDebugStuff; } +void OverlayClient::ProcessPlayEmote(CefRefPtr aEventArgs) +{ + if (!aEventArgs || aEventArgs->GetSize() < 1) + return; + + const std::string eventName = aEventArgs->GetString(0).ToString(); + if (eventName.empty() || eventName.size() > 64) + return; + + PlayEmoteInternal(eventName); +} + void OverlayClient::SetUIVisible(bool aVisible) noexcept { auto pRenderer = GetOverlayRenderHandler(); diff --git a/Code/client/Services/Generic/OverlayService.cpp b/Code/client/Services/Generic/OverlayService.cpp index 7f1d97e00..26391dff1 100644 --- a/Code/client/Services/Generic/OverlayService.cpp +++ b/Code/client/Services/Generic/OverlayService.cpp @@ -18,14 +18,26 @@ #include #include #include +#include #include #include #include +#include #include +#include +#include #include #include - +#include +#include +#include +#include +#include +#include + +#include #include +#include #include #include @@ -38,10 +50,91 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include using TiltedPhoques::OverlayRenderHandler; using TiltedPhoques::OverlayRenderHandlerD3D11; +namespace +{ +bool IsAnyMenuOpen() noexcept +{ + UI* pUI = UI::Get(); + if (!pUI) + return false; + + for (auto* pMenu : pUI->menuStack) + { + if (!pMenu) + continue; + + const BSFixedString* pName = pUI->LookupMenuNameByInstance(pMenu); + if (pName && (std::strcmp(pName->AsAscii(), "HUD Menu") == 0 || std::strcmp(pName->AsAscii(), "HUDMenu") == 0)) + continue; + + return true; + } + + return false; +} + +bool TryGetLocalServerId(uint32_t& aOutId) noexcept +{ + auto* pPlayer = PlayerCharacter::Get(); + if (!pPlayer) + return false; + + auto view = World::Get().view(); + for (entt::entity entity : view) + { + if (view.get(entity).Id != pPlayer->formID) + continue; + + aOutId = view.get(entity).Id; + return true; + } + + return false; +} + +std::string GetActorName(Actor* apActor) noexcept +{ + if (!apActor || !apActor->baseForm) + return {}; + + BSFixedString emptyTag; + const char* name = apActor->baseForm->GetName(emptyTag); + if (!name || name[0] == '\0') + return {}; + + return name; +} + +void PushActorName(OverlayApp* apOverlay, uint32_t aPlayerId, Actor* apActor) noexcept +{ + if (!apOverlay) + return; + + const auto name = GetActorName(apActor); + if (name.empty()) + return; + + auto pArguments = CefListValue::Create(); + pArguments->SetInt(0, aPlayerId); + pArguments->SetString(1, name); + apOverlay->ExecuteAsync("setActorName", pArguments); +} +} + struct D3D11RenderProvider final : OverlayApp::RenderProvider, OverlayRenderHandlerD3D11::Renderer { explicit D3D11RenderProvider(RenderSystemD3D11* apRenderSystem) @@ -60,8 +153,6 @@ struct D3D11RenderProvider final : OverlayApp::RenderProvider, OverlayRenderHand [[nodiscard]] HWND GetWindow() override { return m_pRenderSystem->GetWindow(); } [[nodiscard]] IDXGISwapChain* GetSwapChain() const noexcept override { return m_pRenderSystem->GetSwapChain(); } - [[nodiscard]] ID3D11Device* GetDevice() const noexcept override { return m_pRenderSystem->GetDevice(); } - [[nodiscard]] ID3D11DeviceContext* GetDeviceContext() const noexcept override { return m_pRenderSystem->GetDeviceContext(); } private: RenderSystemD3D11* m_pRenderSystem; @@ -118,13 +209,20 @@ OverlayService::OverlayService(World& aWorld, TransportService& transport, entt: m_chatMessageConnection = aDispatcher.sink().connect<&OverlayService::OnChatMessageReceived>(this); m_playerJoinedConnection = aDispatcher.sink().connect<&OverlayService::OnPlayerJoined>(this); m_playerLeftConnection = aDispatcher.sink().connect<&OverlayService::OnPlayerLeft>(this); + m_playerAvatarConnection = aDispatcher.sink().connect<&OverlayService::OnPlayerProfileImage>(this); m_playerDialogueConnection = aDispatcher.sink().connect<&OverlayService::OnPlayerDialogue>(this); m_playerAddedConnection = m_world.on_destroy().connect<&OverlayService::OnWaitingFor3DRemoved>(this); m_playerRemovedConnection = m_world.on_destroy().connect<&OverlayService::OnPlayerComponentRemoved>(this); m_playerLevelConnection = aDispatcher.sink().connect<&OverlayService::OnPlayerLevel>(this); + m_playerActorNameConnection = aDispatcher.sink().connect<&OverlayService::OnNotifyPlayerActorName>(this); m_cellChangedConnection = aDispatcher.sink().connect<&OverlayService::OnPlayerCellChanged>(this); + m_teleportRequestConnection = aDispatcher.sink().connect<&OverlayService::OnNotifyTeleportRequest>(this); m_teleportConnection = aDispatcher.sink().connect<&OverlayService::OnNotifyTeleport>(this); + m_teleportCountdownConnection = aDispatcher.sink().connect<&OverlayService::OnNotifyTeleportCountdown>(this); m_playerHealthConnection = aDispatcher.sink().connect<&OverlayService::OnNotifyPlayerHealthUpdate>(this); + m_commandListConnection = aDispatcher.sink().connect<&OverlayService::OnNotifyCommandList>(this); + m_playEmoteConnection = aDispatcher.sink().connect<&OverlayService::OnNotifyPlayEmote>(this); + m_cancelEmoteConnection = aDispatcher.sink().connect<&OverlayService::OnNotifyCancelEmote>(this); m_partyJoinedConnection = aDispatcher.sink().connect<&OverlayService::OnPartyJoinedEvent>(this); m_partyLeftConnection = aDispatcher.sink().connect<&OverlayService::OnPartyLeftEvent>(this); } @@ -152,15 +250,6 @@ void OverlayService::Create(RenderSystemD3D11* apRenderSystem) noexcept void OverlayService::Render() noexcept { - // TODO: delete this hack? - static bool s_bi = false; - if (!s_bi) - { - m_pOverlay->GetClient()->GetBrowser()->GetHost()->WasResized(); - - s_bi = true; - } - auto pPlayer = PlayerCharacter::Get(); bool inGame = pPlayer && pPlayer->GetNiNode(); if (inGame && !m_inGame) @@ -284,10 +373,131 @@ void OverlayService::SetPlayerHealthPercentage(uint32_t aFormId) const noexcept m_pOverlay->ExecuteAsync("setHealth", pArguments); } +void OverlayService::SetPartyPinsJson(const std::string& aJson) noexcept +{ + if (!m_pOverlay) + return; + + auto pArguments = CefListValue::Create(); + pArguments->SetString(0, aJson); + m_pOverlay->ExecuteAsync("setPartyPins", pArguments); +} + void OverlayService::OnUpdate(const UpdateEvent&) noexcept { RunDebugDataUpdates(); RunPlayerHealthUpdates(); + UpdateRemoteEmoteLoops(); + + // Allow cancelling emotes from the keyboard even when the menu is closed. + const bool cancelRequested = (GetAsyncKeyState(VK_OEM_PERIOD) & 0x8001) != 0; + const bool allowCancel = g_emoteWheelActive.load() || (!m_active && !IsAnyMenuOpen()); + if (cancelRequested && allowCancel) + { + const bool wasEmoting = g_emoteWheelActive.load(); + if (auto* pPlayer = PlayerCharacter::Get()) + { + if (auto* pExt = pPlayer->GetExtension()) + { + pExt->LatestAnimation = {}; + } + + BSFixedString stopEvent("IdleForceDefaultState"); + BSFixedString stopInstant("IdleStopInstant"); + pPlayer->SendAnimationEvent(&stopInstant); + pPlayer->SendAnimationEvent(&stopEvent); + g_emoteWheelActive.store(false); + g_emoteEventName.clear(); + g_emoteStartValid.store(false); + + if (wasEmoting && m_transport.IsConnected()) + { + uint32_t serverId = 0; + if (TryGetLocalServerId(serverId)) + { + CancelEmoteRequest request{}; + request.ServerId = serverId; + m_transport.Send(request); + } + } + + if (g_emoteEquipmentValid.load()) + RestoreEquipmentSnapshot(pPlayer, g_emoteEquipmentSnapshot, true); + g_emoteEquipmentValid.store(false); + + if (m_pOverlay) + { + auto pArgs = CefListValue::Create(); + pArgs->SetString(0, "Emote cancelled"); + pArgs->SetInt(1, 2000); + m_pOverlay->ExecuteAsync("showBanner", pArgs); + } + } + } + + // Keep looping emotes that have a timeout + if (g_emoteWheelActive.load()) + { + // Keep looping emotes that have a timeout + if (!g_emoteEventName.empty() && g_emoteEventName != "IdleForceDefaultState" && g_emoteEventName != "IdleStopInstant") + { + static constexpr auto kKeepAlive = std::chrono::seconds(2); + const auto now = std::chrono::steady_clock::now(); + if (now - g_emoteLastPlayed > kKeepAlive) + { + if (auto* pPlayer = PlayerCharacter::Get()) + { + if (g_emoteStartValid.load()) + { + // Snap back to the same transform we started from so looping emotes don't drift. + pPlayer->position = g_emoteStartPos; + pPlayer->SetRotation(g_emoteStartRot.x, g_emoteStartRot.y, g_emoteStartRot.z); + } + + BSFixedString keepAlive(g_emoteEventName.c_str()); + pPlayer->SendAnimationEvent(&keepAlive); + g_emoteLastPlayed = now; + } + } + } + } +} + +void OverlayService::UpdateRemoteEmoteLoops() noexcept +{ + if (m_remoteEmotes.empty()) + return; + + static constexpr auto kKeepAlive = std::chrono::seconds(2); + const auto now = std::chrono::steady_clock::now(); + + for (auto it = m_remoteEmotes.begin(); it != m_remoteEmotes.end();) + { + if (it->second.EventName.empty() || + it->second.EventName == "IdleForceDefaultState" || + it->second.EventName == "IdleStopInstant") + { + it = m_remoteEmotes.erase(it); + continue; + } + + Actor* pActor = Utils::GetByServerId(it->first); + if (!pActor || pActor == PlayerCharacter::Get()) + { + it = m_remoteEmotes.erase(it); + continue; + } + + if (now - it->second.LastPlayed > kKeepAlive) + { + pActor->SetWeaponDrawnEx(false); + BSFixedString keepAlive(it->second.EventName.c_str()); + pActor->SendAnimationEvent(&keepAlive); + it->second.LastPlayed = now; + } + + ++it; + } } void OverlayService::OnConnectedEvent(const ConnectedEvent& acEvent) noexcept @@ -297,14 +507,21 @@ void OverlayService::OnConnectedEvent(const ConnectedEvent& acEvent) noexcept auto pArguments = CefListValue::Create(); pArguments->SetInt(0, acEvent.PlayerId); m_pOverlay->ExecuteAsync("setLocalPlayerId", pArguments); + + if (auto* pPlayer = PlayerCharacter::Get()) + { + PushActorName(m_pOverlay.get(), acEvent.PlayerId, pPlayer); + SendLocalActorName(pPlayer); + } } void OverlayService::OnDisconnectedEvent(const DisconnectedEvent&) noexcept { m_pOverlay->ExecuteAsync("disconnect"); + m_localActorName.clear(); } -void OverlayService::OnWaitingFor3DRemoved(entt::registry& aRegistry, entt::entity aEntity) const noexcept +void OverlayService::OnWaitingFor3DRemoved(entt::registry& aRegistry, entt::entity aEntity) noexcept { const auto* pPlayerComponent = m_world.try_get(aEntity); if (!pPlayerComponent) @@ -326,6 +543,29 @@ void OverlayService::OnWaitingFor3DRemoved(entt::registry& aRegistry, entt::enti pArguments->SetInt(1, static_cast(percentage)); m_pOverlay->ExecuteAsync("setPlayer3dLoaded", pArguments); + PushActorName(m_pOverlay.get(), pPlayerComponent->Id, pActor); + + if (pPlayerComponent->Id == m_transport.GetLocalPlayerId()) + SendLocalActorName(pActor); +} + +void OverlayService::SendLocalActorName(Actor* apActor) noexcept +{ + if (!m_transport.IsConnected()) + return; + + const auto name = GetActorName(apActor); + if (name.empty()) + return; + + if (name == m_localActorName) + return; + + m_localActorName = name; + + PlayerActorNameUpdateRequest request{}; + request.ActorName = name; + m_transport.Send(request); } void OverlayService::OnPlayerComponentRemoved(entt::registry& aRegistry, entt::entity aEntity) const noexcept @@ -380,6 +620,7 @@ void OverlayService::OnPlayerJoined(const NotifyPlayerJoined& acMessage) noexcep String cellName = GetCellName(acMessage.WorldSpaceId, acMessage.CellId); pArguments->SetString(3, cellName.c_str()); + pArguments->SetString(4, acMessage.Avatar.c_str()); m_pOverlay->ExecuteAsync("playerConnected", pArguments); } @@ -392,6 +633,17 @@ void OverlayService::OnPlayerLeft(const NotifyPlayerLeft& acMessage) noexcept m_pOverlay->ExecuteAsync("playerDisconnected", pArguments); } +void OverlayService::OnPlayerProfileImage(const NotifyPlayerProfileImage& acMessage) noexcept +{ + if (!m_pOverlay) + return; + + auto pArguments = CefListValue::Create(); + pArguments->SetInt(0, acMessage.PlayerId); + pArguments->SetString(1, acMessage.Avatar.c_str()); + m_pOverlay->ExecuteAsync("playerAvatarUpdated", pArguments); +} + void OverlayService::OnPlayerLevel(const NotifyPlayerLevel& acMessage) noexcept { auto pArguments = CefListValue::Create(); @@ -400,6 +652,17 @@ void OverlayService::OnPlayerLevel(const NotifyPlayerLevel& acMessage) noexcept m_pOverlay->ExecuteAsync("setLevel", pArguments); } +void OverlayService::OnNotifyPlayerActorName(const NotifyPlayerActorName& acMessage) noexcept +{ + if (!m_pOverlay) + return; + + auto pArguments = CefListValue::Create(); + pArguments->SetInt(0, acMessage.PlayerId); + pArguments->SetString(1, acMessage.ActorName.c_str()); + m_pOverlay->ExecuteAsync("setActorName", pArguments); +} + void OverlayService::OnPlayerCellChanged(const NotifyPlayerCellChanged& acMessage) const noexcept { auto pArguments = CefListValue::Create(); @@ -409,6 +672,31 @@ void OverlayService::OnPlayerCellChanged(const NotifyPlayerCellChanged& acMessag m_pOverlay->ExecuteAsync("setCell", pArguments); } +void OverlayService::OnNotifyTeleportRequest(const NotifyTeleportRequest& acMessage) noexcept +{ + if (!m_pOverlay) + return; + + auto pArguments = CefListValue::Create(); + pArguments->SetInt(0, acMessage.RequesterId); + pArguments->SetString(1, acMessage.RequesterName.c_str()); + m_pOverlay->ExecuteAsync("teleportRequest", pArguments); +} + +void OverlayService::OnNotifyTeleportCountdown(const NotifyTeleportCountdown& acMessage) noexcept +{ + if (!m_pOverlay) + return; + + auto pArguments = CefListValue::Create(); + pArguments->SetInt(0, acMessage.TargetPlayerId); + pArguments->SetString(1, acMessage.TargetName.c_str()); + pArguments->SetInt(2, acMessage.DurationSeconds); + pArguments->SetBool(3, acMessage.Cancelled); + pArguments->SetString(4, acMessage.Reason.c_str()); + m_pOverlay->ExecuteAsync("teleportCountdown", pArguments); +} + void OverlayService::OnNotifyTeleport(const NotifyTeleport& acMessage) noexcept { auto& modSystem = m_world.GetModSystem(); @@ -446,6 +734,96 @@ void OverlayService::OnNotifyPlayerHealthUpdate(const NotifyPlayerHealthUpdate& m_pOverlay->ExecuteAsync("setHealth", pArguments); } +namespace +{ +std::string EscapeJson(std::string_view text) +{ + std::string out; + out.reserve(text.size() + 8); + for (char c : text) + { + switch (c) + { + case '\\': out += "\\\\"; break; + case '"': out += "\\\""; break; + case '\n': out += "\\n"; break; + case '\r': out += "\\r"; break; + case '\t': out += "\\t"; break; + default: out += c; break; + } + } + return out; +} +} // namespace + +void OverlayService::OnNotifyCommandList(const NotifyCommandList& acMessage) noexcept +{ + if (!m_pOverlay) + return; + + std::string json = "["; + bool first = true; + for (const auto& command : acMessage.Commands) + { + if (!first) + json += ","; + first = false; + json += "{\"name\":\""; + json += EscapeJson(command.Name); + json += "\",\"description\":\""; + json += EscapeJson(command.Description); + json += "\"}"; + } + json += "]"; + + auto pArguments = CefListValue::Create(); + pArguments->SetString(0, json); + m_pOverlay->ExecuteAsync("commandList", pArguments); +} + +void OverlayService::OnNotifyPlayEmote(const NotifyPlayEmote& acMessage) noexcept +{ + Actor* pActor = Utils::GetByServerId(acMessage.ServerId); + if (!pActor) + { + spdlog::warn("{}: could not find actor server id {:X}", __FUNCTION__, acMessage.ServerId); + return; + } + + if (pActor == PlayerCharacter::Get()) + return; + + if (acMessage.EventName.empty()) + return; + + pActor->SetWeaponDrawnEx(false); + BSFixedString eventName(acMessage.EventName.c_str()); + pActor->SendAnimationEvent(&eventName); + + auto& state = m_remoteEmotes[acMessage.ServerId]; + state.EventName = acMessage.EventName; + state.LastPlayed = std::chrono::steady_clock::now(); +} + +void OverlayService::OnNotifyCancelEmote(const NotifyCancelEmote& acMessage) noexcept +{ + Actor* pActor = Utils::GetByServerId(acMessage.ServerId); + if (!pActor) + { + spdlog::warn("{}: could not find actor server id {:X}", __FUNCTION__, acMessage.ServerId); + return; + } + + if (pActor == PlayerCharacter::Get()) + return; + + BSFixedString stopInstant("IdleStopInstant"); + BSFixedString stopEvent("IdleForceDefaultState"); + pActor->SendAnimationEvent(&stopInstant); + pActor->SendAnimationEvent(&stopEvent); + m_remoteEmotes.erase(acMessage.ServerId); +} + void OverlayService::OnPartyJoinedEvent(const PartyJoinedEvent& acEvent) noexcept { if (acEvent.IsLeader) @@ -472,8 +850,8 @@ void OverlayService::RunDebugDataUpdates() noexcept auto steamStats = m_transport.GetConnectionStatus(); auto pArguments = CefListValue::Create(); - pArguments->SetInt(0, steamStats.m_flOutPacketsPerSec); - pArguments->SetInt(1, steamStats.m_flInPacketsPerSec); + pArguments->SetInt(0, static_cast(steamStats.m_flOutPacketsPerSec)); + pArguments->SetInt(1, static_cast(steamStats.m_flInPacketsPerSec)); pArguments->SetInt(2, steamStats.m_nPing); pArguments->SetInt(3, 0); pArguments->SetInt(4, internalStats.SentBytes); @@ -499,15 +877,56 @@ void OverlayService::RunPlayerHealthUpdates() noexcept lastSendTimePoint = now; static float s_previousPercentage = -1.f; + static int32_t s_previousLevel = -1; - const float newPercentage = CalculateHealthPercentage(PlayerCharacter::Get()); - if (newPercentage == s_previousPercentage) + auto* pPlayer = PlayerCharacter::Get(); + if (!pPlayer) return; - s_previousPercentage = newPercentage; + const float newPercentage = CalculateHealthPercentage(pPlayer); + const int32_t newLevel = static_cast(pPlayer->GetLevel()); - RequestPlayerHealthUpdate request{}; - request.Percentage = newPercentage; + const bool healthChanged = newPercentage != s_previousPercentage; + const bool levelChanged = newLevel != s_previousLevel; - m_transport.Send(request); + if (!healthChanged && !levelChanged) + return; + + uint32_t localId = m_transport.GetLocalPlayerId(); + if (localId == 0) + { + TryGetLocalServerId(localId); + } + const bool canUpdateUi = m_pOverlay && localId != 0; + + if (levelChanged) + { + s_previousLevel = newLevel; + + if (canUpdateUi) + { + auto pArguments = CefListValue::Create(); + pArguments->SetInt(0, localId); + pArguments->SetInt(1, newLevel); + m_pOverlay->ExecuteAsync("setLevel", pArguments); + } + } + + if (healthChanged) + { + s_previousPercentage = newPercentage; + + if (canUpdateUi) + { + auto pArguments = CefListValue::Create(); + pArguments->SetInt(0, localId); + pArguments->SetDouble(1, static_cast(newPercentage)); + m_pOverlay->ExecuteAsync("setHealth", pArguments); + } + + RequestPlayerHealthUpdate request{}; + request.Percentage = newPercentage; + + m_transport.Send(request); + } } diff --git a/Code/client/Services/Generic/PartyMapOverlayService.cpp b/Code/client/Services/Generic/PartyMapOverlayService.cpp new file mode 100644 index 000000000..ebfa653d2 --- /dev/null +++ b/Code/client/Services/Generic/PartyMapOverlayService.cpp @@ -0,0 +1,689 @@ +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +std::string JsonEscapeString(const TiltedPhoques::String& input) +{ + std::string escaped; + escaped.reserve(input.size()); + for (char ch : input) + { + switch (ch) + { + case '\\': escaped += "\\\\"; break; + case '\"': escaped += "\\\""; break; + case '\n': escaped += "\\n"; break; + case '\r': escaped += "\\r"; break; + case '\t': escaped += "\\t"; break; + default: escaped += ch; break; + } + } + return escaped; +} + +uint64_t GetEpochSeconds() noexcept +{ + return static_cast(std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count()); +} + +bool HasMenuOpen(UI* apUI, const char* acName) +{ + if (!apUI || !acName) + return false; + + const BSFixedString desired(acName); + if (apUI->GetMenuOpen(desired)) + return true; + + for (auto* pMenu : apUI->menuStack) + { + if (!pMenu) + continue; + if (auto* pName = apUI->LookupMenuNameByInstance(pMenu)) + { + if (*pName == desired) + return true; + } + } + + return false; +} + +} + +PartyMapOverlayService::PartyMapOverlayService(World& aWorld, entt::dispatcher& aDispatcher) noexcept + : m_world(aWorld) +{ + WorldMapProjector::WarmupAsync(); + + // Update position cache each frame + m_updateConnection = aDispatcher.sink().connect<&PartyMapOverlayService::OnUpdate>(this); + + + // Network-driven worldspace updates and lifecycle events + m_cellChangedConnection = aDispatcher.sink().connect<&PartyMapOverlayService::OnPlayerCellChanged>(this); + m_positionsConnection = aDispatcher.sink().connect<&PartyMapOverlayService::OnPartyPositions>(this); + m_disconnectedConnection = aDispatcher.sink().connect<&PartyMapOverlayService::OnDisconnected>(this); + m_partyJoinedConnection = aDispatcher.sink().connect<&PartyMapOverlayService::OnPartyJoined>(this); + m_partyLeftConnection = aDispatcher.sink().connect<&PartyMapOverlayService::OnPartyLeft>(this); +} + +void PartyMapOverlayService::OnDisconnected(const DisconnectedEvent&) noexcept +{ + std::scoped_lock lock(m_cacheMutex); + m_last.clear(); + m_worlds.clear(); + m_lastPerWorld.clear(); + m_lastScreen.clear(); + m_lastValidDisplayWorldId = 0; +} + +void PartyMapOverlayService::OnPartyJoined(const PartyJoinedEvent&) noexcept +{ + std::scoped_lock lock(m_cacheMutex); + PruneNonPartyEntries(); + PartyPositionsRequest req{}; + m_world.GetTransport().Send(req); +} + +void PartyMapOverlayService::OnPartyLeft(const PartyLeftEvent&) noexcept +{ + std::scoped_lock lock(m_cacheMutex); + m_last.clear(); + m_worlds.clear(); + m_lastPerWorld.clear(); + m_lastScreen.clear(); + m_lastValidDisplayWorldId = 0; +} + +void PartyMapOverlayService::OnPlayerCellChanged(const NotifyPlayerCellChanged& aMsg) noexcept +{ + std::unique_lock lock(m_cacheMutex, std::try_to_lock); + if (!lock.owns_lock()) + return; + // Track worldspace per player when available + auto& modSystem = m_world.GetModSystem(); + + WorldspaceInfo info{}; + info.IsInterior = !aMsg.WorldSpaceId; + if (aMsg.WorldSpaceId) + { + info.WorldSpaceFormId = modSystem.GetGameId(aMsg.WorldSpaceId); + info.HasWorld = info.WorldSpaceFormId != 0; + } + else + { + // Interior cells have no worldspace; skip for now (map UI is worldspace-only) + info.WorldSpaceFormId = 0; + info.HasWorld = false; + } + + m_worlds[aMsg.PlayerId] = info; +} + +void PartyMapOverlayService::OnPartyPositions(const NotifyPartyPositions& aMsg) noexcept +{ + std::unique_lock lock(m_cacheMutex, std::try_to_lock); + if (!lock.owns_lock()) + return; + // Update caches from server-sent party positions (covers far-away/unloaded players) + const uint64_t tick = m_world.GetTick(); + auto& modSystem = m_world.GetModSystem(); + + for (const auto& e : aMsg.Entries) + { + glm::vec3 pos{e.Position.x, e.Position.y, e.Position.z}; + StoreLastInfo(e.PlayerId, pos, tick); + + WorldspaceInfo info{}; + info.IsInterior = e.IsInterior; + if (e.WorldSpaceId) + { + info.WorldSpaceFormId = modSystem.GetGameId(e.WorldSpaceId); + info.HasWorld = info.WorldSpaceFormId != 0; + } + else + { + info.WorldSpaceFormId = 0; + info.HasWorld = false; // interior or unknown + } + m_worlds[e.PlayerId] = info; + + // Track last known position per worldspace; projection is deferred to the render thread. + if (info.HasWorld) + m_lastPerWorld[e.PlayerId][info.WorldSpaceFormId] = pos; + } +} + + +void PartyMapOverlayService::PruneNonPartyEntries() noexcept +{ + const auto& members = m_world.GetPartyService().GetPartyMembers(); + TiltedPhoques::Vector toErase; + toErase.reserve(std::max(m_last.size(), m_worlds.size())); + + for (const auto& [pid, _] : m_last) + { + if (std::find(members.begin(), members.end(), pid) == members.end()) + toErase.push_back(pid); + } + for (auto id : toErase) + m_last.erase(id); + + toErase.clear(); + for (const auto& [pid, _] : m_worlds) + { + if (std::find(members.begin(), members.end(), pid) == members.end()) + toErase.push_back(pid); + } + for (auto id : toErase) + m_worlds.erase(id); + + // Prune per-world history for non-members + toErase.clear(); + for (const auto& [pid, _] : m_lastPerWorld) + { + if (std::find(members.begin(), members.end(), pid) == members.end()) + toErase.push_back(pid); + } + for (auto id : toErase) + m_lastPerWorld.erase(id); + + // Prune last-screen cache for non-members + toErase.clear(); + for (const auto& [pid, _] : m_lastScreen) + { + if (std::find(members.begin(), members.end(), pid) == members.end()) + toErase.push_back(pid); + } + for (auto id : toErase) + m_lastScreen.erase(id); +} + +void PartyMapOverlayService::OnUpdate(const UpdateEvent&) noexcept +{ + std::unique_lock lock(m_cacheMutex, std::try_to_lock); + if (!lock.owns_lock()) + return; + const uint64_t tick = m_world.GetTick(); + + // Cache last known positions for loaded remote players + auto view = m_world.view(); + for (auto e : view) + { + const auto& pc = view.get(e); + const auto& interp = view.get(e); + StoreLastInfo(pc.Id, interp.Position, tick); + } + + // Periodically send our own position to the server for party tracking + { + static auto s_lastPosSend = std::chrono::steady_clock::time_point{}; + constexpr auto cPosInterval = std::chrono::milliseconds(100); + const auto now = std::chrono::steady_clock::now(); + if (now - s_lastPosSend >= cPosInterval) + + { + s_lastPosSend = now; + if (auto* pPc = PlayerCharacter::Get()) + { + PartyPositionUpdateRequest req{}; + req.Position.x = pPc->position.x; + req.Position.y = pPc->position.y; + req.Position.z = pPc->position.z; + + auto& modSystem = m_world.GetModSystem(); + if (auto* pWs = pPc->GetWorldSpace()) + modSystem.GetServerModId(pWs->formID, req.WorldSpaceId); + else + req.WorldSpaceId = {}; + + if (pPc->parentCell) + modSystem.GetServerModId(pPc->parentCell->formID, req.CellId); + else + req.CellId = {}; + + m_world.GetTransport().Send(req); + + CoSaveStorage::LocalPlayerLocation location{}; + const auto existing = m_world.ctx().at().GetLocalPlayerLocation(); + location.HasLocation = true; + location.Position = req.Position; + location.WorldSpaceId = req.WorldSpaceId; + location.CellId = req.CellId; + location.LastSeenEpoch = GetEpochSeconds(); + location.HasExterior = req.WorldSpaceId; + if (location.HasExterior) + { + location.ExteriorPosition = req.Position; + location.ExteriorWorldSpaceId = req.WorldSpaceId; + location.ExteriorCellId = req.CellId; + location.ExteriorLastSeenEpoch = location.LastSeenEpoch; + } + else if (existing && existing->HasExterior) + { + location.HasExterior = true; + location.ExteriorPosition = existing->ExteriorPosition; + location.ExteriorWorldSpaceId = existing->ExteriorWorldSpaceId; + location.ExteriorCellId = existing->ExteriorCellId; + location.ExteriorLastSeenEpoch = existing->ExteriorLastSeenEpoch; + } + m_world.ctx().at().UpdateLocalPlayerLocation(location); + } + } + } + + // Prune very stale entries (>10 minutes) + const uint64_t maxAge = 10ull * 60ull * 1000ull; + for (auto it = m_last.begin(); it != m_last.end();) + { + if (tick - it->second.Tick > maxAge) + it = m_last.erase(it); + else + ++it; + } + + // Build and send pins to CEF overlay at a throttled rate + static auto s_lastSend = std::chrono::steady_clock::time_point{}; + constexpr auto cDelayBetweenUpdates = std::chrono::milliseconds(16); // ~60 FPS for tighter tracking + const auto now = std::chrono::steady_clock::now(); + if (now - s_lastSend < cDelayBetweenUpdates) + return; + s_lastSend = now; + + auto& partyService = m_world.GetPartyService(); + UI* pUI = UI::Get(); + const bool mapOpen = pUI && pUI->GetMenuOpen(BSFixedString("MapMenu")); + if (!mapOpen) + { + m_world.GetOverlayService().SetPartyPinsJson("[]"); + return; + } + + if (!partyService.GetPartyOptions().ShowPartyMemberMarkers()) + { + m_world.GetOverlayService().SetPartyPinsJson("[]"); + return; + } + + const bool fastTravelPromptOpen = HasMenuOpen(pUI, "MessageBoxMenu"); + const bool loadingScreenOpen = HasMenuOpen(pUI, "LoadingMenu") || HasMenuOpen(pUI, "Loading Menu") || + HasMenuOpen(pUI, "FaderMenu") || HasMenuOpen(pUI, "Fader Menu") || + HasMenuOpen(pUI, "LoadWaitSpinner") || HasMenuOpen(pUI, "Load Wait Spinner") || + HasMenuOpen(pUI, "Sleep/Wait Menu"); + if (fastTravelPromptOpen || loadingScreenOpen) + { + m_world.GetOverlayService().SetPartyPinsJson("[]"); + return; + } + + // Determine current worldspace of the local player and the map's display world (root ancestor) + TESWorldSpace* pMyWs = nullptr; + if (auto* pPc = PlayerCharacter::Get()) + pMyWs = pPc->GetWorldSpace(); + TESWorldSpace* pDispWs = WorldMapProjector::GetDisplayWorld(pMyWs); + + if (!pDispWs && m_lastValidDisplayWorldId != 0) + { + if (auto* pForm = TESForm::GetById(m_lastValidDisplayWorldId)) + pDispWs = Cast(pForm); + } + + if (pDispWs) + m_lastValidDisplayWorldId = pDispWs->formID; + + const uint32_t dispWsId = pDispWs ? pDispWs->formID : 0u; + + // Screen size + auto* pRenderer = BGSRenderer::Get(); + const float width = pRenderer ? static_cast(pRenderer->windowWidth) : 0.f; + const float height = pRenderer ? static_cast(pRenderer->windowHeight) : 0.f; + + auto* pCam = PlayerCamera::Get(); + if (!pCam || width <= 0.f || height <= 0.f) + { + // If we can't project, don't draw anything + m_world.GetOverlayService().SetPartyPinsJson("[]"); + return; + } + + // If not in a party or only ourselves, don't draw anything + const auto& members = partyService.GetPartyMembers(); + if (!partyService.IsInParty() || members.size() <= 1) + { + m_world.GetOverlayService().SetPartyPinsJson("[]"); + return; + } + + const uint32_t localId = m_world.GetTransport().GetLocalPlayerId(); + + std::ostringstream os; + os.setf(std::ios::fixed); os << std::setprecision(1); + os << "["; + bool first = true; + const auto sampleNow = std::chrono::steady_clock::now(); + + const auto& playerInfoMap = partyService.GetPlayers(); + + for (uint32_t pid : members) + { + if (pid == localId) + continue; // don't display ourselves on the map + + const auto itPos = m_last.find(pid); + const auto itWs = m_worlds.find(pid); + if (itPos == m_last.end()) + continue; // no position at all yet + + const bool hasWorld = (itWs != m_worlds.end()) && itWs->second.HasWorld && itWs->second.WorldSpaceFormId != 0; + const bool isInterior = (itWs != m_worlds.end()) && itWs->second.IsInterior; + float sx = 0.f, sy = 0.f; + bool drew = false; + const auto& lastInfo = itPos->second; + + glm::vec3 worldPos = lastInfo.Pos; + float dtSeconds = 0.0f; + if (tick > lastInfo.Tick) + dtSeconds = static_cast(tick - lastInfo.Tick) / 1000.0f; + + if (lastInfo.SampleTime.time_since_epoch().count() != 0) + { + const float realDt = std::chrono::duration_cast>(sampleNow - lastInfo.SampleTime).count(); + dtSeconds = std::max(dtSeconds, realDt); + } + + constexpr float cMaxPredictionSeconds = 1.0f; + dtSeconds = std::clamp(dtSeconds, 0.0f, cMaxPredictionSeconds); + const bool usingPrediction = dtSeconds > 0.0f && glm::length2(lastInfo.Velocity) > std::numeric_limits::epsilon(); + if (usingPrediction) + worldPos += lastInfo.Velocity * dtSeconds; + + // 1) If member already in display worldspace, project directly + if (hasWorld && itWs->second.WorldSpaceFormId == dispWsId) + { + NiPoint3 wpos(worldPos); + NiPoint3 spos{}; + if (HUDMenuUtils::WorldPtToScreenPt3(wpos, spos)) + { + sx = std::clamp(spos.x, 0.0f, 1.0f) * width; + sy = (1.f - std::clamp(spos.y, 0.0f, 1.0f)) * height; + drew = true; + if (dispWsId != 0) + m_lastPerWorld[pid][dispWsId] = worldPos; + } + } + + // 2) Otherwise, convert into display world via WRLD ONAM (child->ancestor only) + if (!drew && hasWorld && dispWsId != 0 && itWs->second.WorldSpaceFormId != dispWsId && pDispWs) + { + if (auto* pFromWs = static_cast(TESForm::GetById(itWs->second.WorldSpaceFormId))) + { + glm::vec3 dstPos{}; + if (WorldMapProjector::Convert(pFromWs, worldPos, pDispWs, dstPos)) + { + NiPoint3 wpos(dstPos); + NiPoint3 spos{}; + if (HUDMenuUtils::WorldPtToScreenPt3(wpos, spos)) + { + sx = std::clamp(spos.x, 0.0f, 1.0f) * width; + sy = (1.f - std::clamp(spos.y, 0.0f, 1.0f)) * height; + drew = true; + if (dispWsId != 0) + m_lastPerWorld[pid][dispWsId] = dstPos; + } + } + } + } + + // 3) Approximation using per-player anchors (convert into display world) + if (!drew && hasWorld && dispWsId != 0 && itWs->second.WorldSpaceFormId != dispWsId) + { + glm::vec3 dstPos{}; + if (ComputeCrossWorldApprox(pid, itWs->second.WorldSpaceFormId, worldPos, dispWsId, dstPos)) + { + NiPoint3 wpos(dstPos); + NiPoint3 spos{}; + if (HUDMenuUtils::WorldPtToScreenPt3(wpos, spos)) + { + sx = std::clamp(spos.x, 0.0f, 1.0f) * width; + sy = (1.f - std::clamp(spos.y, 0.0f, 1.0f)) * height; + drew = true; + if (dispWsId != 0) + m_lastPerWorld[pid][dispWsId] = dstPos; + } + } + } + + // 4) Fallback to last known position already in display world + if (!drew) + { + auto itHistPid = m_lastPerWorld.find(pid); + if (itHistPid != m_lastPerWorld.end()) + { + auto itDispPos = itHistPid->second.find(dispWsId); + if (itDispPos != itHistPid->second.end()) + { + const auto& p = itDispPos->second; + NiPoint3 wpos(p); + NiPoint3 spos{}; + if (HUDMenuUtils::WorldPtToScreenPt3(wpos, spos)) + { + sx = std::clamp(spos.x, 0.0f, 1.0f) * width; + sy = (1.f - std::clamp(spos.y, 0.0f, 1.0f)) * height; + drew = true; + } + } + } + } + + // 5) Retain last projected screen position. If a player is currently in an + // interior (no valid worldspace) keep showing their last known map spot + // indefinitely so they don't vanish from the world map. + if (!drew) + { + constexpr uint64_t cKeepMs = 3000; // 3 seconds + auto itLastScr = m_lastScreen.find(pid); + const bool allowStaleFallback = !hasWorld || isInterior; + if (itLastScr != m_lastScreen.end()) + { + const uint64_t age = tick >= itLastScr->second.Tick ? (tick - itLastScr->second.Tick) : 0; + if (allowStaleFallback || age <= cKeepMs) + { + sx = itLastScr->second.sx; + sy = itLastScr->second.sy; + drew = true; + } + } + } + + if (!drew) + continue; // nothing to draw for this member on this map + + // Smooth towards new projected position using an exponential moving average + auto itLS = m_lastScreen.find(pid); + if (itLS != m_lastScreen.end()) + { + const uint64_t dt = tick >= itLS->second.Tick ? (tick - itLS->second.Tick) : 0; + float alpha = 1.0f; + if (dt > 0) + { + constexpr float cSmoothingMs = 45.0f; + alpha = 1.0f - std::exp(-static_cast(dt) / cSmoothingMs); + alpha = std::clamp(alpha, 0.7f, 1.0f); + } + + const float dx = sx - itLS->second.sx; + const float dy = sy - itLS->second.sy; + constexpr float cTeleportPixelsSq = 600.0f * 600.0f; + if ((dx * dx + dy * dy) > cTeleportPixelsSq) + alpha = 1.0f; + else if (usingPrediction && dtSeconds >= 0.2f) + alpha = 1.0f; + + sx = itLS->second.sx + dx * alpha; + sy = itLS->second.sy + dy * alpha; + } + + // Cache last projected (smoothed) position + m_lastScreen[pid] = LastScreen{sx, sy, tick}; + + const auto itInfo = playerInfoMap.find(pid); + const auto* pInfo = itInfo != playerInfoMap.end() ? &itInfo->second : nullptr; + const std::string nameEscaped = pInfo ? JsonEscapeString(pInfo->Name) : std::string{}; + const std::string avatarEscaped = pInfo ? JsonEscapeString(pInfo->Avatar) : std::string{}; + const bool isOutOfBounds = !hasWorld || isInterior; + + if (!first) os << ","; + first = false; + os << "{\"x\":" << sx << ",\"y\":" << sy << ",\"id\":" << pid + << ",\"oob\":" << (isOutOfBounds ? "true" : "false") + << ",\"name\":\"" << nameEscaped << "\",\"avatar\":\"" << avatarEscaped << "\"}"; + } + + // If nothing to draw, send empty list + if (first) + { + m_world.GetOverlayService().SetPartyPinsJson("[]"); + return; + } + + os << "]"; + m_world.GetOverlayService().SetPartyPinsJson(os.str()); +} + + +bool PartyMapOverlayService::ComputeCrossWorldApprox(uint32_t aPlayerId, uint32_t aSrcWsId, const glm::vec3& aSrcPos, + uint32_t aDstWsId, glm::vec3& aOutDstPos) const noexcept +{ + auto itHistPid = m_lastPerWorld.find(aPlayerId); + if (itHistPid == m_lastPerWorld.end()) + return false; + + const auto& perWorld = itHistPid->second; + auto itSrc = perWorld.find(aSrcWsId); + auto itDst = perWorld.find(aDstWsId); + if (itSrc == perWorld.end() || itDst == perWorld.end()) + return false; + + const glm::vec3 srcAnchor = itSrc->second; + const glm::vec3 dstAnchor = itDst->second; + + // Translation-only approximation: assumes no rotation/scale differences. + aOutDstPos = dstAnchor + (aSrcPos - srcAnchor); + return true; +} + +void PartyMapOverlayService::SetWaypointFor(uint32_t aPlayerId) noexcept +{ + const auto itPos = m_last.find(aPlayerId); + if (itPos == m_last.end()) + return; + + const auto itWs = m_worlds.find(aPlayerId); + if (itWs == m_worlds.end() || !itWs->second.HasWorld || itWs->second.WorldSpaceFormId == 0) + return; // cannot set map waypoint without a worldspace + + Vector3_NetQuantize pos{}; + pos.x = itPos->second.Pos.x; + pos.y = itPos->second.Pos.y; + pos.z = itPos->second.Pos.z; + + m_world.GetDispatcher().trigger(SetWaypointEvent(pos, itWs->second.WorldSpaceFormId)); +} + +void PartyMapOverlayService::StoreLastInfo(uint32_t aPlayerId, const glm::vec3& aPos, uint64_t aTick) noexcept +{ + constexpr float cMinDtSeconds = 0.001f; + constexpr float cMaxDtSeconds = 6.0f; + constexpr float cTeleportResetDistanceSq = 4096.0f * 4096.0f; + constexpr float cMaxSpeed = 16000.0f; // units per second, avoids runaway extrapolation + constexpr float cMaxSpeedSq = cMaxSpeed * cMaxSpeed; + constexpr float cVelocityBlend = 0.25f; // keep some of the previous vector to reduce jitter + + const auto now = std::chrono::steady_clock::now(); + + LastInfo info{}; + info.Pos = aPos; + info.Tick = aTick; + info.Velocity = {}; + info.SampleTime = now; + + auto itPrev = m_last.find(aPlayerId); + if (itPrev != m_last.end()) + { + const uint64_t tickDelta = (aTick > itPrev->second.Tick) ? (aTick - itPrev->second.Tick) : 0ull; + float dtSeconds = tickDelta > 0 ? static_cast(tickDelta) / 1000.0f : 0.0f; + + if (itPrev->second.SampleTime.time_since_epoch().count() != 0) + { + const float realDtSeconds = std::chrono::duration_cast>(now - itPrev->second.SampleTime).count(); + if (realDtSeconds >= cMinDtSeconds) + dtSeconds = std::max(dtSeconds, realDtSeconds); + } + + if (dtSeconds < cMinDtSeconds) + { + info.Velocity = itPrev->second.Velocity; + } + else + { + const glm::vec3 delta = aPos - itPrev->second.Pos; + const float distSq = glm::length2(delta); + + if (dtSeconds <= cMaxDtSeconds && distSq <= cTeleportResetDistanceSq) + { + glm::vec3 newVelocity = delta / dtSeconds; + if (glm::length2(newVelocity) > cMaxSpeedSq) + { + newVelocity = {}; + } + else if (glm::length2(itPrev->second.Velocity) > 0.0f) + { + newVelocity = itPrev->second.Velocity * cVelocityBlend + newVelocity * (1.0f - cVelocityBlend); + } + + info.Velocity = newVelocity; + } + } + } + + m_last[aPlayerId] = info; +} diff --git a/Code/client/Services/Generic/PartyMarkerOverlayService.cpp b/Code/client/Services/Generic/PartyMarkerOverlayService.cpp new file mode 100644 index 000000000..c002e0996 --- /dev/null +++ b/Code/client/Services/Generic/PartyMarkerOverlayService.cpp @@ -0,0 +1,124 @@ +#include + +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include + + +static float NormalizeAngle(float a) noexcept +{ + const float pi = static_cast(TiltedPhoques::Pi); + const float twoPi = 2.f * pi; + while (a > pi) + a -= twoPi; + while (a < -pi) + a += twoPi; + return a; +} + +PartyMarkerOverlayService::PartyMarkerOverlayService(World& aWorld, entt::dispatcher& aDispatcher) noexcept + : m_world(aWorld) +{ + // Update cache of last-known positions each frame + m_updateConnection = aDispatcher.sink().connect<&PartyMarkerOverlayService::OnUpdate>(this); + + // Draw markers using ImGui each frame + auto& imgui = m_world.ctx().at(); + m_drawImGuiConnection = imgui.OnDraw.connect<&PartyMarkerOverlayService::OnDraw>(this); + + // Clear on disconnect/party changes + m_disconnectedConnection = aDispatcher.sink().connect<&PartyMarkerOverlayService::OnDisconnected>(this); + m_partyJoinedConnection = aDispatcher.sink().connect<&PartyMarkerOverlayService::OnPartyJoined>(this); + m_partyLeftConnection = aDispatcher.sink().connect<&PartyMarkerOverlayService::OnPartyLeft>(this); +} + +void PartyMarkerOverlayService::OnDisconnected(const DisconnectedEvent&) noexcept +{ + m_last.clear(); +} + +void PartyMarkerOverlayService::OnPartyJoined(const PartyJoinedEvent&) noexcept +{ + // ensure we only keep entries for current party members + PruneNonPartyEntries(); +} + +void PartyMarkerOverlayService::OnPartyLeft(const PartyLeftEvent&) noexcept +{ + m_last.clear(); +} + +void PartyMarkerOverlayService::PruneNonPartyEntries() noexcept +{ + const auto& members = m_world.GetPartyService().GetPartyMembers(); + TiltedPhoques::Vector toErase; + toErase.reserve(m_last.size()); + + for (const auto& [pid, _] : m_last) + { + if (std::find(members.begin(), members.end(), pid) == members.end()) + toErase.push_back(pid); + } + + for (auto id : toErase) + m_last.erase(id); +} + +bool PartyMarkerOverlayService::HasLiveEntityForPlayer(uint32_t aPlayerId) const noexcept +{ + auto view = m_world.view(); + for (auto e : view) + { + if (view.get(e).Id == aPlayerId) + return true; // entity with PlayerComponent exists -> considered loaded + } + return false; +} + +void PartyMarkerOverlayService::OnUpdate(const UpdateEvent&) noexcept +{ + if (!m_world.GetPartyService().IsInParty()) + return; + + const uint64_t tick = m_world.GetTick(); + + // Cache last known positions from live entities + auto view = m_world.view(); + for (auto e : view) + { + const auto& pc = view.get(e); + const auto& interp = view.get(e); + m_last[pc.Id] = LastInfo{interp.Position, tick}; + } + + // Drop very old entries (> 5 minutes) to avoid stale clutter + const uint64_t maxAge = 5ull * 60ull * 1000ull; // in ms if ticks are ms; otherwise harmless large window + for (auto it = m_last.begin(); it != m_last.end(); ) + { + if (tick - it->second.Tick > maxAge) + it = m_last.erase(it); + else + ++it; + } +} + +void PartyMarkerOverlayService::OnDraw() noexcept +{ + // Disabled per user request: remove debug/marker UI when players are in different cells + return; +} + diff --git a/Code/client/Services/Generic/PartyService.cpp b/Code/client/Services/Generic/PartyService.cpp index ed6bbed0a..74107787f 100644 --- a/Code/client/Services/Generic/PartyService.cpp +++ b/Code/client/Services/Generic/PartyService.cpp @@ -18,10 +18,21 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include #include +#include +#include +#include +#include +#include PartyService::PartyService(World& aWorld, entt::dispatcher& aDispatcher, TransportService& aTransportService) noexcept : m_world(aWorld) @@ -35,6 +46,19 @@ PartyService::PartyService(World& aWorld, entt::dispatcher& aDispatcher, Transpo m_partyInviteConnection = aDispatcher.sink().connect<&PartyService::OnPartyInvite>(this); m_partyJoinedConnection = aDispatcher.sink().connect<&PartyService::OnPartyJoined>(this); m_partyLeftConnection = aDispatcher.sink().connect<&PartyService::OnPartyLeft>(this); + m_playerAvatarConnection = aDispatcher.sink().connect<&PartyService::OnPlayerProfileImage>(this); + m_playerActorNameConnection = aDispatcher.sink().connect<&PartyService::OnPlayerActorName>(this); + m_partyOptionsConnection = aDispatcher.sink().connect<&PartyService::OnPartyOptions>(this); + m_partyLeaderCellLockConnection = aDispatcher.sink().connect<&PartyService::OnPartyLeaderCellLock>(this); +} + +const String* PartyService::GetActorName(uint32_t aPlayerId) const noexcept +{ + const auto it = m_actorNames.find(aPlayerId); + if (it == m_actorNames.end()) + return nullptr; + + return &it->second; } void PartyService::CreateParty() const noexcept @@ -80,6 +104,51 @@ void PartyService::ChangePartyLeader(const uint32_t aPlayerId) const noexcept m_transport.Send(changeMessage); } +void PartyService::UpdatePartyOptions(const PartyOptions& aOptions) noexcept +{ + if (!m_inParty || !m_isLeader) + return; + + m_partyOptions = aOptions; + + PartyOptionsUpdateRequest request{}; + request.Options = aOptions; + m_transport.Send(request); +} + +bool PartyService::IsCellLockActiveForLocal() const noexcept +{ + return m_inParty && !m_isLeader && m_partyOptions.LockPartyToLeaderCell(); +} + +bool PartyService::AllowCellChangeDuringLock() const noexcept +{ + if (!IsCellLockActiveForLocal()) + return true; + + return m_transport.GetClock().GetCurrentTick() < m_cellLockTeleportAllowUntil; +} + +void PartyService::NotifyCellLockBlocked() noexcept +{ + if (!IsCellLockActiveForLocal() || AllowCellChangeDuringLock() || m_cellLockTeleportActive) + return; + + const auto currentTick = m_transport.GetClock().GetCurrentTick(); + if (currentTick < m_cellLockBlockedBannerUntil) + return; + + m_cellLockBlockedBannerUntil = currentTick + 2000; + + if (auto* pOverlayApp = m_world.GetOverlayService().GetOverlayApp()) + { + auto pArgs = CefListValue::Create(); + pArgs->SetString(0, "Party leader locked the party to their cell."); + pArgs->SetInt(1, 2000); + pOverlayApp->ExecuteAsync("showBanner", pArgs); + } +} + void PartyService::OnUpdate(const UpdateEvent& acEvent) noexcept { const auto cCurrentTick = m_transport.GetClock().GetCurrentTick(); @@ -89,6 +158,8 @@ void PartyService::OnUpdate(const UpdateEvent& acEvent) noexcept // Update once every second m_nextUpdate = cCurrentTick + 1000; + UpdateCellLockCountdown(cCurrentTick); + auto itor = std::begin(m_invitations); while (itor != std::end(m_invitations)) { @@ -102,11 +173,78 @@ void PartyService::OnUpdate(const UpdateEvent& acEvent) noexcept void PartyService::OnDisconnected(const DisconnectedEvent& acEvent) noexcept { DestroyParty(); + m_actorNames.clear(); } void PartyService::OnPlayerList(const NotifyPlayerList& acPlayerList) noexcept { m_players = acPlayerList.Players; + + for (auto it = m_actorNames.begin(); it != m_actorNames.end();) + { + if (m_players.find(it->first) == m_players.end()) + it = m_actorNames.erase(it); + else + ++it; + } +} + +void PartyService::OnPlayerProfileImage(const NotifyPlayerProfileImage& acMessage) noexcept +{ + auto& entry = m_players[acMessage.PlayerId]; + entry.Avatar = acMessage.Avatar; +} + +void PartyService::OnPlayerActorName(const NotifyPlayerActorName& acMessage) noexcept +{ + if (acMessage.ActorName.empty()) + return; + + m_actorNames[acMessage.PlayerId] = acMessage.ActorName; +} + +void PartyService::OnPartyOptions(const NotifyPartyOptions& acMessage) noexcept +{ + m_partyOptions = acMessage.Options; + + auto pOptions = CefDictionaryValue::Create(); + pOptions->SetBool("syncFastTravelMarkers", m_partyOptions.SyncFastTravelMarkers()); + pOptions->SetBool("showPartyMemberMarkers", m_partyOptions.ShowPartyMemberMarkers()); + pOptions->SetBool("syncDeadBodyLoot", m_partyOptions.SyncDeadBodyLoot()); + pOptions->SetBool("lockPartyToLeaderCell", m_partyOptions.LockPartyToLeaderCell()); + + auto pArguments = CefListValue::Create(); + pArguments->SetDictionary(0, pOptions); + + m_world.GetOverlayService().GetOverlayApp()->ExecuteAsync("partyOptions", pArguments); +} + +void PartyService::OnPartyLeaderCellLock(const NotifyPartyLeaderCellLock& acMessage) noexcept +{ + if (!m_inParty || m_isLeader) + return; + + if (acMessage.Cancelled) + { + m_cellLockTeleportActive = false; + m_cellLockTeleportAllowUntil = 0; + m_cellLockLastCountdown = 0; + ClearCellLockBanner(); + return; + } + + if (!m_partyOptions.LockPartyToLeaderCell()) + return; + + m_cellLockWorldSpaceId = acMessage.WorldSpaceId; + m_cellLockCellId = acMessage.CellId; + m_cellLockPosition = acMessage.Position; + m_cellLockTeleportEndTick = m_transport.GetClock().GetCurrentTick() + (static_cast(acMessage.CountdownSeconds) * 1000); + m_cellLockTeleportActive = true; + m_cellLockTeleportAllowUntil = 0; + m_cellLockLastCountdown = acMessage.CountdownSeconds; + + ShowCellLockBanner(acMessage.CountdownSeconds); } void PartyService::OnPartyInfo(const NotifyPartyInfo& acPartyInfo) noexcept @@ -118,6 +256,9 @@ void PartyService::OnPartyInfo(const NotifyPartyInfo& acPartyInfo) noexcept m_leaderPlayerId = acPartyInfo.LeaderPlayerId; m_partyMembers = acPartyInfo.PlayerIds; + PartyActorNamesRequest actorNamesRequest{}; + m_transport.Send(actorNamesRequest); + // TODO: this can be done a bit prettier if (m_isLeader) { @@ -158,6 +299,9 @@ void PartyService::OnPartyJoined(const NotifyPartyJoined& acPartyJoined) noexcep m_leaderPlayerId = acPartyJoined.LeaderPlayerId; m_partyMembers = acPartyJoined.PlayerIds; + PartyActorNamesRequest actorNamesRequest{}; + m_transport.Send(actorNamesRequest); + m_world.GetDispatcher().trigger(PartyJoinedEvent(m_isLeader)); } @@ -176,4 +320,90 @@ void PartyService::DestroyParty() noexcept m_isLeader = false; m_leaderPlayerId = -1; m_partyMembers.clear(); + m_partyOptions = PartyOptions{}; + m_cellLockTeleportActive = false; + m_cellLockTeleportAllowUntil = 0; + m_cellLockLastCountdown = 0; + m_cellLockBlockedBannerUntil = 0; + ClearCellLockBanner(); +} + +void PartyService::UpdateCellLockCountdown(uint64_t aCurrentTick) noexcept +{ + if (!m_cellLockTeleportActive) + return; + + if (aCurrentTick >= m_cellLockTeleportEndTick) + { + ShowCellLockBanner(0); + TeleportLocalPlayer(m_cellLockWorldSpaceId, m_cellLockCellId, m_cellLockPosition); + m_cellLockTeleportActive = false; + m_cellLockTeleportAllowUntil = aCurrentTick + 5000; + return; + } + + const auto remainingMs = m_cellLockTeleportEndTick - aCurrentTick; + const uint16_t secondsRemaining = static_cast((remainingMs + 999) / 1000); + if (secondsRemaining == m_cellLockLastCountdown) + return; + + m_cellLockLastCountdown = secondsRemaining; + ShowCellLockBanner(secondsRemaining); +} + +void PartyService::ShowCellLockBanner(uint16_t aSecondsRemaining) const noexcept +{ + if (auto* pOverlayApp = m_world.GetOverlayService().GetOverlayApp()) + { + const std::string message = aSecondsRemaining > 0 + ? fmt::format("Party leader changed cells.\nTeleporting in {}s", aSecondsRemaining) + : "Teleporting to party leader..."; + + auto pArgs = CefListValue::Create(); + pArgs->SetString(0, message); + pArgs->SetInt(1, 1100); + pOverlayApp->ExecuteAsync("showBanner", pArgs); + } +} + +void PartyService::ClearCellLockBanner() const noexcept +{ + if (auto* pOverlayApp = m_world.GetOverlayService().GetOverlayApp()) + { + auto pArgs = CefListValue::Create(); + pArgs->SetString(0, ""); + pArgs->SetInt(1, 1); + pOverlayApp->ExecuteAsync("showBanner", pArgs); + } +} + +void PartyService::TeleportLocalPlayer(const GameId& acWorldSpaceId, const GameId& acCellId, const Vector3_NetQuantize& acPosition) const noexcept +{ + auto& modSystem = m_world.GetModSystem(); + + TESObjectCELL* pCell = nullptr; + if (!acWorldSpaceId) + { + const uint32_t cellId = modSystem.GetGameId(acCellId); + pCell = Cast(TESForm::GetById(cellId)); + } + else + { + const uint32_t worldSpaceId = modSystem.GetGameId(acWorldSpaceId); + TESWorldSpace* pWorldSpace = Cast(TESForm::GetById(worldSpaceId)); + if (pWorldSpace) + { + GridCellCoords coordinates = GridCellCoords::CalculateGridCellCoords(acPosition); + pCell = pWorldSpace->LoadCell(coordinates.X, coordinates.Y); + } + } + + if (!pCell) + { + spdlog::error("Party cell lock teleport failed: destination cell not available."); + return; + } + + if (auto* pPlayer = PlayerCharacter::Get()) + pPlayer->MoveTo(pCell, acPosition); } diff --git a/Code/client/Services/Generic/PlayerService.cpp b/Code/client/Services/Generic/PlayerService.cpp index 4130aa8d8..4a46dafcb 100644 --- a/Code/client/Services/Generic/PlayerService.cpp +++ b/Code/client/Services/Generic/PlayerService.cpp @@ -1,6 +1,10 @@ #include #include +#include + +#include +#include #include #include @@ -12,6 +16,7 @@ #include #include #include +#include #include #include @@ -20,6 +25,7 @@ #include #include #include +#include #include @@ -30,7 +36,10 @@ #include #include #include +#include #include +#include +#include PlayerService::PlayerService(World& aWorld, entt::dispatcher& aDispatcher, TransportService& aTransport) noexcept : m_world(aWorld) @@ -66,7 +75,8 @@ void PlayerService::OnConnected(const ConnectedEvent& acEvent) noexcept pKillMove->f = 0.f; TESGlobal* pWorldEncountersEnabled = Cast(TESForm::GetById(0xB8EC1)); - pWorldEncountersEnabled->f = 0.f; + if (m_world.GetSyncModeService().GetLocalMode() != SyncMode::Ghost) + pWorldEncountersEnabled->f = 0.f; } void PlayerService::OnDisconnected(const DisconnectedEvent& acEvent) noexcept @@ -112,6 +122,13 @@ void PlayerService::OnNotifyPlayerRespawn(const NotifyPlayerRespawn& acMessage) void PlayerService::OnGridCellChangeEvent(const GridCellChangeEvent& acEvent) const noexcept { + auto& partyService = m_world.GetPartyService(); + if (partyService.IsCellLockActiveForLocal() && !partyService.AllowCellChangeDuringLock()) + { + partyService.NotifyCellLockBlocked(); + return; + } + uint32_t baseId = 0; uint32_t modId = 0; @@ -129,6 +146,13 @@ void PlayerService::OnGridCellChangeEvent(const GridCellChangeEvent& acEvent) co void PlayerService::OnCellChangeEvent(const CellChangeEvent& acEvent) const noexcept { + auto& partyService = m_world.GetPartyService(); + if (partyService.IsCellLockActiveForLocal() && !partyService.AllowCellChangeDuringLock()) + { + partyService.NotifyCellLockBlocked(); + return; + } + if (acEvent.WorldSpaceId) { EnterExteriorCellRequest message; @@ -149,7 +173,7 @@ void PlayerService::OnCellChangeEvent(const CellChangeEvent& acEvent) const noex void PlayerService::OnPlayerDialogueEvent(const PlayerDialogueEvent& acEvent) const noexcept { - if (!m_transport.IsConnected()) + if (!m_transport.IsConnected() || m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost) return; const auto& partyService = m_world.GetPartyService(); @@ -203,11 +227,39 @@ void PlayerService::RunRespawnUpdates(const double acDeltaTime) noexcept PlayerCharacter* pPlayer = PlayerCharacter::Get(); if (!pPlayer->actorState.IsBleedingOut()) { - m_cachedMainSpellId = pPlayer->magicItems[0] ? pPlayer->magicItems[0]->formID : 0; - m_cachedSecondarySpellId = pPlayer->magicItems[1] ? pPlayer->magicItems[1]->formID : 0; + // Cache equipped items, spells, and shouts so we can restore them after respawn. + m_cachedLeftHandSpellId = pPlayer->magicItems[0] ? pPlayer->magicItems[0]->formID : 0; + m_cachedRightHandSpellId = pPlayer->magicItems[1] ? pPlayer->magicItems[1]->formID : 0; + + TESForm* pLeftEquipped = nullptr; + TESForm* pRightEquipped = nullptr; + + if (m_cachedLeftHandSpellId == 0) + pLeftEquipped = pPlayer->GetEquippedWeapon(0); + if (m_cachedRightHandSpellId == 0) + pRightEquipped = pPlayer->GetEquippedWeapon(1); + + m_cachedLeftHandItemId = pLeftEquipped ? pLeftEquipped->formID : 0; + m_cachedRightHandItemId = pRightEquipped ? pRightEquipped->formID : 0; + + // Detect two-handed weapons so we can equip them via the either-hand slot. + if (pLeftEquipped && pRightEquipped && pLeftEquipped == pRightEquipped) + m_cachedTwoHandedItemId = pLeftEquipped->formID; + else + m_cachedTwoHandedItemId = 0; + + if (auto* pAmmo = pPlayer->GetEquippedAmmo()) + m_cachedAmmoId = pAmmo->formID; + else + m_cachedAmmoId = 0; + m_cachedPowerId = pPlayer->equippedShout ? pPlayer->equippedShout->formID : 0; + m_cachedWeaponDrawn = pPlayer->actorState.IsWeaponDrawn(); s_startTimer = false; + m_waitingForRespawn = false; + m_canRespawn = false; + m_respawnTimer = 0.0; return; } @@ -215,6 +267,9 @@ void PlayerService::RunRespawnUpdates(const double acDeltaTime) noexcept { s_startTimer = true; m_respawnTimer = 5.0; + m_waitingForRespawn = true; + m_canRespawn = false; + FadeOutGame(true, true, 3.0f, true, 2.0f); // If a player dies not by its health reaching 0, getting it up from its bleedout state isn't possible @@ -223,31 +278,242 @@ void PlayerService::RunRespawnUpdates(const double acDeltaTime) noexcept pPlayer->ForceActorValue(ActorValueOwner::ForceMode::DAMAGE, ActorValueInfo::kHealth, 0); pPlayer->PayCrimeGoldToAllFactions(); + + // Notify the UI that the player died and show the death screen. + if (auto* pOverlayApp = m_world.GetOverlayService().GetOverlayApp()) + { + // Enable overlay input for death screen buttons to work + TiltedPhoques::DInputHook::Get().SetEnabled(true); + m_world.GetOverlayService().SetActive(true); + + // Show cursor for death screen + if (auto* pClient = pOverlayApp->GetClient()) + { + if (auto pRenderer = pClient->GetOverlayRenderHandler()) + { + pRenderer->SetCursorVisible(true); + } + } + + auto pArgs = CefListValue::Create(); + int32_t secondsRemaining = static_cast(std::ceil(m_respawnTimer)); + if (secondsRemaining < 0) + secondsRemaining = 0; + pArgs->SetInt(0, secondsRemaining); + pOverlayApp->ExecuteAsync("showDeathScreen", pArgs); + } + + PartyMemberDownedRequest downedRequest{}; + downedRequest.IsDowned = true; + m_transport.Send(downedRequest); + } + + if (!m_waitingForRespawn) + { + // Player has already been revived (e.g. via a healing spell). + return; } m_respawnTimer -= acDeltaTime; + if (m_respawnTimer < 0.0) + m_respawnTimer = 0.0; + + // Update countdown on the death screen. + if (auto* pOverlayApp = m_world.GetOverlayService().GetOverlayApp()) + { + auto pArgs = CefListValue::Create(); + int32_t secondsRemaining = static_cast(std::ceil(m_respawnTimer)); + if (secondsRemaining < 0) + secondsRemaining = 0; + pArgs->SetInt(0, secondsRemaining); + pOverlayApp->ExecuteAsync("updateDeathTimer", pArgs); + } - if (m_respawnTimer <= 0.0) + if (!m_canRespawn && m_respawnTimer <= 0.0) { - pPlayer->RespawnPlayer(); + m_canRespawn = true; + + // Enable the respawn button on the death screen. + if (auto* pOverlayApp = m_world.GetOverlayService().GetOverlayApp()) + { + auto pArgs = CefListValue::Create(); + pOverlayApp->ExecuteAsync("enableRespawnButton", pArgs); + } + } + + // Check if we need to respawn (triggered by button or heal) + if (m_shouldRespawnAtEntrance || m_shouldRespawnInPlace) + { + if (m_shouldRespawnAtEntrance) + { + // Respawn at entrance + pPlayer->RespawnPlayer(); + } + else + { + // Respawn in place (no teleport) + pPlayer->SetNoBleedoutRecovery(false); + pPlayer->DispelAllSpells(); + pPlayer->ForceActorValue(ActorValueOwner::ForceMode::DAMAGE, ActorValueInfo::kHealth, 1000000); + pPlayer->SetNoBleedoutRecovery(true); + } m_knockdownTimer = 1.5; m_knockdownStart = true; + EquipmentSnapshot snapshot{}; + snapshot.LeftSpellId = m_cachedLeftHandSpellId; + snapshot.RightSpellId = m_cachedRightHandSpellId; + snapshot.LeftWeaponId = m_cachedLeftHandItemId; + snapshot.RightWeaponId = m_cachedRightHandItemId; + snapshot.TwoHandWeaponId = m_cachedTwoHandedItemId; + snapshot.AmmoId = m_cachedAmmoId; + snapshot.ShoutId = m_cachedPowerId; + snapshot.WasWeaponDrawn = m_cachedWeaponDrawn; + + RestoreEquipmentSnapshot(pPlayer, snapshot, true); + + SyncCachedEquipment(pPlayer); + m_transport.Send(PlayerRespawnRequest()); + PartyMemberDownedRequest revivedRequest{}; + revivedRequest.IsDowned = false; + m_transport.Send(revivedRequest); + + m_waitingForRespawn = false; + m_canRespawn = false; + m_respawnTimer = 0.0; + m_shouldRespawnAtEntrance = false; + m_shouldRespawnInPlace = false; s_startTimer = false; - auto* pEquipManager = EquipManager::Get(); - TESForm* pSpell = TESForm::GetById(m_cachedMainSpellId); - if (pSpell) - pEquipManager->EquipSpell(pPlayer, pSpell, 0); - pSpell = TESForm::GetById(m_cachedSecondarySpellId); - if (pSpell) - pEquipManager->EquipSpell(pPlayer, pSpell, 1); - pSpell = TESForm::GetById(m_cachedPowerId); - if (pSpell) - pEquipManager->EquipShout(pPlayer, pSpell); + // Hide death screen + if (auto* pOverlayApp = m_world.GetOverlayService().GetOverlayApp()) + { + auto pArgs = CefListValue::Create(); + pOverlayApp->ExecuteAsync("hideDeathScreen", pArgs); + + // Disable overlay input and hide cursor after death screen closes + TiltedPhoques::DInputHook::Get().SetEnabled(false); + m_world.GetOverlayService().SetActive(false); + + if (auto* pClient = pOverlayApp->GetClient()) + { + if (auto pRenderer = pClient->GetOverlayRenderHandler()) + { + pRenderer->SetCursorVisible(false); + } + } + } + } +} + +void PlayerService::SyncCachedEquipment(PlayerCharacter* apPlayer) noexcept +{ + if (!apPlayer) + return; + + auto& defaultObjects = DefaultObjectManager::Get(); + + const auto dispatch = [&](uint32_t itemId, TESForm* pSlot, bool isSpell, bool isShout, bool isAmmo = false) + { + if (!itemId) + return; + + if (!TESForm::GetById(itemId)) + return; + + EquipmentChangeEvent evt{}; + evt.ActorId = apPlayer->formID; + evt.ItemId = itemId; + evt.EquipSlotId = pSlot ? pSlot->formID : 0; + evt.IsSpell = isSpell; + evt.IsShout = isShout; + evt.IsAmmo = isAmmo; + if (!isSpell && !isShout) + evt.Count = 1; + + m_world.GetRunner().Trigger(evt); + }; + + dispatch(m_cachedLeftHandSpellId, defaultObjects.leftEquipSlot, true, false); + dispatch(m_cachedRightHandSpellId, defaultObjects.rightEquipSlot, true, false); + + if (m_cachedTwoHandedItemId) + { + dispatch(m_cachedTwoHandedItemId, defaultObjects.eitherEquipSlot, false, false); + } + else + { + if (m_cachedLeftHandSpellId == 0) + dispatch(m_cachedLeftHandItemId, defaultObjects.leftEquipSlot, false, false); + if (m_cachedRightHandSpellId == 0) + dispatch(m_cachedRightHandItemId, defaultObjects.rightEquipSlot, false, false); + } + + dispatch(m_cachedAmmoId, defaultObjects.rightEquipSlot, false, false, true); + dispatch(m_cachedPowerId, nullptr, false, true); +} + +void PlayerService::RequestManualRespawn() noexcept +{ + try + { + if (!m_isDeathSystemEnabled) + { + spdlog::warn("RequestManualRespawn: Death system not enabled"); + return; + } + + // Only allow manual respawn while the death screen is active and the cooldown has finished. + if (!m_waitingForRespawn || !m_canRespawn) + { + spdlog::warn("RequestManualRespawn: Not waiting for respawn or button not enabled. waiting={}, canRespawn={}", m_waitingForRespawn, m_canRespawn); + return; + } + + // Just set a flag - let RunRespawnUpdates handle the actual respawn + m_shouldRespawnAtEntrance = true; + } + catch (const std::exception& e) + { + spdlog::error("RequestManualRespawn: Exception occurred: {}", e.what()); + m_waitingForRespawn = false; + m_canRespawn = false; + m_respawnTimer = 0.0; + } + catch (...) + { + spdlog::error("RequestManualRespawn: Unknown exception occurred"); + m_waitingForRespawn = false; + m_canRespawn = false; + m_respawnTimer = 0.0; + } +} + +void PlayerService::OnHealRevive() noexcept +{ + try + { + if (!m_isDeathSystemEnabled) + { + spdlog::warn("OnHealRevive: Death system not enabled"); + return; + } + + // Mark that we should respawn the player in place on the next update. + // RunRespawnUpdates will perform the actual revive (health, flags, UI). + m_shouldRespawnInPlace = true; + spdlog::info("OnHealRevive: queued in-place respawn via healing."); + } + catch (const std::exception& e) + { + spdlog::error("OnHealRevive: Exception occurred: {}", e.what()); + } + catch (...) + { + spdlog::error("OnHealRevive: Unknown exception occurred"); } } @@ -340,7 +606,7 @@ void PlayerService::RunBeastFormDetection() const noexcept PlayerCharacter* pPlayer = PlayerCharacter::Get(); if (!pPlayer->race) return; - + if (pPlayer->race->formID == lastRaceFormID) return; diff --git a/Code/client/Services/Generic/QuestService.cpp b/Code/client/Services/Generic/QuestService.cpp index b32f501be..d090c978d 100644 --- a/Code/client/Services/Generic/QuestService.cpp +++ b/Code/client/Services/Generic/QuestService.cpp @@ -3,57 +3,265 @@ #include #include -#include +#include #include +#include #include #include #include +#include #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +using GateRule = QuestService::GateRule; + +uint32_t ParseHex(const std::string& aText) noexcept +{ + try + { + return static_cast(std::stoul(aText, nullptr, 0)); + } + catch (...) + { + return 0; + } +} + +struct RuleMetadata +{ + TiltedPhoques::String IdName{}; + TiltedPhoques::String Name{}; +}; + +bool ParseRuleMetadata(const std::string& aChunk, RuleMetadata& aOut) noexcept +{ + std::regex idNameRx("\"idName\"\\s*:\\s*\"([^\"]+)\""); + std::regex nameRx("\"name\"\\s*:\\s*\"([^\"]+)\""); + + std::smatch match; + + if (std::regex_search(aChunk, match, idNameRx)) + aOut.IdName = match[1].str().c_str(); + + if (std::regex_search(aChunk, match, nameRx)) + aOut.Name = match[1].str().c_str(); + + return !aOut.IdName.empty(); +} + +bool ParseStageRange(const std::string& aChunk, uint16_t& aStageMin, uint16_t& aStageMax, TiltedPhoques::String& aNotes) noexcept +{ + std::regex minRx("\"stageMin\"\\s*:\\s*([0-9]+)"); + std::regex maxRx("\"stageMax\"\\s*:\\s*([0-9]+)"); + std::regex notesRx("\"notes\"\\s*:\\s*\"([^\"]+)\""); + + std::smatch match; + + if (!std::regex_search(aChunk, match, minRx)) + return false; + + aStageMin = static_cast(ParseHex(match[1].str())); + aStageMax = aStageMin; + if (std::regex_search(aChunk, match, maxRx)) + aStageMax = static_cast(ParseHex(match[1].str())); + + if (aStageMax < aStageMin) + std::swap(aStageMin, aStageMax); + + if (std::regex_search(aChunk, match, notesRx)) + aNotes = match[1].str().c_str(); + else + aNotes.clear(); + + return true; +} + +bool GateStatusEquals(const QuestService::GateStatus& aLeft, const QuestService::GateStatus& aRight) noexcept +{ + return aLeft.Active == aRight.Active + && aLeft.FormId == aRight.FormId + && aLeft.Stage == aRight.Stage + && aLeft.IdName == aRight.IdName + && aLeft.Name == aRight.Name + && aLeft.Notes == aRight.Notes; +} + +void LoadRulesFromFile(const std::filesystem::path& aPath, TiltedPhoques::Vector& aOutRules) noexcept +{ + std::ifstream file(aPath); + if (!file.is_open()) + { + spdlog::warn("Quest gating: failed to open {}", aPath.string()); + return; + } + + std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + std::replace(content.begin(), content.end(), '\n', ' '); + std::replace(content.begin(), content.end(), '\r', ' '); + + std::regex chunkRx("\\{[^\\{\\}]*?idName[^\\}]*?\\}"); + std::regex blacklistEntryRx("\\{[^\\{\\}]*?stageMin[^\\}]*?\\}"); + + auto chunkIt = std::sregex_iterator(content.begin(), content.end(), chunkRx); + auto end = std::sregex_iterator(); + for (; chunkIt != end; ++chunkIt) + { + size_t start = static_cast(chunkIt->position()); + size_t limit = content.size(); + auto nextBase = std::next(chunkIt); + if (nextBase != end) + limit = static_cast(nextBase->position()); + + const std::string slice = content.substr(start, limit - start); + RuleMetadata metadata{}; + if (!ParseRuleMetadata(slice, metadata)) + continue; + + bool added = false; + auto entryIt = std::sregex_iterator(slice.begin(), slice.end(), blacklistEntryRx); + for (; entryIt != end; ++entryIt) + { + const auto entry = entryIt->str(); + uint16_t stageMin = 0; + uint16_t stageMax = 0; + TiltedPhoques::String notes{}; + if (!ParseStageRange(entry, stageMin, stageMax, notes)) + continue; + + GateRule rule{}; + rule.IdName = metadata.IdName; + rule.Name = metadata.Name; + rule.Notes = notes; + rule.StageMin = stageMin; + rule.StageMax = stageMax; + aOutRules.push_back(rule); + added = true; + } + + if (!added) + { + uint16_t stageMin = 0; + uint16_t stageMax = 0; + TiltedPhoques::String notes{}; + if (ParseStageRange(slice, stageMin, stageMax, notes)) + { + GateRule rule{}; + rule.IdName = metadata.IdName; + rule.Name = metadata.Name; + rule.Notes = notes; + rule.StageMin = stageMin; + rule.StageMax = stageMax; + aOutRules.push_back(rule); + } + } + } + + if (aOutRules.empty()) + spdlog::warn("Quest gating: no rules parsed from {}", aPath.string()); +} +} // namespace + +using Quest = TESQuest; // Alias for PAPYRUS_FUNCTION namespace string "Quest" static TESQuest* FindQuestByNameId(const String& name) { - auto& questRegistry = ModManager::Get()->quests; - auto it = std::find_if(questRegistry.begin(), questRegistry.end(), [name](auto* it) { return std::strcmp(it->idName.AsAscii(), name.c_str()); }); + auto* pModManager = ModManager::Get(); + if (!pModManager) + return nullptr; + + auto& questRegistry = pModManager->quests; + auto it = std::find_if(questRegistry.begin(), questRegistry.end(), [&name](auto* quest) { + if (!quest) + return false; + const char* idName = quest->idName.AsAscii(); + return idName && std::strcmp(idName, name.c_str()) == 0; + }); return it != questRegistry.end() ? *it : nullptr; } +// Helper: map PlayerId -> Actor* +static Actor* GetActorByPlayerId(uint32_t aPlayerId, entt::registry& aWorld) +{ + auto view = aWorld.view(); + auto it = std::find_if(view.begin(), view.end(), [view, aPlayerId](auto e) { return view.get(e).Id == aPlayerId; }); + if (it == view.end()) + return nullptr; + const auto& formIdComponent = view.get(*it); + return Cast(TESForm::GetById(formIdComponent.Id)); +} + +bool RuleTargetsQuest(const GateRule& aRule, TESQuest* apQuest) noexcept +{ + if (!apQuest) + return false; + + if (!aRule.IdName.empty()) + { + const char* idName = apQuest->idName.AsAscii(); + if (!idName || std::strcmp(idName, aRule.IdName.c_str()) != 0) + return false; + } + + return true; +} + +bool IsQuestCompleted(const TESQuest* apQuest) noexcept +{ + if (!apQuest) + return false; + const uint16_t completedFlag = static_cast(TESQuest::Flags::Completed); + return (apQuest->flags & completedFlag) != 0; +} + QuestService::QuestService(World& aWorld, entt::dispatcher& aDispatcher) : m_world(aWorld) { m_joinedConnection = aDispatcher.sink().connect<&QuestService::OnConnected>(this); m_questUpdateConnection = aDispatcher.sink().connect<&QuestService::OnQuestUpdate>(this); + m_updateConnection = aDispatcher.sink().connect<&QuestService::OnUpdate>(this); - // A note about the Gameevents: - // TESQuestStageItemDoneEvent gets fired to late, we instead use TESQuestStageEvent, because it responds immediately. - // TESQuestInitEvent can be instead managed by start stop quest management. - // bind game event listeners + // Game quest events auto* pEventList = EventDispatcherManager::Get(); pEventList->questStartStopEvent.RegisterSink(this); pEventList->questStageEvent.RegisterSink(this); + pEventList->loadGameEvent.RegisterSink(this); } void QuestService::OnConnected(const ConnectedEvent&) noexcept { - // TODO: this should be followed with whatever the quest leader selected - /* - // deselect any active quests - auto* pPlayer = PlayerCharacter::Get(); - for (auto& objective : pPlayer->objectives) - { - if (auto* pQuest = objective.instance->quest) - pQuest->SetActive(false); - } - */ + m_gateActive = false; + m_gateRescanTimer = 0.0; + m_gateRulesLoaded = false; + m_initialGateScan = false; + m_gateStatus = {}; + LoadGateRules(); + EvaluateGatesFromWorld(); } BSTEventResult QuestService::OnEvent(const TESQuestStartStopEvent* apEvent, const EventDispatcher*) { + EvaluateGateForQuest(apEvent->formId, 0); + + if (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost) + return BSTEventResult::kOk; + if (ScopedQuestOverride::IsOverriden() || !m_world.Get().GetPartyService().IsInParty()) return BSTEventResult::kOk; @@ -63,22 +271,20 @@ BSTEventResult QuestService::OnEvent(const TESQuestStartStopEvent* apEvent, cons { if (IsNonSyncableQuest(pQuest)) return BSTEventResult::kOk; - + if (pQuest->type == TESQuest::Type::None || pQuest->type == TESQuest::Type::Miscellaneous) { - // Perhaps redundant, but necessary. We need the logging and - // the lambda coming up is queued and runs later GameId Id; auto& modSys = m_world.GetModSystem(); if (modSys.GetServerModId(pQuest->formID, Id)) { spdlog::info(__FUNCTION__ ": queuing type none/misc quest gameId {:X} questStage {} questStatus {} questType {} formId {:X} name {}", Id.LogFormat(), pQuest->currentStage, pQuest->IsStopped() ? RequestQuestUpdate::Stopped : RequestQuestUpdate::Started, - static_cast>(pQuest->type), + static_cast>(pQuest->type), pQuest->formID, pQuest->fullName.value.AsAscii()); } } - + m_world.GetRunner().Queue( [&, formId = pQuest->formID, stageId = pQuest->currentStage, stopped = pQuest->IsStopped(), type = pQuest->type]() { @@ -90,7 +296,7 @@ BSTEventResult QuestService::OnEvent(const TESQuestStartStopEvent* apEvent, cons update.Id = Id; update.Stage = stageId; update.Status = stopped ? RequestQuestUpdate::Stopped : RequestQuestUpdate::Started; - update.ClientQuestType = static_cast>(type); + update.ClientQuestType = static_cast>(type); m_world.GetTransport().Send(update); } @@ -102,12 +308,16 @@ BSTEventResult QuestService::OnEvent(const TESQuestStartStopEvent* apEvent, cons BSTEventResult QuestService::OnEvent(const TESQuestStageEvent* apEvent, const EventDispatcher*) { + EvaluateGateForQuest(apEvent->formId, apEvent->stageId); + + if (m_world.GetSyncModeService().GetLocalMode() == SyncMode::Ghost) + return BSTEventResult::kOk; + if (ScopedQuestOverride::IsOverriden() || !m_world.Get().GetPartyService().IsInParty()) return BSTEventResult::kOk; spdlog::info("Quest stage event: {:X}, stage: {}", apEvent->formId, apEvent->stageId); - // there is no reason to even fetch the quest object, since the event provides everything already.... if (TESQuest* pQuest = Cast(TESForm::GetById(apEvent->formId))) { if (IsNonSyncableQuest(pQuest)) @@ -115,8 +325,6 @@ BSTEventResult QuestService::OnEvent(const TESQuestStageEvent* apEvent, const Ev if (pQuest->type == TESQuest::Type::None || pQuest->type == TESQuest::Type::Miscellaneous) { - // Perhaps redundant, but necessary. We need the logging and - // the lambda coming up is queued and runs later GameId Id; auto& modSys = m_world.GetModSystem(); if (modSys.GetServerModId(pQuest->formID, Id)) @@ -150,15 +358,76 @@ BSTEventResult QuestService::OnEvent(const TESQuestStageEvent* apEvent, const Ev return BSTEventResult::kOk; } +BSTEventResult QuestService::OnEvent(const TESLoadGameEvent*, const EventDispatcher*) +{ + m_initialGateScan = false; + m_gateRescanTimer = 0.0; + return BSTEventResult::kOk; +} + void QuestService::OnQuestUpdate(const NotifyQuestUpdate& aUpdate) noexcept +{ + m_world.GetRunner().Queue([this, update = aUpdate]() + { + if (!TryApplyQuestUpdate(update)) + { + spdlog::debug("Quest update pending for {:X}, stage {}", update.Id.LogFormat(), update.Stage); + m_pendingUpdates.push_back(update); + } + }); +} + +void QuestService::OnUpdate(const UpdateEvent& acEvent) noexcept +{ + if (!m_initialGateScan) + { + m_initialGateScan = true; + EvaluateGatesFromWorld(); + } + + FlushPendingUpdates(); + // Re-evaluate gate rules periodically in case we missed events + constexpr double cGateRescanInterval = 2.5; + m_gateRescanTimer += acEvent.Delta; + if (m_gateRescanTimer >= cGateRescanInterval) + { + m_gateRescanTimer = 0.0; + EvaluateGatesFromWorld(); + } +} + +void QuestService::FlushPendingUpdates() noexcept +{ + if (m_pendingUpdates.empty()) + return; + + TiltedPhoques::Vector remaining; + remaining.reserve(m_pendingUpdates.size()); + + for (const auto& update : m_pendingUpdates) + { + if (!TryApplyQuestUpdate(update)) + remaining.push_back(update); + } + + m_pendingUpdates = std::move(remaining); +} + +bool QuestService::TryApplyQuestUpdate(const NotifyQuestUpdate& aUpdate) noexcept { ModSystem& modSystem = World::Get().GetModSystem(); - uint32_t formId = modSystem.GetGameId(aUpdate.Id); + const uint32_t formId = modSystem.GetGameId(aUpdate.Id); + if (formId == 0) + { + spdlog::warn("Quest update awaiting mod resolution, base id: {:X}, mod id: {:X}", aUpdate.Id.BaseId, aUpdate.Id.ModId); + return false; + } + TESQuest* pQuest = Cast(TESForm::GetById(formId)); if (!pQuest) { - spdlog::error("Failed to find quest, base id: {:X}, mod id: {:X}", aUpdate.Id.BaseId, aUpdate.Id.ModId); - return; + spdlog::warn("Quest {:X} not loaded yet, deferring update", formId); + return false; } if (pQuest->type == TESQuest::Type::None || pQuest->type == TESQuest::Type::Miscellaneous) @@ -169,17 +438,23 @@ void QuestService::OnQuestUpdate(const NotifyQuestUpdate& aUpdate) noexcept } bool bResult = false; + + ScopedQuestOverride questOverride; + switch (aUpdate.Status) { case NotifyQuestUpdate::Started: { - pQuest->ScriptSetStage(aUpdate.Stage); + bool startSuccess = false; + pQuest->EnsureQuestStarted(startSuccess, true); pQuest->SetActive(true); + pQuest->ScriptSetStage(aUpdate.Stage); bResult = true; spdlog::info("Remote quest started: {:X}, stage: {}", formId, aUpdate.Stage); break; } case NotifyQuestUpdate::StageUpdate: + pQuest->SetActive(true); pQuest->ScriptSetStage(aUpdate.Stage); bResult = true; spdlog::info("Remote quest updated: {:X}, stage: {}", formId, aUpdate.Stage); @@ -188,11 +463,15 @@ void QuestService::OnQuestUpdate(const NotifyQuestUpdate& aUpdate) noexcept bResult = StopQuest(formId); spdlog::info("Remote quest stopped: {:X}, stage: {}", formId, aUpdate.Stage); break; - default: break; + default: + spdlog::warn("Unhandled quest update status {} for quest {:X}", aUpdate.Status, formId); + break; } if (!bResult) spdlog::error("Failed to update the client quest state, quest: {:X}, stage: {}, status: {}", formId, aUpdate.Stage, aUpdate.Status); + + return bResult; } bool QuestService::StopQuest(uint32_t aformId) @@ -200,6 +479,7 @@ bool QuestService::StopQuest(uint32_t aformId) TESQuest* pQuest = Cast(TESForm::GetById(aformId)); if (pQuest) { + ScopedQuestOverride _; pQuest->SetActive(false); pQuest->SetStopped(); return true; @@ -212,22 +492,164 @@ static constexpr std::array kNonSyncableQuestIds = std::to_array({ 0x2BA16, // Werewolf transformation quest 0x20071D0, // Vampire transformation quest 0x3AC44, // MS13BleakFallsBarrowLeverScene - // 0xFE014801, // Unknown dynamic ID, kept as note, maybe lookup correct ID this game? 0xF2593 // Skill experience quest }); bool QuestService::IsNonSyncableQuest(TESQuest* apQuest) { - // Quests with no quest stages are never synced. Most TESQues::Type:: quests should - // be synced, including Type::None and Type::Miscellaneous, but there are a few - // known exceptions that should be excluded that are in the table. - return apQuest->stages.Empty() + return apQuest->stages.Empty() || std::find(kNonSyncableQuestIds.begin(), kNonSyncableQuestIds.end(), apQuest->formID) != kNonSyncableQuestIds.end(); } void QuestService::DebugDumpQuests() { - auto& quests = ModManager::Get()->quests; + auto* pModManager = ModManager::Get(); + if (!pModManager) + return; + + auto& quests = pModManager->quests; for (TESQuest* pQuest : quests) - spdlog::info("{:X}|{}|{}|{}", pQuest->formID, (uint8_t)pQuest->type, pQuest->priority, pQuest->idName.AsAscii()); + { + if (!pQuest) + continue; + const char* idName = pQuest->idName.AsAscii(); + spdlog::info("{:X}|{}|{}|{}", pQuest->formID, (uint8_t)pQuest->type, pQuest->priority, idName ? idName : ""); + } +} + +void QuestService::EvaluateGateForQuest(uint32_t aFormId, uint16_t aStage) noexcept +{ + if (!m_gateRulesLoaded) + LoadGateRules(); + + const auto it = std::find_if(m_gateRules.begin(), m_gateRules.end(), + [&](const GateRule& rule) { return RuleTargetsQuest(rule, Cast(TESForm::GetById(aFormId))); }); + if (it == m_gateRules.end()) + return; + + // If the event is relevant, recompute the full gate state using current quest data. + EvaluateGatesFromWorld(); +} + +void QuestService::EvaluateGatesFromWorld() noexcept +{ + if (!m_gateRulesLoaded) + LoadGateRules(); + + bool shouldGate = false; + uint32_t matchedQuestId = 0; + uint16_t matchedStage = 0; + const GateRule* matchedRule = nullptr; + TESQuest* matchedQuest = nullptr; + for (const auto& rule : m_gateRules) + { + TESQuest* pQuest = nullptr; + uint32_t formId = 0; + + if (!rule.IdName.empty()) + { + pQuest = FindQuestByNameId(rule.IdName); + if (pQuest) + formId = pQuest->formID; + } + if (!RuleTargetsQuest(rule, pQuest)) + continue; + + if (!pQuest || pQuest->IsStopped() || IsQuestCompleted(pQuest)) + continue; + + if (rule.Matches(pQuest->currentStage)) + { + shouldGate = true; + matchedQuestId = formId; + matchedStage = pQuest->currentStage; + matchedRule = &rule; + matchedQuest = pQuest; + break; + } + } + + GateStatus nextStatus{}; + nextStatus.Active = shouldGate; + if (shouldGate) + { + nextStatus.FormId = matchedQuestId; + nextStatus.Stage = matchedStage; + if (matchedRule) + { + nextStatus.IdName = matchedRule->IdName; + nextStatus.Name = matchedRule->Name; + nextStatus.Notes = matchedRule->Notes; + } + if (nextStatus.Name.empty() && matchedQuest) + { + if (const char* fullName = matchedQuest->fullName.value.AsAscii()) + nextStatus.Name = fullName; + } + if (nextStatus.IdName.empty() && matchedQuest) + { + if (const char* idName = matchedQuest->idName.AsAscii()) + nextStatus.IdName = idName; + } + } + + const bool statusChanged = !GateStatusEquals(m_gateStatus, nextStatus); + m_gateStatus = nextStatus; + + const SyncMode desiredMode = shouldGate ? SyncMode::Ghost : SyncMode::Normal; + + const bool modeChange = + (shouldGate != m_gateActive || m_world.GetSyncModeService().GetLocalMode() != desiredMode); + if (modeChange) + { + m_gateActive = shouldGate; + m_world.GetSyncModeService().SetLocalMode(desiredMode); + if (shouldGate) + spdlog::info("Entering quest sync gate for quest {:X} stage {}", matchedQuestId, matchedStage); + else + spdlog::info("Exiting quest sync gate"); + } + else if (statusChanged && shouldGate) + { + m_world.GetSyncModeService().RefreshOverlaySyncStatus(); + } +} + +void QuestService::LoadGateRules() noexcept +{ + m_gateRulesLoaded = true; + m_gateRules.clear(); + + const auto basePath = TiltedPhoques::GetPath(); + const std::filesystem::path isolationDir = basePath / "Isolation"; + + std::error_code ec; + if (!std::filesystem::exists(isolationDir, ec)) + { + std::filesystem::create_directories(isolationDir, ec); + if (ec) + { + spdlog::warn("Quest gating: failed to create isolation directory {}", isolationDir.string()); + return; + } + spdlog::info("Quest gating: created isolation directory {}", isolationDir.string()); + } + + for (const auto& entry : std::filesystem::directory_iterator(isolationDir, ec)) + { + if (ec) + break; + + if (!entry.is_regular_file()) + continue; + + auto ext = entry.path().extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (ext != ".json") + continue; + + LoadRulesFromFile(entry.path(), m_gateRules); + } + + spdlog::info("Quest gating: loaded {} gate rules from {}", m_gateRules.size(), isolationDir.string()); } diff --git a/Code/client/Services/Generic/SyncModeService.cpp b/Code/client/Services/Generic/SyncModeService.cpp new file mode 100644 index 000000000..436d3cf9d --- /dev/null +++ b/Code/client/Services/Generic/SyncModeService.cpp @@ -0,0 +1,676 @@ +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +// Ghost visual spell (applied to remote player actors only). +constexpr uint32_t kGhostGlowSpellFormId = 0x000D2056; +// TESObjectREFR::RecordFlags::kCollisionsDisabled (CommonLibSSE-Reference) for refs/actors. +constexpr uint32_t kCollisionDisabledFlag = 1u << 4; +constexpr uint32_t kGhostRefFlagMask = kCollisionDisabledFlag | TESForm::IGNORE_FRIENDLY_HITS; + +struct GhostGlowEffect +{ + MagicItem* pSpell = nullptr; + TiltedPhoques::Vector effects; +}; + +GhostGlowEffect GetGhostGlowEffect() noexcept +{ + static bool s_attempted = false; + static GhostGlowEffect s_effect{}; + + if (!s_attempted) + { + s_attempted = true; + + s_effect.pSpell = Cast(TESForm::GetById(kGhostGlowSpellFormId)); + if (!s_effect.pSpell) + { + spdlog::warn("Ghost glow spell {:X} not found (or not a MagicItem); blue glow disabled", kGhostGlowSpellFormId); + return s_effect; + } + + if (s_effect.pSpell->listOfEffects.Empty()) + { + spdlog::warn("Ghost glow spell {:X} has no effects; blue glow disabled", kGhostGlowSpellFormId); + s_effect.pSpell = nullptr; + return s_effect; + } + + // Apply all effects from the spell (some spells bundle multiple visuals). + s_effect.effects.clear(); + for (auto* pEffect : s_effect.pSpell->listOfEffects) + { + if (!pEffect || !pEffect->pEffectSetting) + continue; + s_effect.effects.push_back(pEffect); + } + + if (s_effect.effects.empty()) + { + spdlog::warn("Ghost glow spell {:X} has no valid effects; blue glow disabled", kGhostGlowSpellFormId); + s_effect = {}; + return s_effect; + } + } + + return s_effect; +} + +void ApplyGhostGlowEffect(Actor* apActor) noexcept +{ + if (!apActor) + return; + + const auto effect = GetGhostGlowEffect(); + if (!effect.pSpell || effect.effects.empty()) + return; + + // Prevent our own forced visuals from feeding back into magic sync hooks/events. + ScopedSpellCastOverride _; + + for (auto* pEffectItem : effect.effects) + { + MagicTarget::AddTargetData data{}; + data.pCaster = nullptr; + data.pSpell = effect.pSpell; + data.pEffectItem = pEffectItem; + data.fMagnitude = 1.f; + data.fUnkFloat1 = 1.f; + data.eCastingSource = MagicSystem::CastingSource::CASTING_SOURCE_COUNT; + + apActor->magicTarget.AddTarget(data, false, false); + } +} +} // namespace + +SyncModeService::SyncModeService(World& aWorld, entt::dispatcher& aDispatcher, TransportService& aTransport) noexcept + : m_world(aWorld) + , m_dispatcher(aDispatcher) + , m_transport(aTransport) +{ + m_connectedConnection = aDispatcher.sink().connect<&SyncModeService::OnConnected>(this); + m_disconnectedConnection = aDispatcher.sink().connect<&SyncModeService::OnDisconnected>(this); + m_updateConnection = aDispatcher.sink().connect<&SyncModeService::OnUpdate>(this); + m_playerJoinedConnection = aDispatcher.sink().connect<&SyncModeService::OnPlayerJoined>(this); + m_playerLeftConnection = aDispatcher.sink().connect<&SyncModeService::OnPlayerLeft>(this); + m_notifySyncModeConnection = aDispatcher.sink().connect<&SyncModeService::OnNotifyPlayerSyncMode>(this); + m_remoteRemovedConnection = m_world.on_destroy().connect<&SyncModeService::OnRemoteComponentRemoved>(this); +} + +void SyncModeService::SetLocalMode(const SyncMode aMode) noexcept +{ + const auto previousMode = m_localMode; + if (previousMode == aMode) + return; + + TiltedPhoques::Set refreshIds; + if (previousMode == SyncMode::Ghost && aMode == SyncMode::Normal) + { + auto view = m_world.view(); + for (auto entity : view) + { + const auto& playerComponent = view.get(entity); + if (playerComponent.Id == m_localPlayerId) + continue; + + const auto it = m_remoteModes.find(playerComponent.Id); + if (it != std::end(m_remoteModes) && it->second == SyncMode::Ghost) + continue; + + refreshIds.insert(view.get(entity).Id); + } + } + + m_localMode = aMode; + + UpdateWorldEncounters(); + + if (auto* pCharacterService = m_world.ctx().find(); pCharacterService) + pCharacterService->OnSyncModeChanged(previousMode, m_localMode); + + if (m_transport.IsOnline()) + { + RequestSetSyncMode request{}; + request.Mode = aMode; + m_transport.Send(request); + } + + RefreshGhostStates(); + UpdateOverlaySyncStatus(); + + if (!refreshIds.empty()) + RefreshRemotePlayers(refreshIds); + + if (previousMode == SyncMode::Ghost && aMode == SyncMode::Normal) + { + // Lightweight resync: ask for cell drops and weather so we can clean up obvious gaps. + RequestResync(); + } +} + +void SyncModeService::OnConnected(const ConnectedEvent& acEvent) noexcept +{ + m_localPlayerId = acEvent.PlayerId; + m_remoteModes.clear(); + + UpdateWorldEncounters(); + + if (m_localMode != SyncMode::Normal) + { + RequestSetSyncMode request{}; + request.Mode = m_localMode; + m_transport.Send(request); + } + + RefreshGhostStates(); + UpdateOverlaySyncStatus(); +} + +void SyncModeService::OnDisconnected(const DisconnectedEvent&) noexcept +{ + // Proactively strip ghost state from any remaining remote actors before we clear tracking data. + ClearGhostStates(); + + m_localPlayerId = 0; + m_localMode = SyncMode::Normal; + m_remoteModes.clear(); + m_pending3DRefresh.clear(); + m_glowApplied.clear(); + m_addedExtraGhost.clear(); + m_originalNpcFlags.clear(); + m_originalRefFlagBits.clear(); + UpdateWorldEncounters(); + UpdateOverlaySyncStatus(); +} + +void SyncModeService::OnUpdate(const UpdateEvent&) noexcept +{ + if (!m_transport.IsOnline()) + return; + + RefreshGhostStates(); + + if (!m_pending3DRefresh.empty()) + { + // Process 3D refresh requests outside of the UpdateReference3D hook (more stable during cell loads). + TiltedPhoques::Set pending; + pending.swap(m_pending3DRefresh); + + for (const uint32_t formId : pending) + { + Actor* pActor = Cast(TESForm::GetById(formId)); + if (!pActor) + continue; + + if (pActor == PlayerCharacter::Get()) + continue; + + // Only remote player actors are eligible for ghost visuals. + auto view = m_world.view(); + const auto it = std::find_if(std::begin(view), std::end(view), + [view, formId](entt::entity e) { return view.get(e).Id == formId; }); + if (it == std::end(view)) + continue; + + const auto entity = *it; + const bool isGhosted = m_world.any_of(entity); + if (!isGhosted) + continue; + + ApplyGhostToActor(pActor, true); + } + } +} + +void SyncModeService::OnPlayerJoined(const NotifyPlayerJoined& acMessage) noexcept +{ + m_remoteModes[acMessage.PlayerId] = SyncMode::Normal; +} + +void SyncModeService::OnPlayerLeft(const NotifyPlayerLeft& acMessage) noexcept +{ + m_remoteModes.erase(acMessage.PlayerId); +} + +void SyncModeService::OnNotifyPlayerSyncMode(const NotifyPlayerSyncMode& acMessage) noexcept +{ + SyncMode previousMode = SyncMode::Normal; + entt::entity playerEntity = entt::null; + uint32_t refreshServerId = 0; + bool wasGhosted = false; + + if (acMessage.PlayerId == m_localPlayerId) + { + previousMode = m_localMode; + } + else + { + if (auto it = m_remoteModes.find(acMessage.PlayerId); it != std::end(m_remoteModes)) + previousMode = it->second; + + auto view = m_world.view(); + const auto it = std::find_if(std::begin(view), std::end(view), + [view, playerId = acMessage.PlayerId](entt::entity e) { return view.get(e).Id == playerId; }); + if (it != std::end(view)) + { + playerEntity = *it; + refreshServerId = view.get(playerEntity).Id; + wasGhosted = m_world.any_of(playerEntity); + } + } + + if (acMessage.PlayerId == m_localPlayerId) + m_localMode = acMessage.Mode; + else + m_remoteModes[acMessage.PlayerId] = acMessage.Mode; + + RefreshGhostStates(); + if (acMessage.PlayerId == m_localPlayerId) + UpdateOverlaySyncStatus(); + + if (acMessage.PlayerId != m_localPlayerId && previousMode != acMessage.Mode && refreshServerId != 0) + { + const bool isGhosted = m_world.valid(playerEntity) && m_world.any_of(playerEntity); + if (m_localMode == SyncMode::Normal && previousMode == SyncMode::Ghost && acMessage.Mode == SyncMode::Normal && (wasGhosted || isGhosted)) + { + if (auto* pCharacterService = m_world.ctx().find(); pCharacterService) + pCharacterService->RefreshRemotePlayer(refreshServerId); + } + } +} + +void SyncModeService::UpdateOverlaySyncStatus() const noexcept +{ + auto* pOverlay = m_world.GetOverlayService().GetOverlayApp(); + if (!pOverlay) + return; + + const bool isolated = (m_transport.IsOnline() && m_localMode == SyncMode::Ghost); + std::string title; + std::string detail; + std::string moreInfo; + + if (isolated) + { + title = "Quest Isolation"; + detail = "Sync paused"; + + if (auto* pQuestService = m_world.ctx().find(); pQuestService) + { + const auto& gateStatus = pQuestService->GetGateStatus(); + if (gateStatus.Active) + { + std::string questLabel; + if (!gateStatus.Name.empty()) + questLabel = gateStatus.Name.c_str(); + else if (!gateStatus.IdName.empty()) + questLabel = gateStatus.IdName.c_str(); + + if (!questLabel.empty()) + detail = "Sync paused due to quest: " + questLabel; + + moreInfo = "Stage " + std::to_string(gateStatus.Stage); + if (!gateStatus.IdName.empty()) + moreInfo += " (" + std::string(gateStatus.IdName.c_str()) + ")"; + if (!gateStatus.Notes.empty()) + moreInfo += "\nNotes: " + std::string(gateStatus.Notes.c_str()); + } + } + } + + auto pArgs = CefListValue::Create(); + pArgs->SetBool(0, isolated); + pArgs->SetString(1, title); + pArgs->SetString(2, detail); + pArgs->SetString(3, moreInfo); + + pOverlay->ExecuteAsync("setSyncStatus", pArgs); +} + +void SyncModeService::UpdateWorldEncounters() noexcept +{ + TESGlobal* pWorldEncountersEnabled = Cast(TESForm::GetById(0xB8EC1)); + if (!pWorldEncountersEnabled) + return; + + if (m_localMode == SyncMode::Ghost || !m_transport.IsOnline()) + { + pWorldEncountersEnabled->f = 1.f; + return; + } + + const auto& partyService = m_world.GetPartyService(); + pWorldEncountersEnabled->f = (partyService.IsInParty() && partyService.IsLeader()) ? 1.f : 0.f; +} + +bool SyncModeService::ShouldGhost(const uint32_t aPlayerId) const noexcept +{ + if (aPlayerId == m_localPlayerId) + return m_localMode == SyncMode::Ghost; + + // While we're quest-gated, all remote players should be presented as ghosts locally (regardless of their own mode). + if (m_localMode == SyncMode::Ghost) + return true; + + const auto itor = m_remoteModes.find(aPlayerId); + if (itor != std::end(m_remoteModes)) + return itor->second == SyncMode::Ghost; + + return false; +} + +void SyncModeService::RefreshGhostStates() noexcept +{ + auto view = m_world.view(); + for (auto entity : view) + { + const auto& playerComponent = view.get(entity); + const bool shouldGhost = ShouldGhost(playerComponent.Id); + + const auto* pGhostComponent = m_world.try_get(entity); + const bool isGhosted = pGhostComponent && pGhostComponent->IsGhost; + + if (shouldGhost != isGhosted) + { + ToggleGhostState(entity, shouldGhost); + continue; + } + } +} + +void SyncModeService::ClearGhostStates() noexcept +{ + auto view = m_world.view(); + TiltedPhoques::Vector entities(view.begin(), view.end()); + + for (auto entity : entities) + { + if (!ToggleGhostState(entity, false)) + m_world.remove(entity); + } +} + +bool SyncModeService::ToggleGhostState(const entt::entity aEntity, const bool aGhost) noexcept +{ + const auto* pFormIdComponent = m_world.try_get(aEntity); + if (!pFormIdComponent) + return false; + + Actor* pActor = Cast(TESForm::GetById(pFormIdComponent->Id)); + if (!pActor) + return false; + + if (!ApplyGhostToActor(pActor, aGhost)) + return false; + + if (aGhost) + m_world.emplace_or_replace(aEntity, true); + else + m_world.remove(aEntity); + + return true; +} + +bool SyncModeService::ApplyGhostToActor(Actor* apActor, const bool aGhost) noexcept +{ + if (!apActor) + return false; + + const bool has3D = apActor->GetNiNode() != nullptr; + const uint32_t formId = apActor->formID; + + if (aGhost) + { + const bool firstGhostApplication = m_originalRefFlagBits.find(formId) == std::end(m_originalRefFlagBits); + if (firstGhostApplication) + m_originalRefFlagBits[formId] = apActor->flags & kGhostRefFlagMask; + + apActor->flags |= kGhostRefFlagMask; + + if (auto* pNpc = Cast(apActor->baseForm)) + { + const uint32_t baseId = pNpc->formID; + // Base forms are shared across player clones, so keep a refcount per NPC base. + auto& npcState = m_originalNpcFlags[baseId]; + if (npcState.RefCount == 0) + npcState.Flags = pNpc->actorData.flags; + if (firstGhostApplication) + npcState.RefCount++; + + // Vanilla ghost flag: this is what makes actors behave as "ghosts" (no collision / hard interaction) + // and is more reliable than trying to manually tweak Havok state. + pNpc->actorData.flags |= (1u << 29); // TESActorBaseData::kIsGhost + } + + // Add per-reference ExtraGhost so Havok/activation treat the actor as intangible. + if (auto* pExtraData = apActor->GetExtraDataList()) + { + if (!pExtraData->Contains(ExtraDataType::Ghost)) + { + if (auto* pExtraGhost = Memory::New()) + { + pExtraGhost->ghost = true; + if (!pExtraData->bitfield) + { + pExtraData->bitfield = Memory::Allocate(); + std::memset(pExtraData->bitfield, 0, sizeof(ExtraDataList::Bitfield)); + } + if (pExtraData->Add(ExtraDataType::Ghost, pExtraGhost)) + m_addedExtraGhost.insert(formId); + else + Memory::Delete(pExtraGhost); + } + } + } + + if (has3D) + { + if (m_glowApplied.find(formId) == std::end(m_glowApplied)) + { + ApplyGhostGlowEffect(apActor); + m_glowApplied.insert(formId); + } + apActor->UpdateAlpha(); + apActor->QueueUpdate(); + } + + return true; + } + + m_glowApplied.erase(formId); + if (auto it = m_originalRefFlagBits.find(formId); it != std::end(m_originalRefFlagBits)) + { + apActor->flags = (apActor->flags & ~kGhostRefFlagMask) | it->second; + m_originalRefFlagBits.erase(it); + } + else + { + apActor->flags &= ~kGhostRefFlagMask; + } + if (auto* pNpc = Cast(apActor->baseForm)) + { + const uint32_t baseId = pNpc->formID; + if (auto it = m_originalNpcFlags.find(baseId); it != std::end(m_originalNpcFlags)) + { + auto& npcState = it->second; + if (npcState.RefCount > 0) + npcState.RefCount--; + + if (npcState.RefCount == 0) + { + pNpc->actorData.flags = npcState.Flags; + m_originalNpcFlags.erase(it); + } + else + { + // Keep the ghost flag active while other ghosted actors share this base. + pNpc->actorData.flags |= (1u << 29); + } + } + else + { + pNpc->actorData.flags &= ~(1u << 29); + } + } + if (auto* pExtraData = apActor->GetExtraDataList()) + { + if (m_addedExtraGhost.contains(formId)) + { + if (auto* pExtraGhost = pExtraData->GetByType(ExtraDataType::Ghost)) + { + pExtraData->Remove(ExtraDataType::Ghost, pExtraGhost); + Memory::Delete(pExtraGhost); + } + m_addedExtraGhost.erase(formId); + } + } + if (has3D) + { + apActor->UpdateAlpha(); + apActor->QueueUpdate(); + } + + return true; +} + +void SyncModeService::CollectGhostedRemotePlayerServerIds(TiltedPhoques::Set& aOut) const noexcept +{ + auto view = m_world.view(); + for (auto entity : view) + { + const auto& playerComponent = view.get(entity); + if (playerComponent.Id == m_localPlayerId) + continue; + + aOut.insert(view.get(entity).Id); + } +} + +void SyncModeService::RefreshRemotePlayers(const TiltedPhoques::Set& aServerIds) noexcept +{ + if (aServerIds.empty()) + return; + + auto* pCharacterService = m_world.ctx().find(); + if (!pCharacterService) + return; + + for (const auto serverId : aServerIds) + pCharacterService->RefreshRemotePlayer(serverId); +} + +void SyncModeService::RequestResync() const noexcept +{ + PlayerCharacter* pPlayer = PlayerCharacter::Get(); + if (!pPlayer) + return; + + RequestDroppedItems drops{}; + drops.RequestAll = false; + + auto& modSystem = m_world.GetModSystem(); + if (TESObjectCELL* pCell = pPlayer->parentCell) + { + drops.HasCellFilter = modSystem.GetServerModId(pCell->formID, drops.CellId); + if (auto* pWorldSpace = pCell->worldspace) + drops.HasWorldSpaceFilter = modSystem.GetServerModId(pWorldSpace->formID, drops.WorldSpaceId); + } + + m_transport.Send(drops); + + RequestCurrentWeather weather{}; + m_transport.Send(weather); +} + +void SyncModeService::OnRemoteComponentRemoved(entt::registry& aRegistry, entt::entity aEntity) noexcept +{ + const auto* pGhostComponent = aRegistry.try_get(aEntity); + const auto* pFormIdComponent = aRegistry.try_get(aEntity); + if (!pGhostComponent || !pFormIdComponent) + return; + + Actor* pActor = Cast(TESForm::GetById(pFormIdComponent->Id)); + if (pActor) + ApplyGhostToActor(pActor, false); + + aRegistry.remove(aEntity); +} + +void SyncModeService::OnActor3DUpdated(Actor* apActor) noexcept +{ + if (!apActor) + return; + + // Never change the local player's visuals; only remote player actors are ghosted. + if (apActor == PlayerCharacter::Get()) + return; + + // Only remote player actors are eligible for ghost visuals. + const auto* pExtension = apActor->GetExtension(); + if (!pExtension || !pExtension->IsRemotePlayer()) + return; + + // FaceGen tints can drop when the 3D rebuilds; force a re-apply next update. + { + auto view = m_world.view(); + const uint32_t formId = apActor->formID; + const auto it = std::find_if(std::begin(view), std::end(view), + [view, formId](entt::entity e) { return view.get(e).Id == formId; }); + if (it != std::end(view)) + view.get(*it).Generated = false; + } + + // Force a re-apply after 3D rebuilds/cell transitions (effects can get dropped when 3D reloads). + m_glowApplied.erase(apActor->formID); + + // Defer work to OnUpdate to avoid doing extra engine calls from inside UpdateReference3D. + m_pending3DRefresh.insert(apActor->formID); +} + +void SyncModeService::OnLoadGameReset() noexcept +{ + ClearGhostStates(); + m_pending3DRefresh.clear(); + m_glowApplied.clear(); + m_addedExtraGhost.clear(); + m_originalNpcFlags.clear(); + m_originalRefFlagBits.clear(); + m_remoteModes.clear(); +} diff --git a/Code/client/Services/Generic/TradeService.cpp b/Code/client/Services/Generic/TradeService.cpp new file mode 100644 index 000000000..d6b94ca93 --- /dev/null +++ b/Code/client/Services/Generic/TradeService.cpp @@ -0,0 +1,419 @@ +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace +{ +constexpr uint64_t kInviteCleanupIntervalMs = 1000; + +std::string MakeDisplayName(ModSystem& aModSystem, const Inventory::Entry& aEntry) +{ + const uint32_t formId = aModSystem.GetGameId(aEntry.BaseId); + if (formId) + { + if (auto* pForm = TESForm::GetById(formId)) + { + if (const char* pName = pForm->GetName(); pName && std::strlen(pName) > 0) + return pName; + } + } + + return fmt::format("0x{:08X}:0x{:08X}", aEntry.BaseId.ModId, aEntry.BaseId.BaseId); +} +} // namespace + +TradeService::TradeService(World& aWorld, entt::dispatcher& aDispatcher, TransportService& aTransport) noexcept + : m_world(aWorld) + , m_transport(aTransport) +{ + m_updateConnection = aDispatcher.sink().connect<&TradeService::OnUpdate>(this); + m_disconnectConnection = aDispatcher.sink().connect<&TradeService::OnDisconnected>(this); + m_tradeInviteConnection = aDispatcher.sink().connect<&TradeService::OnTradeInvite>(this); + m_tradeStartedConnection = aDispatcher.sink().connect<&TradeService::OnTradeStarted>(this); + m_tradeStateConnection = aDispatcher.sink().connect<&TradeService::OnTradeState>(this); + m_tradeCancelConnection = aDispatcher.sink().connect<&TradeService::OnTradeCancel>(this); + m_tradeCompleteConnection = aDispatcher.sink().connect<&TradeService::OnTradeComplete>(this); +} + +void TradeService::SendInvite(uint32_t aTargetPlayerId) const noexcept +{ + TradeInviteRequest request{}; + request.TargetPlayerId = aTargetPlayerId; + m_transport.Send(request); +} + +void TradeService::RespondToInvite(uint32_t aRequesterPlayerId, bool aAccept) const noexcept +{ + TradeInviteResponseRequest response{}; + response.RequesterPlayerId = aRequesterPlayerId; + response.Accept = aAccept; + m_transport.Send(response); +} + +void TradeService::CancelTrade() const noexcept +{ + TradeCancelRequest request{}; + m_transport.Send(request); +} + +void TradeService::SetReady(bool aReady) const noexcept +{ + TradeSetReadyRequest request{}; + request.Ready = aReady; + m_transport.Send(request); +} + +void TradeService::UpdateOffer(const TiltedPhoques::Vector& aSelections) noexcept +{ + ApplyOfferSelection(aSelections); +} + +void TradeService::ApplyOfferSelection(const TiltedPhoques::Vector& aSelections) +{ + if (!m_session.Active) + return; + + TiltedPhoques::Vector entries; + entries.reserve(aSelections.size()); + + for (const auto& selection : aSelections) + { + if (selection.Index >= m_session.SelfInventory.size()) + continue; + + if (selection.Count <= 0) + continue; + + const auto& sourceEntry = m_session.SelfInventory[selection.Index]; + if (selection.Count > sourceEntry.Count) + continue; + + Inventory::Entry entry = sourceEntry; + entry.Count = selection.Count; + entries.push_back(entry); + } + + TradeOfferUpdateRequest request{}; + request.Items = entries; + m_transport.Send(request); + + m_session.SelfItems = request.Items; + m_session.SelfReady = false; + + EmitStateToUI(); +} + +void TradeService::OnUpdate(const UpdateEvent&) noexcept +{ + const auto cCurrentTick = m_transport.GetClock().GetCurrentTick(); + + static uint64_t s_nextCleanupTick = 0; + if (s_nextCleanupTick > cCurrentTick) + return; + + s_nextCleanupTick = cCurrentTick + kInviteCleanupIntervalMs; + + auto it = std::begin(m_pendingInvites); + while (it != std::end(m_pendingInvites)) + { + if (it->second <= cCurrentTick) + { + const uint32_t inviterId = it->first; + it = m_pendingInvites.erase(it); + EmitInviteUpdate(inviterId, false); + } + else + { + ++it; + } + } +} + +void TradeService::OnDisconnected(const DisconnectedEvent&) noexcept +{ + if (!m_pendingInvites.empty()) + { + auto invites = m_pendingInvites; + m_pendingInvites.clear(); + for (const auto& [inviterId, _] : invites) + EmitInviteUpdate(inviterId, false); + } + + if (m_session.Active) + { + EmitCancellation(m_session.PartnerId, TradeCancelReason::Cancelled, m_session.InitiatedBySelf); + ClearSession(); + } +} + +void TradeService::OnTradeInvite(const NotifyTradeInvite& acMessage) noexcept +{ + m_pendingInvites[acMessage.InviterPlayerId] = acMessage.ExpiryTick; + EmitInviteUpdate(acMessage.InviterPlayerId, true, acMessage.ExpiryTick); +} + +void TradeService::OnTradeStarted(const NotifyTradeStarted& acMessage) noexcept +{ + m_session.Active = true; + m_session.PartnerId = acMessage.PartnerPlayerId; + m_session.InitiatedBySelf = acMessage.InitiatedBySelf; + m_session.SelfReady = false; + m_session.PartnerReady = false; + m_session.SelfItems.clear(); + m_session.PartnerItems.clear(); + m_session.SelfInventory.clear(); + m_session.CountdownMs = 0; + m_session.CountdownTotalMs = 0; + + m_pendingInvites.erase(acMessage.PartnerPlayerId); + EmitInviteUpdate(acMessage.PartnerPlayerId, false); + + EmitStateToUI(); +} + +void TradeService::OnTradeState(const NotifyTradeState& acMessage) noexcept +{ + if (!m_session.Active || m_session.PartnerId != acMessage.PartnerPlayerId) + { + m_session.Active = true; + m_session.PartnerId = acMessage.PartnerPlayerId; + } + + m_session.SelfReady = acMessage.SelfReady; + m_session.PartnerReady = acMessage.PartnerReady; + m_session.SelfItems = acMessage.SelfItems; + m_session.PartnerItems = acMessage.PartnerItems; + m_session.SelfInventory = acMessage.SelfInventory; + m_session.CountdownMs = acMessage.CountdownMs; + m_session.CountdownTotalMs = acMessage.CountdownTotalMs; + + EmitStateToUI(); +} + +void TradeService::OnTradeCancel(const NotifyTradeCancel& acMessage) noexcept +{ + EmitCancellation(acMessage.PartnerPlayerId, acMessage.Reason, acMessage.WasInitiator); + + if (HasActiveSessionWith(acMessage.PartnerPlayerId)) + ClearSession(); +} + +void TradeService::OnTradeComplete(const NotifyTradeComplete& acMessage) noexcept +{ + auto* pOverlay = m_world.GetOverlayService().GetOverlayApp(); + if (pOverlay) + { + auto pArgs = CefListValue::Create(); + pArgs->SetInt(0, acMessage.PartnerPlayerId); + pOverlay->ExecuteAsync("tradeCompleted", pArgs); + } + + if (HasActiveSessionWith(acMessage.PartnerPlayerId)) + ClearSession(); +} + +void TradeService::ClearSession() noexcept +{ + m_session = TradeSession{}; + EmitStateToUI(); +} + +void TradeService::EmitStateToUI() const noexcept +{ + auto* pOverlay = m_world.GetOverlayService().GetOverlayApp(); + if (!pOverlay) + return; + + auto pArgs = CefListValue::Create(); + pArgs->SetBool(0, m_session.Active); + pArgs->SetInt(1, m_session.PartnerId); + pArgs->SetBool(2, m_session.InitiatedBySelf); + pArgs->SetBool(3, m_session.SelfReady); + pArgs->SetBool(4, m_session.PartnerReady); + + auto& modSystem = m_world.GetModSystem(); + + auto makeDict = [&](const Inventory::Entry& entry) { + auto dict = CefDictionaryValue::Create(); + dict->SetInt("modId", entry.BaseId.ModId); + dict->SetInt("baseId", entry.BaseId.BaseId); + dict->SetBool("isQuestItem", entry.IsQuestItem); + dict->SetString("name", MakeDisplayName(modSystem, entry)); + dict->SetBool("isGold", entry.BaseId.ModId == 0 && entry.BaseId.BaseId == 0x0000000F); + + dict->SetDouble("ExtraCharge", entry.ExtraCharge); + + auto enchantId = CefDictionaryValue::Create(); + enchantId->SetInt("ModId", entry.ExtraEnchantId.ModId); + enchantId->SetInt("BaseId", entry.ExtraEnchantId.BaseId); + dict->SetDictionary("ExtraEnchantId", enchantId); + dict->SetInt("ExtraEnchantCharge", entry.ExtraEnchantCharge); + dict->SetBool("ExtraEnchantRemoveUnequip", entry.ExtraEnchantRemoveUnequip); + + auto enchantData = CefDictionaryValue::Create(); + enchantData->SetBool("IsWeapon", entry.EnchantData.IsWeapon); + auto effectsList = CefListValue::Create(); + for (size_t effectIndex = 0; effectIndex < entry.EnchantData.Effects.size(); ++effectIndex) + { + const auto& effect = entry.EnchantData.Effects[effectIndex]; + auto effectDict = CefDictionaryValue::Create(); + effectDict->SetDouble("Magnitude", effect.Magnitude); + effectDict->SetInt("Area", effect.Area); + effectDict->SetInt("Duration", effect.Duration); + effectDict->SetDouble("RawCost", effect.RawCost); + + auto effectId = CefDictionaryValue::Create(); + effectId->SetInt("ModId", effect.EffectId.ModId); + effectId->SetInt("BaseId", effect.EffectId.BaseId); + effectDict->SetDictionary("EffectId", effectId); + + effectsList->SetDictionary(static_cast(effectIndex), effectDict); + } + enchantData->SetList("Effects", effectsList); + dict->SetDictionary("EnchantData", enchantData); + + dict->SetDouble("ExtraHealth", entry.ExtraHealth); + + auto poisonId = CefDictionaryValue::Create(); + poisonId->SetInt("ModId", entry.ExtraPoisonId.ModId); + poisonId->SetInt("BaseId", entry.ExtraPoisonId.BaseId); + dict->SetDictionary("ExtraPoisonId", poisonId); + dict->SetInt("ExtraPoisonCount", entry.ExtraPoisonCount); + + dict->SetInt("ExtraSoulLevel", entry.ExtraSoulLevel); + dict->SetBool("ExtraWorn", entry.ExtraWorn); + dict->SetBool("ExtraWornLeft", entry.ExtraWornLeft); + return dict; + }; + + std::vector usedCounts(m_session.SelfInventory.size(), 0); + + auto selfList = CefListValue::Create(); + for (size_t i = 0; i < m_session.SelfItems.size(); ++i) + { + const auto& item = m_session.SelfItems[i]; + auto dict = makeDict(item); + dict->SetInt("count", std::abs(item.Count)); + + std::optional match; + const auto needed = std::abs(item.Count); + for (uint32_t idx = 0; idx < m_session.SelfInventory.size(); ++idx) + { + const auto& inventoryEntry = m_session.SelfInventory[idx]; + if (!inventoryEntry.CanBeMerged(item)) + continue; + + const int32_t available = inventoryEntry.Count; + const int32_t alreadyUsed = usedCounts[idx]; + if (alreadyUsed + needed > available) + continue; + + match = idx; + usedCounts[idx] += needed; + break; + } + + if (match) + dict->SetInt("inventoryIndex", static_cast(*match)); + + selfList->SetDictionary(static_cast(i), dict); + } + pArgs->SetList(5, selfList); + + auto partnerList = CefListValue::Create(); + for (size_t i = 0; i < m_session.PartnerItems.size(); ++i) + { + const auto& item = m_session.PartnerItems[i]; + auto dict = makeDict(item); + dict->SetInt("count", std::abs(item.Count)); + partnerList->SetDictionary(static_cast(i), dict); + } + pArgs->SetList(6, partnerList); + + auto inventoryList = CefListValue::Create(); + for (size_t i = 0; i < m_session.SelfInventory.size(); ++i) + { + const auto& entry = m_session.SelfInventory[i]; + auto dict = makeDict(entry); + dict->SetInt("count", entry.Count); + dict->SetInt("inventoryIndex", static_cast(i)); + dict->SetInt("offeredCount", i < usedCounts.size() ? usedCounts[i] : 0); + inventoryList->SetDictionary(static_cast(i), dict); + } + pArgs->SetList(7, inventoryList); + pArgs->SetInt(8, static_cast(m_session.CountdownMs)); + pArgs->SetInt(9, static_cast(m_session.CountdownTotalMs)); + + pOverlay->ExecuteAsync("tradeStateUpdated", pArgs); +} + +void TradeService::EmitInviteUpdate(uint32_t aInviterId, bool aAdded, uint64_t aExpiryTick) const noexcept +{ + auto* pOverlay = m_world.GetOverlayService().GetOverlayApp(); + if (!pOverlay) + return; + + auto pArgs = CefListValue::Create(); + pArgs->SetInt(0, aInviterId); + if (aAdded) + { + pArgs->SetDouble(1, static_cast(aExpiryTick)); + pOverlay->ExecuteAsync("tradeInviteReceived", pArgs); + } + else + { + pOverlay->ExecuteAsync("tradeInviteExpired", pArgs); + } +} + +void TradeService::EmitCancellation(uint32_t aPartnerId, TradeCancelReason aReason, bool aWasInitiator) const noexcept +{ + auto* pOverlay = m_world.GetOverlayService().GetOverlayApp(); + if (!pOverlay) + return; + + auto pArgs = CefListValue::Create(); + pArgs->SetInt(0, aPartnerId); + pArgs->SetInt(1, static_cast(aReason)); + pArgs->SetBool(2, aWasInitiator); + + pOverlay->ExecuteAsync("tradeCancelled", pArgs); +} + +bool TradeService::HasActiveSessionWith(uint32_t aPartnerId) const noexcept +{ + return m_session.Active && m_session.PartnerId == aPartnerId; +} diff --git a/Code/client/Services/Generic/TransportService.cpp b/Code/client/Services/Generic/TransportService.cpp index 0f3f684dc..ae50d1fd1 100644 --- a/Code/client/Services/Generic/TransportService.cpp +++ b/Code/client/Services/Generic/TransportService.cpp @@ -20,10 +20,14 @@ #include #include #include +#include #include +#include #include #include +#include +#include // #include @@ -78,6 +82,9 @@ bool TransportService::Send(const ClientMessage& acMessage) const noexcept { ScopedAllocator _{s_allocator}; + if (!IsAllowedOutbound(acMessage)) + return true; // Intentionally dropped in ghost mode; treat as success to avoid retries/log spam. + Buffer buffer(1 << 16); Buffer::Writer writer(&buffer); writer.WriteBits(0, 8); // Write first byte as packet needs it @@ -93,6 +100,15 @@ bool TransportService::Send(const ClientMessage& acMessage) const noexcept return false; } +void TransportService::SetLoginCredentials(const std::string& acUsername, const std::string& acPassword) noexcept +{ + m_loginUsername = acUsername; + if (Credential::LooksLikePasswordHash(acPassword)) + m_loginPassword = acPassword; + else + m_loginPassword = Credential::HashPassword(acPassword); +} + void TransportService::OnConsume(const void* apData, uint32_t aSize) { ServerMessageFactory factory; @@ -106,6 +122,9 @@ void TransportService::OnConsume(const void* apData, uint32_t aSize) return; } + if (!IsAllowedInbound(*pMessage)) + return; + m_messageHandlers[pMessage->GetOpcode()](pMessage); } @@ -125,7 +144,11 @@ void TransportService::OnConnected() // TODO: think about user opt out request.DiscordId = m_world.ctx().at().GetUser().id; auto* pNpc = Cast(pPlayer->baseForm); - if (pNpc) + if (!m_loginUsername.empty()) + { + request.Username = m_loginUsername; + } + else if (pNpc) { request.Username = pNpc->fullName.value.AsAscii(); } @@ -133,6 +156,8 @@ void TransportService::OnConnected() { request.Username = "Some dragon boi"; } + request.Password = m_loginPassword; + m_loginPassword.clear(); auto* const cpModManager = ModManager::Get(); @@ -247,9 +272,19 @@ void TransportService::HandleAuthenticationResponse(const AuthenticationResponse ErrorInfo += "]}"; break; } - case AR::kWrongPassword: + case AR::kWrongAccountPassword: { - ErrorInfo += "\"error\": \"wrong_password\""; + ErrorInfo += "\"error\": \"wrong_account_password\""; + break; + } + case AR::kWrongServerPassword: + { + ErrorInfo += "\"error\": \"wrong_server_password\""; + break; + } + case AR::kDuplicateUser: + { + ErrorInfo += "\"error\": \"duplicate_user\""; break; } case AR::kServerFull: @@ -277,3 +312,99 @@ void TransportService::HandleNotifySettingsChange(const NotifySettingsChange& ac m_world.SetServerSettings(acMessage.Settings); m_dispatcher.trigger(acMessage.Settings); } + +bool TransportService::IsAllowedOutbound(const ClientMessage& acMessage) const noexcept +{ + // Only filter once we are connected and ghosting + if (!m_connected || m_world.GetSyncModeService().GetLocalMode() != SyncMode::Ghost) + return true; + + const auto opcode = static_cast(acMessage.GetOpcode()); + switch (opcode) + { + // Minimal presence + movement for the local player while ghosting + case kAssignCharacterRequest: + case kClientReferencesMoveRequest: + case kShiftGridCellRequest: + case kEnterExteriorCellRequest: + case kEnterInteriorCellRequest: + return true; + // Cosmetic-only: keep remote ghost visuals correct while quest-gated + case kRequestEquipmentChanges: + case kPlayEmoteRequest: + case kCancelEmoteRequest: + return true; + // Keep sync mode negotiation working + case kRequestSetSyncMode: + return true; + // Party management + chat while quest-gated + case kPartyInviteRequest: + case kPartyAcceptInviteRequest: + case kPartyLeaveRequest: + case kPartyCreateRequest: + case kPartyChangeLeaderRequest: + case kPartyKickRequest: + case kPartyActorNamesRequest: + case kPlayerActorNameUpdateRequest: + case kSendChatMessageRequest: + // Trade while quest-gated (no world sync) + case kTradeInviteRequest: + case kTradeInviteResponseRequest: + case kTradeOfferUpdateRequest: + case kTradeSetReadyRequest: + case kTradeCancelRequest: + return true; + default: break; + } + + return false; +} + +bool TransportService::IsAllowedInbound(const ServerMessage& acMessage) const noexcept +{ + if (!m_connected || m_world.GetSyncModeService().GetLocalMode() != SyncMode::Ghost) + return true; + + const auto opcode = static_cast(acMessage.GetOpcode()); + switch (opcode) + { + // Handshake / presence only + case kAuthenticationResponse: + case kAssignCharacterResponse: + case kServerTimeSettings: + case kNotifyPlayerList: + case kNotifyPlayerJoined: + case kNotifyPlayerLeft: + case kNotifyPlayerActorName: + case kNotifyPlayerSyncMode: + case kStringCacheUpdate: + case kNotifyCommandList: + case kNotifyPartyInfo: + case kNotifyPartyInvite: + case kNotifyPartyJoined: + case kNotifyPartyLeft: + case kNotifyChatMessageBroadcast: + case kNotifyTradeInvite: + case kNotifyTradeStarted: + case kNotifyTradeState: + case kNotifyTradeCancel: + case kNotifyTradeComplete: + return true; + + // Bare minimum to render remote players + case kCharacterSpawnRequest: + case kNotifySpawnData: + case kNotifyRemoveCharacter: + case kServerReferencesMoveRequest: + // Cosmetic-only: keep remote ghost visuals correct while quest-gated + case kNotifyEquipmentChanges: + case kNotifyDrawWeapon: + case kNotifyPlayEmote: + case kNotifyCancelEmote: + return true; + + default: break; + } + + return false; +} diff --git a/Code/client/Services/ImguiService.h b/Code/client/Services/ImguiService.h index 036eab2c6..bcb4b6683 100644 --- a/Code/client/Services/ImguiService.h +++ b/Code/client/Services/ImguiService.h @@ -4,6 +4,7 @@ struct RenderSystemD3D9; struct RenderSystemD3D11; +struct ImFont; // forward declare from Dear ImGui /** * @brief Draws the ImGui UI. @@ -25,9 +26,14 @@ struct ImguiService LRESULT WndProcHandler(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); void RawInputHandler(RAWINPUT& aRawinput); + // Returns loaded "Skyrim"-like UI font (Futura Condensed) or nullptr if unavailable + ImFont* GetSkyrimFont() const noexcept { return m_skyrimFont; } + entt::sink> OnDraw; private: ImGuiImpl::ImGuiDriver m_imDriver; entt::sigh m_drawSignal; + + ImFont* m_skyrimFont{nullptr}; }; diff --git a/Code/client/Services/InventoryService.h b/Code/client/Services/InventoryService.h index 87235ce85..f2d1498ea 100644 --- a/Code/client/Services/InventoryService.h +++ b/Code/client/Services/InventoryService.h @@ -1,14 +1,16 @@ #pragma once +#include + struct World; struct TransportService; +class Actor; -struct UpdateEvent; -struct NotifyObjectInventoryChanges; -struct NotifyInventoryChanges; -struct InventoryChangeEvent; -struct EquipmentChangeEvent; -struct NotifyEquipmentChanges; +#include +#include +#include +#include +#include /** * @brief Manages inventories of actors and containers. @@ -51,6 +53,22 @@ struct InventoryService void OnNotifyEquipmentChanges(const NotifyEquipmentChanges& acMessage) noexcept; private: + void ApplyEquipmentChange(Actor* pActor, const NotifyEquipmentChanges& acMessage) noexcept; + void ProcessPendingEquipment() noexcept; + void ProcessPendingEquipmentChanges() noexcept; + void ProcessPendingEquipmentRequests() noexcept; + void ProcessPendingInventoryChanges() noexcept; + bool SendEquipmentChange(const EquipmentChangeEvent& acEvent) noexcept; + enum class ActorReadinessStatus : uint8_t + { + Ready, + MissingActor, + Missing3D, + MissingContainerData + }; + ActorReadinessStatus EvaluateActorReadiness(Actor* pActor) const noexcept; + static const char* DescribeReadiness(ActorReadinessStatus aStatus) noexcept; + /** * Checks whether local actors their weapon draw states have changed, * and if so, send the new states to the server. @@ -62,6 +80,9 @@ struct InventoryService */ void RunNakedNPCBugChecks() noexcept; + bool TryApplyInventoryChange(const NotifyInventoryChanges& acMessage) noexcept; + bool TryApplyEquipmentChange(const NotifyEquipmentChanges& acMessage) noexcept; + World& m_world; entt::dispatcher& m_dispatcher; TransportService& m_transport; @@ -71,4 +92,8 @@ struct InventoryService entt::scoped_connection m_equipmentConnection; entt::scoped_connection m_inventoryChangeConnection; entt::scoped_connection m_equipmentChangeConnection; + + TiltedPhoques::Vector m_pendingEquipmentRequests; + TiltedPhoques::Vector m_pendingEquipmentChanges; + TiltedPhoques::Vector m_pendingInventoryChanges; }; diff --git a/Code/client/Services/MagicService.h b/Code/client/Services/MagicService.h index 1a4416c38..d665d6691 100644 --- a/Code/client/Services/MagicService.h +++ b/Code/client/Services/MagicService.h @@ -8,9 +8,16 @@ #include #include #include +#include +#include +#include +#include +#include +struct Actor; struct World; struct TransportService; +struct SpellItem; struct UpdateEvent; struct SpellCastEvent; @@ -20,6 +27,8 @@ struct RemoveSpellEvent; struct NotifySpellCast; struct NotifyInterruptCast; +struct NotifyHealingProximity; +struct NotifyPartyMemberDowned; /** * @brief Handles magic spell casting and magic effects. @@ -52,7 +61,7 @@ struct MagicService /** * @brief Sends local spell cast to the server. */ - void OnSpellCastEvent(const SpellCastEvent& acSpellCastEvent) const noexcept; + void OnSpellCastEvent(const SpellCastEvent& acSpellCastEvent) noexcept; /** * @brief Casts a spell based on a server message. */ @@ -60,7 +69,7 @@ struct MagicService /** * @brief Sends local interruption of spell cast to the server. */ - void OnInterruptCastEvent(const InterruptCastEvent& acEvent) const noexcept; + void OnInterruptCastEvent(const InterruptCastEvent& acEvent) noexcept; /** * @brief Interrupts a spell cast based on a server message. */ @@ -81,6 +90,14 @@ struct MagicService * @brief Handles removal of a spell */ void OnNotifyRemoveSpell(const NotifyRemoveSpell& acMessage) noexcept; + /** + * @brief Handles healing proximity for reviving downed players + */ + void OnNotifyHealingProximity(const NotifyHealingProximity& acMessage) noexcept; + /** + * @brief Receives party downed/revived events and triggers UI/chat and per-player reveal effect. + */ + void OnNotifyPartyMemberDowned(const NotifyPartyMemberDowned& acMessage) noexcept; private: /** @@ -95,11 +112,52 @@ struct MagicService * Apply the "reveal players" effect on remote players. */ void UpdateRevealOtherPlayersEffect(bool aForceTrigger = false) noexcept; + /** + * @brief Apply the reveal effect to currently downed party members only. + */ + void UpdateRevealDownedPlayersEffect() noexcept; World& m_world; entt::dispatcher& m_dispatcher; TransportService& m_transport; bool m_revealingOtherPlayers = false; + bool m_revealingDownedPlayers = false; + + struct DownedMemberInfo + { + uint32_t PlayerId = 0; + float PositionX = 0.f; + float PositionY = 0.f; + float PositionZ = 0.f; + }; + + struct ReviveChannelState + { + uint32_t CasterServerId = 0; + float RequiredSeconds = 0.f; + float AccumulatedSeconds = 0.f; + std::chrono::steady_clock::time_point LastPingAt{}; + std::string HealerName; + }; + + struct HealerChannelState + { + bool Active = false; + float RequiredSeconds = 0.f; + float AccumulatedSeconds = 0.f; + std::chrono::steady_clock::time_point LastUpdate{}; + float StartingMagickaValue = 0.f; + bool HasStartingMagicka = false; + bool CostBuffApplied = false; + }; + + std::unordered_map m_downedPartyMembers; + std::optional m_victimReviveState; + HealerChannelState m_healerChannelState; + std::array m_localHealingHandsSources{}; + bool m_isLocalHealingHandsActive = false; + uint32_t m_activeHealingHandsSpellId = 0; + double m_healingHandsPingAccumulator = 0.0; entt::scoped_connection m_updateConnection; entt::scoped_connection m_spellCastEventConnection; @@ -110,6 +168,8 @@ struct MagicService entt::scoped_connection m_notifyAddTargetConnection; entt::scoped_connection m_removeSpellEventConnection; entt::scoped_connection m_notifyRemoveSpell; + entt::scoped_connection m_notifyHealingProximityConnection; + entt::scoped_connection m_notifyPartyMemberDownedConnection; /* * @brief Queued magic effects. @@ -166,6 +226,25 @@ struct MagicService private: std::queue m_queuedEffects; std::queue m_queuedRemoteEffects; + + void UpdateReviveChannels(double aDeltaSeconds) noexcept; + void UpdateHealerChannel(double aDeltaSeconds) noexcept; + void UpdateHealingHandsBroadcast(double aDeltaSeconds) noexcept; + [[nodiscard]] bool IsHealingHandsSpell(uint32_t aSpellFormId, const SpellItem* apSpell = nullptr) const noexcept; + [[nodiscard]] float GetRequiredReviveDuration(float aRestorationLevel) const noexcept; + void UpdateVictimReviveUi(const ReviveChannelState& aState) const noexcept; + void StopVictimReviveUi() noexcept; + void UpdateHealerUi() const noexcept; + void StopHealerUi() noexcept; + void ApplyHealerCostModifiers(Actor* apCaster) noexcept; + void ClearHealerCostModifiers(Actor* apCaster) noexcept; + [[nodiscard]] bool HasDownedPartyMemberInRange(float aRange) noexcept; + [[nodiscard]] Actor* FindActorByServerId(uint32_t aServerId) const noexcept; + [[nodiscard]] std::string ResolvePlayerName(uint32_t aServerId) const; + [[nodiscard]] std::optional GetLocalServerId() const noexcept; + bool SendHealingProximityPing(uint32_t aSpellFormId) noexcept; + void ResetLocalHealingHandsState() noexcept; + void HandleHealingHandsInterrupt(MagicSystem::CastingSource aSource) noexcept; }; // Exposed so we can increase log level of just this tricky code, in debugger or a build. diff --git a/Code/client/Services/MapService.h b/Code/client/Services/MapService.h index 734c55ae9..da87d529c 100644 --- a/Code/client/Services/MapService.h +++ b/Code/client/Services/MapService.h @@ -1,17 +1,25 @@ #pragma once +#include +#include +#include + struct World; struct TransportService; +struct UpdateEvent; struct SetWaypointEvent; struct RemoveWaypointEvent; struct NotifySetWaypoint; struct NotifyRemoveWaypoint; +struct NotifyPartyInfo; +struct NotifyPartyFastTravelMarkers; +struct PartyJoinedEvent; /** - * @brief Handles logic related to the local player. + * @brief Handles map-related synchronization (waypoints, party fast travel markers). */ -struct MapService +struct MapService : BSTEventSink { MapService(World& aWorld, entt::dispatcher& aDispatcher, TransportService& aTransport) noexcept; ~MapService() noexcept = default; @@ -19,18 +27,39 @@ struct MapService TP_NOCOPYMOVE(MapService); protected: + void OnUpdate(const UpdateEvent& acEvent) noexcept; void OnSetWaypoint(const SetWaypointEvent& acMessage) noexcept; void OnRemoveWaypoint(const RemoveWaypointEvent& acMessage) noexcept; void OnNotifySetWaypoint(const NotifySetWaypoint& acMessage) noexcept; void OnNotifyRemoveWaypoint(const NotifyRemoveWaypoint& acMessage) noexcept; + void OnNotifyPartyInfo(const NotifyPartyInfo& acMessage) noexcept; + void OnNotifyPartyFastTravelMarkers(const NotifyPartyFastTravelMarkers& acMessage) noexcept; + void OnPartyJoined(const PartyJoinedEvent& acMessage) noexcept; + BSTEventResult OnEvent(const TESLoadGameEvent*, const EventDispatcher*) override; private: World& m_world; entt::dispatcher& m_dispatcher; TransportService& m_transport; + void SendFastTravelMarkers(const TiltedPhoques::Vector& aMarkers, bool aAllowEmpty, bool aFullSync) noexcept; + TiltedPhoques::Vector CollectLocalFastTravelMarkers() const noexcept; + void ProcessPendingFastTravelMarkers() noexcept; + void SyncFastTravelMarkers(bool aForceSendEvenIfEmpty) noexcept; + + uint64_t m_nextMarkerScanTick{0}; + TiltedPhoques::Set m_knownFastTravelMarkers{}; + TiltedPhoques::Vector m_pendingFastTravelMarkers{}; + + TiltedPhoques::Set m_lastPartyMembers{}; + uint32_t m_lastLeaderPlayerId{0}; + + entt::scoped_connection m_updateConnection; entt::scoped_connection m_playerSetWaypointConnection; entt::scoped_connection m_playerRemoveWaypointConnection; entt::scoped_connection m_playerNotifySetWaypointConnection; entt::scoped_connection m_playerNotifyRemoveWaypointConnection; + entt::scoped_connection m_partyInfoConnection; + entt::scoped_connection m_partyFastTravelMarkersConnection; + entt::scoped_connection m_partyJoinedConnection; }; diff --git a/Code/client/Services/NameTagService.h b/Code/client/Services/NameTagService.h new file mode 100644 index 000000000..b0066eaa4 --- /dev/null +++ b/Code/client/Services/NameTagService.h @@ -0,0 +1,133 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include + +struct World; +struct PlayerCharacter; +struct Actor; +struct NiPoint3; +struct RenderSystemD3D11; + +/** + * @brief Draws 3D-aware name tags above remote players. + * + * The service is intentionally data-driven via Style so that future UI/UX + * customisation can plug into it without having to touch the rendering logic. + */ +struct NameTagService +{ + enum class Mode : uint8_t + { + Detailed = 0, + Basic = 1, + Hidden = 2, + Normal = 3 + }; + + enum class NamePreference : uint8_t + { + Username = 0, + Actor = 1 + }; + + struct Style + { + ImVec4 BackgroundColor{0.f, 0.f, 0.f, 0.65f}; + ImVec4 BorderColor{1.f, 1.f, 1.f, 0.35f}; + ImVec4 TextColor{0.95f, 0.95f, 0.95f, 1.f}; + ImVec4 LevelTextColor{0.85f, 0.92f, 1.f, 0.95f}; + ImVec4 AccentColor{0.42f, 0.65f, 1.f, 0.9f}; + ImVec4 AvatarRingColor{1.f, 1.f, 1.f, 0.45f}; + ImVec4 PlaceholderAvatarColor{0.35f, 0.45f, 0.65f, 0.7f}; + float VerticalOffset = 24.f; + float ForwardOffset = 10.f; + float MinScale = 0.55f; + float MaxScale = 1.1f; + float ScaleReferenceDistance = 450.f; + float ScaleFalloff = 1.2f; + float DepthPerspectiveBoost = 0.25f; + float DepthNear = 0.15f; + float DepthFar = 0.9f; + float MaxDistance = 3500.f; + float FadeDistance = 2500.f; + float PaddingX = 14.f; + float PaddingY = 6.f; + float CornerRounding = 6.f; + float LevelFontScale = 0.75f; + float NameLevelSpacing = 4.f; + float AvatarSize = 38.f; + float CompactAvatarScale = 0.84f; + float AvatarSpacing = 10.f; + float AccentThickness = 2.5f; + float VisibilityEpsilon = 1e-3f; + float NearBypassDistance = 600.f; + float AvatarRetryDelayMs = 3500.f; + uint32_t AvatarMaxBytes = 256u * 1024u; + }; + + NameTagService(World& aWorld, [[maybe_unused]] entt::dispatcher& aDispatcher) noexcept; + ~NameTagService() noexcept = default; + + TP_NOCOPYMOVE(NameTagService); + + [[nodiscard]] Style& GetStyle() noexcept { return m_style; } + void SetStyle(const Style& aStyle) noexcept { m_style = aStyle; } + [[nodiscard]] Mode GetMode() const noexcept { return m_mode; } + void SetMode(Mode aMode) noexcept; + void SetNamePreference(NamePreference aPreference) noexcept { m_namePreference = aPreference; } + +private: + struct VisibilityInfo + { + bool Visible = true; + uint64_t Tick = 0; + }; + + struct AvatarTexture + { + Microsoft::WRL::ComPtr Texture; + ImVec2 Size{0.f, 0.f}; + std::string Signature; + uint64_t LastAttemptTick = 0; + uint64_t LastSeenTick = 0; + bool Failed = false; + }; + + void OnDraw() noexcept; + void OnUpdate(const UpdateEvent& acEvent) noexcept; + + [[nodiscard]] bool ShouldRenderTag(uint32_t aPlayerId) const noexcept; + [[nodiscard]] bool ComputeLineOfSight(PlayerCharacter* apLocalPlayer, Actor* apRemoteActor) const noexcept; + [[nodiscard]] bool ProjectWorldPoint(const NiPoint3& aWorldPoint, ImVec2& aScreenPos, float& aDepth, const ImVec2& aViewport) const noexcept; + [[nodiscard]] NiPoint3 BuildAnchorPoint(Actor* apActor) const noexcept; + [[nodiscard]] static ImU32 ColorWithAlpha(const ImVec4& aColor, float aAlpha) noexcept; + void GarbageCollectAvatarCache(uint64_t aNowTick) noexcept; + [[nodiscard]] const AvatarTexture* ResolveAvatarTexture(uint32_t aPlayerId, std::string_view aAvatarData, uint64_t aNowTick); + [[nodiscard]] bool DecodeAvatarData(std::string_view aAvatarData, std::vector& aDecodedData) const; + [[nodiscard]] bool CreateAvatarTexture(const std::vector& aDecodedData, AvatarTexture& aEntry) noexcept; + [[nodiscard]] ID3D11Device* AcquireD3DDevice() noexcept; + + World& m_world; + Style m_style{}; + Mode m_mode = Mode::Normal; + NamePreference m_namePreference = NamePreference::Username; + RenderSystemD3D11* m_renderSystem = nullptr; + + std::unordered_map m_visibility; + std::unordered_map m_avatarCache; + Microsoft::WRL::ComPtr m_cachedDevice; + + entt::scoped_connection m_drawConnection; + entt::scoped_connection m_updateConnection; +}; diff --git a/Code/client/Services/OverlayClient.h b/Code/client/Services/OverlayClient.h index b495b0e5f..9b386a8b5 100644 --- a/Code/client/Services/OverlayClient.h +++ b/Code/client/Services/OverlayClient.h @@ -3,6 +3,7 @@ #include "OverlayClient.hpp" struct TransportService; +struct EquipmentSnapshot; namespace TiltedPhoques { @@ -27,9 +28,24 @@ struct OverlayClient : TiltedPhoques::OverlayClient void ProcessRevealPlayersMessage(); void ProcessChatMessage(CefRefPtr aEventArgs); void ProcessSetTimeCommand(CefRefPtr aEventArgs); - void ProcessTeleportMessage(CefRefPtr aEventArgs); + void ProcessTeleportRequestMessage(CefRefPtr aEventArgs); + void ProcessTeleportResponseMessage(CefRefPtr aEventArgs); + void ProcessSetProfilePicture(CefRefPtr aEventArgs); + void ProcessSetNameTagMode(CefRefPtr aEventArgs); + void ProcessSetPlayerNamePreference(CefRefPtr aEventArgs); + void ProcessSetPartyOptions(CefRefPtr aEventArgs); void ProcessToggleDebugUI(); + void ProcessPlayEmote(CefRefPtr aEventArgs); void SetUIVisible(bool aVisible) noexcept; TransportService& m_transport; }; + +extern std::atomic g_emoteWheelActive; +extern std::string g_emoteEventName; +extern std::chrono::steady_clock::time_point g_emoteLastPlayed; +extern NiPoint3 g_emoteStartPos; +extern NiPoint3 g_emoteStartRot; +extern std::atomic g_emoteStartValid; +extern std::atomic g_emoteEquipmentValid; +extern EquipmentSnapshot g_emoteEquipmentSnapshot; diff --git a/Code/client/Services/OverlayService.h b/Code/client/Services/OverlayService.h index 3eedcae12..208437eb9 100644 --- a/Code/client/Services/OverlayService.h +++ b/Code/client/Services/OverlayService.h @@ -1,6 +1,9 @@ #pragma once #include +#include +#include +#include namespace TiltedPhoques { @@ -20,15 +23,23 @@ struct NotifyChatMessageBroadcast; struct NotifyPlayerList; struct NotifyPlayerJoined; struct NotifyPlayerLeft; +struct NotifyPlayerProfileImage; struct NotifyPlayerDialogue; struct ConnectionErrorEvent; struct NotifyPlayerLevel; +struct NotifyPlayerActorName; struct NotifyPlayerCellChanged; +struct NotifyTeleportRequest; +struct NotifyTeleportCountdown; struct NotifyTeleport; struct NotifyPlayerHealthUpdate; +struct NotifyCommandList; +struct NotifyPlayEmote; +struct NotifyCancelEmote; enum ChatMessageTypes; struct PartyJoinedEvent; struct PartyLeftEvent; +struct Actor; using TiltedPhoques::OverlayApp; @@ -64,27 +75,45 @@ struct OverlayService void SetPlayerHealthPercentage(uint32_t aFormId) const noexcept; + // Send world map party pins to the CEF UI as a JSON string: [{"x":float,"y":float,"id":number}, ...] + void SetPartyPinsJson(const std::string& aJson) noexcept; + protected: void OnUpdate(const UpdateEvent&) noexcept; void OnConnectedEvent(const ConnectedEvent&) noexcept; void OnDisconnectedEvent(const DisconnectedEvent&) noexcept; - void OnWaitingFor3DRemoved(entt::registry& aRegistry, entt::entity aEntity) const noexcept; + void OnWaitingFor3DRemoved(entt::registry& aRegistry, entt::entity aEntity) noexcept; void OnPlayerComponentRemoved(entt::registry& aRegistry, entt::entity aEntity) const noexcept; void OnConnectionError(const ConnectionErrorEvent& acConnectedEvent) const noexcept; void OnChatMessageReceived(const NotifyChatMessageBroadcast&) noexcept; void OnPlayerDialogue(const NotifyPlayerDialogue&) noexcept; void OnPlayerJoined(const NotifyPlayerJoined&) noexcept; void OnPlayerLeft(const NotifyPlayerLeft&) noexcept; + void OnPlayerProfileImage(const NotifyPlayerProfileImage&) noexcept; void OnPlayerLevel(const NotifyPlayerLevel&) noexcept; + void OnNotifyPlayerActorName(const NotifyPlayerActorName& acMessage) noexcept; void OnPlayerCellChanged(const NotifyPlayerCellChanged& acMessage) const noexcept; + void OnNotifyTeleportRequest(const NotifyTeleportRequest& acMessage) noexcept; + void OnNotifyTeleportCountdown(const NotifyTeleportCountdown& acMessage) noexcept; void OnNotifyTeleport(const NotifyTeleport& acMessage) noexcept; void OnNotifyPlayerHealthUpdate(const NotifyPlayerHealthUpdate& acMessage) noexcept; + void OnNotifyCommandList(const NotifyCommandList& acMessage) noexcept; + void OnNotifyPlayEmote(const NotifyPlayEmote& acMessage) noexcept; + void OnNotifyCancelEmote(const NotifyCancelEmote& acMessage) noexcept; void OnPartyJoinedEvent(const PartyJoinedEvent& acEvent) noexcept; void OnPartyLeftEvent(const PartyLeftEvent& acEvent) noexcept; + void SendLocalActorName(Actor* apActor) noexcept; private: + struct RemoteEmoteState + { + std::string EventName; + std::chrono::steady_clock::time_point LastPlayed{}; + }; + void RunDebugDataUpdates() noexcept; void RunPlayerHealthUpdates() noexcept; + void UpdateRemoteEmoteLoops() noexcept; CefRefPtr m_pOverlay{nullptr}; TiltedPhoques::UniquePtr m_pProvider; @@ -104,12 +133,22 @@ struct OverlayService entt::scoped_connection m_playerDialogueConnection; entt::scoped_connection m_playerJoinedConnection; entt::scoped_connection m_playerLeftConnection; + entt::scoped_connection m_playerAvatarConnection; entt::scoped_connection m_playerAddedConnection; entt::scoped_connection m_playerRemovedConnection; entt::scoped_connection m_playerLevelConnection; + entt::scoped_connection m_playerActorNameConnection; entt::scoped_connection m_cellChangedConnection; entt::scoped_connection m_teleportConnection; + entt::scoped_connection m_teleportRequestConnection; + entt::scoped_connection m_teleportCountdownConnection; entt::scoped_connection m_playerHealthConnection; + entt::scoped_connection m_commandListConnection; + entt::scoped_connection m_playEmoteConnection; + entt::scoped_connection m_cancelEmoteConnection; entt::scoped_connection m_partyJoinedConnection; entt::scoped_connection m_partyLeftConnection; + + std::unordered_map m_remoteEmotes; + std::string m_localActorName; }; diff --git a/Code/client/Services/PartyMapOverlayService.h b/Code/client/Services/PartyMapOverlayService.h new file mode 100644 index 000000000..d4fcffa23 --- /dev/null +++ b/Code/client/Services/PartyMapOverlayService.h @@ -0,0 +1,88 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +struct World; + +// CEF overlay to show party pins on the world map and set waypoints. +// - Shows party members and lets you set the in‑game waypoint to a member's +// last known position/worldspace (no quests / fake actors). +// - Optional: can be extended to auto-follow a member's position. +struct PartyMapOverlayService +{ + PartyMapOverlayService(World& aWorld, entt::dispatcher& aDispatcher) noexcept; + ~PartyMapOverlayService() = default; + + TP_NOCOPYMOVE(PartyMapOverlayService); + +private: + struct LastInfo + { + glm::vec3 Pos{}; + uint64_t Tick{0}; + glm::vec3 Velocity{}; + std::chrono::steady_clock::time_point SampleTime{}; + }; + + struct WorldspaceInfo + { + uint32_t WorldSpaceFormId{0}; // numeric TES form id (game id) + bool HasWorld{false}; + bool IsInterior{false}; + }; + + struct LastScreen + { + float sx{0.f}; + float sy{0.f}; + uint64_t Tick{0}; + }; + + void OnUpdate(const UpdateEvent&) noexcept; + void OnDisconnected(const DisconnectedEvent&) noexcept; + void OnPartyJoined(const PartyJoinedEvent&) noexcept; + void OnPartyLeft(const PartyLeftEvent&) noexcept; + void OnPlayerCellChanged(const NotifyPlayerCellChanged& aMsg) noexcept; + void OnPartyPositions(const NotifyPartyPositions& aMsg) noexcept; + + void PruneNonPartyEntries() noexcept; + + void SetWaypointFor(uint32_t aPlayerId) noexcept; + void StoreLastInfo(uint32_t aPlayerId, const glm::vec3& aPos, uint64_t aTick) noexcept; + + // Approximate cross-world conversion using per-player anchors + bool ComputeCrossWorldApprox(uint32_t aPlayerId, uint32_t aSrcWsId, const glm::vec3& aSrcPos, + uint32_t aDstWsId, glm::vec3& aOutDstPos) const noexcept; + + World& m_world; + + // caches + std::unordered_map m_last; // playerId -> last known pos (current world) + std::unordered_map m_worlds; // playerId -> worldspace id (current world) + // history of last known positions per worldspace (keyed by worldspace FormID) + std::unordered_map> m_lastPerWorld; + // last projected screen pos cache to hide brief transition gaps + std::unordered_map m_lastScreen; + uint32_t m_lastValidDisplayWorldId{0}; + mutable std::mutex m_cacheMutex; + + // connections + entt::scoped_connection m_updateConnection; + entt::scoped_connection m_disconnectedConnection; + entt::scoped_connection m_partyJoinedConnection; + entt::scoped_connection m_partyLeftConnection; + entt::scoped_connection m_cellChangedConnection; + entt::scoped_connection m_positionsConnection; +}; diff --git a/Code/client/Services/PartyMarkerOverlayService.h b/Code/client/Services/PartyMarkerOverlayService.h new file mode 100644 index 000000000..57ac36a83 --- /dev/null +++ b/Code/client/Services/PartyMarkerOverlayService.h @@ -0,0 +1,56 @@ +#pragma once + +#include + +#include +#include +#include +#include + +struct World; +struct ImguiService; + +/** + * Draws simple directional name markers for party members that are NOT currently + * spawned locally (i.e., in a different cell/world). Pairs with QuestService which + * shows native objective markers for in-range members. + */ +struct PartyMarkerOverlayService +{ + PartyMarkerOverlayService(World& aWorld, entt::dispatcher& aDispatcher) noexcept; + ~PartyMarkerOverlayService() = default; + + TP_NOCOPYMOVE(PartyMarkerOverlayService); + +private: + // Data we cache per player for far-range markers + struct LastInfo + { + NiPoint3 Pos{}; // last known world position + uint64_t Tick{0}; // last update tick + }; + + // Event handlers + void OnUpdate(const UpdateEvent&) noexcept; + void OnDraw() noexcept; + void OnDisconnected(const DisconnectedEvent&) noexcept; + void OnPartyJoined(const PartyJoinedEvent&) noexcept; + void OnPartyLeft(const PartyLeftEvent&) noexcept; + + // Helpers + void PruneNonPartyEntries() noexcept; + bool HasLiveEntityForPlayer(uint32_t aPlayerId) const noexcept; + + World& m_world; + + // last known position for players + std::unordered_map m_last; + + // connections + entt::scoped_connection m_updateConnection; + entt::scoped_connection m_drawImGuiConnection; + entt::scoped_connection m_disconnectedConnection; + entt::scoped_connection m_partyJoinedConnection; + entt::scoped_connection m_partyLeftConnection; +}; + diff --git a/Code/client/Services/PartyService.h b/Code/client/Services/PartyService.h index 2198adec7..9fe6495e3 100644 --- a/Code/client/Services/PartyService.h +++ b/Code/client/Services/PartyService.h @@ -1,15 +1,23 @@ #pragma once +#include +#include +#include +#include + struct World; struct ImguiService; struct TransportService; struct UpdateEvent; struct DisconnectedEvent; -struct NotifyPlayerList; struct NotifyPartyInfo; struct NotifyPartyInvite; struct NotifyPartyJoined; struct NotifyPartyLeft; +struct NotifyPlayerProfileImage; +struct NotifyPlayerActorName; +struct NotifyPartyOptions; +struct NotifyPartyLeaderCellLock; /** * @brief Manages the party of the local player. @@ -26,8 +34,13 @@ struct PartyService [[nodiscard]] uint32_t GetLeaderPlayerId() const noexcept { return m_leaderPlayerId; } const Vector& GetPartyMembers() const noexcept { return m_partyMembers; } - const Map& GetPlayers() const noexcept { return m_players; } + const Map& GetPlayers() const noexcept { return m_players; } + const String* GetActorName(uint32_t aPlayerId) const noexcept; + const PartyOptions& GetPartyOptions() const noexcept { return m_partyOptions; } Map& GetInvitations() noexcept { return m_invitations; } + [[nodiscard]] bool IsCellLockActiveForLocal() const noexcept; + [[nodiscard]] bool AllowCellChangeDuringLock() const noexcept; + void NotifyCellLockBlocked() noexcept; void CreateParty() const noexcept; void LeaveParty() const noexcept; @@ -35,6 +48,7 @@ struct PartyService void AcceptInvite(const uint32_t aInviterId) const noexcept; void KickPartyMember(const uint32_t aPlayerId) const noexcept; void ChangePartyLeader(const uint32_t aPlayerId) const noexcept; + void UpdatePartyOptions(const PartyOptions& aOptions) noexcept; protected: void OnUpdate(const UpdateEvent& acEvent) noexcept; @@ -44,11 +58,20 @@ struct PartyService void OnPartyInvite(const NotifyPartyInvite& acPartyInvite) noexcept; void OnPartyJoined(const NotifyPartyJoined& acPartyJoined) noexcept; void OnPartyLeft(const NotifyPartyLeft& acPartyLeft) noexcept; + void OnPlayerProfileImage(const NotifyPlayerProfileImage& acMessage) noexcept; + void OnPlayerActorName(const NotifyPlayerActorName& acMessage) noexcept; + void OnPartyOptions(const NotifyPartyOptions& acMessage) noexcept; + void OnPartyLeaderCellLock(const NotifyPartyLeaderCellLock& acMessage) noexcept; private: void DestroyParty() noexcept; + void UpdateCellLockCountdown(uint64_t aCurrentTick) noexcept; + void ShowCellLockBanner(uint16_t aSecondsRemaining) const noexcept; + void ClearCellLockBanner() const noexcept; + void TeleportLocalPlayer(const GameId& acWorldSpaceId, const GameId& acCellId, const Vector3_NetQuantize& acPosition) const noexcept; - Map m_players; + Map m_players; + Map m_actorNames; Map m_invitations; uint64_t m_nextUpdate{0}; @@ -56,6 +79,15 @@ struct PartyService bool m_isLeader = false; uint32_t m_leaderPlayerId; Vector m_partyMembers; + PartyOptions m_partyOptions{}; + bool m_cellLockTeleportActive = false; + uint64_t m_cellLockTeleportEndTick{0}; + uint64_t m_cellLockTeleportAllowUntil{0}; + uint16_t m_cellLockLastCountdown{0}; + uint64_t m_cellLockBlockedBannerUntil{0}; + GameId m_cellLockWorldSpaceId{}; + GameId m_cellLockCellId{}; + Vector3_NetQuantize m_cellLockPosition{}; World& m_world; TransportService& m_transport; @@ -67,4 +99,8 @@ struct PartyService entt::scoped_connection m_partyInviteConnection; entt::scoped_connection m_partyJoinedConnection; entt::scoped_connection m_partyLeftConnection; + entt::scoped_connection m_playerAvatarConnection; + entt::scoped_connection m_playerActorNameConnection; + entt::scoped_connection m_partyOptionsConnection; + entt::scoped_connection m_partyLeaderCellLockConnection; }; diff --git a/Code/client/Services/PlayerService.h b/Code/client/Services/PlayerService.h index 468a051cc..22507bffe 100644 --- a/Code/client/Services/PlayerService.h +++ b/Code/client/Services/PlayerService.h @@ -2,6 +2,7 @@ struct World; struct TransportService; +struct PlayerCharacter; struct UpdateEvent; struct ConnectedEvent; @@ -26,6 +27,12 @@ struct PlayerService TP_NOCOPYMOVE(PlayerService); + // Called from UI (CEF) when the player presses the respawn button. + void RequestManualRespawn() noexcept; + + // Called when a party member revives the local player with a healing spell. + void OnHealRevive() noexcept; + protected: void OnUpdate(const UpdateEvent& acEvent) noexcept; void OnConnected(const ConnectedEvent& acEvent) noexcept; @@ -41,7 +48,7 @@ struct PlayerService private: /** - * @brief Run the respawn timer, and if it hits 0, respawn the player. + * @brief Run the respawn timer and drive death screen updates. */ void RunRespawnUpdates(const double acDeltaTime) noexcept; void RunPostDeathUpdates(const double acDeltaTime) noexcept; @@ -51,6 +58,7 @@ struct PlayerService void RunDifficultyUpdates() const noexcept; void RunLevelUpdates() const noexcept; void RunBeastFormDetection() const noexcept; + void SyncCachedEquipment(PlayerCharacter* apPlayer) noexcept; void ToggleDeathSystem(bool aSet) noexcept; @@ -64,15 +72,28 @@ struct PlayerService bool m_isDeathSystemEnabled = true; + // True while the player is in bleedout and the death screen is active. + bool m_waitingForRespawn = false; + // Becomes true when the cooldown has finished and the respawn button can be pressed. + bool m_canRespawn = false; + // Set by button click/heal to trigger respawn from RunRespawnUpdates + bool m_shouldRespawnAtEntrance = false; + bool m_shouldRespawnInPlace = false; + bool m_knockdownStart = false; double m_knockdownTimer = 0.0; bool m_godmodeStart = false; double m_godmodeTimer = 0.0; - uint32_t m_cachedMainSpellId = 0; - uint32_t m_cachedSecondarySpellId = 0; + uint32_t m_cachedLeftHandSpellId = 0; + uint32_t m_cachedRightHandSpellId = 0; + uint32_t m_cachedLeftHandItemId = 0; + uint32_t m_cachedRightHandItemId = 0; + uint32_t m_cachedTwoHandedItemId = 0; + uint32_t m_cachedAmmoId = 0; uint32_t m_cachedPowerId = 0; + bool m_cachedWeaponDrawn = false; entt::scoped_connection m_updateConnection; entt::scoped_connection m_connectedConnection; diff --git a/Code/client/Services/QuestService.h b/Code/client/Services/QuestService.h index b4f1b58fc..78e3f7402 100644 --- a/Code/client/Services/QuestService.h +++ b/Code/client/Services/QuestService.h @@ -2,7 +2,11 @@ #include #include +#include #include +#include +#include +#include struct NotifyQuestUpdate; @@ -10,10 +14,8 @@ struct TESQuest; /** * @brief Handles quest sync - * - * This service is currently not in use. */ -class QuestService final : public BSTEventSink, BSTEventSink +class QuestService final : public BSTEventSink, public BSTEventSink, public BSTEventSink { public: QuestService(World&, entt::dispatcher&); @@ -23,19 +25,62 @@ class QuestService final : public BSTEventSink, BSTEvent static void DebugDumpQuests(); static bool StopQuest(uint32_t aformId); + struct GateRule + { + TiltedPhoques::String IdName{}; + TiltedPhoques::String Name{}; + TiltedPhoques::String Notes{}; + uint16_t StageMin{0}; + uint16_t StageMax{0}; + + [[nodiscard]] bool Matches(uint16_t aStage) const noexcept { return aStage >= StageMin && aStage <= StageMax; } + }; + + struct GateStatus + { + bool Active{false}; + uint32_t FormId{0}; + uint16_t Stage{0}; + TiltedPhoques::String IdName{}; + TiltedPhoques::String Name{}; + TiltedPhoques::String Notes{}; + }; + + [[nodiscard]] const GateStatus& GetGateStatus() const noexcept { return m_gateStatus; } + private: friend struct QuestEventHandler; void OnConnected(const ConnectedEvent&) noexcept; + // Game quest events BSTEventResult OnEvent(const TESQuestStartStopEvent*, const EventDispatcher*) override; BSTEventResult OnEvent(const TESQuestStageEvent*, const EventDispatcher*) override; + BSTEventResult OnEvent(const TESLoadGameEvent*, const EventDispatcher*) override; + // Network quest updates void OnQuestUpdate(const NotifyQuestUpdate&) noexcept; + void OnUpdate(const UpdateEvent&) noexcept; + bool TryApplyQuestUpdate(const NotifyQuestUpdate& aUpdate) noexcept; + void FlushPendingUpdates() noexcept; + void EvaluateGateForQuest(uint32_t aFormId, uint16_t aStage) noexcept; + void EvaluateGatesFromWorld() noexcept; + void LoadGateRules() noexcept; World& m_world; + // Existing connections entt::scoped_connection m_joinedConnection; entt::scoped_connection m_leftConnection; entt::scoped_connection m_questUpdateConnection; + entt::scoped_connection m_updateConnection; + + TiltedPhoques::Vector m_pendingUpdates; + TiltedPhoques::Vector m_gateRules; + GateStatus m_gateStatus{}; + + double m_gateRescanTimer{0.0}; + bool m_initialGateScan{false}; + bool m_gateActive{false}; + bool m_gateRulesLoaded{false}; }; diff --git a/Code/client/Services/SyncModeService.h b/Code/client/Services/SyncModeService.h new file mode 100644 index 000000000..b3cae60a3 --- /dev/null +++ b/Code/client/Services/SyncModeService.h @@ -0,0 +1,83 @@ +#pragma once + +#include + +#include + +struct World; +struct TransportService; +struct Actor; +struct CharacterService; + +struct ConnectedEvent; +struct DisconnectedEvent; +struct UpdateEvent; +struct NotifyPlayerJoined; +struct NotifyPlayerLeft; +struct NotifyPlayerSyncMode; +struct ShaderReferenceEffect; + +/** + * @brief Keeps track of player sync modes and applies ghost visuals locally. + */ +struct SyncModeService +{ + SyncModeService(World& aWorld, entt::dispatcher& aDispatcher, TransportService& aTransport) noexcept; + ~SyncModeService() noexcept = default; + + TP_NOCOPYMOVE(SyncModeService); + + void SetLocalMode(SyncMode aMode) noexcept; + [[nodiscard]] SyncMode GetLocalMode() const noexcept { return m_localMode; } + void OnActor3DUpdated(Actor* apActor) noexcept; + void RefreshOverlaySyncStatus() const noexcept { UpdateOverlaySyncStatus(); } + // Reset ghost visuals/state when the world is being torn down (e.g., save load). + void OnLoadGameReset() noexcept; + +private: + void OnConnected(const ConnectedEvent& acEvent) noexcept; + void OnDisconnected(const DisconnectedEvent&) noexcept; + void OnUpdate(const UpdateEvent&) noexcept; + void OnPlayerJoined(const NotifyPlayerJoined& acMessage) noexcept; + void OnPlayerLeft(const NotifyPlayerLeft& acMessage) noexcept; + void OnNotifyPlayerSyncMode(const NotifyPlayerSyncMode& acMessage) noexcept; + void OnRemoteComponentRemoved(entt::registry& aRegistry, entt::entity aEntity) noexcept; + void RequestResync() const noexcept; + void UpdateOverlaySyncStatus() const noexcept; + void UpdateWorldEncounters() noexcept; + + bool ShouldGhost(uint32_t aPlayerId) const noexcept; + void RefreshGhostStates() noexcept; + void ClearGhostStates() noexcept; + bool ToggleGhostState(entt::entity aEntity, bool aGhost) noexcept; + bool ApplyGhostToActor(Actor* apActor, bool aGhost) noexcept; + void CollectGhostedRemotePlayerServerIds(TiltedPhoques::Set& aOut) const noexcept; + void RefreshRemotePlayers(const TiltedPhoques::Set& aServerIds) noexcept; + + World& m_world; + entt::dispatcher& m_dispatcher; + TransportService& m_transport; + + SyncMode m_localMode{SyncMode::Normal}; + uint32_t m_localPlayerId{0}; + + TiltedPhoques::Map m_remoteModes; + TiltedPhoques::Set m_pending3DRefresh; + TiltedPhoques::Set m_glowApplied; + struct NpcFlagState + { + uint32_t Flags{0}; + mutable uint32_t RefCount{0}; + }; + TiltedPhoques::Map m_originalNpcFlags; + TiltedPhoques::Map m_originalRefFlagBits; + TiltedPhoques::Set m_addedExtraGhost; + + entt::scoped_connection m_connectedConnection; + entt::scoped_connection m_disconnectedConnection; + entt::scoped_connection m_updateConnection; + entt::scoped_connection m_playerJoinedConnection; + entt::scoped_connection m_playerLeftConnection; + entt::scoped_connection m_notifySyncModeConnection; + entt::scoped_connection m_remoteRemovedConnection; +}; diff --git a/Code/client/Services/TradeService.h b/Code/client/Services/TradeService.h new file mode 100644 index 000000000..56c74f678 --- /dev/null +++ b/Code/client/Services/TradeService.h @@ -0,0 +1,84 @@ +#pragma once + +#include +#include + +#include + +struct World; +struct TransportService; + +struct UpdateEvent; +struct DisconnectedEvent; +struct NotifyTradeInvite; +struct NotifyTradeStarted; +struct NotifyTradeState; +struct NotifyTradeCancel; +struct NotifyTradeComplete; + +/** + * @brief Handles client-side trade state and messaging to the UI overlay. + */ +struct TradeService +{ + TradeService(World& aWorld, entt::dispatcher& aDispatcher, TransportService& aTransport) noexcept; + ~TradeService() noexcept = default; + + TP_NOCOPYMOVE(TradeService); + + void SendInvite(uint32_t aTargetPlayerId) const noexcept; + void RespondToInvite(uint32_t aRequesterPlayerId, bool aAccept) const noexcept; + void CancelTrade() const noexcept; + void SetReady(bool aReady) const noexcept; + struct OfferSelection + { + uint32_t Index{0}; + int32_t Count{0}; + }; + + void UpdateOffer(const TiltedPhoques::Vector& aSelections) noexcept; + +private: + void OnUpdate(const UpdateEvent& acEvent) noexcept; + void OnDisconnected(const DisconnectedEvent&) noexcept; + void OnTradeInvite(const NotifyTradeInvite& acMessage) noexcept; + void OnTradeStarted(const NotifyTradeStarted& acMessage) noexcept; + void OnTradeState(const NotifyTradeState& acMessage) noexcept; + void OnTradeCancel(const NotifyTradeCancel& acMessage) noexcept; + void OnTradeComplete(const NotifyTradeComplete& acMessage) noexcept; + + void ClearSession() noexcept; + void EmitStateToUI() const noexcept; + void EmitInviteUpdate(uint32_t aInviterId, bool aAdded, uint64_t aExpiryTick = 0) const noexcept; + void EmitCancellation(uint32_t aPartnerId, TradeCancelReason aReason, bool aWasInitiator) const noexcept; + bool HasActiveSessionWith(uint32_t aPartnerId) const noexcept; + void ApplyOfferSelection(const TiltedPhoques::Vector& aSelections); + + World& m_world; + TransportService& m_transport; + + struct TradeSession + { + bool Active{false}; + uint32_t PartnerId{0}; + bool InitiatedBySelf{false}; + bool SelfReady{false}; + bool PartnerReady{false}; + TiltedPhoques::Vector SelfItems; + TiltedPhoques::Vector PartnerItems; + TiltedPhoques::Vector SelfInventory; + uint32_t CountdownMs{0}; + uint32_t CountdownTotalMs{0}; + }; + + TradeSession m_session; + TiltedPhoques::Map m_pendingInvites; + + entt::scoped_connection m_updateConnection; + entt::scoped_connection m_disconnectConnection; + entt::scoped_connection m_tradeInviteConnection; + entt::scoped_connection m_tradeStartedConnection; + entt::scoped_connection m_tradeStateConnection; + entt::scoped_connection m_tradeCancelConnection; + entt::scoped_connection m_tradeCompleteConnection; +}; diff --git a/Code/client/Services/TransportService.h b/Code/client/Services/TransportService.h index 8f2c3503e..b7d94cdc6 100644 --- a/Code/client/Services/TransportService.h +++ b/Code/client/Services/TransportService.h @@ -5,6 +5,7 @@ #include #include +#include struct ImguiService; struct UpdateEvent; @@ -13,6 +14,7 @@ struct AuthenticationResponse; struct NotifySettingsChange; struct World; +struct SyncModeService; using TiltedPhoques::Client; @@ -35,6 +37,8 @@ struct TransportService : Client [[nodiscard]] bool IsOnline() const noexcept { return m_connected; } void SetServerPassword(const std::string& acPassword) noexcept { m_serverPassword = acPassword; } + void SetLoginCredentials(const std::string& acUsername, const std::string& acPassword) noexcept; + [[nodiscard]] std::string GetLoginUsername() const noexcept { return m_loginUsername.c_str(); } const uint32_t& GetLocalPlayerId() const noexcept { return m_localPlayerId; } protected: @@ -48,10 +52,15 @@ struct TransportService : Client void HandleNotifySettingsChange(const NotifySettingsChange& acMessage) noexcept; private: + bool IsAllowedOutbound(const ClientMessage& acMessage) const noexcept; + bool IsAllowedInbound(const ServerMessage& acMessage) const noexcept; + World& m_world; entt::dispatcher& m_dispatcher; bool m_connected; String m_serverPassword{}; + String m_loginUsername{}; + String m_loginPassword{}; uint32_t m_localPlayerId; entt::scoped_connection m_updateConnection; diff --git a/Code/client/Sync/DropExecutionContext.cpp b/Code/client/Sync/DropExecutionContext.cpp new file mode 100644 index 000000000..3740ddc99 --- /dev/null +++ b/Code/client/Sync/DropExecutionContext.cpp @@ -0,0 +1,43 @@ +#include "DropExecutionContext.h" + +namespace DropExecution +{ +namespace +{ + thread_local Mode g_mode = Mode::None; + thread_local uint32_t g_actorFormId = 0; + thread_local uint64_t g_dropId = 0; +} + +Mode GetCurrentMode() noexcept +{ + return g_mode; +} + +uint32_t GetCurrentActor() noexcept +{ + return g_actorFormId; +} + +uint64_t GetCurrentDrop() noexcept +{ + return g_dropId; +} + +Scope::Scope(Mode aMode, uint32_t aActorFormId, uint64_t aDropId) noexcept + : m_previousMode(g_mode) + , m_previousActor(g_actorFormId) + , m_previousDrop(g_dropId) +{ + g_mode = aMode; + g_actorFormId = aActorFormId; + g_dropId = aDropId; +} + +Scope::~Scope() +{ + g_mode = m_previousMode; + g_actorFormId = m_previousActor; + g_dropId = m_previousDrop; +} +} // namespace DropExecution diff --git a/Code/client/Sync/DropExecutionContext.h b/Code/client/Sync/DropExecutionContext.h new file mode 100644 index 000000000..64222c6fa --- /dev/null +++ b/Code/client/Sync/DropExecutionContext.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +namespace DropExecution +{ +enum class Mode +{ + None, + LocalDrop, + RemoteDrop, + RemotePickup, + LocalPickup +}; + +Mode GetCurrentMode() noexcept; +uint32_t GetCurrentActor() noexcept; +uint64_t GetCurrentDrop() noexcept; + +struct Scope +{ + Scope(Mode aMode, uint32_t aActorFormId, uint64_t aDropId) noexcept; + ~Scope(); + +private: + Mode m_previousMode{Mode::None}; + uint32_t m_previousActor{0}; + uint64_t m_previousDrop{0}; +}; +} // namespace DropExecution diff --git a/Code/client/Sync/DropManager.cpp b/Code/client/Sync/DropManager.cpp new file mode 100644 index 000000000..f5eda5207 --- /dev/null +++ b/Code/client/Sync/DropManager.cpp @@ -0,0 +1,293 @@ +#include "DropManager.h" + +#include +#include + +namespace DropManager +{ +namespace +{ + TiltedPhoques::Map s_localDrops; + TiltedPhoques::Map s_serverDrops; + TiltedPhoques::Map s_handleBindings; + TiltedPhoques::Map s_referenceBindings; + StorageListener* s_pStorageListener = nullptr; + + void UnbindHandle(uint32_t handleBits) noexcept + { + if (handleBits == 0) + return; + + s_handleBindings.erase(handleBits); + } + + void BindHandle(uint32_t handleBits, uint64_t dropId) noexcept + { + if (handleBits == 0) + return; + s_handleBindings[handleBits] = dropId; + } +} // namespace + +Guid RegisterLocalDrop(LocalDropData data) noexcept +{ + Guid clientDropId = data.ClientDropId; + if (clientDropId.IsEmpty()) + clientDropId = Guid::Random(); + + data.ClientDropId = clientDropId; + s_localDrops[clientDropId] = std::move(data); + return clientDropId; +} + +std::optional ConsumeLocalDrop(const Guid& clientDropId) noexcept +{ + const auto it = s_localDrops.find(clientDropId); + if (it == s_localDrops.end()) + return std::nullopt; + + auto data = it->second; + s_localDrops.erase(it); + return data; +} + +void TrackServerDrop(uint64_t dropId, const ServerDropData& data) noexcept +{ + ServerDropData merged = data; + + const bool existed = s_serverDrops.find(dropId) != std::end(s_serverDrops); + GameId previousReference{}; + if (existed) + { + auto& current = s_serverDrops[dropId]; + previousReference = current.ReferenceId; + if (!merged.ServerId) + merged.ServerId = current.ServerId; + if (!merged.HandleBits) + merged.HandleBits = current.HandleBits; + if (!merged.ActorFormId && current.ActorFormId) + merged.ActorFormId = current.ActorFormId; + if (!merged.ReferenceId && current.ReferenceId) + merged.ReferenceId = current.ReferenceId; + if (!merged.CellId && current.CellId) + merged.CellId = current.CellId; + if (!merged.WorldSpaceId && current.WorldSpaceId) + merged.WorldSpaceId = current.WorldSpaceId; + if (merged.Type == ServerItemType::Dropped && current.Type == ServerItemType::CreationEngine) + merged.Type = current.Type; + if (!merged.HasVelocity && current.HasVelocity) + { + merged.HasVelocity = true; + merged.Velocity = current.Velocity; + } + } + + s_serverDrops[dropId] = merged; + if (merged.HandleBits) + BindHandle(merged.HandleBits, dropId); + if (previousReference && previousReference != merged.ReferenceId) + s_referenceBindings.erase(previousReference); + if (merged.ReferenceId) + s_referenceBindings[merged.ReferenceId] = dropId; + + if (s_pStorageListener) + s_pStorageListener->OnServerDropTracked(dropId, merged); + + auto logLevel = existed ? spdlog::level::debug : spdlog::level::info; + spdlog::log(logLevel, "DropManager: tracked drop {} server {:X} actor {:X} item {:X}:{:X} type {} loc ({:.2f}, {:.2f}, {:.2f}) handle {:X}", dropId, merged.ServerId, merged.ActorFormId, + merged.Item.BaseId.ModId, merged.Item.BaseId.BaseId, merged.Type == ServerItemType::CreationEngine ? "ce" : "drop", merged.Location.x, merged.Location.y, merged.Location.z, merged.HandleBits); +} + +bool BindHandleToServerDrop(uint64_t dropId, uint32_t actorFormId, uint32_t handleBits) noexcept +{ + auto dropIt = s_serverDrops.find(dropId); + if (dropIt == s_serverDrops.end()) + return false; + + auto& drop = dropIt.value(); + if (drop.HandleBits != 0 && drop.HandleBits != handleBits) + UnbindHandle(drop.HandleBits); + drop.ActorFormId = actorFormId; + drop.HandleBits = handleBits; + BindHandle(handleBits, dropId); + if (s_pStorageListener) + s_pStorageListener->OnDropHandleBound(dropId, handleBits); + + spdlog::info("DropManager: bound handle {:X} to drop {} actor {:X}", handleBits, dropId, actorFormId); + return true; +} + +void ClearHandleBinding(uint64_t dropId) noexcept +{ + auto dropIt = s_serverDrops.find(dropId); + if (dropIt == s_serverDrops.end()) + return; + + auto& drop = dropIt.value(); + if (drop.HandleBits == 0) + return; + + const uint32_t previousHandle = drop.HandleBits; + UnbindHandle(previousHandle); + drop.HandleBits = 0; + + if (s_pStorageListener) + s_pStorageListener->OnDropHandleBound(dropId, 0); + + spdlog::info("DropManager: cleared handle {:X} for drop {}", previousHandle, dropId); +} + +void SetReferenceForDrop(uint64_t dropId, const GameId& referenceId) noexcept +{ + if (!referenceId) + return; + + if (s_serverDrops.find(dropId) == s_serverDrops.end()) + return; + + auto& drop = s_serverDrops[dropId]; + if (drop.ReferenceId == referenceId) + return; + + if (drop.ReferenceId) + s_referenceBindings.erase(drop.ReferenceId); + + drop.ReferenceId = referenceId; + s_referenceBindings[referenceId] = dropId; + if (s_pStorageListener) + s_pStorageListener->OnServerDropTracked(dropId, drop); +} + +std::optional GetDropIdForHandle(uint32_t handleBits) noexcept +{ + if (handleBits == 0) + return std::nullopt; + + const auto handleIt = s_handleBindings.find(handleBits); + if (handleIt == s_handleBindings.end()) + return std::nullopt; + + return handleIt->second; +} + +std::optional GetDropIdForReference(const GameId& referenceId) noexcept +{ + if (!referenceId) + return std::nullopt; + + const auto it = s_referenceBindings.find(referenceId); + if (it == s_referenceBindings.end()) + return std::nullopt; + + return it->second; +} + +std::optional GetHandleForDrop(uint64_t dropId) noexcept +{ + const auto it = s_serverDrops.find(dropId); + if (it == s_serverDrops.end()) + return std::nullopt; + + if (it->second.HandleBits == 0) + return std::nullopt; + + return it->second.HandleBits; +} + +std::optional GetServerDrop(uint64_t dropId) noexcept +{ + const auto it = s_serverDrops.find(dropId); + if (it == s_serverDrops.end()) + return std::nullopt; + + return it->second; +} + +bool UpdateServerDropTransform(uint64_t dropId, const NiPoint3& acLocation, const NiPoint3& acRotation, const GameId& acCellId, const GameId& acWorldSpaceId, const GameId& acReferenceId, bool aHasVelocity, + const NiPoint3& acVelocity) noexcept +{ + if (s_serverDrops.find(dropId) == s_serverDrops.end()) + return false; + + auto& drop = s_serverDrops[dropId]; + const GameId previousReference = drop.ReferenceId; + drop.Location = acLocation; + drop.Rotation = acRotation; + if (aHasVelocity) + { + drop.HasVelocity = true; + drop.Velocity = acVelocity; + } + if (acCellId) + drop.CellId = acCellId; + if (acWorldSpaceId) + drop.WorldSpaceId = acWorldSpaceId; + if (acReferenceId) + { + drop.ReferenceId = acReferenceId; + if (previousReference != drop.ReferenceId) + { + if (previousReference) + s_referenceBindings.erase(previousReference); + s_referenceBindings[drop.ReferenceId] = dropId; + } + } + + if (s_pStorageListener) + s_pStorageListener->OnServerDropTracked(dropId, drop); + + return true; +} + +std::optional FindDropBySignature(const GameId& aBaseId, const NiPoint3& aLocation, float aRadiusSq) noexcept +{ + uint64_t bestDropId = 0; + float bestDistanceSq = aRadiusSq; + + for (const auto& [dropId, data] : s_serverDrops) + { + if (data.Item.BaseId != aBaseId) + continue; + + const float dx = data.Location.x - aLocation.x; + const float dy = data.Location.y - aLocation.y; + const float dz = data.Location.z - aLocation.z; + const float distSq = dx * dx + dy * dy + dz * dz; + if (distSq <= bestDistanceSq) + { + bestDistanceSq = distSq; + bestDropId = dropId; + } + } + + if (bestDropId == 0) + { + spdlog::debug("DropManager: no drop match for {:X}:{:X} near ({:.2f}, {:.2f}, {:.2f})", aBaseId.ModId, aBaseId.BaseId, aLocation.x, aLocation.y, aLocation.z); + return std::nullopt; + } + + spdlog::info("DropManager: matched drop {} for {:X}:{:X} within {:.2f}", bestDropId, aBaseId.ModId, aBaseId.BaseId, std::sqrt(bestDistanceSq)); + return bestDropId; +} + +void RemoveServerDrop(uint64_t dropId) noexcept +{ + const auto it = s_serverDrops.find(dropId); + if (it == s_serverDrops.end()) + return; + + UnbindHandle(it->second.HandleBits); + if (it->second.ReferenceId) + s_referenceBindings.erase(it->second.ReferenceId); + s_serverDrops.erase(it); + if (s_pStorageListener) + s_pStorageListener->OnServerDropRemoved(dropId); + + spdlog::info("DropManager: removed drop {}", dropId); +} + +void SetStorageListener(StorageListener* apListener) noexcept +{ + s_pStorageListener = apListener; +} +} // namespace DropManager diff --git a/Code/client/Sync/DropManager.h b/Code/client/Sync/DropManager.h new file mode 100644 index 000000000..e5bdfd217 --- /dev/null +++ b/Code/client/Sync/DropManager.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace DropManager +{ +struct LocalDropData +{ + uint32_t ActorFormId{}; + Inventory::Entry Item{}; + NiPoint3 Location{}; + NiPoint3 Rotation{}; + uint32_t HandleBits{}; + GameId CellId{}; + GameId WorldSpaceId{}; + Guid ClientDropId{}; + GameId ReferenceId{}; +}; + +struct ServerDropData +{ + uint32_t ServerId{}; + uint32_t ActorFormId{}; + ServerItemType Type{ServerItemType::Dropped}; + Inventory::Entry Item{}; + NiPoint3 Location{}; + NiPoint3 Rotation{}; + NiPoint3 Velocity{}; + bool HasVelocity{false}; + uint32_t HandleBits{}; + GameId CellId{}; + GameId WorldSpaceId{}; + GameId ReferenceId{}; +}; + +struct StorageListener +{ + virtual ~StorageListener() = default; + virtual void OnServerDropTracked(uint64_t aDropId, const ServerDropData& acData) noexcept = 0; + virtual void OnDropHandleBound(uint64_t aDropId, uint32_t aHandleBits) noexcept = 0; + virtual void OnServerDropRemoved(uint64_t aDropId) noexcept = 0; +}; + +Guid RegisterLocalDrop(LocalDropData data) noexcept; +std::optional ConsumeLocalDrop(const Guid& clientDropId) noexcept; + +void TrackServerDrop(uint64_t dropId, const ServerDropData& data) noexcept; +bool BindHandleToServerDrop(uint64_t dropId, uint32_t actorFormId, uint32_t handleBits) noexcept; +void ClearHandleBinding(uint64_t dropId) noexcept; +void SetReferenceForDrop(uint64_t dropId, const GameId& referenceId) noexcept; + +std::optional GetDropIdForHandle(uint32_t handleBits) noexcept; +std::optional GetDropIdForReference(const GameId& referenceId) noexcept; +std::optional GetHandleForDrop(uint64_t dropId) noexcept; +std::optional GetServerDrop(uint64_t dropId) noexcept; +std::optional FindDropBySignature(const GameId& aBaseId, const NiPoint3& aLocation, float aRadiusSq) noexcept; +bool UpdateServerDropTransform(uint64_t dropId, const NiPoint3& acLocation, const NiPoint3& acRotation, const GameId& acCellId, const GameId& acWorldSpaceId, const GameId& acReferenceId, bool aHasVelocity = false, + const NiPoint3& acVelocity = {}) noexcept; + +void RemoveServerDrop(uint64_t dropId) noexcept; +void SetStorageListener(StorageListener* apListener) noexcept; +} // namespace DropManager diff --git a/Code/client/Systems/ModSystem.cpp b/Code/client/Systems/ModSystem.cpp index 8586d2e25..b3e52f541 100644 --- a/Code/client/Systems/ModSystem.cpp +++ b/Code/client/Systems/ModSystem.cpp @@ -50,6 +50,13 @@ bool ModSystem::GetServerModId(uint32_t aGameId, GameId& aServerId) const noexce uint32_t ModSystem::GetGameId(uint32_t aServerId, uint32_t aFormId) const noexcept { + if (aServerId == std::numeric_limits::max()) + { + aFormId &= 0x00FFFFFFu; + aFormId |= 0xFF000000u; + return aFormId; + } + auto itor = m_serverToGame.find(aServerId); if (itor != std::end(m_serverToGame)) { diff --git a/Code/client/World.cpp b/Code/client/World.cpp index b0596604b..6c90f5d36 100644 --- a/Code/client/World.cpp +++ b/Code/client/World.cpp @@ -13,19 +13,29 @@ #include #include #include +#include +#include #include #include +#include #include #include #include #include #include #include +#include +#include +#include +#include +#include + + #include #include -#include +#include World::World() : m_runner(m_dispatcher) @@ -37,16 +47,21 @@ World::World() ctx().emplace(*this, m_dispatcher); ctx().emplace(*this, m_transport, m_dispatcher); ctx().emplace(ctx().at()); + // Quest gating needs to observe ConnectedEvent before gameplay replication starts. + ctx().emplace(*this, m_dispatcher); + ctx().emplace(*this, m_dispatcher, m_transport); ctx().emplace(*this, m_dispatcher, m_transport); ctx().emplace(m_dispatcher, *this, m_transport, ctx().at()); ctx().emplace(m_dispatcher); ctx().emplace(m_dispatcher); ctx().emplace(*this, m_dispatcher, m_transport); ctx().emplace(*this, m_dispatcher, m_transport); - ctx().emplace(*this, m_dispatcher); ctx().emplace(*this, m_dispatcher, m_transport); + ctx().emplace(*this, m_dispatcher, m_transport); ctx().emplace(*this, m_dispatcher, m_transport); ctx().emplace(*this, m_dispatcher, m_transport); + ctx().emplace(*this, m_dispatcher, m_transport); + ctx().emplace(*this, m_dispatcher, m_transport); ctx().emplace(*this, m_dispatcher, m_transport); ctx().emplace(*this, m_transport, m_dispatcher); ctx().emplace(*this, m_dispatcher, m_transport); @@ -54,6 +69,10 @@ World::World() ctx().emplace(*this, m_transport, m_dispatcher); ctx().emplace(*this, m_transport, m_dispatcher); ctx().emplace(*this, m_dispatcher, m_transport); + ctx().emplace(*this, m_dispatcher); + ctx().emplace(*this, m_dispatcher); + ctx().emplace(*this, m_dispatcher); + ctx().emplace(*this, m_dispatcher); BehaviorVar::Get()->Init(); } diff --git a/Code/client/World.h b/Code/client/World.h index 2c3fee2eb..50234c1dc 100644 --- a/Code/client/World.h +++ b/Code/client/World.h @@ -3,11 +3,14 @@ #include #include #include +#include #include #include #include #include #include +#include +#include #include @@ -28,12 +31,18 @@ struct World : entt::registry const PartyService& GetPartyService() const noexcept { return ctx().at(); } CharacterService& GetCharacterService() noexcept { return ctx().at(); } const CharacterService& GetCharacterService() const noexcept { return ctx().at(); } + TradeService& GetTradeService() noexcept { return ctx().at(); } + const TradeService& GetTradeService() const noexcept { return ctx().at(); } OverlayService& GetOverlayService() noexcept { return ctx().at(); } const OverlayService& GetOverlayService() const noexcept { return ctx().at(); } DebugService& GetDebugService() noexcept { return ctx().at(); } const DebugService& GetDebugService() const noexcept { return ctx().at(); } MagicService& GetMagicService() noexcept { return ctx().at(); } const MagicService& GetMagicService() const noexcept { return ctx().at(); } + SyncModeService& GetSyncModeService() noexcept { return ctx().at(); } + const SyncModeService& GetSyncModeService() const noexcept { return ctx().at(); } + CoSaveService& GetCoSaveService() noexcept { return ctx().at(); } + const CoSaveService& GetCoSaveService() const noexcept { return ctx().at(); } auto& GetDispatcher() noexcept { return m_dispatcher; } diff --git a/Code/client/xmake.lua b/Code/client/xmake.lua index 459c5d576..e8f48c981 100644 --- a/Code/client/xmake.lua +++ b/Code/client/xmake.lua @@ -10,6 +10,18 @@ target(name) add_headerfiles("**.h|Games/Skyrim/**|Services/Vivox/**") add_files("**.cpp|Games/Skyrim/**|Services/Vivox/**") + after_build(function(target) + local isolationdir = path.join(target:scriptdir(), "..", "..", "Isolation") + if os.isdir(isolationdir) then + local outdir = target:targetdir() or path.directory(target:targetfile()) + local dest = path.join(outdir, "Isolation") + os.mkdir(outdir) + os.tryrm(dest) + -- Copy the folder itself into the output dir (avoids Isolation/Isolation nesting). + os.cp(isolationdir, outdir) + end + end) + after_install(function(target) -- copy dlls for _, pkg_with_dlls in ipairs({"cef", "discord"}) do @@ -17,10 +29,23 @@ target(name) local bindir = path.join(linkdir, "..", "bin") os.cp(bindir, target:installdir()) end - -- copy ui + -- copy ui assets needed by overlay/branding local uidir = path.join(target:scriptdir(), "..", "skyrim_ui", "src") os.cp(path.join(uidir, "assets", "images", "cursor.dds"), path.join(target:installdir(), "bin", "assets", "images", "cursor.dds")) os.cp(path.join(uidir, "assets", "images", "cursor.png"), path.join(target:installdir(), "bin", "assets", "images", "cursor.png")) + -- font used for Skyrim-like branding + os.cp(path.join(uidir, "assets", "fonts", "futura-condensed", "futura-condensed-medium.otf"), + path.join(target:installdir(), "bin", "assets", "fonts", "futura-condensed", "futura-condensed-medium.otf")) + -- quest isolation data + local isolationdir = path.join(target:scriptdir(), "..", "..", "Isolation") + if os.isdir(isolationdir) then + local bindir = path.join(target:installdir(), "bin") + local dest = path.join(bindir, "Isolation") + os.mkdir(bindir) + os.tryrm(dest) + -- Copy the folder itself into the bin dir (avoids Isolation/Isolation nesting). + os.cp(isolationdir, bindir) + end os.rm(path.join(target:installdir(), "bin", "**Tests.exe")) end) @@ -38,12 +63,14 @@ target(name) "TiltedReverse", "TiltedHooks", "TiltedUi", + "ESLoader", {inherit = true} ) add_packages( "tiltedcore", "spdlog", + "fmt", "hopscotch-map", "cryptopp", "gamenetworkingsockets", @@ -54,7 +81,8 @@ target(name) "entt", "glm", "mem", - "xbyak") + "xbyak", + "fmt") if has_config("vivox") then add_files("Services/Vivox/**.cpp") diff --git a/Code/common/CredentialHash.cpp b/Code/common/CredentialHash.cpp new file mode 100644 index 000000000..91e5d2fd0 --- /dev/null +++ b/Code/common/CredentialHash.cpp @@ -0,0 +1,73 @@ +#include "CredentialHash.h" + +#include "Sha256.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ +constexpr char kHexAlphabet[] = "0123456789abcdef"; + +TiltedPhoques::String ToHex(const uint8_t* apData, size_t aSize) noexcept +{ + TiltedPhoques::String result; + result.reserve(aSize * 2); + + for (size_t i = 0; i < aSize; ++i) + { + const uint8_t value = apData[i]; + result.push_back(kHexAlphabet[(value >> 4) & 0x0F]); + result.push_back(kHexAlphabet[value & 0x0F]); + } + + return result; +} +} // namespace + +namespace Credential +{ +TiltedPhoques::String HashPassword(std::string_view aPlainPassword) noexcept +{ + return Sha256::HashHex(aPlainPassword); +} + +TiltedPhoques::String DeriveServerPassword(std::string_view aClientHash, std::string_view aSalt) noexcept +{ + std::string combined; + combined.reserve(aClientHash.size() + aSalt.size()); + combined.append(aSalt.data(), aSalt.size()); + combined.append(aClientHash.data(), aClientHash.size()); + + return Sha256::HashHex(combined); +} + +TiltedPhoques::String GenerateSalt(std::size_t aBytes) noexcept +{ + if (aBytes == 0) + aBytes = 16; + + std::vector buffer(aBytes); + + std::random_device rd; + std::mt19937 generator(rd()); + std::uniform_int_distribution distribution(0, 255); + + for (auto& byte : buffer) + byte = static_cast(distribution(generator)); + + return ToHex(buffer.data(), buffer.size()); +} + +bool LooksLikePasswordHash(std::string_view aValue) noexcept +{ + if (aValue.length() != 64) + return false; + + return std::all_of(aValue.begin(), aValue.end(), [](unsigned char c) { return std::isxdigit(c) != 0; }); +} +} // namespace Credential diff --git a/Code/common/CredentialHash.h b/Code/common/CredentialHash.h new file mode 100644 index 000000000..1f8f1179c --- /dev/null +++ b/Code/common/CredentialHash.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +#include + +namespace Credential +{ +TiltedPhoques::String HashPassword(std::string_view aPlainPassword) noexcept; +TiltedPhoques::String DeriveServerPassword(std::string_view aClientHash, std::string_view aSalt) noexcept; +TiltedPhoques::String GenerateSalt(std::size_t aBytes = 16) noexcept; +bool LooksLikePasswordHash(std::string_view aValue) noexcept; +} // namespace Credential diff --git a/Code/common/GameServerInstance.h b/Code/common/GameServerInstance.h index 732f3a83c..7609a7b5e 100644 --- a/Code/common/GameServerInstance.h +++ b/Code/common/GameServerInstance.h @@ -1,5 +1,8 @@ #pragma once +#include +#include + class IGameServerInstance { public: @@ -14,4 +17,7 @@ class IGameServerInstance // update the server logic virtual void Update() = 0; + virtual Console::ConsoleRegistry::ExecutionResult ExecuteConsoleCommand(const TiltedPhoques::String& aCommand) = 0; + + virtual void GetStatus(ServerStatusSnapshot& aOutStatus) const = 0; }; diff --git a/Code/common/ServerStatusSnapshot.h b/Code/common/ServerStatusSnapshot.h new file mode 100644 index 000000000..ea3fc25ed --- /dev/null +++ b/Code/common/ServerStatusSnapshot.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +struct ServerPlayerStatusSnapshot +{ + uint32_t PlayerId{}; + TiltedPhoques::String Username; + uint32_t CellBaseId{}; + uint32_t CellModId{}; + uint32_t WorldBaseId{}; + uint32_t WorldModId{}; + int32_t GridX{}; + int32_t GridY{}; + bool HasPosition{false}; + float PositionX{}; + float PositionY{}; + float PositionZ{}; +}; + +struct ServerStatusSnapshot +{ + uint32_t UptimeSeconds{}; + TiltedPhoques::Vector Players; +}; diff --git a/Code/common/Sha256.cpp b/Code/common/Sha256.cpp new file mode 100644 index 000000000..c503c87c5 --- /dev/null +++ b/Code/common/Sha256.cpp @@ -0,0 +1,171 @@ +#include "Sha256.h" + +#include + +#include +#include + +namespace +{ +constexpr uint32_t kSha256K[64] = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2}; + +inline uint32_t RotateRight(uint32_t aValue, uint32_t aCount) +{ + return (aValue >> aCount) | (aValue << (32 - aCount)); +} + +void ProcessChunk(const uint8_t* apChunk, uint32_t (&aState)[8]) +{ + uint32_t w[64]; + + for (uint32_t i = 0; i < 16; ++i) + { + w[i] = (static_cast(apChunk[i * 4]) << 24) | (static_cast(apChunk[i * 4 + 1]) << 16) | + (static_cast(apChunk[i * 4 + 2]) << 8) | (static_cast(apChunk[i * 4 + 3])); + } + + for (uint32_t i = 16; i < 64; ++i) + { + const uint32_t s0 = RotateRight(w[i - 15], 7) ^ RotateRight(w[i - 15], 18) ^ (w[i - 15] >> 3); + const uint32_t s1 = RotateRight(w[i - 2], 17) ^ RotateRight(w[i - 2], 19) ^ (w[i - 2] >> 10); + w[i] = w[i - 16] + s0 + w[i - 7] + s1; + } + + uint32_t a = aState[0]; + uint32_t b = aState[1]; + uint32_t c = aState[2]; + uint32_t d = aState[3]; + uint32_t e = aState[4]; + uint32_t f = aState[5]; + uint32_t g = aState[6]; + uint32_t h = aState[7]; + + for (uint32_t i = 0; i < 64; ++i) + { + const uint32_t s1 = RotateRight(e, 6) ^ RotateRight(e, 11) ^ RotateRight(e, 25); + const uint32_t ch = (e & f) ^ (~e & g); + const uint32_t temp1 = h + s1 + ch + kSha256K[i] + w[i]; + const uint32_t s0 = RotateRight(a, 2) ^ RotateRight(a, 13) ^ RotateRight(a, 22); + const uint32_t maj = (a & b) ^ (a & c) ^ (b & c); + const uint32_t temp2 = s0 + maj; + + h = g; + g = f; + f = e; + e = d + temp1; + d = c; + c = b; + b = a; + a = temp1 + temp2; + } + + aState[0] += a; + aState[1] += b; + aState[2] += c; + aState[3] += d; + aState[4] += e; + aState[5] += f; + aState[6] += g; + aState[7] += h; +} + +std::array FinaliseHash(uint32_t (&aState)[8]) +{ + std::array digest{}; + + for (uint32_t i = 0; i < 8; ++i) + { + digest[i * 4] = static_cast((aState[i] >> 24) & 0xFF); + digest[i * 4 + 1] = static_cast((aState[i] >> 16) & 0xFF); + digest[i * 4 + 2] = static_cast((aState[i] >> 8) & 0xFF); + digest[i * 4 + 3] = static_cast(aState[i] & 0xFF); + } + + return digest; +} +} // namespace + +namespace Sha256 +{ +std::array Hash(const uint8_t* apData, std::size_t aLength) noexcept +{ + uint32_t state[8] = { + 0x6a09e667, + 0xbb67ae85, + 0x3c6ef372, + 0xa54ff53a, + 0x510e527f, + 0x9b05688c, + 0x1f83d9ab, + 0x5be0cd19, + }; + + const std::size_t fullChunks = aLength / 64; + + for (std::size_t i = 0; i < fullChunks; ++i) + ProcessChunk(apData + i * 64, state); + + uint8_t buffer[64]; + const std::size_t remaining = aLength % 64; + std::memset(buffer, 0, sizeof(buffer)); + if (remaining > 0) + std::memcpy(buffer, apData + fullChunks * 64, remaining); + + buffer[remaining] = 0x80; + + if (remaining >= 56) + { + ProcessChunk(buffer, state); + std::memset(buffer, 0, sizeof(buffer)); + } + + const uint64_t bitLength = static_cast(aLength) * 8; + buffer[63] = static_cast(bitLength); + buffer[62] = static_cast(bitLength >> 8); + buffer[61] = static_cast(bitLength >> 16); + buffer[60] = static_cast(bitLength >> 24); + buffer[59] = static_cast(bitLength >> 32); + buffer[58] = static_cast(bitLength >> 40); + buffer[57] = static_cast(bitLength >> 48); + buffer[56] = static_cast(bitLength >> 56); + + ProcessChunk(buffer, state); + + return FinaliseHash(state); +} + +std::array Hash(std::string_view aInput) noexcept +{ + return Hash(reinterpret_cast(aInput.data()), aInput.size()); +} + +TiltedPhoques::String HashHex(const uint8_t* apData, std::size_t aLength) noexcept +{ + const auto hash = Hash(apData, aLength); + + TiltedPhoques::String hex; + hex.reserve(hash.size() * 2); + + static constexpr char kHexDigits[] = "0123456789abcdef"; + for (uint8_t byte : hash) + { + hex.push_back(kHexDigits[(byte >> 4) & 0x0F]); + hex.push_back(kHexDigits[byte & 0x0F]); + } + + return hex; +} + +TiltedPhoques::String HashHex(std::string_view aInput) noexcept +{ + return HashHex(reinterpret_cast(aInput.data()), aInput.size()); +} +} // namespace Sha256 diff --git a/Code/common/Sha256.h b/Code/common/Sha256.h new file mode 100644 index 000000000..7a84f690c --- /dev/null +++ b/Code/common/Sha256.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include +#include + +#include + +namespace Sha256 +{ +std::array Hash(std::string_view aInput) noexcept; +std::array Hash(const uint8_t* apData, std::size_t aLength) noexcept; + +TiltedPhoques::String HashHex(std::string_view aInput) noexcept; +TiltedPhoques::String HashHex(const uint8_t* apData, std::size_t aLength) noexcept; +} // namespace Sha256 diff --git a/Code/components/console/ConsoleRegistry.cpp b/Code/components/console/ConsoleRegistry.cpp index ee587c249..6e8ac52be 100644 --- a/Code/components/console/ConsoleRegistry.cpp +++ b/Code/components/console/ConsoleRegistry.cpp @@ -101,6 +101,7 @@ void ConsoleRegistry::RegisterNatives() { m_out->info("/{}: {}", c->m_name, c->m_desc); } + m_out->info("Server chat: type a message without '/' to broadcast."); m_out->info("<------Variables-({})--->", m_settings.size()); for (SettingBase* s : m_settings) { diff --git a/Code/components/es_loader/ESLoader.cpp b/Code/components/es_loader/ESLoader.cpp index 22e3fd6f2..2d9137c0b 100644 --- a/Code/components/es_loader/ESLoader.cpp +++ b/Code/components/es_loader/ESLoader.cpp @@ -1,13 +1,425 @@ #include "ESLoader.h" +#include +#include +#include +#include #include #include +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif +# ifndef NOMINMAX +# define NOMINMAX +# endif +# include +#else +# include +#endif #include #include #include +namespace +{ +struct Mo2Context +{ + fs::path dataPath; + fs::path loadOrderPath; + fs::path pluginsPath; + String profile; +}; + +std::once_flag s_loadOrderOnceFlag; +ESLoader::PluginCollection s_cachedLoadOrder; +TiltedPhoques::Map s_cachedMasterFiles; +bool s_loadOrderSuccess = false; + +fs::path ResolveExecutableDirectory() noexcept +{ +#if defined(_WIN32) + std::array buffer{}; + const DWORD length = GetModuleFileNameW(nullptr, buffer.data(), static_cast(buffer.size())); + if (length != 0 && length < buffer.size()) + return fs::path(buffer.data()).parent_path(); +#else + std::error_code ec; + auto exePath = fs::canonical("/proc/self/exe", ec); + if (!ec) + return exePath.parent_path(); +#endif + + return {}; +} + +String Trim(String value) +{ + auto notSpace = [](int ch) { return !std::isspace(static_cast(ch)); }; + value.erase(value.begin(), std::find_if(value.begin(), value.end(), notSpace)); + value.erase(std::find_if(value.rbegin(), value.rend(), notSpace).base(), value.end()); + return value; +} + +String ParseMo2ByteArray(String value) +{ + constexpr std::string_view kPrefix = "@ByteArray("; + + if (value.rfind(kPrefix.data(), 0) == 0 && !value.empty()) + { + const auto endPos = value.rfind(')'); + if (endPos != String::npos && endPos > kPrefix.size()) + { + value = value.substr(kPrefix.size(), endPos - kPrefix.size()); + } + } + + if (!value.empty() && value.front() == '"' && value.back() == '"' && value.size() > 1) + { + value = value.substr(1, value.size() - 2); + } + + String result; + result.reserve(value.size()); + + for (size_t i = 0; i < value.size(); ++i) + { + const char ch = value[i]; + if (ch == '\\' && i + 1 < value.size()) + { + result.push_back(value[i + 1]); + ++i; + } + else + { + result.push_back(ch); + } + } + + return result; +} + +fs::path NormalizeMo2Path(String value) +{ + if (value.empty()) + return {}; + + for (auto& ch : value) + { + if (ch == '\\') + ch = '/'; + } + +#if defined(__linux__) + if (value.size() > 1 && value[1] == ':' && (value[0] == 'Z' || value[0] == 'z')) + { + value.erase(0, 2); + if (value.empty() || (value.front() != '/' && value.front() != '\\')) + value.insert(value.begin(), '/'); + } +#endif + + return fs::path(value); +} + +enum class PluginKind +{ + kUnknown, + kStandard, + kLite, +}; + +bool HasEslFlag(const fs::path& aPluginPath) noexcept +{ + if (aPluginPath.empty()) + return false; + + std::ifstream file(aPluginPath, std::ios::binary); + if (!file.is_open()) + return false; + + Record header{}; + file.read(reinterpret_cast(&header), sizeof(header)); + if (!file) + return false; + + if (header.GetType() != FormEnum::TES4) + return false; + + return (header.GetFlags() & Record::FLAGS::kLightFile) != 0; +} + +PluginKind ResolvePluginKind(const fs::path& aDataRoot, const String& aFilename) noexcept +{ + if (aFilename.empty()) + return PluginKind::kUnknown; + + const char extensionType = static_cast(std::tolower(static_cast(aFilename.back()))); + if (extensionType == 'l') + return PluginKind::kLite; + + if (extensionType != 'm' && extensionType != 'p') + return PluginKind::kUnknown; + + // ESL-flagged .esp/.esm plugins still use light IDs. + if (HasEslFlag(aDataRoot / aFilename)) + return PluginKind::kLite; + + return PluginKind::kStandard; +} + +uint32_t FormIdPrefixFromPlugin(const ESLoader::PluginData& aPlugin) noexcept +{ + if (aPlugin.m_isLite) + return 0xFE000000u | (static_cast(aPlugin.m_liteId) << 12); + return static_cast(aPlugin.m_standardId) << 24; +} + +std::optional ParseMo2Instance(const fs::path& aRootPath, const fs::path& aIniPath, const char* aForcedProfile) +{ + std::ifstream iniStream(aIniPath.c_str()); + if (!iniStream.is_open()) + return std::nullopt; + + String currentSection; + String selectedProfile; + String gamePath; + String line; + + while (std::getline(iniStream, line)) + { + if (line.empty()) + continue; + + if (line.front() == '[' && line.back() == ']') + { + currentSection = line.substr(1, line.size() - 2); + continue; + } + + if (currentSection != "General") + continue; + + const auto delimiter = line.find('='); + if (delimiter == String::npos) + continue; + + String key = Trim(line.substr(0, delimiter)); + String value = Trim(line.substr(delimiter + 1)); + + if (key == "selected_profile") + selectedProfile = ParseMo2ByteArray(value); + else if (key == "gamePath") + gamePath = ParseMo2ByteArray(value); + + if (!selectedProfile.empty() && !gamePath.empty()) + break; + } + + const String configuredProfile = selectedProfile; + const String forcedProfile = (aForcedProfile && aForcedProfile[0] != '\0') ? String(aForcedProfile) : String{}; + + if (!forcedProfile.empty()) + { + selectedProfile = forcedProfile; + } + + if (selectedProfile.empty() || gamePath.empty()) + return std::nullopt; + + const fs::path dataRoot = NormalizeMo2Path(gamePath) / "Data"; + fs::path profilePath = aRootPath / "profiles" / selectedProfile; + std::error_code profileEc; + if (!fs::exists(profilePath, profileEc) || profileEc) + { + if (!configuredProfile.empty() && configuredProfile != selectedProfile) + { + if (!forcedProfile.empty()) + { + spdlog::warn("ESLoader: MO2 profile override '{}' not found, falling back to '{}'", forcedProfile.c_str(), configuredProfile.c_str()); + } + + profilePath = aRootPath / "profiles" / configuredProfile; + selectedProfile = configuredProfile; + profileEc.clear(); + } + } + + if (!fs::exists(profilePath, profileEc) || profileEc) + { + spdlog::warn("ESLoader: MO2 profile '{}' not found under {}", selectedProfile.c_str(), (aRootPath / "profiles").string()); + return std::nullopt; + } + + Mo2Context context{}; + context.dataPath = dataRoot; + context.loadOrderPath = profilePath / "loadorder.txt"; + context.pluginsPath = profilePath / "plugins.txt"; + context.profile = selectedProfile; + + return context; +} + +std::optional SearchMo2Root(fs::path start, const char* aForcedProfile, std::vector* aTrace) +{ + if (start.empty()) + return std::nullopt; + + std::error_code ec; + auto canonical = fs::weakly_canonical(start, ec); + if (!ec) + start = canonical; + else + { + canonical = fs::absolute(start, ec); + if (!ec) + start = canonical; + } + + fs::path search = start; + fs::path previous; + + while (true) + { + if (aTrace) + aTrace->push_back(search.string()); + + const fs::path iniPath = search / "ModOrganizer.ini"; + std::error_code existsEc; + if (fs::exists(iniPath, existsEc) && !existsEc) + { + if (auto context = ParseMo2Instance(search, iniPath, aForcedProfile)) + return context; + } + + if (!search.has_parent_path() || search == previous) + break; + + previous = search; + search = search.parent_path(); + } + + return std::nullopt; +} + +std::optional DetectMo2Instance() +{ + std::vector candidates; + std::vector trace; + + const char* envInstance = std::getenv("MO2_INSTANCE_DIR"); + const char* envProfile = std::getenv("MO2_PROFILE"); + + if (envInstance && envInstance[0] != '\0') + candidates.emplace_back(NormalizeMo2Path(envInstance)); + + if (auto exeDir = ResolveExecutableDirectory(); !exeDir.empty()) + candidates.push_back(exeDir); + + std::error_code ec; + auto cwd = fs::current_path(ec); + if (!ec) + candidates.push_back(cwd); + +#if defined(_WIN32) + if (const char* localAppData = std::getenv("LOCALAPPDATA"); localAppData && localAppData[0] != '\0') + { + candidates.emplace_back(fs::path(localAppData) / "ModOrganizer"); + candidates.emplace_back(fs::path(localAppData) / "ModOrganizer" / "Skyrim Special Edition"); + } +#endif + + for (const auto& root : candidates) + { + if (auto context = SearchMo2Root(root, envProfile, &trace)) + return context; + } + + if (!trace.empty()) + { + std::string message = "ESLoader: no Mod Organizer installation detected. searched paths: "; + for (size_t i = 0; i < trace.size(); ++i) + { + message += trace[i]; + if (i + 1 < trace.size()) + message += ", "; + } + spdlog::info(message); + } + + return std::nullopt; +} + +bool LoadPluginsTxtFromPath(const fs::path& aPluginsPath, const fs::path& aDataRoot, Vector& outOrder) +{ + std::ifstream pluginsFile(aPluginsPath.c_str()); + if (!pluginsFile.is_open()) + return false; + + uint8_t standardId = 0x0; + uint16_t liteId = 0x0; + + while (!pluginsFile.eof()) + { + String line; + std::getline(pluginsFile, line); + + if (line.empty() || line[0] == '#') + continue; + + if (!line.empty() && line[0] == '*') + line.erase(line.begin()); + + line.erase(std::remove(line.begin(), line.end(), '\r'), line.end()); + if (line.empty()) + continue; + + ESLoader::PluginData plugin{}; + plugin.m_filename = line; + const auto kind = ResolvePluginKind(aDataRoot, line); + switch (kind) + { + case PluginKind::kStandard: + plugin.m_standardId = standardId++; + plugin.m_isLite = false; + outOrder.push_back(plugin); + break; + case PluginKind::kLite: + plugin.m_liteId = liteId++; + plugin.m_isLite = true; + outOrder.push_back(plugin); + break; + default: + break; + } + } + + return !outOrder.empty(); +} + +bool LoadPluginsTxtFromLocalAppData(const fs::path& aDataRoot, Vector& outOrder) +{ +#if defined(_WIN32) + if (char* localAppData = std::getenv("LOCALAPPDATA")) + { + const fs::path pluginsPath = fs::path(localAppData) / "Skyrim Special Edition" / "plugins.txt"; + return LoadPluginsTxtFromPath(pluginsPath, aDataRoot, outOrder); + } +#endif + + return false; +} +} // namespace + namespace ESLoader { String ReadZString(Buffer::Reader& aReader) noexcept @@ -28,41 +440,119 @@ String ReadWString(Buffer::Reader& aReader) noexcept ESLoader::ESLoader() { - m_directory = fs::current_path() / "Data"; //< Keep upper case to match Skyrim's file system + if (auto context = DetectMo2Instance()) + { + m_directory = context->dataPath; + m_loadOrderFile = context->loadOrderPath; + m_pluginsFile = context->pluginsPath; + spdlog::info("Detected Mod Organizer 2 profile '{}' (Data: {}, loadorder: {}).", context->profile, m_directory.string(), m_loadOrderFile.string()); + } + else + { + m_directory = fs::current_path() / "Data"; //< Keep upper case to match Skyrim's file system + m_loadOrderFile = m_directory / "loadorder.txt"; + } } UniquePtr ESLoader::BuildRecordCollection() noexcept { if (!fs::is_directory(m_directory)) { - // spdlog::warn("Data directory not found."); - return nullptr; + spdlog::warn("Data directory not found at {} (MO2 VFS may still provide files)", m_directory.string()); + // Continue; we will attempt to open files directly via VFS paths. } if (!LoadLoadOrder()) { - return nullptr; + spdlog::warn("No load order found; will fallback to base ESM set if available."); } - return MakeUnique(); - - /* + // Load all plugins from discovered list and index records auto recordCollection = LoadFiles(); - recordCollection->BuildReferences(); + if (!recordCollection) + return nullptr; + + // Optional: build cross-record references if needed elsewhere + // recordCollection->BuildReferences(); return std::move(recordCollection); - */ } bool ESLoader::LoadLoadOrder() { + std::call_once(s_loadOrderOnceFlag, [this]() { + s_loadOrderSuccess = LoadLoadOrderFromDisk(); + if (s_loadOrderSuccess) + { + s_cachedLoadOrder = m_loadOrder; + s_cachedMasterFiles = m_masterFiles; + } + }); + + if (!s_loadOrderSuccess) + return false; + + m_loadOrder = s_cachedLoadOrder; + m_masterFiles = s_cachedMasterFiles; + return true; +} + +bool ESLoader::LoadLoadOrderFromDisk() +{ + m_loadOrder.clear(); + m_masterFiles.clear(); + std::ifstream loadOrderFile; - auto loadOrderPath = m_directory / "loadorder.txt"; - loadOrderFile.open(loadOrderPath.c_str()); + const fs::path loadOrderPath = !m_loadOrderFile.empty() ? m_loadOrderFile : (m_directory / "loadorder.txt"); + if (!loadOrderPath.empty()) + loadOrderFile.open(loadOrderPath.c_str()); + else + loadOrderFile.setstate(std::ios::failbit); + if (loadOrderFile.fail()) { - spdlog::warn("Failed to open loadorder.txt"); - return false; + bool loaded = false; + bool usedProfilePlugins = false; + + if (!m_pluginsFile.empty()) + { + loaded = LoadPluginsTxtFromPath(m_pluginsFile, m_directory, m_loadOrder); + usedProfilePlugins = loaded; + } + + if (!loaded) + loaded = LoadPluginsTxtFromLocalAppData(m_directory, m_loadOrder); + + if (loaded) + { + for (const auto& p : m_loadOrder) + { + m_masterFiles[p.m_filename] = FormIdPrefixFromPlugin(p); + } + + if (usedProfilePlugins) + spdlog::info("ESLoader: queued {} plugins from {}", m_loadOrder.size(), m_pluginsFile.string()); + else + spdlog::info("ESLoader: queued {} plugins from plugins.txt in LOCALAPPDATA", m_loadOrder.size()); + + return true; + } + // Fallback to base ESMs minimal set + spdlog::warn("Failed to open loadorder.txt at {}; falling back to base ESM set", loadOrderPath.string()); + Vector base = {"Skyrim.esm", "Update.esm", "Dawnguard.esm", "HearthFires.esm", "Dragonborn.esm"}; + uint8_t standardId = 0x0; + for (auto& name : base) + { + PluginData plugin; + plugin.m_filename = name; + plugin.m_standardId = standardId; + plugin.m_isLite = false; + m_loadOrder.push_back(plugin); + m_masterFiles[name] = FormIdPrefixFromPlugin(plugin); + standardId += 1; + } + spdlog::info("ESLoader: queued {} base-game plugins (fallback set)", m_loadOrder.size()); + return true; } uint8_t standardId = 0x0; @@ -72,36 +562,42 @@ bool ESLoader::LoadLoadOrder() { String line; std::getline(loadOrderFile, line); - if (line[0] == '#' || line.empty()) + if (line.empty() || line[0] == '#') + continue; + + // Trim CR (Linux/Windows) + line.erase(std::remove(line.begin(), line.end(), '\r'), line.end()); + if (line.empty()) continue; PluginData plugin; plugin.m_filename = line; - // On Linux, the carriage return won't be taken into account - line.erase(std::remove(line.begin(), line.end(), '\r'), line.end()); + const auto kind = ResolvePluginKind(m_directory, line); - char extensionType = line.back(); - - switch (extensionType) + switch (kind) { - case 'm': m_masterFiles[line] = standardId; - case 'p': + case PluginKind::kStandard: plugin.m_standardId = standardId; - standardId += 0x01; plugin.m_isLite = false; m_loadOrder.push_back(plugin); + m_masterFiles[line] = FormIdPrefixFromPlugin(plugin); + standardId += 0x01; break; - case 'l': + case PluginKind::kLite: plugin.m_liteId = liteId; - liteId += 0x0001; plugin.m_isLite = true; m_loadOrder.push_back(plugin); + m_masterFiles[line] = FormIdPrefixFromPlugin(plugin); + liteId += 0x0001; break; - default: spdlog::error("Extension in loadorder.txt not recognized: {}", line); + default: + spdlog::error("Extension in loadorder.txt not recognized: {}", line); } } + spdlog::info("ESLoader: queued {} plugins from {}", m_loadOrder.size(), loadOrderPath.string()); + return true; } @@ -137,14 +633,9 @@ UniquePtr ESLoader::LoadFiles() fs::path ESLoader::GetPath(String& aFilename) { - for (const auto& entry : fs::directory_iterator(m_directory)) - { - String filename = entry.path().filename().string().c_str(); - if (filename == aFilename) - return entry.path(); - } - - return fs::path(); + // Prefer direct path; MO2 VFS will usually resolve this without enumerating the directory + fs::path direct = m_directory / aFilename; + return direct; } } // namespace ESLoader diff --git a/Code/components/es_loader/ESLoader.h b/Code/components/es_loader/ESLoader.h index f9377c9d4..2fe102e34 100644 --- a/Code/components/es_loader/ESLoader.h +++ b/Code/components/es_loader/ESLoader.h @@ -1,6 +1,6 @@ #pragma once -#include +#include "TESFile.h" namespace fs = std::filesystem; @@ -36,12 +36,15 @@ class ESLoader private: bool LoadLoadOrder(); + bool LoadLoadOrderFromDisk(); UniquePtr LoadFiles(); fs::path GetPath(String& aFilename); - fs::path m_directory = ""; + fs::path m_directory{}; + fs::path m_loadOrderFile{}; + fs::path m_pluginsFile{}; Vector m_loadOrder{}; - TiltedPhoques::Map m_masterFiles{}; + TiltedPhoques::Map m_masterFiles{}; }; } // namespace ESLoader diff --git a/Code/components/es_loader/RecordCollection.h b/Code/components/es_loader/RecordCollection.h index 6ebd40e0a..7aa41397b 100644 --- a/Code/components/es_loader/RecordCollection.h +++ b/Code/components/es_loader/RecordCollection.h @@ -36,6 +36,9 @@ struct RecordCollection WRLD& GetWorldById(uint32_t aFormId) noexcept { return m_worlds[aFormId]; } NAVM& GetNavMeshById(uint32_t aFormId) noexcept { return m_navMeshes[aFormId]; } + // Accessors to iterate over records (read-only) + const Map& GetWorlds() const noexcept { return m_worlds; } + void BuildReferences(); private: diff --git a/Code/components/es_loader/Records/Chunks.cpp b/Code/components/es_loader/Records/Chunks.cpp index 2103cda49..d8538f4a3 100644 --- a/Code/components/es_loader/Records/Chunks.cpp +++ b/Code/components/es_loader/Records/Chunks.cpp @@ -2,9 +2,45 @@ #include "Record.h" #include +#include +#include namespace Chunks { +namespace +{ +uint32_t GetLimit(const char* aEnv, uint32_t aFallback) noexcept +{ + const char* env = std::getenv(aEnv); + if (!env || env[0] == '\0') + return aFallback; + char* end = nullptr; + const unsigned long long value = std::strtoull(env, &end, 10); + if (!end || end == env || value == 0) + return aFallback; + if (value > std::numeric_limits::max()) + return std::numeric_limits::max(); + return static_cast(value); +} + +uint32_t MaxVmadScripts() noexcept +{ + static uint32_t limit = GetLimit("ESLOADER_MAX_VMAD_SCRIPTS", 1024); + return limit; +} + +uint32_t MaxVmadProperties() noexcept +{ + static uint32_t limit = GetLimit("ESLOADER_MAX_VMAD_PROPERTIES", 4096); + return limit; +} + +uint32_t MaxVmadArrayElements() noexcept +{ + static uint32_t limit = GetLimit("ESLOADER_MAX_VMAD_ARRAY", 4096); + return limit; +} +} // namespace uint32_t ReadFormId(Buffer::Reader& aReader, Map& aParentToFormIdPrefix) { @@ -13,7 +49,8 @@ uint32_t ReadFormId(Buffer::Reader& aReader, Map& aParentToFo uint32_t realBaseId = ESLoader::TESFile::GetFormIdPrefix(formId, aParentToFormIdPrefix); - formId &= 0x00FFFFFF; + const uint32_t mask = ((realBaseId & 0xFF000000u) == 0xFE000000u) ? 0xFFFu : 0x00FFFFFFu; + formId &= mask; formId += realBaseId; return formId; @@ -25,6 +62,12 @@ VMAD::VMAD(Buffer::Reader& aReader, Map& aParentToFormIdPrefi aReader.ReadBytes(reinterpret_cast(&m_objectFormat), 2); aReader.ReadBytes(reinterpret_cast(&m_scriptCount), 2); + if (m_scriptCount > MaxVmadScripts()) + { + spdlog::error("VMAD script count {} exceeds limit {}", m_scriptCount, MaxVmadScripts()); + return; + } + m_scripts.reserve(m_scriptCount); for (uint16_t i = 0; i < m_scriptCount; i++) @@ -36,6 +79,12 @@ VMAD::VMAD(Buffer::Reader& aReader, Map& aParentToFormIdPrefi aReader.ReadBytes(&script.m_status, 1); aReader.ReadBytes(reinterpret_cast(&script.m_propertyCount), 2); + if (script.m_propertyCount > MaxVmadProperties()) + { + spdlog::error("VMAD property count {} exceeds limit {}", script.m_propertyCount, MaxVmadProperties()); + return; + } + for (uint16_t j = 0; j < script.m_propertyCount; j++) { ScriptProperty scriptProperty; @@ -45,7 +94,11 @@ VMAD::VMAD(Buffer::Reader& aReader, Map& aParentToFormIdPrefi aReader.ReadBytes(reinterpret_cast(&scriptProperty.m_type), 1); aReader.ReadBytes(reinterpret_cast(&scriptProperty.m_status), 1); - scriptProperty.ParseValue(aReader, m_objectFormat, aParentToFormIdPrefix); + if (!scriptProperty.ParseValue(aReader, m_objectFormat, aParentToFormIdPrefix)) + { + spdlog::error("VMAD property parse failed"); + return; + } script.m_properties.push_back(scriptProperty); } @@ -54,7 +107,7 @@ VMAD::VMAD(Buffer::Reader& aReader, Map& aParentToFormIdPrefi } } -void ScriptProperty::ParseValue(Buffer::Reader& aReader, int16_t aObjectFormat, Map& aParentToFormIdPrefix) noexcept +bool ScriptProperty::ParseValue(Buffer::Reader& aReader, int16_t aObjectFormat, Map& aParentToFormIdPrefix) noexcept { switch (m_type) { @@ -91,17 +144,25 @@ void ScriptProperty::ParseValue(Buffer::Reader& aReader, int16_t aObjectFormat, { uint32_t sizeOfArray = 0; aReader.ReadBytes(reinterpret_cast(&sizeOfArray), 4); + if (sizeOfArray > MaxVmadArrayElements()) + { + spdlog::error("VMAD array size {} exceeds limit {}", sizeOfArray, MaxVmadArrayElements()); + return false; + } for (uint32_t i = 0; i < sizeOfArray; i++) { ScriptProperty scriptProperty; scriptProperty.m_type = GetPropertyType(m_type); - ParseValue(aReader, aObjectFormat, aParentToFormIdPrefix); + if (!scriptProperty.ParseValue(aReader, aObjectFormat, aParentToFormIdPrefix)) + return false; m_dataArray.push_back(scriptProperty.m_dataSingleValue); } break; } } + + return true; } ScriptProperty::Type ScriptProperty::GetPropertyType(Type aArrayType) noexcept diff --git a/Code/components/es_loader/Records/Chunks.h b/Code/components/es_loader/Records/Chunks.h index 7ba8012b2..aae83ab7f 100644 --- a/Code/components/es_loader/Records/Chunks.h +++ b/Code/components/es_loader/Records/Chunks.h @@ -42,7 +42,7 @@ struct ScriptProperty } m_string{nullptr, 0}; }; - void ParseValue(Buffer::Reader& aReader, int16_t aObjectFormat, TiltedPhoques::Map& aParentToFormIdPrefix) noexcept; + bool ParseValue(Buffer::Reader& aReader, int16_t aObjectFormat, TiltedPhoques::Map& aParentToFormIdPrefix) noexcept; Type GetPropertyType(Type aArrayType) noexcept; String m_name; @@ -330,6 +330,24 @@ struct DNAM float m_waterLevel; }; +// ONAM: Relation of this world to its parent for map markers +struct ONAM +{ + ONAM() {} + ONAM(Buffer::Reader& aReader) + { + aReader.ReadBytes(reinterpret_cast(&m_worldMapScale), sizeof(float)); + aReader.ReadBytes(reinterpret_cast(&m_cellOffsetX4096), sizeof(float)); + aReader.ReadBytes(reinterpret_cast(&m_cellOffsetY4096), sizeof(float)); + aReader.ReadBytes(reinterpret_cast(&m_cellOffsetZ4096), sizeof(float)); + } + + float m_worldMapScale{}; // -1 = hide markers + float m_cellOffsetX4096{}; // Cell X Offset * 4096 (in game units) + float m_cellOffsetY4096{}; // Cell Y Offset * 4096 (in game units) + float m_cellOffsetZ4096{}; // Cell Z Offset * 4096 (in game units) +}; + struct NVNM { NVNM() {} diff --git a/Code/components/es_loader/Records/Record.cpp b/Code/components/es_loader/Records/Record.cpp index c97054a63..79da1d686 100644 --- a/Code/components/es_loader/Records/Record.cpp +++ b/Code/components/es_loader/Records/Record.cpp @@ -1,7 +1,43 @@ #include "Record.h" +#include +#include +#include #include +namespace +{ +size_t GetMaxDecompressedSize() noexcept +{ + static size_t limit = []() { + constexpr size_t kDefaultLimit = 256 * 1024 * 1024; // 256 MiB + const char* env = std::getenv("ESLOADER_MAX_DECOMPRESS"); + if (!env || env[0] == '\0') + return kDefaultLimit; + char* end = nullptr; + const unsigned long long value = std::strtoull(env, &end, 10); + if (!end || end == env || value == 0) + return kDefaultLimit; + if (value > std::numeric_limits::max()) + return std::numeric_limits::max(); + return static_cast(value); + }(); + return limit; +} + +std::string FourCC(uint32_t aValue) +{ + char text[5] = { + static_cast(aValue & 0xFF), + static_cast((aValue >> 8) & 0xFF), + static_cast((aValue >> 16) & 0xFF), + static_cast((aValue >> 24) & 0xFF), + '\0', + }; + return std::string(text); +} +} // namespace + void Record::CopyRecordData(Record& aRhs) { m_formType = aRhs.m_formType; @@ -15,7 +51,8 @@ void Record::CopyRecordData(Record& aRhs) void Record::SetBaseId(uint32_t aBaseId) { - m_formId &= 0x00FFFFFF; + const uint32_t mask = ((aBaseId & 0xFF000000u) == 0xFE000000u) ? 0xFFFu : 0x00FFFFFFu; + m_formId &= mask; m_formId += aBaseId; } @@ -27,8 +64,20 @@ void Record::IterateChunks(const std::function& Buffer pDecompressed; if (Compressed()) { + if (m_dataSize < 4) + { + spdlog::error("Record {} form {:08X} has invalid compressed size {}", FourCC(static_cast(m_formType)), m_formId, m_dataSize); + return; + } + uint32_t size = 0; reader.ReadBytes(reinterpret_cast(&size), 4); + const size_t maxSize = GetMaxDecompressedSize(); + if (size == 0 || size > maxSize) + { + spdlog::error("Record {} form {:08X} requested {} bytes (limit {})", FourCC(static_cast(m_formType)), m_formId, size, maxSize); + return; + } pDecompressed.Resize(size); const uint32_t fieldSize = m_dataSize - 4; @@ -38,9 +87,17 @@ void Record::IterateChunks(const std::function& } uint32_t largeDataSize = 0; + size_t chunkCount = 0; + const size_t readerLimit = Compressed() ? pDecompressed.GetSize() : m_dataSize; while (!reader.Eof()) { + const size_t chunkHeaderPos = reader.GetBytePosition(); + if (chunkHeaderPos + sizeof(Record::Chunk) > readerLimit) + { + spdlog::error("Record {} form {:08X} chunk header overflows buffer at {}", FourCC(static_cast(m_formType)), m_formId, chunkHeaderPos); + break; + } Record::Chunk* pChunk = reinterpret_cast(reader.GetDataAtPosition()); reader.Advance(sizeof(Record::Chunk)); @@ -49,21 +106,45 @@ void Record::IterateChunks(const std::function& { dataSize = largeDataSize; } + if (dataSize == 0) + { + break; + } // Chunk XXXX will have the next data size stored at the start of the field // Doesn't ever seem to trigger, but it's in the spec so might as well leave it in // https://en.uesp.net/wiki/Skyrim_Mod:Mod_File_Format#Fields if (pChunk->m_chunkId == ChunkId::XXXX_ID) { + if (reader.GetBytePosition() + 4 > readerLimit) + { + spdlog::error("Record {} form {:08X} XXXX chunk overflows buffer at {}", FourCC(static_cast(m_formType)), m_formId, reader.GetBytePosition()); + break; + } reader.ReadBytes(reinterpret_cast(&largeDataSize), 4); reader.Reverse(4); } + const size_t chunkDataPos = reader.GetBytePosition(); + const size_t remaining = (chunkDataPos <= readerLimit) ? (readerLimit - chunkDataPos) : 0; + if (dataSize > remaining) + { + spdlog::error("Record {} form {:08X} chunk {} size {} exceeds remaining {}", FourCC(static_cast(m_formType)), + m_formId, FourCC(static_cast(pChunk->m_chunkId)), dataSize, remaining); + break; + } + Buffer::Reader chunk(reader); reader.Advance(dataSize); aCallback(pChunk->m_chunkId, chunk); + + if (++chunkCount > 1000000) + { + spdlog::error("Record {} form {:08X} exceeded max chunk count", FourCC(static_cast(m_formType)), m_formId); + break; + } } } diff --git a/Code/components/es_loader/Records/Record.h b/Code/components/es_loader/Records/Record.h index 49f486985..50db2525d 100644 --- a/Code/components/es_loader/Records/Record.h +++ b/Code/components/es_loader/Records/Record.h @@ -16,6 +16,7 @@ class Record enum FLAGS { kMasterFile = 1, + kLightFile = 0x200, kCompressed = 0x40000, kIgnored = 0x1000, kIsMarker = 0x800000, diff --git a/Code/components/es_loader/Records/WRLD.cpp b/Code/components/es_loader/Records/WRLD.cpp index 9d22baf33..30cd8626f 100644 --- a/Code/components/es_loader/Records/WRLD.cpp +++ b/Code/components/es_loader/Records/WRLD.cpp @@ -21,6 +21,7 @@ void WRLD::ParseChunks(WRLD& aSourceRecord, Map& aParentToFor m_parentId = id; } break; + case ChunkId::ONAM_ID: m_onam = Chunks::ONAM(aReader); break; case ChunkId::ZNAM_ID: aReader.ReadBytes(reinterpret_cast(&m_musicId), sizeof(m_musicId)); break; case ChunkId::NAMA_ID: aReader.ReadBytes(reinterpret_cast(&m_lodMultiplier), sizeof(m_lodMultiplier)); break; } diff --git a/Code/components/es_loader/Records/WRLD.h b/Code/components/es_loader/Records/WRLD.h index a5cc9f83e..93607dd03 100644 --- a/Code/components/es_loader/Records/WRLD.h +++ b/Code/components/es_loader/Records/WRLD.h @@ -15,7 +15,8 @@ class WRLD : public Record std::optional m_centerCell; std::optional m_climateId; std::optional m_landData; - std::optional m_parentId; + std::optional m_parentId; // WNAM: Parent worldspace formID + std::optional m_onam; // ONAM: Map marker relation to parent (scale + cell offsets) uint32_t m_musicId; float m_lodMultiplier; diff --git a/Code/components/es_loader/TESFile.cpp b/Code/components/es_loader/TESFile.cpp index c794e245f..014fa221b 100644 --- a/Code/components/es_loader/TESFile.cpp +++ b/Code/components/es_loader/TESFile.cpp @@ -2,10 +2,26 @@ #include #include +#include namespace ESLoader { -TESFile::TESFile(Map& aMasterFiles) +namespace +{ +std::string FourCC(uint32_t aValue) +{ + char text[5] = { + static_cast(aValue & 0xFF), + static_cast((aValue >> 8) & 0xFF), + static_cast((aValue >> 16) & 0xFF), + static_cast((aValue >> 24) & 0xFF), + '\0', + }; + return std::string(text); +} +} // namespace + +TESFile::TESFile(Map& aMasterFiles) : m_masterFiles(aMasterFiles) { } @@ -26,17 +42,23 @@ bool TESFile::LoadFile(const std::filesystem::path& acPath) noexcept { m_filename = acPath.filename().string(); - const uintmax_t fileSize = std::filesystem::file_size(acPath); - m_buffer.Resize(fileSize); - - std::ifstream file(acPath, std::ios::binary); + std::ifstream file(acPath, std::ios::binary | std::ios::ate); if (file.fail()) { - spdlog::error("Failed to open plugin {}", m_filename); + spdlog::warn("Failed to open plugin {} ({}). Skipping.", m_filename, acPath.string()); return false; } + const auto fileSize = static_cast(file.tellg()); + file.seekg(0, std::ios::beg); + m_buffer.Resize(fileSize); + file.read(reinterpret_cast(m_buffer.GetWriteData()), fileSize); + if (!file) + { + spdlog::error("Short read while loading plugin {} ({}).", m_filename, acPath.string()); + return false; + } return true; } @@ -62,6 +84,8 @@ bool TESFile::ReadGroupOrRecord(Buffer::Reader& aReader, RecordCollection& aReco if (aReader.Eof()) return false; + const size_t recordOffset = aReader.GetBytePosition(); + const size_t bufferSize = m_buffer.GetSize(); uint32_t type = 0; aReader.ReadBytes(reinterpret_cast(&type), 4); uint32_t size = 0; @@ -70,16 +94,38 @@ bool TESFile::ReadGroupOrRecord(Buffer::Reader& aReader, RecordCollection& aReco if (type == static_cast(FormEnum::GRUP)) { - const size_t endOfGroup = aReader.GetBytePosition() + size; + size_t endOfGroup = recordOffset + size; + if (endOfGroup > bufferSize) + { + spdlog::warn("ESLoader: {} GRUP size {} exceeds file size {}; clamping", m_filename.c_str(), size, bufferSize); + endOfGroup = bufferSize; + } aReader.Advance(sizeof(Group)); while (aReader.GetBytePosition() < endOfGroup) { - ReadGroupOrRecord(aReader, aRecordCollection); + const size_t before = aReader.GetBytePosition(); + if (!ReadGroupOrRecord(aReader, aRecordCollection)) + { + spdlog::warn("ESLoader: {} GRUP parse hit EOF at {} of {}", m_filename.c_str(), before, endOfGroup); + break; + } + const size_t after = aReader.GetBytePosition(); + if (after <= before) + { + spdlog::warn("ESLoader: {} GRUP parse made no progress at {}", m_filename.c_str(), before); + break; + } } } else // Records { + if (recordOffset + sizeof(Record) + size > bufferSize) + { + spdlog::warn("ESLoader: {} record {} size {} overflows file at {}", m_filename.c_str(), FourCC(type), size, recordOffset); + return false; + } + Record* pRecord = reinterpret_cast(m_buffer.GetWriteData() + aReader.GetBytePosition()); switch (pRecord->GetType()) @@ -95,11 +141,12 @@ bool TESFile::ReadGroupOrRecord(Buffer::Reader& aReader, RecordCollection& aReco uint8_t parentId = 0; for (const Chunks::MAST& master : fileHeader.m_masterFiles) { - m_parentToFormIdPrefix[parentId] = ((uint32_t)m_masterFiles[master.m_masterName]) << 24; + m_parentToFormIdPrefix[parentId] = m_masterFiles[master.m_masterName]; parentId++; } m_parentToFormIdPrefix[parentId] = m_formIdPrefix; + m_parentToFormIdPrefix[0xFE] = m_formIdPrefix; break; } diff --git a/Code/components/es_loader/TESFile.h b/Code/components/es_loader/TESFile.h index 53791f1d3..194de05d0 100644 --- a/Code/components/es_loader/TESFile.h +++ b/Code/components/es_loader/TESFile.h @@ -15,7 +15,7 @@ class TESFile { public: TESFile() = default; - TESFile(TiltedPhoques::Map& aMasterFiles); + TESFile(TiltedPhoques::Map& aMasterFiles); void Setup(uint8_t aStandardId); void Setup(uint16_t aLiteId); @@ -41,7 +41,7 @@ class TESFile }; uint32_t m_formIdPrefix = 0; - TiltedPhoques::Map& m_masterFiles; + TiltedPhoques::Map& m_masterFiles; TiltedPhoques::Map m_parentToFormIdPrefix{}; }; diff --git a/Code/encoding/ChatMessageTypes.h b/Code/encoding/ChatMessageTypes.h index 30b386814..fc198925f 100644 --- a/Code/encoding/ChatMessageTypes.h +++ b/Code/encoding/ChatMessageTypes.h @@ -6,5 +6,6 @@ enum ChatMessageType : unsigned char kGlobalChat, kPlayerDialogue, kPartyChat, - kLocalChat + kLocalChat, + kWhisper }; diff --git a/Code/encoding/Messages/AuthenticationRequest.cpp b/Code/encoding/Messages/AuthenticationRequest.cpp index 97d1fb712..77ef011ec 100644 --- a/Code/encoding/Messages/AuthenticationRequest.cpp +++ b/Code/encoding/Messages/AuthenticationRequest.cpp @@ -9,6 +9,7 @@ void AuthenticationRequest::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) Serialization::WriteString(aWriter, Version); UserMods.Serialize(aWriter); Serialization::WriteString(aWriter, Username); + Serialization::WriteString(aWriter, Password); WorldSpaceId.Serialize(aWriter); CellId.Serialize(aWriter); Serialization::WriteVarInt(aWriter, Level); @@ -26,6 +27,7 @@ void AuthenticationRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReade Version = Serialization::ReadString(aReader); UserMods.Deserialize(aReader); Username = Serialization::ReadString(aReader); + Password = Serialization::ReadString(aReader); WorldSpaceId.Deserialize(aReader); CellId.Deserialize(aReader); Level = Serialization::ReadVarInt(aReader) & 0xFFFF; diff --git a/Code/encoding/Messages/AuthenticationRequest.h b/Code/encoding/Messages/AuthenticationRequest.h index 8fbec27dc..858794f2a 100644 --- a/Code/encoding/Messages/AuthenticationRequest.h +++ b/Code/encoding/Messages/AuthenticationRequest.h @@ -22,7 +22,7 @@ struct AuthenticationRequest final : ClientMessage bool operator==(const AuthenticationRequest& achRhs) const noexcept { - return GetOpcode() == achRhs.GetOpcode() && DiscordId == achRhs.DiscordId && SKSEActive == achRhs.SKSEActive && MO2Active == achRhs.MO2Active && Token == achRhs.Token && Version == achRhs.Version && UserMods == achRhs.UserMods && Username == achRhs.Username && + return GetOpcode() == achRhs.GetOpcode() && DiscordId == achRhs.DiscordId && SKSEActive == achRhs.SKSEActive && MO2Active == achRhs.MO2Active && Token == achRhs.Token && Version == achRhs.Version && UserMods == achRhs.UserMods && Username == achRhs.Username && Password == achRhs.Password && WorldSpaceId == achRhs.WorldSpaceId && CellId == achRhs.CellId && Level == achRhs.Level && PlayerTime == achRhs.PlayerTime; } @@ -34,6 +34,7 @@ struct AuthenticationRequest final : ClientMessage String Version{}; Mods UserMods{}; String Username{}; + String Password{}; GameId WorldSpaceId{}; GameId CellId{}; uint16_t Level{}; diff --git a/Code/encoding/Messages/AuthenticationResponse.h b/Code/encoding/Messages/AuthenticationResponse.h index 97bfb30fb..d7e943235 100644 --- a/Code/encoding/Messages/AuthenticationResponse.h +++ b/Code/encoding/Messages/AuthenticationResponse.h @@ -14,7 +14,9 @@ struct AuthenticationResponse final : ServerMessage kWrongVersion, kModsMismatch, kClientModsDisallowed, - kWrongPassword, + kWrongAccountPassword, + kWrongServerPassword, + kDuplicateUser, kServerFull }; diff --git a/Code/encoding/Messages/CancelEmoteRequest.cpp b/Code/encoding/Messages/CancelEmoteRequest.cpp new file mode 100644 index 000000000..056dea155 --- /dev/null +++ b/Code/encoding/Messages/CancelEmoteRequest.cpp @@ -0,0 +1,13 @@ +#include + +void CancelEmoteRequest::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, ServerId); +} + +void CancelEmoteRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ClientMessage::DeserializeRaw(aReader); + + ServerId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; +} diff --git a/Code/encoding/Messages/CancelEmoteRequest.h b/Code/encoding/Messages/CancelEmoteRequest.h new file mode 100644 index 000000000..fb437a99c --- /dev/null +++ b/Code/encoding/Messages/CancelEmoteRequest.h @@ -0,0 +1,20 @@ +#pragma once + +#include "Message.h" + +struct CancelEmoteRequest final : ClientMessage +{ + static constexpr ClientOpcode Opcode = kCancelEmoteRequest; + + CancelEmoteRequest() + : ClientMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const CancelEmoteRequest& acRhs) const noexcept { return ServerId == acRhs.ServerId && GetOpcode() == acRhs.GetOpcode(); } + + uint32_t ServerId{}; +}; diff --git a/Code/encoding/Messages/ClientMessageFactory.h b/Code/encoding/Messages/ClientMessageFactory.h index 30e7984cd..a6e031842 100644 --- a/Code/encoding/Messages/ClientMessageFactory.h +++ b/Code/encoding/Messages/ClientMessageFactory.h @@ -9,6 +9,11 @@ #include #include #include +#include +#include +#include +#include +#include #include #include #include @@ -17,6 +22,11 @@ #include #include #include +#include +#include +#include +#include +#include #include #include #include @@ -51,11 +61,24 @@ #include #include #include +#include #include #include #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include using TiltedPhoques::UniquePtr; @@ -66,11 +89,11 @@ struct ClientMessageFactory template static auto Visit(T&& func) { 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, + AuthenticationRequest, AssignCharacterRequest, CancelAssignmentRequest, ClientReferencesMoveRequest, EnterInteriorCellRequest, RequestInventoryChanges, RequestActorDrop, RequestPickupDroppedItem, RequestDroppedItems, RequestDroppedItemMove, RequestDroppedItemPhysicsDisabled, RequestFactionsChanges, RequestQuestUpdate, PartyInviteRequest, PartyAcceptInviteRequest, PartyLeaveRequest, PartyCreateRequest, + PartyChangeLeaderRequest, PartyKickRequest, TradeInviteRequest, TradeInviteResponseRequest, TradeOfferUpdateRequest, TradeSetReadyRequest, TradeCancelRequest, 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, RemoveSpellRequest, SetTimeCommandRequest>; + RequestEquipmentChanges, SendChatMessageRequest, TeleportCommandRequest, PlayerRespawnRequest, DialogueRequest, SubtitleRequest, PlayerDialogueRequest, PlayerLevelRequest, TeleportRequest, TeleportResponse, RequestPlayerHealthUpdate, RequestWeatherChange, RequestCurrentWeather, RequestSetWaypoint, + RequestRemoveWaypoint, PartyPositionsRequest, PartyPositionUpdateRequest, PartyMemberDownedRequest, PartyActorNamesRequest, PlayerActorNameUpdateRequest, PlayerProfileImageUpdateRequest, RemoveSpellRequest, SetTimeCommandRequest, HealingProximityRequest, PartyFastTravelMarkersRequest, PartyOptionsUpdateRequest, RequestSetSyncMode, PlayEmoteRequest, CancelEmoteRequest>; return s_visitor(std::forward(func)); } diff --git a/Code/encoding/Messages/HealingProximityRequest.cpp b/Code/encoding/Messages/HealingProximityRequest.cpp new file mode 100644 index 000000000..402421d7f --- /dev/null +++ b/Code/encoding/Messages/HealingProximityRequest.cpp @@ -0,0 +1,25 @@ +#include + +void HealingProximityRequest::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, CasterId); + Serialization::WriteFloat(aWriter, CasterX); + Serialization::WriteFloat(aWriter, CasterY); + Serialization::WriteFloat(aWriter, CasterZ); + Serialization::WriteVarInt(aWriter, SpellFormId.ModId); + Serialization::WriteVarInt(aWriter, SpellFormId.BaseId); + Serialization::WriteVarInt(aWriter, CasterRestorationLevel); +} + +void HealingProximityRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ClientMessage::DeserializeRaw(aReader); + + CasterId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; + CasterX = Serialization::ReadFloat(aReader); + CasterY = Serialization::ReadFloat(aReader); + CasterZ = Serialization::ReadFloat(aReader); + SpellFormId.ModId = Serialization::ReadVarInt(aReader) & 0xFF; + SpellFormId.BaseId = Serialization::ReadVarInt(aReader) & 0xFFFFFF; + CasterRestorationLevel = Serialization::ReadVarInt(aReader) & 0xFFFF; +} diff --git a/Code/encoding/Messages/HealingProximityRequest.h b/Code/encoding/Messages/HealingProximityRequest.h new file mode 100644 index 000000000..4636c6664 --- /dev/null +++ b/Code/encoding/Messages/HealingProximityRequest.h @@ -0,0 +1,35 @@ +#pragma once + +#include "Message.h" +#include + +struct HealingProximityRequest final : ClientMessage +{ + static constexpr ClientOpcode Opcode = kRequestHealingProximity; + + HealingProximityRequest() + : ClientMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const HealingProximityRequest& acRhs) const noexcept + { + return CasterId == acRhs.CasterId && + CasterX == acRhs.CasterX && + CasterY == acRhs.CasterY && + CasterZ == acRhs.CasterZ && + SpellFormId == acRhs.SpellFormId && + CasterRestorationLevel == acRhs.CasterRestorationLevel && + GetOpcode() == acRhs.GetOpcode(); + } + + uint32_t CasterId; + float CasterX; + float CasterY; + float CasterZ; + GameId SpellFormId{}; + uint16_t CasterRestorationLevel = 0; +}; diff --git a/Code/encoding/Messages/NotifyActorDrop.cpp b/Code/encoding/Messages/NotifyActorDrop.cpp new file mode 100644 index 000000000..bb8ac03c2 --- /dev/null +++ b/Code/encoding/Messages/NotifyActorDrop.cpp @@ -0,0 +1,48 @@ +#include +#include + +void NotifyActorDrop::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, ServerId); + Serialization::WriteVarInt(aWriter, ActorFormId); + Item.Serialize(aWriter); + Serialization::WriteVarInt(aWriter, DropId); + Serialization::WriteVarInt(aWriter, SpawnEpoch); + Serialization::WriteBool(aWriter, HasClientDropId); + if (HasClientDropId) + ClientDropId.Serialize(aWriter); + Serialization::WriteBool(aWriter, HasLocation); + if (HasLocation) + Location.Serialize(aWriter); + Serialization::WriteBool(aWriter, HasRotation); + if (HasRotation) + Rotation.Serialize(aWriter); + + CellId.Serialize(aWriter); + WorldSpaceId.Serialize(aWriter); + ReferenceId.Serialize(aWriter); +} + +void NotifyActorDrop::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ServerMessage::DeserializeRaw(aReader); + + ServerId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; + ActorFormId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; + Item.Deserialize(aReader); + DropId = Serialization::ReadVarInt(aReader); + SpawnEpoch = Serialization::ReadVarInt(aReader); + HasClientDropId = Serialization::ReadBool(aReader); + if (HasClientDropId) + ClientDropId.Deserialize(aReader); + HasLocation = Serialization::ReadBool(aReader); + if (HasLocation) + Location.Deserialize(aReader); + HasRotation = Serialization::ReadBool(aReader); + if (HasRotation) + Rotation.Deserialize(aReader); + + CellId.Deserialize(aReader); + WorldSpaceId.Deserialize(aReader); + ReferenceId.Deserialize(aReader); +} diff --git a/Code/encoding/Messages/NotifyActorDrop.h b/Code/encoding/Messages/NotifyActorDrop.h new file mode 100644 index 000000000..09f01823f --- /dev/null +++ b/Code/encoding/Messages/NotifyActorDrop.h @@ -0,0 +1,42 @@ +#pragma once + +#include "Message.h" +#include +#include +#include +#include + +struct NotifyActorDrop final : ServerMessage +{ + static constexpr ServerOpcode Opcode = kNotifyActorDrop; + + NotifyActorDrop() + : ServerMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const NotifyActorDrop& acRhs) const noexcept + { + return GetOpcode() == acRhs.GetOpcode() && ServerId == acRhs.ServerId && ActorFormId == acRhs.ActorFormId && Item == acRhs.Item && DropId == acRhs.DropId && SpawnEpoch == acRhs.SpawnEpoch && HasLocation == acRhs.HasLocation && + (!HasLocation || Location == acRhs.Location) && HasRotation == acRhs.HasRotation && (!HasRotation || Rotation == acRhs.Rotation) && HasClientDropId == acRhs.HasClientDropId && + (!HasClientDropId || ClientDropId == acRhs.ClientDropId) && CellId == acRhs.CellId && WorldSpaceId == acRhs.WorldSpaceId && ReferenceId == acRhs.ReferenceId; + } + + uint32_t ServerId{}; + uint32_t ActorFormId{}; + Inventory::Entry Item{}; + uint64_t DropId{}; + uint64_t SpawnEpoch{}; + bool HasClientDropId = false; + Guid ClientDropId{}; + bool HasLocation = false; + Vector3_NetQuantize Location{}; + bool HasRotation = false; + Vector3_NetQuantize Rotation{}; + GameId CellId{}; + GameId WorldSpaceId{}; + GameId ReferenceId{}; +}; diff --git a/Code/encoding/Messages/NotifyCancelEmote.cpp b/Code/encoding/Messages/NotifyCancelEmote.cpp new file mode 100644 index 000000000..026c5e67e --- /dev/null +++ b/Code/encoding/Messages/NotifyCancelEmote.cpp @@ -0,0 +1,13 @@ +#include + +void NotifyCancelEmote::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, ServerId); +} + +void NotifyCancelEmote::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ServerMessage::DeserializeRaw(aReader); + + ServerId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; +} diff --git a/Code/encoding/Messages/NotifyCancelEmote.h b/Code/encoding/Messages/NotifyCancelEmote.h new file mode 100644 index 000000000..983739b1d --- /dev/null +++ b/Code/encoding/Messages/NotifyCancelEmote.h @@ -0,0 +1,20 @@ +#pragma once + +#include "Message.h" + +struct NotifyCancelEmote final : ServerMessage +{ + static constexpr ServerOpcode Opcode = kNotifyCancelEmote; + + NotifyCancelEmote() + : ServerMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const NotifyCancelEmote& acRhs) const noexcept { return ServerId == acRhs.ServerId && GetOpcode() == acRhs.GetOpcode(); } + + uint32_t ServerId{}; +}; diff --git a/Code/encoding/Messages/NotifyCommandList.cpp b/Code/encoding/Messages/NotifyCommandList.cpp new file mode 100644 index 000000000..e8c03eba0 --- /dev/null +++ b/Code/encoding/Messages/NotifyCommandList.cpp @@ -0,0 +1,30 @@ +#include +#include + +void NotifyCommandList::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, Commands.size()); + + for (const auto& command : Commands) + { + Serialization::WriteString(aWriter, command.Name); + Serialization::WriteString(aWriter, command.Description); + } +} + +void NotifyCommandList::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ServerMessage::DeserializeRaw(aReader); + + const auto count = Serialization::ReadVarInt(aReader) & 0xFFFF; + Commands.clear(); + Commands.reserve(count); + + for (auto i = 0u; i < count; ++i) + { + CommandEntry entry{}; + entry.Name = Serialization::ReadString(aReader); + entry.Description = Serialization::ReadString(aReader); + Commands.push_back(std::move(entry)); + } +} diff --git a/Code/encoding/Messages/NotifyCommandList.h b/Code/encoding/Messages/NotifyCommandList.h new file mode 100644 index 000000000..52d7adbfb --- /dev/null +++ b/Code/encoding/Messages/NotifyCommandList.h @@ -0,0 +1,35 @@ +#pragma once + +#include "Message.h" + +using TiltedPhoques::String; + +struct NotifyCommandList final : ServerMessage +{ + static constexpr ServerOpcode Opcode = kNotifyCommandList; + + NotifyCommandList() + : ServerMessage(Opcode) + { + } + + virtual ~NotifyCommandList() = default; + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + struct CommandEntry + { + String Name{}; + String Description{}; + + bool operator==(const CommandEntry& aRhs) const noexcept + { + return Name == aRhs.Name && Description == aRhs.Description; + } + }; + + bool operator==(const NotifyCommandList& acRhs) const noexcept { return Commands == acRhs.Commands && GetOpcode() == acRhs.GetOpcode(); } + + TiltedPhoques::Vector Commands{}; +}; diff --git a/Code/encoding/Messages/NotifyDroppedItemMove.cpp b/Code/encoding/Messages/NotifyDroppedItemMove.cpp new file mode 100644 index 000000000..00921f59f --- /dev/null +++ b/Code/encoding/Messages/NotifyDroppedItemMove.cpp @@ -0,0 +1,41 @@ +#include + +void NotifyDroppedItemMove::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, DropId); + Serialization::WriteBool(aWriter, HasLocation); + if (HasLocation) + Location.Serialize(aWriter); + Serialization::WriteBool(aWriter, HasRotation); + if (HasRotation) + Rotation.Serialize(aWriter); + Serialization::WriteBool(aWriter, HasVelocity); + if (HasVelocity) + Velocity.Serialize(aWriter); + Serialization::WriteBool(aWriter, HasAngularVelocity); + if (HasAngularVelocity) + AngularVelocity.Serialize(aWriter); + CellId.Serialize(aWriter); + WorldSpaceId.Serialize(aWriter); + ReferenceId.Serialize(aWriter); +} + +void NotifyDroppedItemMove::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + DropId = Serialization::ReadVarInt(aReader); + HasLocation = Serialization::ReadBool(aReader); + if (HasLocation) + Location.Deserialize(aReader); + HasRotation = Serialization::ReadBool(aReader); + if (HasRotation) + Rotation.Deserialize(aReader); + HasVelocity = Serialization::ReadBool(aReader); + if (HasVelocity) + Velocity.Deserialize(aReader); + HasAngularVelocity = Serialization::ReadBool(aReader); + if (HasAngularVelocity) + AngularVelocity.Deserialize(aReader); + CellId.Deserialize(aReader); + WorldSpaceId.Deserialize(aReader); + ReferenceId.Deserialize(aReader); +} diff --git a/Code/encoding/Messages/NotifyDroppedItemMove.h b/Code/encoding/Messages/NotifyDroppedItemMove.h new file mode 100644 index 000000000..9d6e815fb --- /dev/null +++ b/Code/encoding/Messages/NotifyDroppedItemMove.h @@ -0,0 +1,48 @@ +#pragma once + +#include "Message.h" +#include +#include + +struct NotifyDroppedItemMove final : ServerMessage +{ + static constexpr ServerOpcode Opcode = kNotifyDroppedItemMove; + + NotifyDroppedItemMove() + : ServerMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const NotifyDroppedItemMove& acRhs) const noexcept + { + return GetOpcode() == acRhs.GetOpcode() + && DropId == acRhs.DropId + && HasLocation == acRhs.HasLocation + && (!HasLocation || Location == acRhs.Location) + && HasRotation == acRhs.HasRotation + && (!HasRotation || Rotation == acRhs.Rotation) + && HasVelocity == acRhs.HasVelocity + && (!HasVelocity || Velocity == acRhs.Velocity) + && HasAngularVelocity == acRhs.HasAngularVelocity + && (!HasAngularVelocity || AngularVelocity == acRhs.AngularVelocity) + && CellId == acRhs.CellId + && WorldSpaceId == acRhs.WorldSpaceId + && ReferenceId == acRhs.ReferenceId; + } + + uint64_t DropId{}; + bool HasLocation{false}; + Vector3_NetQuantize Location{}; + bool HasRotation{false}; + Vector3_NetQuantize Rotation{}; + bool HasVelocity{false}; + Vector3_NetQuantize Velocity{}; + bool HasAngularVelocity{false}; + Vector3_NetQuantize AngularVelocity{}; + GameId CellId{}; + GameId WorldSpaceId{}; + GameId ReferenceId{}; +}; diff --git a/Code/encoding/Messages/NotifyDroppedItemPhysicsDisabled.cpp b/Code/encoding/Messages/NotifyDroppedItemPhysicsDisabled.cpp new file mode 100644 index 000000000..1ad1a5c9a --- /dev/null +++ b/Code/encoding/Messages/NotifyDroppedItemPhysicsDisabled.cpp @@ -0,0 +1,29 @@ +#include + +void NotifyDroppedItemPhysicsDisabled::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, DropId); + Serialization::WriteBool(aWriter, HasLocation); + if (HasLocation) + Location.Serialize(aWriter); + Serialization::WriteBool(aWriter, HasRotation); + if (HasRotation) + Rotation.Serialize(aWriter); + CellId.Serialize(aWriter); + WorldSpaceId.Serialize(aWriter); + ReferenceId.Serialize(aWriter); +} + +void NotifyDroppedItemPhysicsDisabled::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + DropId = Serialization::ReadVarInt(aReader); + HasLocation = Serialization::ReadBool(aReader); + if (HasLocation) + Location.Deserialize(aReader); + HasRotation = Serialization::ReadBool(aReader); + if (HasRotation) + Rotation.Deserialize(aReader); + CellId.Deserialize(aReader); + WorldSpaceId.Deserialize(aReader); + ReferenceId.Deserialize(aReader); +} diff --git a/Code/encoding/Messages/NotifyDroppedItemPhysicsDisabled.h b/Code/encoding/Messages/NotifyDroppedItemPhysicsDisabled.h new file mode 100644 index 000000000..f9b1d8ec2 --- /dev/null +++ b/Code/encoding/Messages/NotifyDroppedItemPhysicsDisabled.h @@ -0,0 +1,40 @@ +#pragma once + +#include "Message.h" +#include +#include + +struct NotifyDroppedItemPhysicsDisabled final : ServerMessage +{ + static constexpr ServerOpcode Opcode = kNotifyDroppedItemPhysicsDisabled; + + NotifyDroppedItemPhysicsDisabled() + : ServerMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const NotifyDroppedItemPhysicsDisabled& acRhs) const noexcept + { + return GetOpcode() == acRhs.GetOpcode() + && DropId == acRhs.DropId + && HasLocation == acRhs.HasLocation + && (!HasLocation || Location == acRhs.Location) + && HasRotation == acRhs.HasRotation + && (!HasRotation || Rotation == acRhs.Rotation) + && CellId == acRhs.CellId + && WorldSpaceId == acRhs.WorldSpaceId + && ReferenceId == acRhs.ReferenceId; + } + + uint64_t DropId{}; + bool HasLocation{false}; + Vector3_NetQuantize Location{}; + bool HasRotation{false}; + Vector3_NetQuantize Rotation{}; + GameId CellId{}; + GameId WorldSpaceId{}; + GameId ReferenceId{}; +}; diff --git a/Code/encoding/Messages/NotifyDroppedItemPickedUp.cpp b/Code/encoding/Messages/NotifyDroppedItemPickedUp.cpp new file mode 100644 index 000000000..234672a7a --- /dev/null +++ b/Code/encoding/Messages/NotifyDroppedItemPickedUp.cpp @@ -0,0 +1,49 @@ +#include +#include + +void NotifyDroppedItemPickedUp::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + // Base fields + Serialization::WriteVarInt(aWriter, ServerId); + Item.Serialize(aWriter); + Serialization::WriteVarInt(aWriter, DropId); + + // Optional location information + Serialization::WriteBool(aWriter, HasLocation); + if (HasLocation) + Location.Serialize(aWriter); + + // Optional rotation information + Serialization::WriteBool(aWriter, HasRotation); + if (HasRotation) + Rotation.Serialize(aWriter); + + // Cell and world‑space identifiers (always present) + CellId.Serialize(aWriter); + WorldSpaceId.Serialize(aWriter); + ReferenceId.Serialize(aWriter); +} + +void NotifyDroppedItemPickedUp::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + // Base fields + ServerMessage::DeserializeRaw(aReader); + ServerId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; + Item.Deserialize(aReader); + DropId = Serialization::ReadVarInt(aReader); + + // Optional location information + HasLocation = Serialization::ReadBool(aReader); + if (HasLocation) + Location.Deserialize(aReader); + + // Optional rotation information + HasRotation = Serialization::ReadBool(aReader); + if (HasRotation) + Rotation.Deserialize(aReader); + + // Cell and world‑space identifiers + CellId.Deserialize(aReader); + WorldSpaceId.Deserialize(aReader); + ReferenceId.Deserialize(aReader); +} diff --git a/Code/encoding/Messages/NotifyDroppedItemPickedUp.h b/Code/encoding/Messages/NotifyDroppedItemPickedUp.h new file mode 100644 index 000000000..4bd5c2654 --- /dev/null +++ b/Code/encoding/Messages/NotifyDroppedItemPickedUp.h @@ -0,0 +1,47 @@ +#pragma once + +#include "Message.h" +#include +#include +#include + +struct NotifyDroppedItemPickedUp final : ServerMessage +{ + static constexpr ServerOpcode Opcode = kNotifyDroppedItemPickedUp; + + NotifyDroppedItemPickedUp() + : ServerMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const NotifyDroppedItemPickedUp& acRhs) const noexcept + { + return GetOpcode() == acRhs.GetOpcode() + && ServerId == acRhs.ServerId + && Item == acRhs.Item + && DropId == acRhs.DropId + && HasLocation == acRhs.HasLocation + && (!HasLocation || Location == acRhs.Location) + && HasRotation == acRhs.HasRotation + && (!HasRotation || Rotation == acRhs.Rotation) + && CellId == acRhs.CellId + && WorldSpaceId == acRhs.WorldSpaceId + && ReferenceId == acRhs.ReferenceId; + } + + uint32_t ServerId{}; + Inventory::Entry Item{}; + uint64_t DropId{}; + + // Additional location information for remote pickups + bool HasLocation{false}; + Vector3_NetQuantize Location{}; + bool HasRotation{false}; + Vector3_NetQuantize Rotation{}; + GameId CellId{}; + GameId WorldSpaceId{}; + GameId ReferenceId{}; +}; diff --git a/Code/encoding/Messages/NotifyDroppedItems.cpp b/Code/encoding/Messages/NotifyDroppedItems.cpp new file mode 100644 index 000000000..0b31ec5d5 --- /dev/null +++ b/Code/encoding/Messages/NotifyDroppedItems.cpp @@ -0,0 +1,79 @@ +#include +#include + +void NotifyDroppedItems::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, RequestId); + Serialization::WriteVarInt(aWriter, Entries.size()); + for (const auto& entry : Entries) + { + Serialization::WriteVarInt(aWriter, entry.DropId); + Serialization::WriteVarInt(aWriter, entry.ServerId); + Serialization::WriteVarInt(aWriter, entry.ActorFormId); + Serialization::WriteVarInt(aWriter, static_cast(entry.Type)); + Serialization::WriteVarInt(aWriter, entry.SpawnEpoch); + entry.Item.Serialize(aWriter); + + Serialization::WriteBool(aWriter, entry.HasLocation); + if (entry.HasLocation) + entry.Location.Serialize(aWriter); + + Serialization::WriteBool(aWriter, entry.HasRotation); + if (entry.HasRotation) + entry.Rotation.Serialize(aWriter); + + entry.CellId.Serialize(aWriter); + entry.WorldSpaceId.Serialize(aWriter); + entry.ReferenceId.Serialize(aWriter); + } + + Serialization::WriteVarInt(aWriter, CreationEnginePickedUpReferences.size()); + for (const auto& referenceId : CreationEnginePickedUpReferences) + referenceId.Serialize(aWriter); +} + +void NotifyDroppedItems::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ServerMessage::DeserializeRaw(aReader); + + RequestId = Serialization::ReadVarInt(aReader); + const auto count = Serialization::ReadVarInt(aReader); + Entries.clear(); + Entries.reserve(count); + + for (size_t i = 0; i < count; ++i) + { + Entry entry{}; + entry.DropId = Serialization::ReadVarInt(aReader); + entry.ServerId = Serialization::ReadVarInt(aReader); + entry.ActorFormId = Serialization::ReadVarInt(aReader); + const auto typeValue = Serialization::ReadVarInt(aReader); + entry.Type = typeValue == static_cast(ServerItemType::CreationEngine) ? ServerItemType::CreationEngine : ServerItemType::Dropped; + entry.SpawnEpoch = Serialization::ReadVarInt(aReader); + entry.Item.Deserialize(aReader); + + entry.HasLocation = Serialization::ReadBool(aReader); + if (entry.HasLocation) + entry.Location.Deserialize(aReader); + + entry.HasRotation = Serialization::ReadBool(aReader); + if (entry.HasRotation) + entry.Rotation.Deserialize(aReader); + + entry.CellId.Deserialize(aReader); + entry.WorldSpaceId.Deserialize(aReader); + entry.ReferenceId.Deserialize(aReader); + + Entries.push_back(std::move(entry)); + } + + const auto pickupCount = Serialization::ReadVarInt(aReader); + CreationEnginePickedUpReferences.clear(); + CreationEnginePickedUpReferences.reserve(pickupCount); + for (size_t i = 0; i < pickupCount; ++i) + { + GameId referenceId{}; + referenceId.Deserialize(aReader); + CreationEnginePickedUpReferences.push_back(std::move(referenceId)); + } +} diff --git a/Code/encoding/Messages/NotifyDroppedItems.h b/Code/encoding/Messages/NotifyDroppedItems.h new file mode 100644 index 000000000..10bd6785f --- /dev/null +++ b/Code/encoding/Messages/NotifyDroppedItems.h @@ -0,0 +1,56 @@ +#pragma once + +#include "Message.h" +#include +#include +#include +#include +#include + +struct NotifyDroppedItems final : ServerMessage +{ + struct Entry + { + uint64_t DropId{}; + uint32_t ServerId{}; + uint32_t ActorFormId{}; + ServerItemType Type{ServerItemType::Dropped}; + Inventory::Entry Item{}; + bool HasLocation{false}; + Vector3_NetQuantize Location{}; + bool HasRotation{false}; + Vector3_NetQuantize Rotation{}; + GameId CellId{}; + GameId WorldSpaceId{}; + GameId ReferenceId{}; + uint64_t SpawnEpoch{}; + + bool operator==(const Entry& acRhs) const noexcept + { + return DropId == acRhs.DropId && ServerId == acRhs.ServerId && ActorFormId == acRhs.ActorFormId && Type == acRhs.Type && Item == acRhs.Item && HasLocation == acRhs.HasLocation && + (!HasLocation || Location == acRhs.Location) && HasRotation == acRhs.HasRotation && (!HasRotation || Rotation == acRhs.Rotation) && CellId == acRhs.CellId && WorldSpaceId == acRhs.WorldSpaceId && + ReferenceId == acRhs.ReferenceId && SpawnEpoch == acRhs.SpawnEpoch; + } + }; + + static constexpr ServerOpcode Opcode = kNotifyDroppedItems; + + NotifyDroppedItems() + : ServerMessage(Opcode) + { + } + + virtual ~NotifyDroppedItems() = default; + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const NotifyDroppedItems& acRhs) const noexcept + { + return GetOpcode() == acRhs.GetOpcode() && RequestId == acRhs.RequestId && Entries == acRhs.Entries && CreationEnginePickedUpReferences == acRhs.CreationEnginePickedUpReferences; + } + + uint32_t RequestId{}; + TiltedPhoques::Vector Entries{}; + TiltedPhoques::Vector CreationEnginePickedUpReferences{}; +}; diff --git a/Code/encoding/Messages/NotifyHealingProximity.cpp b/Code/encoding/Messages/NotifyHealingProximity.cpp new file mode 100644 index 000000000..82f8ecb42 --- /dev/null +++ b/Code/encoding/Messages/NotifyHealingProximity.cpp @@ -0,0 +1,25 @@ +#include + +void NotifyHealingProximity::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, CasterId); + Serialization::WriteFloat(aWriter, CasterX); + Serialization::WriteFloat(aWriter, CasterY); + Serialization::WriteFloat(aWriter, CasterZ); + Serialization::WriteVarInt(aWriter, SpellFormId.ModId); + Serialization::WriteVarInt(aWriter, SpellFormId.BaseId); + Serialization::WriteVarInt(aWriter, CasterRestorationLevel); +} + +void NotifyHealingProximity::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ServerMessage::DeserializeRaw(aReader); + + CasterId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; + CasterX = Serialization::ReadFloat(aReader); + CasterY = Serialization::ReadFloat(aReader); + CasterZ = Serialization::ReadFloat(aReader); + SpellFormId.ModId = Serialization::ReadVarInt(aReader) & 0xFF; + SpellFormId.BaseId = Serialization::ReadVarInt(aReader) & 0xFFFFFF; + CasterRestorationLevel = Serialization::ReadVarInt(aReader) & 0xFFFF; +} diff --git a/Code/encoding/Messages/NotifyHealingProximity.h b/Code/encoding/Messages/NotifyHealingProximity.h new file mode 100644 index 000000000..b3ef2884c --- /dev/null +++ b/Code/encoding/Messages/NotifyHealingProximity.h @@ -0,0 +1,35 @@ +#pragma once + +#include "Message.h" +#include + +struct NotifyHealingProximity final : ServerMessage +{ + static constexpr ServerOpcode Opcode = kNotifyHealingProximity; + + NotifyHealingProximity() + : ServerMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const NotifyHealingProximity& acRhs) const noexcept + { + return CasterId == acRhs.CasterId && + CasterX == acRhs.CasterX && + CasterY == acRhs.CasterY && + CasterZ == acRhs.CasterZ && + SpellFormId == acRhs.SpellFormId && + CasterRestorationLevel == acRhs.CasterRestorationLevel && + GetOpcode() == acRhs.GetOpcode(); + } + + uint32_t CasterId; + float CasterX; + float CasterY; + float CasterZ; + GameId SpellFormId{}; + uint16_t CasterRestorationLevel = 0; +}; diff --git a/Code/encoding/Messages/NotifyInventoryChanges.cpp b/Code/encoding/Messages/NotifyInventoryChanges.cpp index 9036ec5de..f63be028d 100644 --- a/Code/encoding/Messages/NotifyInventoryChanges.cpp +++ b/Code/encoding/Messages/NotifyInventoryChanges.cpp @@ -5,7 +5,7 @@ void NotifyInventoryChanges::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter { Serialization::WriteVarInt(aWriter, ServerId); Item.Serialize(aWriter); - Serialization::WriteBool(aWriter, Drop); + Serialization::WriteBool(aWriter, Silent); } void NotifyInventoryChanges::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept @@ -14,5 +14,5 @@ void NotifyInventoryChanges::DeserializeRaw(TiltedPhoques::Buffer::Reader& aRead ServerId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; Item.Deserialize(aReader); - Drop = Serialization::ReadBool(aReader); + Silent = Serialization::ReadBool(aReader); } diff --git a/Code/encoding/Messages/NotifyInventoryChanges.h b/Code/encoding/Messages/NotifyInventoryChanges.h index 0153a24e1..5d5b70c37 100644 --- a/Code/encoding/Messages/NotifyInventoryChanges.h +++ b/Code/encoding/Messages/NotifyInventoryChanges.h @@ -3,7 +3,6 @@ #include "Message.h" #include - struct NotifyInventoryChanges final : ServerMessage { static constexpr ServerOpcode Opcode = kNotifyInventoryChanges; @@ -16,9 +15,12 @@ struct NotifyInventoryChanges final : ServerMessage void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; - bool operator==(const NotifyInventoryChanges& acRhs) const noexcept { return GetOpcode() == acRhs.GetOpcode() && ServerId == acRhs.ServerId && Item == acRhs.Item && Drop == acRhs.Drop; } + bool operator==(const NotifyInventoryChanges& acRhs) const noexcept + { + return GetOpcode() == acRhs.GetOpcode() && ServerId == acRhs.ServerId && Item == acRhs.Item && Silent == acRhs.Silent; + } uint32_t ServerId{}; Inventory::Entry Item{}; - bool Drop = false; + bool Silent = false; }; diff --git a/Code/encoding/Messages/NotifyPartyFastTravelMarkers.cpp b/Code/encoding/Messages/NotifyPartyFastTravelMarkers.cpp new file mode 100644 index 000000000..0aaa34360 --- /dev/null +++ b/Code/encoding/Messages/NotifyPartyFastTravelMarkers.cpp @@ -0,0 +1,26 @@ +#include + +void NotifyPartyFastTravelMarkers::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + aWriter.WriteBits(static_cast(Markers.size()), 16); + for (const auto& marker : Markers) + marker.Serialize(aWriter); +} + +void NotifyPartyFastTravelMarkers::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ServerMessage::DeserializeRaw(aReader); + + uint64_t count = 0; + aReader.ReadBits(count, 16); + + Markers.clear(); + Markers.reserve(count); + for (uint64_t i = 0; i < count; ++i) + { + GameId marker{}; + marker.Deserialize(aReader); + Markers.push_back(marker); + } +} + diff --git a/Code/encoding/Messages/NotifyPartyFastTravelMarkers.h b/Code/encoding/Messages/NotifyPartyFastTravelMarkers.h new file mode 100644 index 000000000..041f5933c --- /dev/null +++ b/Code/encoding/Messages/NotifyPartyFastTravelMarkers.h @@ -0,0 +1,27 @@ +#pragma once + +#include "Message.h" + +#include + +// Server -> Client: announce party fast travel (map) markers that should be discovered locally. +struct NotifyPartyFastTravelMarkers final : ServerMessage +{ + static constexpr ServerOpcode Opcode = kNotifyPartyFastTravelMarkers; + + NotifyPartyFastTravelMarkers() + : ServerMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const NotifyPartyFastTravelMarkers& acRhs) const noexcept + { + return GetOpcode() == acRhs.GetOpcode() && Markers == acRhs.Markers; + } + + TiltedPhoques::Vector Markers{}; +}; + diff --git a/Code/encoding/Messages/NotifyPartyLeaderCellLock.cpp b/Code/encoding/Messages/NotifyPartyLeaderCellLock.cpp new file mode 100644 index 000000000..c1ea39f4a --- /dev/null +++ b/Code/encoding/Messages/NotifyPartyLeaderCellLock.cpp @@ -0,0 +1,21 @@ +#include + +#include + +void NotifyPartyLeaderCellLock::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + WorldSpaceId.Serialize(aWriter); + CellId.Serialize(aWriter); + Position.Serialize(aWriter); + Serialization::WriteVarInt(aWriter, CountdownSeconds); + Serialization::WriteBool(aWriter, Cancelled); +} + +void NotifyPartyLeaderCellLock::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + WorldSpaceId.Deserialize(aReader); + CellId.Deserialize(aReader); + Position.Deserialize(aReader); + CountdownSeconds = static_cast(Serialization::ReadVarInt(aReader) & 0xFFFF); + Cancelled = Serialization::ReadBool(aReader); +} diff --git a/Code/encoding/Messages/NotifyPartyLeaderCellLock.h b/Code/encoding/Messages/NotifyPartyLeaderCellLock.h new file mode 100644 index 000000000..36adc47bb --- /dev/null +++ b/Code/encoding/Messages/NotifyPartyLeaderCellLock.h @@ -0,0 +1,29 @@ +#pragma once + +#include "Message.h" +#include +#include + +struct NotifyPartyLeaderCellLock final : ServerMessage +{ + static constexpr ServerOpcode Opcode = kNotifyPartyLeaderCellLock; + + NotifyPartyLeaderCellLock() + : ServerMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const NotifyPartyLeaderCellLock& acRhs) const noexcept + { + return GetOpcode() == acRhs.GetOpcode() && WorldSpaceId == acRhs.WorldSpaceId && CellId == acRhs.CellId && Position == acRhs.Position && CountdownSeconds == acRhs.CountdownSeconds && Cancelled == acRhs.Cancelled; + } + + GameId WorldSpaceId{}; + GameId CellId{}; + Vector3_NetQuantize Position{}; + uint16_t CountdownSeconds{}; + bool Cancelled{}; +}; diff --git a/Code/encoding/Messages/NotifyPartyMemberDowned.cpp b/Code/encoding/Messages/NotifyPartyMemberDowned.cpp new file mode 100644 index 000000000..2bc44b1c3 --- /dev/null +++ b/Code/encoding/Messages/NotifyPartyMemberDowned.cpp @@ -0,0 +1,31 @@ +#include + +void NotifyPartyMemberDowned::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, ServerId); + Serialization::WriteVarInt(aWriter, PlayerId); + Serialization::WriteBool(aWriter, IsDowned); + Serialization::WriteFloat(aWriter, PositionX); + Serialization::WriteFloat(aWriter, PositionY); + Serialization::WriteFloat(aWriter, PositionZ); + Serialization::WriteVarInt(aWriter, WorldSpaceId.ModId); + Serialization::WriteVarInt(aWriter, WorldSpaceId.BaseId); + Serialization::WriteVarInt(aWriter, CellId.ModId); + Serialization::WriteVarInt(aWriter, CellId.BaseId); +} + +void NotifyPartyMemberDowned::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ServerMessage::DeserializeRaw(aReader); + + ServerId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; + PlayerId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; + IsDowned = Serialization::ReadBool(aReader); + PositionX = Serialization::ReadFloat(aReader); + PositionY = Serialization::ReadFloat(aReader); + PositionZ = Serialization::ReadFloat(aReader); + WorldSpaceId.ModId = Serialization::ReadVarInt(aReader) & 0xFF; + WorldSpaceId.BaseId = Serialization::ReadVarInt(aReader) & 0xFFFFFF; + CellId.ModId = Serialization::ReadVarInt(aReader) & 0xFF; + CellId.BaseId = Serialization::ReadVarInt(aReader) & 0xFFFFFF; +} diff --git a/Code/encoding/Messages/NotifyPartyMemberDowned.h b/Code/encoding/Messages/NotifyPartyMemberDowned.h new file mode 100644 index 000000000..2f42d52a2 --- /dev/null +++ b/Code/encoding/Messages/NotifyPartyMemberDowned.h @@ -0,0 +1,58 @@ +#pragma once + +#include "Message.h" +#include + +/** + * Server -> Client notification sent to every party member when a party member becomes + * downed (bleedout/unrecoverable) or is revived. + * + * Carries enough information for clients to: + * - Identify the player (server-side player id) + * - Know whether they are currently downed + * - Optionally render UI/chat feedback about revival (e.g., via Healing Hands) + * - Optionally place markers or effects using the position/worldspace/cell + */ +struct NotifyPartyMemberDowned final : ServerMessage +{ + static constexpr ServerOpcode Opcode = kNotifyPartyMemberDowned; + + NotifyPartyMemberDowned() + : ServerMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const NotifyPartyMemberDowned& acRhs) const noexcept + { + return PlayerId == acRhs.PlayerId && + ServerId == acRhs.ServerId && + IsDowned == acRhs.IsDowned && + PositionX == acRhs.PositionX && + PositionY == acRhs.PositionY && + PositionZ == acRhs.PositionZ && + WorldSpaceId == acRhs.WorldSpaceId && + CellId == acRhs.CellId && + GetOpcode() == acRhs.GetOpcode(); + } + + // Numeric identifier of the party member's actor on the server (entt id) + uint32_t ServerId{0}; + + // Server-side player id of the downed/revived party member + uint32_t PlayerId{0}; + + // True if the player is downed (in bleedout/unrecoverable), false if they have been revived + bool IsDowned{false}; + + // World position of the player at the time of the event (can be used for markers/effects) + float PositionX{0.f}; + float PositionY{0.f}; + float PositionZ{0.f}; + + // World identification for context (same shape used elsewhere in party/location messages) + GameId WorldSpaceId{}; + GameId CellId{}; +}; diff --git a/Code/encoding/Messages/NotifyPartyOptions.cpp b/Code/encoding/Messages/NotifyPartyOptions.cpp new file mode 100644 index 000000000..76c6ff33e --- /dev/null +++ b/Code/encoding/Messages/NotifyPartyOptions.cpp @@ -0,0 +1,11 @@ +#include + +void NotifyPartyOptions::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Options.Serialize(aWriter); +} + +void NotifyPartyOptions::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + Options.Deserialize(aReader); +} diff --git a/Code/encoding/Messages/NotifyPartyOptions.h b/Code/encoding/Messages/NotifyPartyOptions.h new file mode 100644 index 000000000..d9933fb55 --- /dev/null +++ b/Code/encoding/Messages/NotifyPartyOptions.h @@ -0,0 +1,26 @@ +#pragma once + +#include "Message.h" + +#include + +// Server -> Client: announce party option changes. +struct NotifyPartyOptions final : ServerMessage +{ + static constexpr ServerOpcode Opcode = kNotifyPartyOptions; + + NotifyPartyOptions() + : ServerMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const NotifyPartyOptions& acRhs) const noexcept + { + return GetOpcode() == acRhs.GetOpcode() && Options == acRhs.Options; + } + + PartyOptions Options{}; +}; diff --git a/Code/encoding/Messages/NotifyPartyPositions.cpp b/Code/encoding/Messages/NotifyPartyPositions.cpp new file mode 100644 index 000000000..53b32d67f --- /dev/null +++ b/Code/encoding/Messages/NotifyPartyPositions.cpp @@ -0,0 +1,35 @@ +#include + +void NotifyPartyPositions::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept { + // Opcode is written by ServerMessage::SerializeRaw + aWriter.WriteBits(static_cast(Entries.size()), 16); + for (const auto& e : Entries) { + aWriter.WriteBits(static_cast(e.PlayerId), 32); + e.Position.Serialize(aWriter); + e.WorldSpaceId.Serialize(aWriter); + e.CellId.Serialize(aWriter); + aWriter.WriteBits(e.IsInterior ? 1u : 0u, 1); + } +} + +void NotifyPartyPositions::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept { + ServerMessage::DeserializeRaw(aReader); + + uint64_t count = 0; + aReader.ReadBits(count, 16); + Entries.clear(); + Entries.reserve(count); + for (uint64_t i = 0; i < count; ++i) { + Entry e{}; + uint64_t pid = 0; + aReader.ReadBits(pid, 32); + e.PlayerId = static_cast(pid); + e.Position.Deserialize(aReader); + e.WorldSpaceId.Deserialize(aReader); + e.CellId.Deserialize(aReader); + uint64_t interior = 0; + aReader.ReadBits(interior, 1); + e.IsInterior = interior != 0; + Entries.push_back(e); + } +} diff --git a/Code/encoding/Messages/NotifyPartyPositions.h b/Code/encoding/Messages/NotifyPartyPositions.h new file mode 100644 index 000000000..f6346028b --- /dev/null +++ b/Code/encoding/Messages/NotifyPartyPositions.h @@ -0,0 +1,29 @@ +#pragma once + +#include "Message.h" +#include +#include + +struct NotifyPartyPositions final : ServerMessage { + static constexpr ServerOpcode Opcode = kNotifyPartyPositions; + + NotifyPartyPositions() : ServerMessage(Opcode) {} + + struct Entry { + uint32_t PlayerId{}; + Vector3_NetQuantize Position{}; + GameId WorldSpaceId{}; + GameId CellId{}; + bool IsInterior{false}; + + bool operator==(const Entry& rhs) const noexcept { + return PlayerId == rhs.PlayerId && Position == rhs.Position && WorldSpaceId == rhs.WorldSpaceId && + CellId == rhs.CellId && IsInterior == rhs.IsInterior; + } + }; + + TiltedPhoques::Vector Entries; + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; +}; diff --git a/Code/encoding/Messages/NotifyPlayEmote.cpp b/Code/encoding/Messages/NotifyPlayEmote.cpp new file mode 100644 index 000000000..3010aec65 --- /dev/null +++ b/Code/encoding/Messages/NotifyPlayEmote.cpp @@ -0,0 +1,15 @@ +#include + +void NotifyPlayEmote::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, ServerId); + Serialization::WriteString(aWriter, EventName); +} + +void NotifyPlayEmote::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ServerMessage::DeserializeRaw(aReader); + + ServerId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; + EventName = Serialization::ReadString(aReader); +} diff --git a/Code/encoding/Messages/NotifyPlayEmote.h b/Code/encoding/Messages/NotifyPlayEmote.h new file mode 100644 index 000000000..518f1b350 --- /dev/null +++ b/Code/encoding/Messages/NotifyPlayEmote.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Message.h" + +using TiltedPhoques::String; + +struct NotifyPlayEmote final : ServerMessage +{ + static constexpr ServerOpcode Opcode = kNotifyPlayEmote; + + NotifyPlayEmote() + : ServerMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const NotifyPlayEmote& acRhs) const noexcept { return ServerId == acRhs.ServerId && EventName == acRhs.EventName && GetOpcode() == acRhs.GetOpcode(); } + + uint32_t ServerId{}; + String EventName{}; +}; diff --git a/Code/encoding/Messages/NotifyPlayerActorName.cpp b/Code/encoding/Messages/NotifyPlayerActorName.cpp new file mode 100644 index 000000000..9c6611ef2 --- /dev/null +++ b/Code/encoding/Messages/NotifyPlayerActorName.cpp @@ -0,0 +1,16 @@ +#include + +#include + +void NotifyPlayerActorName::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, PlayerId); + Serialization::WriteString(aWriter, ActorName); +} + +void NotifyPlayerActorName::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ServerMessage::DeserializeRaw(aReader); + PlayerId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; + ActorName = Serialization::ReadString(aReader); +} diff --git a/Code/encoding/Messages/NotifyPlayerActorName.h b/Code/encoding/Messages/NotifyPlayerActorName.h new file mode 100644 index 000000000..43b9b48e3 --- /dev/null +++ b/Code/encoding/Messages/NotifyPlayerActorName.h @@ -0,0 +1,26 @@ +#pragma once + +#include "Message.h" + +using TiltedPhoques::String; + +struct NotifyPlayerActorName final : ServerMessage +{ + static constexpr ServerOpcode Opcode = kNotifyPlayerActorName; + + NotifyPlayerActorName() + : ServerMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const NotifyPlayerActorName& acRhs) const noexcept + { + return GetOpcode() == acRhs.GetOpcode() && PlayerId == acRhs.PlayerId && ActorName == acRhs.ActorName; + } + + uint32_t PlayerId{}; + String ActorName{}; +}; diff --git a/Code/encoding/Messages/NotifyPlayerJoined.cpp b/Code/encoding/Messages/NotifyPlayerJoined.cpp index d13b68d2d..650a79021 100644 --- a/Code/encoding/Messages/NotifyPlayerJoined.cpp +++ b/Code/encoding/Messages/NotifyPlayerJoined.cpp @@ -5,6 +5,7 @@ void NotifyPlayerJoined::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) co { Serialization::WriteVarInt(aWriter, PlayerId); Serialization::WriteString(aWriter, Username); + Serialization::WriteString(aWriter, Avatar); WorldSpaceId.Serialize(aWriter); CellId.Serialize(aWriter); Serialization::WriteVarInt(aWriter, Level); @@ -16,6 +17,7 @@ void NotifyPlayerJoined::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) PlayerId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; Username = Serialization::ReadString(aReader); + Avatar = Serialization::ReadString(aReader); WorldSpaceId.Deserialize(aReader); CellId.Deserialize(aReader); Level = Serialization::ReadVarInt(aReader) & 0xFFFF; diff --git a/Code/encoding/Messages/NotifyPlayerJoined.h b/Code/encoding/Messages/NotifyPlayerJoined.h index bd34fd042..a8de69958 100644 --- a/Code/encoding/Messages/NotifyPlayerJoined.h +++ b/Code/encoding/Messages/NotifyPlayerJoined.h @@ -18,10 +18,11 @@ struct NotifyPlayerJoined final : ServerMessage void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; - bool operator==(const NotifyPlayerJoined& acRhs) const noexcept { return GetOpcode() == acRhs.GetOpcode() && PlayerId == acRhs.PlayerId && Username == acRhs.Username && WorldSpaceId == acRhs.WorldSpaceId && CellId == acRhs.CellId && Level == acRhs.Level; } + bool operator==(const NotifyPlayerJoined& acRhs) const noexcept { return GetOpcode() == acRhs.GetOpcode() && PlayerId == acRhs.PlayerId && Username == acRhs.Username && Avatar == acRhs.Avatar && WorldSpaceId == acRhs.WorldSpaceId && CellId == acRhs.CellId && Level == acRhs.Level; } uint32_t PlayerId{}; String Username{}; + String Avatar{}; GameId WorldSpaceId{}; GameId CellId{}; uint16_t Level{}; diff --git a/Code/encoding/Messages/NotifyPlayerList.cpp b/Code/encoding/Messages/NotifyPlayerList.cpp index 7985c9aad..0a7d6d88b 100644 --- a/Code/encoding/Messages/NotifyPlayerList.cpp +++ b/Code/encoding/Messages/NotifyPlayerList.cpp @@ -8,7 +8,8 @@ void NotifyPlayerList::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) cons for (auto& player : Players) { Serialization::WriteVarInt(aWriter, player.first); - Serialization::WriteString(aWriter, player.second); + Serialization::WriteString(aWriter, player.second.Name); + Serialization::WriteString(aWriter, player.second.Avatar); } } @@ -21,6 +22,9 @@ void NotifyPlayerList::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) no for (auto i = 0u; i < count; ++i) { uint32_t id = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; - Players[id] = Serialization::ReadString(aReader); + PlayerListEntry entry{}; + entry.Name = Serialization::ReadString(aReader); + entry.Avatar = Serialization::ReadString(aReader); + Players[id] = std::move(entry); } } diff --git a/Code/encoding/Messages/NotifyPlayerList.h b/Code/encoding/Messages/NotifyPlayerList.h index 2d8a40f04..4d3c4d274 100644 --- a/Code/encoding/Messages/NotifyPlayerList.h +++ b/Code/encoding/Messages/NotifyPlayerList.h @@ -18,7 +18,18 @@ struct NotifyPlayerList final : ServerMessage void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + struct PlayerListEntry + { + String Name{}; + String Avatar{}; + + bool operator==(const PlayerListEntry& aRhs) const noexcept + { + return Name == aRhs.Name && Avatar == aRhs.Avatar; + } + }; + bool operator==(const NotifyPlayerList& acRhs) const noexcept { return Players == acRhs.Players && GetOpcode() == acRhs.GetOpcode(); } - TiltedPhoques::Map Players{}; + TiltedPhoques::Map Players{}; }; diff --git a/Code/encoding/Messages/NotifyPlayerProfileImage.cpp b/Code/encoding/Messages/NotifyPlayerProfileImage.cpp new file mode 100644 index 000000000..ee0f95bda --- /dev/null +++ b/Code/encoding/Messages/NotifyPlayerProfileImage.cpp @@ -0,0 +1,17 @@ +#include + +#include + +void NotifyPlayerProfileImage::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, PlayerId); + Serialization::WriteString(aWriter, Avatar); +} + +void NotifyPlayerProfileImage::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ServerMessage::DeserializeRaw(aReader); + PlayerId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; + Avatar = Serialization::ReadString(aReader); +} + diff --git a/Code/encoding/Messages/NotifyPlayerProfileImage.h b/Code/encoding/Messages/NotifyPlayerProfileImage.h new file mode 100644 index 000000000..2877c5407 --- /dev/null +++ b/Code/encoding/Messages/NotifyPlayerProfileImage.h @@ -0,0 +1,25 @@ +#pragma once + +#include "Message.h" + +struct NotifyPlayerProfileImage final : ServerMessage +{ + static constexpr ServerOpcode Opcode = kNotifyPlayerProfileImage; + + NotifyPlayerProfileImage() + : ServerMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const NotifyPlayerProfileImage& acRhs) const noexcept + { + return GetOpcode() == acRhs.GetOpcode() && PlayerId == acRhs.PlayerId && Avatar == acRhs.Avatar; + } + + uint32_t PlayerId{}; + String Avatar{}; +}; + diff --git a/Code/encoding/Messages/NotifyPlayerSyncMode.cpp b/Code/encoding/Messages/NotifyPlayerSyncMode.cpp new file mode 100644 index 000000000..1373b2a4a --- /dev/null +++ b/Code/encoding/Messages/NotifyPlayerSyncMode.cpp @@ -0,0 +1,20 @@ +#include + +void NotifyPlayerSyncMode::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + aWriter.WriteBits(PlayerId, 32); + aWriter.WriteBits(static_cast(Mode), 8); +} + +void NotifyPlayerSyncMode::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ServerMessage::DeserializeRaw(aReader); + + uint64_t playerId = 0; + aReader.ReadBits(playerId, 32); + PlayerId = static_cast(playerId & 0xFFFFFFFF); + + uint64_t mode = 0; + aReader.ReadBits(mode, 8); + Mode = static_cast(mode & 0xFF); +} diff --git a/Code/encoding/Messages/NotifyPlayerSyncMode.h b/Code/encoding/Messages/NotifyPlayerSyncMode.h new file mode 100644 index 000000000..50346298f --- /dev/null +++ b/Code/encoding/Messages/NotifyPlayerSyncMode.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Message.h" + +#include + +struct NotifyPlayerSyncMode final : ServerMessage +{ + static constexpr ServerOpcode Opcode = kNotifyPlayerSyncMode; + + NotifyPlayerSyncMode() + : ServerMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const NotifyPlayerSyncMode& acRhs) const noexcept { return GetOpcode() == acRhs.GetOpcode() && PlayerId == acRhs.PlayerId && Mode == acRhs.Mode; } + + uint32_t PlayerId{}; + SyncMode Mode{SyncMode::Normal}; +}; diff --git a/Code/encoding/Messages/NotifyTeleportCountdown.cpp b/Code/encoding/Messages/NotifyTeleportCountdown.cpp new file mode 100644 index 000000000..cfe64de56 --- /dev/null +++ b/Code/encoding/Messages/NotifyTeleportCountdown.cpp @@ -0,0 +1,21 @@ +#include + +#include + +void NotifyTeleportCountdown::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, TargetPlayerId); + Serialization::WriteString(aWriter, TargetName); + Serialization::WriteVarInt(aWriter, DurationSeconds); + Serialization::WriteBool(aWriter, Cancelled); + Serialization::WriteString(aWriter, Reason); +} + +void NotifyTeleportCountdown::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + TargetPlayerId = static_cast(Serialization::ReadVarInt(aReader) & 0xFFFF); + TargetName = Serialization::ReadString(aReader); + DurationSeconds = static_cast(Serialization::ReadVarInt(aReader) & 0xFFFF); + Cancelled = Serialization::ReadBool(aReader); + Reason = Serialization::ReadString(aReader); +} diff --git a/Code/encoding/Messages/NotifyTeleportCountdown.h b/Code/encoding/Messages/NotifyTeleportCountdown.h new file mode 100644 index 000000000..b0e20fbdc --- /dev/null +++ b/Code/encoding/Messages/NotifyTeleportCountdown.h @@ -0,0 +1,29 @@ +#pragma once + +#include "Message.h" + +struct NotifyTeleportCountdown final : ServerMessage +{ + static constexpr ServerOpcode Opcode = kNotifyTeleportCountdown; + + NotifyTeleportCountdown() + : ServerMessage(Opcode) + { + } + + virtual ~NotifyTeleportCountdown() = default; + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const NotifyTeleportCountdown& acRhs) const noexcept + { + return GetOpcode() == acRhs.GetOpcode() && TargetPlayerId == acRhs.TargetPlayerId && TargetName == acRhs.TargetName && DurationSeconds == acRhs.DurationSeconds && Cancelled == acRhs.Cancelled && Reason == acRhs.Reason; + } + + uint16_t TargetPlayerId{}; + TiltedPhoques::String TargetName{}; + uint16_t DurationSeconds{}; + bool Cancelled{}; + TiltedPhoques::String Reason{}; +}; diff --git a/Code/encoding/Messages/NotifyTeleportRequest.cpp b/Code/encoding/Messages/NotifyTeleportRequest.cpp new file mode 100644 index 000000000..390be0d32 --- /dev/null +++ b/Code/encoding/Messages/NotifyTeleportRequest.cpp @@ -0,0 +1,15 @@ +#include + +#include + +void NotifyTeleportRequest::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, RequesterId); + Serialization::WriteString(aWriter, RequesterName); +} + +void NotifyTeleportRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + RequesterId = static_cast(Serialization::ReadVarInt(aReader) & 0xFFFF); + RequesterName = Serialization::ReadString(aReader); +} diff --git a/Code/encoding/Messages/NotifyTeleportRequest.h b/Code/encoding/Messages/NotifyTeleportRequest.h new file mode 100644 index 000000000..f36523201 --- /dev/null +++ b/Code/encoding/Messages/NotifyTeleportRequest.h @@ -0,0 +1,26 @@ +#pragma once + +#include "Message.h" + +struct NotifyTeleportRequest final : ServerMessage +{ + static constexpr ServerOpcode Opcode = kNotifyTeleportRequest; + + NotifyTeleportRequest() + : ServerMessage(Opcode) + { + } + + virtual ~NotifyTeleportRequest() = default; + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const NotifyTeleportRequest& acRhs) const noexcept + { + return GetOpcode() == acRhs.GetOpcode() && RequesterId == acRhs.RequesterId && RequesterName == acRhs.RequesterName; + } + + uint16_t RequesterId{}; + TiltedPhoques::String RequesterName{}; +}; diff --git a/Code/encoding/Messages/NotifyTradeCancel.cpp b/Code/encoding/Messages/NotifyTradeCancel.cpp new file mode 100644 index 000000000..6f066230e --- /dev/null +++ b/Code/encoding/Messages/NotifyTradeCancel.cpp @@ -0,0 +1,17 @@ +#include + +void NotifyTradeCancel::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, PartnerPlayerId); + Serialization::WriteVarInt(aWriter, static_cast(Reason)); + Serialization::WriteBool(aWriter, WasInitiator); +} + +void NotifyTradeCancel::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ServerMessage::DeserializeRaw(aReader); + + PartnerPlayerId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; + Reason = static_cast(Serialization::ReadVarInt(aReader)); + WasInitiator = Serialization::ReadBool(aReader); +} diff --git a/Code/encoding/Messages/NotifyTradeCancel.h b/Code/encoding/Messages/NotifyTradeCancel.h new file mode 100644 index 000000000..67ab5edc2 --- /dev/null +++ b/Code/encoding/Messages/NotifyTradeCancel.h @@ -0,0 +1,31 @@ +#pragma once + +#include "Message.h" + +enum class TradeCancelReason : uint8_t +{ + Declined = 0, + Cancelled, + PartnerBusy, + SelfBusy, + PlayerLeft, + Timeout, + FailedValidation +}; + +struct NotifyTradeCancel final : ServerMessage +{ + static constexpr ServerOpcode Opcode = kNotifyTradeCancel; + + NotifyTradeCancel() + : ServerMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + uint32_t PartnerPlayerId{}; + TradeCancelReason Reason{TradeCancelReason::Cancelled}; + bool WasInitiator{false}; +}; diff --git a/Code/encoding/Messages/NotifyTradeComplete.cpp b/Code/encoding/Messages/NotifyTradeComplete.cpp new file mode 100644 index 000000000..37e7984c8 --- /dev/null +++ b/Code/encoding/Messages/NotifyTradeComplete.cpp @@ -0,0 +1,13 @@ +#include + +void NotifyTradeComplete::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, PartnerPlayerId); +} + +void NotifyTradeComplete::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ServerMessage::DeserializeRaw(aReader); + + PartnerPlayerId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; +} diff --git a/Code/encoding/Messages/NotifyTradeComplete.h b/Code/encoding/Messages/NotifyTradeComplete.h new file mode 100644 index 000000000..ad99ffe15 --- /dev/null +++ b/Code/encoding/Messages/NotifyTradeComplete.h @@ -0,0 +1,18 @@ +#pragma once + +#include "Message.h" + +struct NotifyTradeComplete final : ServerMessage +{ + static constexpr ServerOpcode Opcode = kNotifyTradeComplete; + + NotifyTradeComplete() + : ServerMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + uint32_t PartnerPlayerId{}; +}; diff --git a/Code/encoding/Messages/NotifyTradeInvite.cpp b/Code/encoding/Messages/NotifyTradeInvite.cpp new file mode 100644 index 000000000..97730c57f --- /dev/null +++ b/Code/encoding/Messages/NotifyTradeInvite.cpp @@ -0,0 +1,15 @@ +#include + +void NotifyTradeInvite::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, InviterPlayerId); + Serialization::WriteVarInt(aWriter, ExpiryTick); +} + +void NotifyTradeInvite::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ServerMessage::DeserializeRaw(aReader); + + InviterPlayerId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; + ExpiryTick = Serialization::ReadVarInt(aReader); +} diff --git a/Code/encoding/Messages/NotifyTradeInvite.h b/Code/encoding/Messages/NotifyTradeInvite.h new file mode 100644 index 000000000..a8e97dd02 --- /dev/null +++ b/Code/encoding/Messages/NotifyTradeInvite.h @@ -0,0 +1,19 @@ +#pragma once + +#include "Message.h" + +struct NotifyTradeInvite final : ServerMessage +{ + static constexpr ServerOpcode Opcode = kNotifyTradeInvite; + + NotifyTradeInvite() + : ServerMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + uint32_t InviterPlayerId{}; + uint64_t ExpiryTick{}; +}; diff --git a/Code/encoding/Messages/NotifyTradeStarted.cpp b/Code/encoding/Messages/NotifyTradeStarted.cpp new file mode 100644 index 000000000..9542bed72 --- /dev/null +++ b/Code/encoding/Messages/NotifyTradeStarted.cpp @@ -0,0 +1,15 @@ +#include + +void NotifyTradeStarted::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, PartnerPlayerId); + Serialization::WriteBool(aWriter, InitiatedBySelf); +} + +void NotifyTradeStarted::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ServerMessage::DeserializeRaw(aReader); + + PartnerPlayerId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; + InitiatedBySelf = Serialization::ReadBool(aReader); +} diff --git a/Code/encoding/Messages/NotifyTradeStarted.h b/Code/encoding/Messages/NotifyTradeStarted.h new file mode 100644 index 000000000..9c083f45d --- /dev/null +++ b/Code/encoding/Messages/NotifyTradeStarted.h @@ -0,0 +1,19 @@ +#pragma once + +#include "Message.h" + +struct NotifyTradeStarted final : ServerMessage +{ + static constexpr ServerOpcode Opcode = kNotifyTradeStarted; + + NotifyTradeStarted() + : ServerMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + uint32_t PartnerPlayerId{}; + bool InitiatedBySelf{false}; +}; diff --git a/Code/encoding/Messages/NotifyTradeState.cpp b/Code/encoding/Messages/NotifyTradeState.cpp new file mode 100644 index 000000000..34ed8dda2 --- /dev/null +++ b/Code/encoding/Messages/NotifyTradeState.cpp @@ -0,0 +1,60 @@ +#include + +void NotifyTradeState::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, PartnerPlayerId); + Serialization::WriteBool(aWriter, SelfReady); + Serialization::WriteBool(aWriter, PartnerReady); + Serialization::WriteVarInt(aWriter, CountdownMs); + Serialization::WriteVarInt(aWriter, CountdownTotalMs); + + Serialization::WriteVarInt(aWriter, SelfItems.size()); + for (const auto& item : SelfItems) + item.Serialize(aWriter); + + Serialization::WriteVarInt(aWriter, PartnerItems.size()); + for (const auto& item : PartnerItems) + item.Serialize(aWriter); + + Serialization::WriteVarInt(aWriter, SelfInventory.size()); + for (const auto& item : SelfInventory) + item.Serialize(aWriter); +} + +void NotifyTradeState::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ServerMessage::DeserializeRaw(aReader); + + PartnerPlayerId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; + SelfReady = Serialization::ReadBool(aReader); + PartnerReady = Serialization::ReadBool(aReader); + CountdownMs = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; + CountdownTotalMs = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; + + auto selfCount = Serialization::ReadVarInt(aReader); + SelfItems.reserve(selfCount); + for (size_t i = 0; i < selfCount; ++i) + { + Inventory::Entry entry; + entry.Deserialize(aReader); + SelfItems.push_back(entry); + } + + auto partnerCount = Serialization::ReadVarInt(aReader); + PartnerItems.reserve(partnerCount); + for (size_t i = 0; i < partnerCount; ++i) + { + Inventory::Entry entry; + entry.Deserialize(aReader); + PartnerItems.push_back(entry); + } + + auto inventoryCount = Serialization::ReadVarInt(aReader); + SelfInventory.reserve(inventoryCount); + for (size_t i = 0; i < inventoryCount; ++i) + { + Inventory::Entry entry; + entry.Deserialize(aReader); + SelfInventory.push_back(entry); + } +} diff --git a/Code/encoding/Messages/NotifyTradeState.h b/Code/encoding/Messages/NotifyTradeState.h new file mode 100644 index 000000000..e1fc6a989 --- /dev/null +++ b/Code/encoding/Messages/NotifyTradeState.h @@ -0,0 +1,27 @@ +#pragma once + +#include "Message.h" + +#include + +struct NotifyTradeState final : ServerMessage +{ + static constexpr ServerOpcode Opcode = kNotifyTradeState; + + NotifyTradeState() + : ServerMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + uint32_t PartnerPlayerId{}; + bool SelfReady{false}; + bool PartnerReady{false}; + uint32_t CountdownMs{0}; + uint32_t CountdownTotalMs{0}; + TiltedPhoques::Vector SelfItems; + TiltedPhoques::Vector PartnerItems; + TiltedPhoques::Vector SelfInventory; +}; diff --git a/Code/encoding/Messages/PartyActorNamesRequest.cpp b/Code/encoding/Messages/PartyActorNamesRequest.cpp new file mode 100644 index 000000000..42cd77de4 --- /dev/null +++ b/Code/encoding/Messages/PartyActorNamesRequest.cpp @@ -0,0 +1,9 @@ +#include + +void PartyActorNamesRequest::SerializeRaw(TiltedPhoques::Buffer::Writer&) const noexcept +{ +} + +void PartyActorNamesRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader&) noexcept +{ +} diff --git a/Code/encoding/Messages/PartyActorNamesRequest.h b/Code/encoding/Messages/PartyActorNamesRequest.h new file mode 100644 index 000000000..75b5e549e --- /dev/null +++ b/Code/encoding/Messages/PartyActorNamesRequest.h @@ -0,0 +1,17 @@ +#pragma once + +#include "Message.h" + +// Client -> Server: request current party actor names. +struct PartyActorNamesRequest final : ClientMessage +{ + static constexpr ClientOpcode Opcode = kPartyActorNamesRequest; + + PartyActorNamesRequest() + : ClientMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; +}; diff --git a/Code/encoding/Messages/PartyFastTravelMarkersRequest.cpp b/Code/encoding/Messages/PartyFastTravelMarkersRequest.cpp new file mode 100644 index 000000000..c3762a8df --- /dev/null +++ b/Code/encoding/Messages/PartyFastTravelMarkersRequest.cpp @@ -0,0 +1,30 @@ +#include + +void PartyFastTravelMarkersRequest::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + aWriter.WriteBits(FullSync ? 1u : 0u, 1); + aWriter.WriteBits(static_cast(Markers.size()), 16); + for (const auto& marker : Markers) + marker.Serialize(aWriter); +} + +void PartyFastTravelMarkersRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ClientMessage::DeserializeRaw(aReader); + + uint64_t fullSync = 0; + aReader.ReadBits(fullSync, 1); + FullSync = fullSync != 0; + + uint64_t count = 0; + aReader.ReadBits(count, 16); + + Markers.clear(); + Markers.reserve(count); + for (uint64_t i = 0; i < count; ++i) + { + GameId marker{}; + marker.Deserialize(aReader); + Markers.push_back(marker); + } +} diff --git a/Code/encoding/Messages/PartyFastTravelMarkersRequest.h b/Code/encoding/Messages/PartyFastTravelMarkersRequest.h new file mode 100644 index 000000000..ce4d38172 --- /dev/null +++ b/Code/encoding/Messages/PartyFastTravelMarkersRequest.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Message.h" + +#include + +// Client -> Server: send newly discovered fast travel (map) markers within the party. +struct PartyFastTravelMarkersRequest final : ClientMessage +{ + static constexpr ClientOpcode Opcode = kPartyFastTravelMarkersRequest; + + PartyFastTravelMarkersRequest() + : ClientMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + // When true, Markers represents the sender's complete marker set and should replace the cached set server-side. + bool FullSync{false}; + TiltedPhoques::Vector Markers{}; +}; diff --git a/Code/encoding/Messages/PartyMemberDownedRequest.cpp b/Code/encoding/Messages/PartyMemberDownedRequest.cpp new file mode 100644 index 000000000..b93aa97e1 --- /dev/null +++ b/Code/encoding/Messages/PartyMemberDownedRequest.cpp @@ -0,0 +1,14 @@ +#include + +#include + +void PartyMemberDownedRequest::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteBool(aWriter, IsDowned); +} + +void PartyMemberDownedRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ClientMessage::DeserializeRaw(aReader); + IsDowned = Serialization::ReadBool(aReader); +} diff --git a/Code/encoding/Messages/PartyMemberDownedRequest.h b/Code/encoding/Messages/PartyMemberDownedRequest.h new file mode 100644 index 000000000..75e4a8e83 --- /dev/null +++ b/Code/encoding/Messages/PartyMemberDownedRequest.h @@ -0,0 +1,20 @@ +#pragma once + +#include "Message.h" + +struct PartyMemberDownedRequest final : ClientMessage +{ + static constexpr ClientOpcode Opcode = kPartyMemberDownedRequest; + + PartyMemberDownedRequest() + : ClientMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const PartyMemberDownedRequest& acRhs) const noexcept { return IsDowned == acRhs.IsDowned && GetOpcode() == acRhs.GetOpcode(); } + + bool IsDowned{false}; +}; diff --git a/Code/encoding/Messages/PartyOptionsUpdateRequest.cpp b/Code/encoding/Messages/PartyOptionsUpdateRequest.cpp new file mode 100644 index 000000000..079e2b386 --- /dev/null +++ b/Code/encoding/Messages/PartyOptionsUpdateRequest.cpp @@ -0,0 +1,11 @@ +#include + +void PartyOptionsUpdateRequest::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Options.Serialize(aWriter); +} + +void PartyOptionsUpdateRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + Options.Deserialize(aReader); +} diff --git a/Code/encoding/Messages/PartyOptionsUpdateRequest.h b/Code/encoding/Messages/PartyOptionsUpdateRequest.h new file mode 100644 index 000000000..a7ab7ba52 --- /dev/null +++ b/Code/encoding/Messages/PartyOptionsUpdateRequest.h @@ -0,0 +1,21 @@ +#pragma once + +#include "Message.h" + +#include + +// Client -> Server: update party options (leader-only). +struct PartyOptionsUpdateRequest final : ClientMessage +{ + static constexpr ClientOpcode Opcode = kPartyOptionsUpdateRequest; + + PartyOptionsUpdateRequest() + : ClientMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + PartyOptions Options{}; +}; diff --git a/Code/encoding/Messages/PartyPositionUpdateRequest.cpp b/Code/encoding/Messages/PartyPositionUpdateRequest.cpp new file mode 100644 index 000000000..147af307b --- /dev/null +++ b/Code/encoding/Messages/PartyPositionUpdateRequest.cpp @@ -0,0 +1,19 @@ +#include +#include + +void PartyPositionUpdateRequest::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Position.Serialize(aWriter); + WorldSpaceId.Serialize(aWriter); + CellId.Serialize(aWriter); +} + +void PartyPositionUpdateRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ClientMessage::DeserializeRaw(aReader); + + Position.Deserialize(aReader); + WorldSpaceId.Deserialize(aReader); + CellId.Deserialize(aReader); +} + diff --git a/Code/encoding/Messages/PartyPositionUpdateRequest.h b/Code/encoding/Messages/PartyPositionUpdateRequest.h new file mode 100644 index 000000000..e50592047 --- /dev/null +++ b/Code/encoding/Messages/PartyPositionUpdateRequest.h @@ -0,0 +1,24 @@ +#pragma once + +#include "Message.h" +#include +#include + +// Client -> Server: send the local player's current location for party tracking +struct PartyPositionUpdateRequest final : ClientMessage +{ + static constexpr ClientOpcode Opcode = kPartyPositionUpdateRequest; + + PartyPositionUpdateRequest() + : ClientMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + Vector3_NetQuantize Position; + GameId WorldSpaceId; // 0/empty for interiors + GameId CellId; +}; + diff --git a/Code/encoding/Messages/PartyPositionsRequest.cpp b/Code/encoding/Messages/PartyPositionsRequest.cpp new file mode 100644 index 000000000..cff0703d2 --- /dev/null +++ b/Code/encoding/Messages/PartyPositionsRequest.cpp @@ -0,0 +1,9 @@ +#include + +void PartyPositionsRequest::SerializeRaw(TiltedPhoques::Buffer::Writer&) const noexcept +{ +} + +void PartyPositionsRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader&) noexcept +{ +} diff --git a/Code/encoding/Messages/PartyPositionsRequest.h b/Code/encoding/Messages/PartyPositionsRequest.h new file mode 100644 index 000000000..9022324fd --- /dev/null +++ b/Code/encoding/Messages/PartyPositionsRequest.h @@ -0,0 +1,17 @@ +#pragma once + +#include "Message.h" + +// Client -> Server: request a full snapshot of party positions. +struct PartyPositionsRequest final : ClientMessage +{ + static constexpr ClientOpcode Opcode = kPartyPositionsRequest; + + PartyPositionsRequest() + : ClientMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; +}; diff --git a/Code/encoding/Messages/PlayEmoteRequest.cpp b/Code/encoding/Messages/PlayEmoteRequest.cpp new file mode 100644 index 000000000..5eadd66b2 --- /dev/null +++ b/Code/encoding/Messages/PlayEmoteRequest.cpp @@ -0,0 +1,15 @@ +#include + +void PlayEmoteRequest::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, ServerId); + Serialization::WriteString(aWriter, EventName); +} + +void PlayEmoteRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ClientMessage::DeserializeRaw(aReader); + + ServerId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; + EventName = Serialization::ReadString(aReader); +} diff --git a/Code/encoding/Messages/PlayEmoteRequest.h b/Code/encoding/Messages/PlayEmoteRequest.h new file mode 100644 index 000000000..9e2a8f82f --- /dev/null +++ b/Code/encoding/Messages/PlayEmoteRequest.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Message.h" + +using TiltedPhoques::String; + +struct PlayEmoteRequest final : ClientMessage +{ + static constexpr ClientOpcode Opcode = kPlayEmoteRequest; + + PlayEmoteRequest() + : ClientMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const PlayEmoteRequest& acRhs) const noexcept { return ServerId == acRhs.ServerId && EventName == acRhs.EventName && GetOpcode() == acRhs.GetOpcode(); } + + uint32_t ServerId{}; + String EventName{}; +}; diff --git a/Code/encoding/Messages/PlayerActorNameUpdateRequest.cpp b/Code/encoding/Messages/PlayerActorNameUpdateRequest.cpp new file mode 100644 index 000000000..27c8afea6 --- /dev/null +++ b/Code/encoding/Messages/PlayerActorNameUpdateRequest.cpp @@ -0,0 +1,14 @@ +#include + +#include + +void PlayerActorNameUpdateRequest::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteString(aWriter, ActorName); +} + +void PlayerActorNameUpdateRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ClientMessage::DeserializeRaw(aReader); + ActorName = Serialization::ReadString(aReader); +} diff --git a/Code/encoding/Messages/PlayerActorNameUpdateRequest.h b/Code/encoding/Messages/PlayerActorNameUpdateRequest.h new file mode 100644 index 000000000..403e022dd --- /dev/null +++ b/Code/encoding/Messages/PlayerActorNameUpdateRequest.h @@ -0,0 +1,25 @@ +#pragma once + +#include "Message.h" + +using TiltedPhoques::String; + +struct PlayerActorNameUpdateRequest final : ClientMessage +{ + static constexpr ClientOpcode Opcode = kPlayerActorNameUpdateRequest; + + PlayerActorNameUpdateRequest() + : ClientMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const PlayerActorNameUpdateRequest& acRhs) const noexcept + { + return GetOpcode() == acRhs.GetOpcode() && ActorName == acRhs.ActorName; + } + + String ActorName{}; +}; diff --git a/Code/encoding/Messages/PlayerProfileImageUpdateRequest.cpp b/Code/encoding/Messages/PlayerProfileImageUpdateRequest.cpp new file mode 100644 index 000000000..48e0c1b70 --- /dev/null +++ b/Code/encoding/Messages/PlayerProfileImageUpdateRequest.cpp @@ -0,0 +1,15 @@ +#include + +#include + +void PlayerProfileImageUpdateRequest::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteString(aWriter, ImageData); +} + +void PlayerProfileImageUpdateRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ClientMessage::DeserializeRaw(aReader); + ImageData = Serialization::ReadString(aReader); +} + diff --git a/Code/encoding/Messages/PlayerProfileImageUpdateRequest.h b/Code/encoding/Messages/PlayerProfileImageUpdateRequest.h new file mode 100644 index 000000000..48a40135c --- /dev/null +++ b/Code/encoding/Messages/PlayerProfileImageUpdateRequest.h @@ -0,0 +1,21 @@ +#pragma once + +#include "Message.h" + +struct PlayerProfileImageUpdateRequest final : ClientMessage +{ + static constexpr ClientOpcode Opcode = kPlayerProfileImageUpdateRequest; + + PlayerProfileImageUpdateRequest() + : ClientMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const PlayerProfileImageUpdateRequest& acRhs) const noexcept { return GetOpcode() == acRhs.GetOpcode() && ImageData == acRhs.ImageData; } + + String ImageData{}; +}; + diff --git a/Code/encoding/Messages/RequestActorDrop.cpp b/Code/encoding/Messages/RequestActorDrop.cpp new file mode 100644 index 000000000..714ce95d3 --- /dev/null +++ b/Code/encoding/Messages/RequestActorDrop.cpp @@ -0,0 +1,40 @@ +#include +#include + +void RequestActorDrop::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, ServerId); + Serialization::WriteVarInt(aWriter, ActorFormId); + Item.Serialize(aWriter); + ClientDropId.Serialize(aWriter); + Serialization::WriteBool(aWriter, HasLocation); + if (HasLocation) + Location.Serialize(aWriter); + Serialization::WriteBool(aWriter, HasRotation); + if (HasRotation) + Rotation.Serialize(aWriter); + + CellId.Serialize(aWriter); + WorldSpaceId.Serialize(aWriter); + ReferenceId.Serialize(aWriter); +} + +void RequestActorDrop::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ClientMessage::DeserializeRaw(aReader); + + ServerId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; + ActorFormId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; + Item.Deserialize(aReader); + ClientDropId.Deserialize(aReader); + HasLocation = Serialization::ReadBool(aReader); + if (HasLocation) + Location.Deserialize(aReader); + HasRotation = Serialization::ReadBool(aReader); + if (HasRotation) + Rotation.Deserialize(aReader); + + CellId.Deserialize(aReader); + WorldSpaceId.Deserialize(aReader); + ReferenceId.Deserialize(aReader); +} diff --git a/Code/encoding/Messages/RequestActorDrop.h b/Code/encoding/Messages/RequestActorDrop.h new file mode 100644 index 000000000..4471a4396 --- /dev/null +++ b/Code/encoding/Messages/RequestActorDrop.h @@ -0,0 +1,39 @@ +#pragma once + +#include "Message.h" +#include +#include +#include +#include + +struct RequestActorDrop final : ClientMessage +{ + static constexpr ClientOpcode Opcode = kRequestActorDrop; + + RequestActorDrop() + : ClientMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const RequestActorDrop& acRhs) const noexcept + { + return GetOpcode() == acRhs.GetOpcode() && ServerId == acRhs.ServerId && ActorFormId == acRhs.ActorFormId && Item == acRhs.Item && ClientDropId == acRhs.ClientDropId && HasLocation == acRhs.HasLocation && + (!HasLocation || Location == acRhs.Location) && HasRotation == acRhs.HasRotation && (!HasRotation || Rotation == acRhs.Rotation) && CellId == acRhs.CellId && WorldSpaceId == acRhs.WorldSpaceId && + ReferenceId == acRhs.ReferenceId; + } + + uint32_t ServerId{}; + uint32_t ActorFormId{}; + Inventory::Entry Item{}; + Guid ClientDropId{}; + bool HasLocation = false; + Vector3_NetQuantize Location{}; + bool HasRotation = false; + Vector3_NetQuantize Rotation{}; + GameId CellId{}; + GameId WorldSpaceId{}; + GameId ReferenceId{}; +}; diff --git a/Code/encoding/Messages/RequestDroppedItemMove.cpp b/Code/encoding/Messages/RequestDroppedItemMove.cpp new file mode 100644 index 000000000..84fda9a01 --- /dev/null +++ b/Code/encoding/Messages/RequestDroppedItemMove.cpp @@ -0,0 +1,43 @@ +#include + +void RequestDroppedItemMove::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, ServerId); + Serialization::WriteVarInt(aWriter, DropId); + Serialization::WriteBool(aWriter, HasLocation); + if (HasLocation) + Location.Serialize(aWriter); + Serialization::WriteBool(aWriter, HasRotation); + if (HasRotation) + Rotation.Serialize(aWriter); + Serialization::WriteBool(aWriter, HasVelocity); + if (HasVelocity) + Velocity.Serialize(aWriter); + Serialization::WriteBool(aWriter, HasAngularVelocity); + if (HasAngularVelocity) + AngularVelocity.Serialize(aWriter); + CellId.Serialize(aWriter); + WorldSpaceId.Serialize(aWriter); + ReferenceId.Serialize(aWriter); +} + +void RequestDroppedItemMove::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ServerId = Serialization::ReadVarInt(aReader); + DropId = Serialization::ReadVarInt(aReader); + HasLocation = Serialization::ReadBool(aReader); + if (HasLocation) + Location.Deserialize(aReader); + HasRotation = Serialization::ReadBool(aReader); + if (HasRotation) + Rotation.Deserialize(aReader); + HasVelocity = Serialization::ReadBool(aReader); + if (HasVelocity) + Velocity.Deserialize(aReader); + HasAngularVelocity = Serialization::ReadBool(aReader); + if (HasAngularVelocity) + AngularVelocity.Deserialize(aReader); + CellId.Deserialize(aReader); + WorldSpaceId.Deserialize(aReader); + ReferenceId.Deserialize(aReader); +} diff --git a/Code/encoding/Messages/RequestDroppedItemMove.h b/Code/encoding/Messages/RequestDroppedItemMove.h new file mode 100644 index 000000000..577564e83 --- /dev/null +++ b/Code/encoding/Messages/RequestDroppedItemMove.h @@ -0,0 +1,50 @@ +#pragma once + +#include "Message.h" +#include +#include + +struct RequestDroppedItemMove final : ClientMessage +{ + static constexpr ClientOpcode Opcode = kRequestDroppedItemMove; + + RequestDroppedItemMove() + : ClientMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const RequestDroppedItemMove& acRhs) const noexcept + { + return GetOpcode() == acRhs.GetOpcode() + && ServerId == acRhs.ServerId + && DropId == acRhs.DropId + && HasLocation == acRhs.HasLocation + && (!HasLocation || Location == acRhs.Location) + && HasRotation == acRhs.HasRotation + && (!HasRotation || Rotation == acRhs.Rotation) + && HasVelocity == acRhs.HasVelocity + && (!HasVelocity || Velocity == acRhs.Velocity) + && HasAngularVelocity == acRhs.HasAngularVelocity + && (!HasAngularVelocity || AngularVelocity == acRhs.AngularVelocity) + && CellId == acRhs.CellId + && WorldSpaceId == acRhs.WorldSpaceId + && ReferenceId == acRhs.ReferenceId; + } + + uint32_t ServerId{}; + uint64_t DropId{}; + bool HasLocation{false}; + Vector3_NetQuantize Location{}; + bool HasRotation{false}; + Vector3_NetQuantize Rotation{}; + bool HasVelocity{false}; + Vector3_NetQuantize Velocity{}; + bool HasAngularVelocity{false}; + Vector3_NetQuantize AngularVelocity{}; + GameId CellId{}; + GameId WorldSpaceId{}; + GameId ReferenceId{}; +}; diff --git a/Code/encoding/Messages/RequestDroppedItemPhysicsDisabled.cpp b/Code/encoding/Messages/RequestDroppedItemPhysicsDisabled.cpp new file mode 100644 index 000000000..84f10b38b --- /dev/null +++ b/Code/encoding/Messages/RequestDroppedItemPhysicsDisabled.cpp @@ -0,0 +1,31 @@ +#include + +void RequestDroppedItemPhysicsDisabled::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, ServerId); + Serialization::WriteVarInt(aWriter, DropId); + Serialization::WriteBool(aWriter, HasLocation); + if (HasLocation) + Location.Serialize(aWriter); + Serialization::WriteBool(aWriter, HasRotation); + if (HasRotation) + Rotation.Serialize(aWriter); + CellId.Serialize(aWriter); + WorldSpaceId.Serialize(aWriter); + ReferenceId.Serialize(aWriter); +} + +void RequestDroppedItemPhysicsDisabled::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ServerId = Serialization::ReadVarInt(aReader); + DropId = Serialization::ReadVarInt(aReader); + HasLocation = Serialization::ReadBool(aReader); + if (HasLocation) + Location.Deserialize(aReader); + HasRotation = Serialization::ReadBool(aReader); + if (HasRotation) + Rotation.Deserialize(aReader); + CellId.Deserialize(aReader); + WorldSpaceId.Deserialize(aReader); + ReferenceId.Deserialize(aReader); +} diff --git a/Code/encoding/Messages/RequestDroppedItemPhysicsDisabled.h b/Code/encoding/Messages/RequestDroppedItemPhysicsDisabled.h new file mode 100644 index 000000000..a742f73aa --- /dev/null +++ b/Code/encoding/Messages/RequestDroppedItemPhysicsDisabled.h @@ -0,0 +1,42 @@ +#pragma once + +#include "Message.h" +#include +#include + +struct RequestDroppedItemPhysicsDisabled final : ClientMessage +{ + static constexpr ClientOpcode Opcode = kRequestDroppedItemPhysicsDisabled; + + RequestDroppedItemPhysicsDisabled() + : ClientMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const RequestDroppedItemPhysicsDisabled& acRhs) const noexcept + { + return GetOpcode() == acRhs.GetOpcode() + && ServerId == acRhs.ServerId + && DropId == acRhs.DropId + && HasLocation == acRhs.HasLocation + && (!HasLocation || Location == acRhs.Location) + && HasRotation == acRhs.HasRotation + && (!HasRotation || Rotation == acRhs.Rotation) + && CellId == acRhs.CellId + && WorldSpaceId == acRhs.WorldSpaceId + && ReferenceId == acRhs.ReferenceId; + } + + uint32_t ServerId{}; + uint64_t DropId{}; + bool HasLocation{false}; + Vector3_NetQuantize Location{}; + bool HasRotation{false}; + Vector3_NetQuantize Rotation{}; + GameId CellId{}; + GameId WorldSpaceId{}; + GameId ReferenceId{}; +}; diff --git a/Code/encoding/Messages/RequestDroppedItems.cpp b/Code/encoding/Messages/RequestDroppedItems.cpp new file mode 100644 index 000000000..a328ff6c8 --- /dev/null +++ b/Code/encoding/Messages/RequestDroppedItems.cpp @@ -0,0 +1,69 @@ +#include +#include + +void RequestDroppedItems::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, RequestId); + Serialization::WriteBool(aWriter, RequestAll); + Serialization::WriteBool(aWriter, HasCellFilter); + if (HasCellFilter) + CellId.Serialize(aWriter); + + Serialization::WriteBool(aWriter, HasWorldSpaceFilter); + if (HasWorldSpaceFilter) + WorldSpaceId.Serialize(aWriter); + + Serialization::WriteVarInt(aWriter, Discoveries.size()); + for (const auto& entry : Discoveries) + { + entry.ReferenceId.Serialize(aWriter); + entry.CellId.Serialize(aWriter); + entry.WorldSpaceId.Serialize(aWriter); + entry.Item.Serialize(aWriter); + + Serialization::WriteBool(aWriter, entry.HasLocation); + if (entry.HasLocation) + entry.Location.Serialize(aWriter); + + Serialization::WriteBool(aWriter, entry.HasRotation); + if (entry.HasRotation) + entry.Rotation.Serialize(aWriter); + } +} + +void RequestDroppedItems::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ClientMessage::DeserializeRaw(aReader); + + RequestId = Serialization::ReadVarInt(aReader); + RequestAll = Serialization::ReadBool(aReader); + HasCellFilter = Serialization::ReadBool(aReader); + if (HasCellFilter) + CellId.Deserialize(aReader); + + HasWorldSpaceFilter = Serialization::ReadBool(aReader); + if (HasWorldSpaceFilter) + WorldSpaceId.Deserialize(aReader); + + const auto discoveryCount = Serialization::ReadVarInt(aReader); + Discoveries.clear(); + Discoveries.reserve(discoveryCount); + for (size_t i = 0; i < discoveryCount; ++i) + { + DiscoveryEntry entry{}; + entry.ReferenceId.Deserialize(aReader); + entry.CellId.Deserialize(aReader); + entry.WorldSpaceId.Deserialize(aReader); + entry.Item.Deserialize(aReader); + + entry.HasLocation = Serialization::ReadBool(aReader); + if (entry.HasLocation) + entry.Location.Deserialize(aReader); + + entry.HasRotation = Serialization::ReadBool(aReader); + if (entry.HasRotation) + entry.Rotation.Deserialize(aReader); + + Discoveries.push_back(std::move(entry)); + } +} diff --git a/Code/encoding/Messages/RequestDroppedItems.h b/Code/encoding/Messages/RequestDroppedItems.h new file mode 100644 index 000000000..171c34375 --- /dev/null +++ b/Code/encoding/Messages/RequestDroppedItems.h @@ -0,0 +1,53 @@ +#pragma once + +#include "Message.h" +#include +#include +#include + +struct RequestDroppedItems final : ClientMessage +{ + struct DiscoveryEntry + { + GameId ReferenceId{}; + GameId CellId{}; + GameId WorldSpaceId{}; + Inventory::Entry Item{}; + bool HasLocation{false}; + Vector3_NetQuantize Location{}; + bool HasRotation{false}; + Vector3_NetQuantize Rotation{}; + + bool operator==(const DiscoveryEntry& acRhs) const noexcept + { + return ReferenceId == acRhs.ReferenceId && CellId == acRhs.CellId && WorldSpaceId == acRhs.WorldSpaceId && Item == acRhs.Item && HasLocation == acRhs.HasLocation && + (!HasLocation || Location == acRhs.Location) && HasRotation == acRhs.HasRotation && (!HasRotation || Rotation == acRhs.Rotation); + } + }; + + static constexpr ClientOpcode Opcode = kRequestDroppedItems; + + RequestDroppedItems() + : ClientMessage(Opcode) + { + } + + virtual ~RequestDroppedItems() = default; + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const RequestDroppedItems& acRhs) const noexcept + { + return GetOpcode() == acRhs.GetOpcode() && RequestAll == acRhs.RequestAll && HasCellFilter == acRhs.HasCellFilter && (!HasCellFilter || CellId == acRhs.CellId) && HasWorldSpaceFilter == acRhs.HasWorldSpaceFilter && + (!HasWorldSpaceFilter || WorldSpaceId == acRhs.WorldSpaceId) && Discoveries == acRhs.Discoveries; + } + + uint32_t RequestId{}; + bool RequestAll{false}; + bool HasCellFilter{false}; + GameId CellId{}; + bool HasWorldSpaceFilter{false}; + GameId WorldSpaceId{}; + TiltedPhoques::Vector Discoveries{}; +}; diff --git a/Code/encoding/Messages/RequestInventoryChanges.cpp b/Code/encoding/Messages/RequestInventoryChanges.cpp index 8fed22475..b807fc374 100644 --- a/Code/encoding/Messages/RequestInventoryChanges.cpp +++ b/Code/encoding/Messages/RequestInventoryChanges.cpp @@ -5,7 +5,6 @@ void RequestInventoryChanges::SerializeRaw(TiltedPhoques::Buffer::Writer& aWrite { Serialization::WriteVarInt(aWriter, ServerId); Item.Serialize(aWriter); - Serialization::WriteBool(aWriter, Drop); Serialization::WriteBool(aWriter, UpdateClients); } @@ -15,6 +14,5 @@ void RequestInventoryChanges::DeserializeRaw(TiltedPhoques::Buffer::Reader& aRea ServerId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; Item.Deserialize(aReader); - Drop = Serialization::ReadBool(aReader); UpdateClients = Serialization::ReadBool(aReader); } diff --git a/Code/encoding/Messages/RequestInventoryChanges.h b/Code/encoding/Messages/RequestInventoryChanges.h index 43e89547c..30f48d0bb 100644 --- a/Code/encoding/Messages/RequestInventoryChanges.h +++ b/Code/encoding/Messages/RequestInventoryChanges.h @@ -17,10 +17,12 @@ struct RequestInventoryChanges final : ClientMessage void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; - bool operator==(const RequestInventoryChanges& acRhs) const noexcept { return GetOpcode() == acRhs.GetOpcode() && ServerId == acRhs.ServerId && Item == acRhs.Item && Drop == acRhs.Drop && UpdateClients == acRhs.UpdateClients; } + bool operator==(const RequestInventoryChanges& acRhs) const noexcept + { + return GetOpcode() == acRhs.GetOpcode() && ServerId == acRhs.ServerId && Item == acRhs.Item && UpdateClients == acRhs.UpdateClients; + } uint32_t ServerId{}; Inventory::Entry Item{}; - bool Drop = false; bool UpdateClients = true; }; diff --git a/Code/encoding/Messages/RequestPickupDroppedItem.cpp b/Code/encoding/Messages/RequestPickupDroppedItem.cpp new file mode 100644 index 000000000..8ca3e990b --- /dev/null +++ b/Code/encoding/Messages/RequestPickupDroppedItem.cpp @@ -0,0 +1,42 @@ +#include +#include + +void RequestPickupDroppedItem::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, ServerId); + Serialization::WriteVarInt(aWriter, DropId); + Item.Serialize(aWriter); + + Serialization::WriteBool(aWriter, HasLocation); + if (HasLocation) + Location.Serialize(aWriter); + + Serialization::WriteBool(aWriter, HasRotation); + if (HasRotation) + Rotation.Serialize(aWriter); + + CellId.Serialize(aWriter); + WorldSpaceId.Serialize(aWriter); + ReferenceId.Serialize(aWriter); +} + +void RequestPickupDroppedItem::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ClientMessage::DeserializeRaw(aReader); + + ServerId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; + DropId = Serialization::ReadVarInt(aReader); + Item.Deserialize(aReader); + + HasLocation = Serialization::ReadBool(aReader); + if (HasLocation) + Location.Deserialize(aReader); + + HasRotation = Serialization::ReadBool(aReader); + if (HasRotation) + Rotation.Deserialize(aReader); + + CellId.Deserialize(aReader); + WorldSpaceId.Deserialize(aReader); + ReferenceId.Deserialize(aReader); +} diff --git a/Code/encoding/Messages/RequestPickupDroppedItem.h b/Code/encoding/Messages/RequestPickupDroppedItem.h new file mode 100644 index 000000000..f9c34189e --- /dev/null +++ b/Code/encoding/Messages/RequestPickupDroppedItem.h @@ -0,0 +1,45 @@ +#pragma once + +#include "Message.h" +#include +#include +#include + +struct RequestPickupDroppedItem final : ClientMessage +{ + static constexpr ClientOpcode Opcode = kRequestPickupDroppedItem; + + RequestPickupDroppedItem() + : ClientMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const RequestPickupDroppedItem& acRhs) const noexcept + { + return GetOpcode() == acRhs.GetOpcode() + && ServerId == acRhs.ServerId + && DropId == acRhs.DropId + && Item == acRhs.Item + && HasLocation == acRhs.HasLocation + && (!HasLocation || Location == acRhs.Location) + && HasRotation == acRhs.HasRotation + && (!HasRotation || Rotation == acRhs.Rotation) + && CellId == acRhs.CellId + && WorldSpaceId == acRhs.WorldSpaceId + && ReferenceId == acRhs.ReferenceId; + } + + uint32_t ServerId{}; + uint64_t DropId{}; + Inventory::Entry Item{}; + bool HasLocation{false}; + Vector3_NetQuantize Location{}; + bool HasRotation{false}; + Vector3_NetQuantize Rotation{}; + GameId CellId{}; + GameId WorldSpaceId{}; + GameId ReferenceId{}; +}; diff --git a/Code/encoding/Messages/RequestSetSyncMode.cpp b/Code/encoding/Messages/RequestSetSyncMode.cpp new file mode 100644 index 000000000..fe3b2089b --- /dev/null +++ b/Code/encoding/Messages/RequestSetSyncMode.cpp @@ -0,0 +1,15 @@ +#include + +void RequestSetSyncMode::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + aWriter.WriteBits(static_cast(Mode), 8); +} + +void RequestSetSyncMode::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ClientMessage::DeserializeRaw(aReader); + + uint64_t mode = 0; + aReader.ReadBits(mode, 8); + Mode = static_cast(mode & 0xFF); +} diff --git a/Code/encoding/Messages/RequestSetSyncMode.h b/Code/encoding/Messages/RequestSetSyncMode.h new file mode 100644 index 000000000..014873db6 --- /dev/null +++ b/Code/encoding/Messages/RequestSetSyncMode.h @@ -0,0 +1,22 @@ +#pragma once + +#include "Message.h" + +#include + +struct RequestSetSyncMode final : ClientMessage +{ + static constexpr ClientOpcode Opcode = kRequestSetSyncMode; + + RequestSetSyncMode() + : ClientMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const RequestSetSyncMode& acRhs) const noexcept { return GetOpcode() == acRhs.GetOpcode() && Mode == acRhs.Mode; } + + SyncMode Mode{SyncMode::Normal}; +}; diff --git a/Code/encoding/Messages/ServerMessageFactory.h b/Code/encoding/Messages/ServerMessageFactory.h index 942c30fec..50f32c18f 100644 --- a/Code/encoding/Messages/ServerMessageFactory.h +++ b/Code/encoding/Messages/ServerMessageFactory.h @@ -10,6 +10,11 @@ #include #include #include +#include +#include +#include +#include +#include #include #include #include @@ -18,6 +23,11 @@ #include #include #include +#include +#include +#include +#include +#include #include #include #include @@ -53,6 +63,10 @@ #include #include #include +#include +#include +#include +#include #include #include #include @@ -60,6 +74,16 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include using TiltedPhoques::UniquePtr; @@ -70,11 +94,11 @@ struct ServerMessageFactory template static auto Visit(T&& func) { auto s_visitor = CreateMessageVisitor< - 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, + AuthenticationResponse, AssignCharacterResponse, ServerReferencesMoveRequest, ServerTimeSettings, CharacterSpawnRequest, NotifyInventoryChanges, NotifyActorDrop, NotifyDroppedItemPickedUp, NotifyDroppedItems, NotifyDroppedItemMove, NotifyDroppedItemPhysicsDisabled, StringCacheUpdate, NotifyFactionsChanges, NotifyRemoveCharacter, NotifyQuestUpdate, NotifyPlayerList, NotifyPartyInfo, NotifyPartyInvite, + NotifyPartyJoined, NotifyPartyLeft, NotifyTradeInvite, NotifyTradeStarted, NotifyTradeState, NotifyTradeCancel, NotifyTradeComplete, NotifyActorValueChanges, 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, NotifySetTimeResult, NotifyRemoveSpell>; + NotifySubtitle, NotifyPlayerDialogue, NotifyActorTeleport, NotifyRelinquishControl, NotifyPlayerLeft, NotifyPlayerJoined, NotifyPlayerActorName, NotifyPlayerProfileImage, NotifyDialogue, NotifySubtitle, NotifyPlayerDialogue, NotifyPlayerLevel, NotifyPlayerCellChanged, NotifyTeleportRequest, NotifyTeleportCountdown, NotifyTeleport, NotifyPlayerHealthUpdate, NotifySettingsChange, + NotifyWeatherChange, NotifySetWaypoint, NotifyRemoveWaypoint, NotifySetTimeResult, NotifyPartyPositions, NotifyRemoveSpell, NotifyHealingProximity, NotifyPartyMemberDowned, NotifyPartyFastTravelMarkers, NotifyPlayerSyncMode, NotifyCommandList, NotifyPlayEmote, NotifyCancelEmote, NotifyPartyOptions, NotifyPartyLeaderCellLock>; return s_visitor(std::forward(func)); } diff --git a/Code/encoding/Messages/TeleportResponse.cpp b/Code/encoding/Messages/TeleportResponse.cpp new file mode 100644 index 000000000..ce18d4168 --- /dev/null +++ b/Code/encoding/Messages/TeleportResponse.cpp @@ -0,0 +1,15 @@ +#include + +#include + +void TeleportResponse::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, RequesterId); + Serialization::WriteBool(aWriter, Accepted); +} + +void TeleportResponse::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + RequesterId = static_cast(Serialization::ReadVarInt(aReader) & 0xFFFF); + Accepted = Serialization::ReadBool(aReader); +} diff --git a/Code/encoding/Messages/TeleportResponse.h b/Code/encoding/Messages/TeleportResponse.h new file mode 100644 index 000000000..c2b39c977 --- /dev/null +++ b/Code/encoding/Messages/TeleportResponse.h @@ -0,0 +1,24 @@ +#pragma once + +#include "Message.h" + +struct TeleportResponse final : ClientMessage +{ + static constexpr ClientOpcode Opcode = kTeleportResponse; + + TeleportResponse() + : ClientMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool operator==(const TeleportResponse& acRhs) const noexcept + { + return GetOpcode() == acRhs.GetOpcode() && RequesterId == acRhs.RequesterId && Accepted == acRhs.Accepted; + } + + uint16_t RequesterId{}; + bool Accepted{}; +}; diff --git a/Code/encoding/Messages/TradeCancelRequest.cpp b/Code/encoding/Messages/TradeCancelRequest.cpp new file mode 100644 index 000000000..b0136223b --- /dev/null +++ b/Code/encoding/Messages/TradeCancelRequest.cpp @@ -0,0 +1,10 @@ +#include + +void TradeCancelRequest::SerializeRaw(TiltedPhoques::Buffer::Writer&) const noexcept +{ +} + +void TradeCancelRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ClientMessage::DeserializeRaw(aReader); +} diff --git a/Code/encoding/Messages/TradeCancelRequest.h b/Code/encoding/Messages/TradeCancelRequest.h new file mode 100644 index 000000000..aeca15abb --- /dev/null +++ b/Code/encoding/Messages/TradeCancelRequest.h @@ -0,0 +1,16 @@ +#pragma once + +#include "Message.h" + +struct TradeCancelRequest final : ClientMessage +{ + static constexpr ClientOpcode Opcode = kTradeCancelRequest; + + TradeCancelRequest() + : ClientMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; +}; diff --git a/Code/encoding/Messages/TradeInviteRequest.cpp b/Code/encoding/Messages/TradeInviteRequest.cpp new file mode 100644 index 000000000..ce97a4c32 --- /dev/null +++ b/Code/encoding/Messages/TradeInviteRequest.cpp @@ -0,0 +1,13 @@ +#include + +void TradeInviteRequest::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, TargetPlayerId); +} + +void TradeInviteRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ClientMessage::DeserializeRaw(aReader); + + TargetPlayerId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; +} diff --git a/Code/encoding/Messages/TradeInviteRequest.h b/Code/encoding/Messages/TradeInviteRequest.h new file mode 100644 index 000000000..87b31f74f --- /dev/null +++ b/Code/encoding/Messages/TradeInviteRequest.h @@ -0,0 +1,18 @@ +#pragma once + +#include "Message.h" + +struct TradeInviteRequest final : ClientMessage +{ + static constexpr ClientOpcode Opcode = kTradeInviteRequest; + + TradeInviteRequest() + : ClientMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + uint32_t TargetPlayerId{}; +}; diff --git a/Code/encoding/Messages/TradeInviteResponseRequest.cpp b/Code/encoding/Messages/TradeInviteResponseRequest.cpp new file mode 100644 index 000000000..d1f1f368e --- /dev/null +++ b/Code/encoding/Messages/TradeInviteResponseRequest.cpp @@ -0,0 +1,15 @@ +#include + +void TradeInviteResponseRequest::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, RequesterPlayerId); + Serialization::WriteBool(aWriter, Accept); +} + +void TradeInviteResponseRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ClientMessage::DeserializeRaw(aReader); + + RequesterPlayerId = Serialization::ReadVarInt(aReader) & 0xFFFFFFFF; + Accept = Serialization::ReadBool(aReader); +} diff --git a/Code/encoding/Messages/TradeInviteResponseRequest.h b/Code/encoding/Messages/TradeInviteResponseRequest.h new file mode 100644 index 000000000..b954fd90c --- /dev/null +++ b/Code/encoding/Messages/TradeInviteResponseRequest.h @@ -0,0 +1,19 @@ +#pragma once + +#include "Message.h" + +struct TradeInviteResponseRequest final : ClientMessage +{ + static constexpr ClientOpcode Opcode = kTradeInviteResponseRequest; + + TradeInviteResponseRequest() + : ClientMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + uint32_t RequesterPlayerId{}; + bool Accept{false}; +}; diff --git a/Code/encoding/Messages/TradeOfferUpdateRequest.cpp b/Code/encoding/Messages/TradeOfferUpdateRequest.cpp new file mode 100644 index 000000000..86b4226b4 --- /dev/null +++ b/Code/encoding/Messages/TradeOfferUpdateRequest.cpp @@ -0,0 +1,22 @@ +#include + +void TradeOfferUpdateRequest::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteVarInt(aWriter, Items.size()); + for (const auto& item : Items) + item.Serialize(aWriter); +} + +void TradeOfferUpdateRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ClientMessage::DeserializeRaw(aReader); + + const auto count = Serialization::ReadVarInt(aReader); + Items.reserve(count); + for (size_t i = 0; i < count; ++i) + { + Inventory::Entry entry; + entry.Deserialize(aReader); + Items.push_back(entry); + } +} diff --git a/Code/encoding/Messages/TradeOfferUpdateRequest.h b/Code/encoding/Messages/TradeOfferUpdateRequest.h new file mode 100644 index 000000000..5467619b1 --- /dev/null +++ b/Code/encoding/Messages/TradeOfferUpdateRequest.h @@ -0,0 +1,20 @@ +#pragma once + +#include "Message.h" + +#include + +struct TradeOfferUpdateRequest final : ClientMessage +{ + static constexpr ClientOpcode Opcode = kTradeOfferUpdateRequest; + + TradeOfferUpdateRequest() + : ClientMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + TiltedPhoques::Vector Items; +}; diff --git a/Code/encoding/Messages/TradeSetReadyRequest.cpp b/Code/encoding/Messages/TradeSetReadyRequest.cpp new file mode 100644 index 000000000..0e0c4e109 --- /dev/null +++ b/Code/encoding/Messages/TradeSetReadyRequest.cpp @@ -0,0 +1,13 @@ +#include + +void TradeSetReadyRequest::SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + Serialization::WriteBool(aWriter, Ready); +} + +void TradeSetReadyRequest::DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + ClientMessage::DeserializeRaw(aReader); + + Ready = Serialization::ReadBool(aReader); +} diff --git a/Code/encoding/Messages/TradeSetReadyRequest.h b/Code/encoding/Messages/TradeSetReadyRequest.h new file mode 100644 index 000000000..c83497ea3 --- /dev/null +++ b/Code/encoding/Messages/TradeSetReadyRequest.h @@ -0,0 +1,18 @@ +#pragma once + +#include "Message.h" + +struct TradeSetReadyRequest final : ClientMessage +{ + static constexpr ClientOpcode Opcode = kTradeSetReadyRequest; + + TradeSetReadyRequest() + : ClientMessage(Opcode) + { + } + + void SerializeRaw(TiltedPhoques::Buffer::Writer& aWriter) const noexcept override; + void DeserializeRaw(TiltedPhoques::Buffer::Reader& aReader) noexcept override; + + bool Ready{false}; +}; diff --git a/Code/encoding/Opcodes.h b/Code/encoding/Opcodes.h index 8f0b10592..ff4a961a7 100644 --- a/Code/encoding/Opcodes.h +++ b/Code/encoding/Opcodes.h @@ -16,6 +16,11 @@ enum ClientOpcode : unsigned char kPartyCreateRequest, kPartyChangeLeaderRequest, kPartyKickRequest, + kTradeInviteRequest, + kTradeInviteResponseRequest, + kTradeOfferUpdateRequest, + kTradeSetReadyRequest, + kTradeCancelRequest, kRequestActorValueChanges, kRequestActorMaxValueChanges, kRequestHealthChangeBroadcast, @@ -28,6 +33,11 @@ enum ClientOpcode : unsigned char kRequestOwnershipClaim, kRequestObjectInventoryChanges, kRequestInventoryChanges, + kRequestActorDrop, + kRequestPickupDroppedItem, + kRequestDroppedItems, + kRequestDroppedItemMove, + kRequestDroppedItemPhysicsDisabled, kSpellCastRequest, kInterruptCastRequest, kAddTargetRequest, @@ -48,12 +58,25 @@ enum ClientOpcode : unsigned char kPlayerDialogueRequest, kPlayerLevelRequest, kTeleportRequest, + kTeleportResponse, kRequestPlayerHealthUpdate, kRequestWeatherChange, kRequestCurrentWeather, kRequestSetWaypoint, kRequestRemoveWaypoint, + kPartyPositionsRequest, + kPartyPositionUpdateRequest, + kPartyMemberDownedRequest, + kPlayerProfileImageUpdateRequest, kSetTimeCommandRequest, + kRequestHealingProximity, + kPartyFastTravelMarkersRequest, + kRequestSetSyncMode, + kPlayEmoteRequest, + kCancelEmoteRequest, + kPlayerActorNameUpdateRequest, + kPartyActorNamesRequest, + kPartyOptionsUpdateRequest, kClientOpcodeMax }; @@ -72,6 +95,11 @@ enum ServerOpcode : unsigned char kNotifyPartyInvite, kNotifyPartyJoined, kNotifyPartyLeft, + kNotifyTradeInvite, + kNotifyTradeStarted, + kNotifyTradeState, + kNotifyTradeCancel, + kNotifyTradeComplete, kNotifyActorValueChanges, kNotifyActorMaxValueChanges, kNotifyHealthChangeBroadcast, @@ -83,6 +111,11 @@ enum ServerOpcode : unsigned char kNotifyOwnershipTransfer, kNotifyObjectInventoryChanges, kNotifyInventoryChanges, + kNotifyActorDrop, + kNotifyDroppedItemPickedUp, + kNotifyDroppedItems, + kNotifyDroppedItemMove, + kNotifyDroppedItemPhysicsDisabled, kNotifySpellCast, kNotifyInterruptCast, kNotifyAddTarget, @@ -108,6 +141,8 @@ enum ServerOpcode : unsigned char kNotifyPlayerJoined, kNotifyPlayerLevel, kNotifyPlayerCellChanged, + kNotifyTeleportRequest, + kNotifyTeleportCountdown, kNotifyTeleport, kNotifyPlayerHealthUpdate, kNotifySettingsChange, @@ -115,5 +150,17 @@ enum ServerOpcode : unsigned char kNotifySetWaypoint, kNotifyRemoveWaypoint, kNotifySetTimeResult, + kNotifyPartyPositions, + kNotifyHealingProximity, + kNotifyPartyMemberDowned, + kNotifyPlayerProfileImage, + kNotifyPartyFastTravelMarkers, + kNotifyPlayerSyncMode, + kNotifyCommandList, + kNotifyPlayEmote, + kNotifyCancelEmote, + kNotifyPlayerActorName, + kNotifyPartyOptions, + kNotifyPartyLeaderCellLock, kServerOpcodeMax }; diff --git a/Code/encoding/Structs/Guid.cpp b/Code/encoding/Structs/Guid.cpp new file mode 100644 index 000000000..fe33924a6 --- /dev/null +++ b/Code/encoding/Structs/Guid.cpp @@ -0,0 +1,144 @@ +#include "Guid.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ +constexpr std::array kHyphenOffsets{4, 6, 8, 10}; + +std::array MakeSeedMaterial() noexcept +{ + std::array material{}; + bool seeded = false; + + try + { + std::random_device rd; + for (auto& value : material) + { + value = rd(); + } + seeded = true; + } + catch (...) + { + } + + if (!seeded) + { + const auto now = static_cast(std::chrono::high_resolution_clock::now().time_since_epoch().count()); + const auto tidHash = static_cast(std::hash{}(std::this_thread::get_id())); + const auto addr = reinterpret_cast(&material); + + material[0] ^= static_cast(now); + material[1] ^= static_cast(now >> 32); + material[2] ^= tidHash; + material[3] ^= static_cast(addr); + material[4] ^= static_cast(addr >> 32); + + uint32_t stir = 0x9E3779B9u; + for (size_t i = 5; i < material.size(); ++i) + { + stir ^= (stir << 13); + stir ^= (stir >> 17); + stir ^= (stir << 5); + material[i] ^= stir; + } + } + + return material; +} + +std::mt19937_64 CreateSeededRng() noexcept +{ + auto seeds = MakeSeedMaterial(); + std::seed_seq seq(seeds.begin(), seeds.end()); + return std::mt19937_64(seq); +} + +std::mt19937_64& GetRng() noexcept +{ + static std::mt19937_64 g_rng = CreateSeededRng(); + return g_rng; +} + +std::mutex& GetRngMutex() noexcept +{ + static std::mutex g_mutex; + return g_mutex; +} + +} // namespace + +Guid Guid::Random() noexcept +{ + Guid guid{}; + + auto& rng = GetRng(); + auto& mutex = GetRngMutex(); + std::lock_guard _(mutex); + for (size_t i = 0; i < guid.Bytes.size(); i += 8) + { + uint64_t chunk = rng(); + for (size_t j = 0; j < 8; ++j) + { + guid.Bytes[i + j] = static_cast(chunk & 0xFF); + chunk >>= 8; + } + } + + // RFC 4122 version 4 + variant bits + guid.Bytes[6] = static_cast((guid.Bytes[6] & 0x0F) | 0x40); + guid.Bytes[8] = static_cast((guid.Bytes[8] & 0x3F) | 0x80); + + return guid; +} + +bool Guid::IsEmpty() const noexcept +{ + for (const auto byte : Bytes) + { + if (byte != 0) + return false; + } + + return true; +} + +void Guid::Clear() noexcept +{ + Bytes.fill(0); +} + +void Guid::Serialize(Buffer::Writer& aWriter) const noexcept +{ + auto* pData = const_cast(Bytes.data()); + aWriter.WriteBytes(pData, Bytes.size()); +} + +void Guid::Deserialize(Buffer::Reader& aReader) noexcept +{ + aReader.ReadBytes(Bytes.data(), Bytes.size()); +} + +std::string Guid::ToString() const +{ + std::string result; + result.reserve(36); + static constexpr char kHexDigits[] = "0123456789abcdef"; + + for (size_t i = 0; i < Bytes.size(); ++i) + { + result.push_back(kHexDigits[(Bytes[i] >> 4) & 0x0F]); + result.push_back(kHexDigits[Bytes[i] & 0x0F]); + if (std::find(kHyphenOffsets.begin(), kHyphenOffsets.end(), i + 1) != kHyphenOffsets.end()) + result.push_back('-'); + } + + return result; +} diff --git a/Code/encoding/Structs/Guid.h b/Code/encoding/Structs/Guid.h new file mode 100644 index 000000000..2706bb94e --- /dev/null +++ b/Code/encoding/Structs/Guid.h @@ -0,0 +1,55 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +using TiltedPhoques::Buffer; + +struct Guid +{ + std::array Bytes{}; + + static Guid Random() noexcept; + + bool operator==(const Guid& acRhs) const noexcept { return Bytes == acRhs.Bytes; } + bool operator!=(const Guid& acRhs) const noexcept { return !(*this == acRhs); } + + bool IsEmpty() const noexcept; + void Clear() noexcept; + + void Serialize(Buffer::Writer& aWriter) const noexcept; + void Deserialize(Buffer::Reader& aReader) noexcept; + + std::string ToString() const; +}; + +namespace std +{ +template <> struct hash +{ + size_t operator()(const Guid& acValue) const noexcept + { + static_assert(sizeof(Guid::Bytes) == 16, "Guid must be 128-bit"); + + size_t hash = 0; + // combine two 64-bit halves to generate a stable hash + for (size_t i = 0; i < acValue.Bytes.size(); i += 8) + { + uint64_t part = 0; + for (size_t j = 0; j < 8; ++j) + { + part <<= 8; + part |= static_cast(acValue.Bytes[i + j]); + } + hash ^= std::hash{}(part) + 0x9e3779b97f4a7c15ULL + (hash << 6) + (hash >> 2); + } + + return hash; + } +}; +} // namespace std diff --git a/Code/encoding/Structs/PartyOptions.cpp b/Code/encoding/Structs/PartyOptions.cpp new file mode 100644 index 000000000..70de90b4f --- /dev/null +++ b/Code/encoding/Structs/PartyOptions.cpp @@ -0,0 +1,40 @@ +#include +#include + +void PartyOptions::Serialize(TiltedPhoques::Buffer::Writer& aWriter) const noexcept +{ + TiltedPhoques::Serialization::WriteVarInt(aWriter, FlagsMask); +} + +void PartyOptions::Deserialize(TiltedPhoques::Buffer::Reader& aReader) noexcept +{ + FlagsMask = static_cast(TiltedPhoques::Serialization::ReadVarInt(aReader)); +} + +void PartyOptions::SetFlag(uint32_t aFlag, bool aEnabled) noexcept +{ + if (aEnabled) + FlagsMask |= aFlag; + else + FlagsMask &= ~aFlag; +} + +void PartyOptions::SetSyncFastTravelMarkers(bool aEnabled) noexcept +{ + SetFlag(kSyncFastTravelMarkers, aEnabled); +} + +void PartyOptions::SetShowPartyMemberMarkers(bool aEnabled) noexcept +{ + SetFlag(kShowPartyMemberMarkers, aEnabled); +} + +void PartyOptions::SetSyncDeadBodyLoot(bool aEnabled) noexcept +{ + SetFlag(kSyncDeadBodyLoot, aEnabled); +} + +void PartyOptions::SetLockPartyToLeaderCell(bool aEnabled) noexcept +{ + SetFlag(kLockPartyToLeaderCell, aEnabled); +} diff --git a/Code/encoding/Structs/PartyOptions.h b/Code/encoding/Structs/PartyOptions.h new file mode 100644 index 000000000..10e2512fc --- /dev/null +++ b/Code/encoding/Structs/PartyOptions.h @@ -0,0 +1,35 @@ +#pragma once + +using TiltedPhoques::Buffer; + +struct PartyOptions +{ + enum Flags : uint32_t + { + kSyncFastTravelMarkers = 1u << 0, + kShowPartyMemberMarkers = 1u << 1, + kSyncDeadBodyLoot = 1u << 2, + kLockPartyToLeaderCell = 1u << 3, + }; + + uint32_t FlagsMask{ kShowPartyMemberMarkers }; + + bool operator==(const PartyOptions& acRhs) const noexcept { return FlagsMask == acRhs.FlagsMask; } + bool operator!=(const PartyOptions& acRhs) const noexcept { return !(*this == acRhs); } + + void Serialize(TiltedPhoques::Buffer::Writer& aWriter) const noexcept; + void Deserialize(TiltedPhoques::Buffer::Reader& aReader) noexcept; + + bool SyncFastTravelMarkers() const noexcept { return (FlagsMask & kSyncFastTravelMarkers) != 0u; } + bool ShowPartyMemberMarkers() const noexcept { return (FlagsMask & kShowPartyMemberMarkers) != 0u; } + bool SyncDeadBodyLoot() const noexcept { return (FlagsMask & kSyncDeadBodyLoot) != 0u; } + bool LockPartyToLeaderCell() const noexcept { return (FlagsMask & kLockPartyToLeaderCell) != 0u; } + + void SetSyncFastTravelMarkers(bool aEnabled) noexcept; + void SetShowPartyMemberMarkers(bool aEnabled) noexcept; + void SetSyncDeadBodyLoot(bool aEnabled) noexcept; + void SetLockPartyToLeaderCell(bool aEnabled) noexcept; + +private: + void SetFlag(uint32_t aFlag, bool aEnabled) noexcept; +}; diff --git a/Code/encoding/Structs/ServerItemType.h b/Code/encoding/Structs/ServerItemType.h new file mode 100644 index 000000000..3f3bd71b7 --- /dev/null +++ b/Code/encoding/Structs/ServerItemType.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +enum class ServerItemType : uint8_t +{ + Dropped = 0, + CreationEngine = 1 +}; diff --git a/Code/encoding/Structs/ServerSettings.cpp b/Code/encoding/Structs/ServerSettings.cpp index bdf8f77ad..c1ffd1808 100644 --- a/Code/encoding/Structs/ServerSettings.cpp +++ b/Code/encoding/Structs/ServerSettings.cpp @@ -5,7 +5,7 @@ using TiltedPhoques::Serialization; bool ServerSettings::operator==(const ServerSettings& acRhs) const noexcept { - return Difficulty == acRhs.Difficulty && GreetingsEnabled == acRhs.GreetingsEnabled && PvpEnabled == acRhs.PvpEnabled && SyncPlayerHomes == acRhs.SyncPlayerHomes && DeathSystemEnabled == acRhs.DeathSystemEnabled && AutoPartyJoin == acRhs.AutoPartyJoin; + return Difficulty == acRhs.Difficulty && GreetingsEnabled == acRhs.GreetingsEnabled && PvpEnabled == acRhs.PvpEnabled && SyncPlayerHomes == acRhs.SyncPlayerHomes && DeathSystemEnabled == acRhs.DeathSystemEnabled && SyncPlayerCalendar == acRhs.SyncPlayerCalendar && SyncPartyFastTravelMarkers == acRhs.SyncPartyFastTravelMarkers && AutoPartyJoin == acRhs.AutoPartyJoin; } bool ServerSettings::operator!=(const ServerSettings& acRhs) const noexcept @@ -21,6 +21,7 @@ void ServerSettings::Serialize(TiltedPhoques::Buffer::Writer& aWriter) const noe Serialization::WriteBool(aWriter, SyncPlayerHomes); Serialization::WriteBool(aWriter, DeathSystemEnabled); Serialization::WriteBool(aWriter, SyncPlayerCalendar); + Serialization::WriteBool(aWriter, SyncPartyFastTravelMarkers); Serialization::WriteBool(aWriter, AutoPartyJoin); } @@ -32,5 +33,6 @@ void ServerSettings::Deserialize(TiltedPhoques::Buffer::Reader& aReader) noexcep SyncPlayerHomes = Serialization::ReadBool(aReader); DeathSystemEnabled = Serialization::ReadBool(aReader); SyncPlayerCalendar = Serialization::ReadBool(aReader); + SyncPartyFastTravelMarkers = Serialization::ReadBool(aReader); AutoPartyJoin = Serialization::ReadBool(aReader); } diff --git a/Code/encoding/Structs/ServerSettings.h b/Code/encoding/Structs/ServerSettings.h index 02a49302f..04fc625e0 100644 --- a/Code/encoding/Structs/ServerSettings.h +++ b/Code/encoding/Structs/ServerSettings.h @@ -16,5 +16,6 @@ struct ServerSettings bool SyncPlayerHomes{}; bool DeathSystemEnabled{}; bool SyncPlayerCalendar{}; + bool SyncPartyFastTravelMarkers{}; bool AutoPartyJoin{}; }; diff --git a/Code/encoding/Structs/SyncMode.h b/Code/encoding/Structs/SyncMode.h new file mode 100644 index 000000000..c44c98aaf --- /dev/null +++ b/Code/encoding/Structs/SyncMode.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +enum class SyncMode : uint8_t +{ + Normal = 0, + Ghost = 1, +}; + diff --git a/Code/immersive_launcher/stubs/DllBlocklist.cpp b/Code/immersive_launcher/stubs/DllBlocklist.cpp index 9eaea50ca..d3edbdf3e 100644 --- a/Code/immersive_launcher/stubs/DllBlocklist.cpp +++ b/Code/immersive_launcher/stubs/DllBlocklist.cpp @@ -48,7 +48,7 @@ const DllGreyEntry kDllGreyList[] = { L"EngineFixes.dll", L"Data\\SKSE\\Plugins\\EngineFixes.toml", - "^VerboseLogging\\s*=", // Matches EngineFixes Release 6.x series. + "(^|\\r?\\n)\\s*VerboseLogging\\s*=", // Matches EngineFixes Release 6.x series. "# SKYRIM TOGETHER REBORN marker for EngineFixes required compatibility settings v2, DO NOT CHANGE THIS LINE", L"For EngineFixes to work with Skyrim Together Reborn, some settings are required:\n" @@ -81,7 +81,7 @@ const DllGreyEntry kDllGreyList[] = { L"EngineFixes.dll", L"Data\\SKSE\\Plugins\\EngineFixes.toml", - "^bVerboseLogging\\s*=", // Matches EngineFixes Release 7.x series. + "(^|\\r?\\n)\\s*bVerboseLogging\\s*=", // Matches EngineFixes Release 7.x series. "", // No sig regex L"", // No prompt "", // No sig to insert diff --git a/Code/server/Components/ModsComponent.cpp b/Code/server/Components/ModsComponent.cpp index e4a85e60f..133f7efad 100644 --- a/Code/server/Components/ModsComponent.cpp +++ b/Code/server/Components/ModsComponent.cpp @@ -34,8 +34,8 @@ uint32_t ModsComponent::AddLite(const String& acpFilename) noexcept void ModsComponent::AddServerMod(const ESLoader::PluginData& acData) { - // kind of a hack since we want to store both, so we take the two byte value - m_serverMods.emplace(acData.m_filename, Entry{acData.m_liteId, 1}); + const uint32_t id = acData.m_isLite ? acData.m_liteId : acData.m_standardId; + m_serverMods.emplace(acData.m_filename, Entry{id, 1}); } bool ModsComponent::IsInstalled(const String& acpFilename) const noexcept diff --git a/Code/server/Events/AdminPacketEvent.h b/Code/server/Events/AdminPacketEvent.h deleted file mode 100644 index 764714348..000000000 --- a/Code/server/Events/AdminPacketEvent.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -using TiltedPhoques::ConnectionId_t; - -/** - * @brief Wrapper for admin messages. - */ -template struct AdminPacketEvent -{ - AdminPacketEvent(T* aPacket, ConnectionId_t aConnectionId) - : Packet(*aPacket) - , ConnectionId(aConnectionId) - { - } - - constexpr AdminPacketEvent(const AdminPacketEvent& acRhs) = default; - - constexpr AdminPacketEvent& operator=(const AdminPacketEvent& acRhs) - { - Packet = acRhs.Packet; - ConnectionId = acRhs.ConnectionId; - - return *this; - } - - T& Packet; - ConnectionId_t ConnectionId; -}; diff --git a/Code/server/Game/Player.cpp b/Code/server/Game/Player.cpp index 8bfbcf798..94fd2f3c4 100644 --- a/Code/server/Game/Player.cpp +++ b/Code/server/Game/Player.cpp @@ -24,9 +24,12 @@ Player::Player(Player&& aRhs) noexcept , m_discordId{std::exchange(aRhs.m_discordId, 0)} , m_endpoint{std::exchange(aRhs.m_endpoint, {})} , m_username{std::exchange(aRhs.m_username, {})} + , m_avatar{std::exchange(aRhs.m_avatar, {})} + , m_actorName{std::exchange(aRhs.m_actorName, {})} , m_party{std::exchange(aRhs.m_party, {})} , m_questLog{std::exchange(aRhs.m_questLog, {})} , m_cell{std::exchange(aRhs.m_cell, {})} + , m_syncMode{std::exchange(aRhs.m_syncMode, SyncMode::Normal)} { } @@ -65,6 +68,16 @@ void Player::SetUsername(String aUsername) noexcept m_username = std::move(aUsername); } +void Player::SetAvatar(String aAvatar) noexcept +{ + m_avatar = std::move(aAvatar); +} + +void Player::SetActorName(String aActorName) noexcept +{ + m_actorName = std::move(aActorName); +} + void Player::SetMods(Vector aMods) noexcept { m_mods = std::move(aMods); diff --git a/Code/server/Game/Player.h b/Code/server/Game/Player.h index 0ae7af3bb..fa1b5c212 100644 --- a/Code/server/Game/Player.h +++ b/Code/server/Game/Player.h @@ -1,5 +1,7 @@ #pragma once +#include + struct ServerMessage; struct Player { @@ -17,10 +19,13 @@ struct Player [[nodiscard]] std::optional GetCharacter() const noexcept { return m_character; } [[nodiscard]] PartyComponent& GetParty() noexcept { return m_party; } [[nodiscard]] const String& GetUsername() const noexcept { return m_username; } + [[nodiscard]] const String& GetAvatar() const noexcept { return m_avatar; } + [[nodiscard]] const String& GetActorName() const noexcept { return m_actorName; } [[nodiscard]] const String& GetEndPoint() const noexcept { return m_endpoint; } [[nodiscard]] const uint64_t GetDiscordId() const noexcept { return m_discordId; } [[nodiscard]] const uint32_t GetStringCacheId() const noexcept { return m_stringCacheId; } [[nodiscard]] const uint16_t GetLevel() const noexcept { return m_level; } + [[nodiscard]] SyncMode GetSyncMode() const noexcept { return m_syncMode; } [[nodiscard]] CellIdComponent& GetCellComponent() noexcept; [[nodiscard]] const CellIdComponent& GetCellComponent() const noexcept; @@ -30,12 +35,15 @@ struct Player void SetDiscordId(uint64_t aDiscordId) noexcept; void SetEndpoint(String aEndpoint) noexcept; void SetUsername(String aUsername) noexcept; + void SetAvatar(String aAvatar) noexcept; + void SetActorName(String aActorName) noexcept; void SetMods(Vector aMods) noexcept; void SetModIds(Vector aModIds) noexcept; void SetCharacter(entt::entity aCharacter) noexcept; void SetStringCacheId(uint32_t aStringCacheId) noexcept; // TODO(cosideci): update on level up void SetLevel(uint16_t aLevel) noexcept; + void SetSyncMode(SyncMode aMode) noexcept { m_syncMode = aMode; } void SetCellComponent(const CellIdComponent& aCellComponent) noexcept; @@ -50,9 +58,12 @@ struct Player uint64_t m_discordId{0}; String m_endpoint; String m_username; + String m_avatar; + String m_actorName; PartyComponent m_party; QuestLogComponent m_questLog; CellIdComponent m_cell; uint32_t m_stringCacheId{0}; uint16_t m_level{0}; + SyncMode m_syncMode{SyncMode::Normal}; }; diff --git a/Code/server/GameServer.cpp b/Code/server/GameServer.cpp index ba4176b83..8484bc7ef 100644 --- a/Code/server/GameServer.cpp +++ b/Code/server/GameServer.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -12,25 +11,94 @@ #include #include -#include -#include +#include + #include #include #include +#include #include #include +#include +#include +#include +#include #include #include +#include +#include +#include constexpr size_t kMaxServerNameLength = 128u; +namespace +{ +Player* FindPlayerByUsernameInsensitive(World& world, const TiltedPhoques::String& username) +{ + if (username.empty()) + return nullptr; + + TiltedPhoques::String needle = username; + std::transform(needle.begin(), needle.end(), needle.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + + for (Player* player : world.GetPlayerManager()) + { + if (!player) + continue; + + TiltedPhoques::String candidate = player->GetUsername(); + std::transform(candidate.begin(), candidate.end(), candidate.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (candidate == needle) + return player; + } + + return nullptr; +} + +TiltedPhoques::String TrimCopy(const TiltedPhoques::String& value) +{ + TiltedPhoques::String trimmed = value; + while (!trimmed.empty() && std::isspace(static_cast(trimmed.front()))) + trimmed.erase(trimmed.begin()); + while (!trimmed.empty() && std::isspace(static_cast(trimmed.back()))) + trimmed.pop_back(); + return trimmed; +} + +bool ParseBroadcastCommand(const TiltedPhoques::String& command, TiltedPhoques::String& outMessage) +{ + const TiltedPhoques::String trimmed = TrimCopy(command); + if (trimmed.empty() || trimmed.front() != '/') + return false; + + TiltedPhoques::String withoutPrefix = trimmed.substr(1); + withoutPrefix = TrimCopy(withoutPrefix); + if (withoutPrefix.empty()) + return false; + + size_t splitPos = withoutPrefix.find_first_of(" \t"); + TiltedPhoques::String name = splitPos == TiltedPhoques::String::npos ? withoutPrefix : withoutPrefix.substr(0, splitPos); + std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (name != "broadcast") + return false; + + if (splitPos == TiltedPhoques::String::npos) + { + outMessage.clear(); + return true; + } + + outMessage = TrimCopy(withoutPrefix.substr(splitPos + 1)); + return true; +} +} // namespace + // -- Cvars -- Console::Setting uServerPort{"GameServer:uPort", "Which port to host the server on", 10578u}; Console::Setting uMaxPlayerCount{"GameServer:uMaxPlayerCount", "Maximum number of players allowed on the server (going over the default of 8 is not recommended)", 8u}; Console::Setting bPremiumTickrate{"GameServer:bPremiumMode", "Use premium tick rate", true}; Console::StringSetting sServerName{"GameServer:sServerName", "Name that shows up in the server list", "Dedicated Together Server"}; -Console::StringSetting sAdminPassword{"GameServer:sAdminPassword", "Admin authentication password", ""}; Console::StringSetting sPassword{"GameServer:sPassword", "Server password", ""}; // Gameplay @@ -142,6 +210,7 @@ ServerSettings GetSettings() settings.SyncPlayerHomes = bSyncPlayerHomes; settings.DeathSystemEnabled = bEnableDeathSystem; settings.SyncPlayerCalendar = bSyncPlayerCalendar; + settings.SyncPartyFastTravelMarkers = true; settings.AutoPartyJoin = bAutoPartyJoin; return settings; } @@ -274,20 +343,6 @@ void GameServer::BindMessageHandlers() HandleAuthenticationRequest(aConnectionId, pRealMessage); }; - auto adminHandlerGenerator = [this](auto& x) - { - using T = typename std::remove_reference_t::Type; - - m_adminMessageHandlers[T::Opcode] = [this](UniquePtr& apMessage, ConnectionId_t aConnectionId) - { - const auto pRealMessage = CastUnique(std::move(apMessage)); - m_pWorld->GetDispatcher().trigger(AdminPacketEvent(pRealMessage.get(), aConnectionId)); - }; - - return false; - }; - - ClientAdminMessageFactory::Visit(adminHandlerGenerator); } void GameServer::BindServerCommands() @@ -319,6 +374,68 @@ void GameServer::BindServerCommands() } }); + m_commands.RegisterCommand( + "AdminAdd", "Add a username to the admin list", + [&](Console::ArgStack& aStack) + { + auto out = spdlog::get("ConOut"); + const TiltedPhoques::String username = aStack.Pop(); + if (username.empty()) + { + out->error("AdminAdd "); + return; + } + + if (m_pWorld->GetAdminService().AddAdmin(username)) + { + out->info("Added admin '{}'.", username.c_str()); + if (auto* player = FindPlayerByUsernameInsensitive(*m_pWorld, username)) + m_pWorld->ctx().at().SendCommandList(player); + } + else + out->error("Failed to add admin '{}'.", username.c_str()); + }); + + m_commands.RegisterCommand( + "AdminRemove", "Remove a username from the admin list", + [&](Console::ArgStack& aStack) + { + auto out = spdlog::get("ConOut"); + const TiltedPhoques::String username = aStack.Pop(); + if (username.empty()) + { + out->error("AdminRemove "); + return; + } + + if (m_pWorld->GetAdminService().RemoveAdmin(username)) + { + out->info("Removed admin '{}'.", username.c_str()); + if (auto* player = FindPlayerByUsernameInsensitive(*m_pWorld, username)) + m_pWorld->ctx().at().SendCommandList(player); + } + else + out->error("Failed to remove admin '{}'.", username.c_str()); + }); + + m_commands.RegisterCommand<>( + "AdminList", "List all admins", + [&](Console::ArgStack&) + { + auto out = spdlog::get("ConOut"); + TiltedPhoques::Vector admins; + m_pWorld->GetAdminService().GetAdmins(admins); + if (admins.empty()) + { + out->info("No admins configured."); + return; + } + + out->info("<------Admins-({})--->", admins.size()); + for (const auto& name : admins) + out->info("{}", name.c_str()); + }); + m_commands.RegisterCommand<>( "mods", "List all installed mods on this server", [&](Console::ArgStack&) @@ -361,8 +478,8 @@ void GameServer::BindServerCommands() { auto out = spdlog::get("ConOut"); - auto hour = aStack.Pop(); - auto minute = aStack.Pop(); + const int hour = static_cast(aStack.Pop()); + const int minute = static_cast(aStack.Pop()); auto timescale = m_pWorld->GetCalendarService().GetTimeScale(); bool time_set_successfully = m_pWorld->GetCalendarService().SetTime(hour, minute, timescale); @@ -383,9 +500,9 @@ void GameServer::BindServerCommands() { auto out = spdlog::get("ConOut"); - auto day = aStack.Pop(); - auto month = aStack.Pop(); - auto year = aStack.Pop(); + const int day = static_cast(aStack.Pop()); + const int month = static_cast(aStack.Pop()); + const float year = static_cast(aStack.Pop()); bool time_set_successfully = m_pWorld->GetCalendarService().SetDate(day, month, year); @@ -399,112 +516,6 @@ void GameServer::BindServerCommands() } }); - 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.*/ @@ -563,21 +574,6 @@ void GameServer::OnConsume(const void* apData, const uint32_t aSize, const Conne ViewBuffer buf((uint8_t*)apData, aSize); Buffer::Reader reader(&buf); - // TODO: ClientAdminMessageFactory - /*if (m_adminSessions.contains(aConnectionId)) [[unlikely]] - { - const ClientAdminMessageFactory factory; - auto pMessage = factory.Extract(reader); - if (!pMessage) - { - spdlog::error("Couldn't parse packet from {:x}", aConnectionId); - return; - } - - m_adminMessageHandlers[pMessage->GetOpcode()](pMessage, aConnectionId); - } - else - {*/ const ClientMessageFactory factory; auto pMessage = factory.Extract(reader); if (!pMessage) @@ -587,7 +583,6 @@ void GameServer::OnConsume(const void* apData, const uint32_t aSize, const Conne } m_messageHandlers[pMessage->GetOpcode()](pMessage, aConnectionId); - //} } void GameServer::OnConnection(const ConnectionId_t aHandle) @@ -598,8 +593,6 @@ void GameServer::OnConnection(const ConnectionId_t aHandle) void GameServer::OnDisconnection(const ConnectionId_t aConnectionId, EDisconnectReason aReason) { - m_adminSessions.erase(aConnectionId); - auto* pPlayer = m_pWorld->GetPlayerManager().GetByConnectionId(aConnectionId); spdlog::info("Connection ended {:x} - '{}' disconnected", aConnectionId, (pPlayer != NULL ? pPlayer->GetUsername().c_str() : "NULL")); @@ -622,7 +615,7 @@ void GameServer::OnDisconnection(const ConnectionId_t aConnectionId, EDisconnect notify.Username = pPlayer->GetUsername(); SendToPlayers(notify); - entt::entity playerCharacter = pPlayer->GetCharacter().value_or(static_cast(0)); + entt::entity playerCharacter = pPlayer->GetCharacter().value_or(entt::null); // Cleanup all entities that we own auto ownerView = m_pWorld->view(); @@ -665,36 +658,39 @@ void GameServer::Send(const ConnectionId_t aConnectionId, const ServerMessage& a s_allocator.Reset(); } -void GameServer::Send(ConnectionId_t aConnectionId, const ServerAdminMessage& acServerMessage) const -{ - static thread_local TiltedPhoques::ScratchAllocator s_allocator{1 << 18}; - - Buffer buffer(1 << 20); - Buffer::Writer writer(&buffer); - writer.WriteBits(0, 8); // Skip the first byte as it is used by packet - - acServerMessage.Serialize(writer); - - TiltedPhoques::PacketView packet(reinterpret_cast(buffer.GetWriteData()), static_cast(writer.Size())); - Server::Send(aConnectionId, &packet); - - s_allocator.Reset(); -} void GameServer::SendToLoaded(const ServerMessage& acServerMessage) const { + TiltedPhoques::Vector players; + players.reserve(m_pWorld->GetPlayerManager().Count()); + for (Player* pPlayer : m_pWorld->GetPlayerManager()) { - if (pPlayer->GetCellComponent()) + players.push_back(pPlayer->GetConnectionId()); + } + + for (auto connectionId : players) + { + Player* pPlayer = m_pWorld->GetPlayerManager().GetByConnectionId(connectionId); + if (pPlayer && pPlayer->GetCellComponent()) pPlayer->Send(acServerMessage); } } void GameServer::SendToPlayers(const ServerMessage& acServerMessage, const Player* apExcludedPlayer) const { + TiltedPhoques::Vector players; + players.reserve(m_pWorld->GetPlayerManager().Count()); + for (Player* pPlayer : m_pWorld->GetPlayerManager()) { - if (pPlayer != apExcludedPlayer) + players.push_back(pPlayer->GetConnectionId()); + } + + for (auto connectionId : players) + { + Player* pPlayer = m_pWorld->GetPlayerManager().GetByConnectionId(connectionId); + if (pPlayer && pPlayer != apExcludedPlayer) pPlayer->Send(acServerMessage); } } @@ -723,9 +719,19 @@ bool GameServer::SendToPlayersInRange(const ServerMessage& acServerMessage, cons if (const auto* characterComponent = m_pWorld->try_get(acOrigin)) isDragon = characterComponent->IsDragon(); + TiltedPhoques::Vector players; + players.reserve(m_pWorld->GetPlayerManager().Count()); + for (Player* pPlayer : m_pWorld->GetPlayerManager()) { - if (cellComponent.IsInRange(pPlayer->GetCellComponent(), isDragon) && pPlayer != apExcludedPlayer) + players.push_back(pPlayer->GetConnectionId()); + } + + for (auto connectionId : players) + { + Player* pPlayer = m_pWorld->GetPlayerManager().GetByConnectionId(connectionId); + + if (pPlayer && cellComponent.IsInRange(pPlayer->GetCellComponent(), isDragon) && pPlayer != apExcludedPlayer) pPlayer->Send(acServerMessage); } @@ -740,9 +746,19 @@ void GameServer::SendToParty(const ServerMessage& acServerMessage, const PartyCo return; } + TiltedPhoques::Vector players; + players.reserve(m_pWorld->GetPlayerManager().Count()); + for (Player* pPlayer : m_pWorld->GetPlayerManager()) { - if (pPlayer == apExcludeSender) + players.push_back(pPlayer->GetConnectionId()); + } + + for (auto connectionId : players) + { + Player* pPlayer = m_pWorld->GetPlayerManager().GetByConnectionId(connectionId); + + if (!pPlayer || pPlayer == apExcludeSender) continue; const auto& partyComponent = pPlayer->GetParty(); @@ -772,9 +788,19 @@ void GameServer::SendToPartyInRange(const ServerMessage& acServerMessage, const const auto& cellComponent = view.get(*it); + TiltedPhoques::Vector players; + players.reserve(m_pWorld->GetPlayerManager().Count()); + for (Player* pPlayer : m_pWorld->GetPlayerManager()) { - if (pPlayer == apExcludeSender) + players.push_back(pPlayer->GetConnectionId()); + } + + for (auto connectionId : players) + { + Player* pPlayer = m_pWorld->GetPlayerManager().GetByConnectionId(connectionId); + + if (!pPlayer || pPlayer == apExcludeSender) continue; if (!cellComponent.IsInRange(pPlayer->GetCellComponent(), false)) @@ -858,16 +884,51 @@ 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() || adminPasswordUsed) + const auto sanitizedUsername = SanitizeUsername(acRequest->Username); + if (!sanitizedUsername.empty()) { - if (adminPasswordUsed) + auto iequals = [](const String& a, const String& b) -> bool { + if (a.size() != b.size()) + return false; + for (size_t i = 0; i < a.size(); ++i) + { + if (std::tolower(static_cast(a[i])) != std::tolower(static_cast(b[i]))) + return false; + } + return true; + }; + + for (auto* pExisting : m_pWorld->GetPlayerManager()) { - m_adminSessions.insert(aConnectionId); - spdlog::warn("New admin session for {:x} '{}'", aConnectionId, remoteAddress); + if (!pExisting) + continue; + + if (iequals(pExisting->GetUsername(), sanitizedUsername)) + { + spdlog::info("New player {:x} '{}' denied: username '{}' already connected", aConnectionId, remoteAddress, sanitizedUsername.c_str()); + sendKick(RT::kDuplicateUser); + return; + } } + } + auto& loginService = m_pWorld->ctx().at(); + const auto loginResult = loginService.VerifyOrCreateUser(sanitizedUsername, acRequest->Password); + + if (loginResult != LoginService::LoginResult::Ok) + { + spdlog::info("New player {:x} '{}' failed to authenticate for user '{}'", aConnectionId, remoteAddress, sanitizedUsername.c_str()); + sendKick(RT::kWrongAccountPassword); + return; + } + + const auto storedAvatar = loginService.GetAvatar(sanitizedUsername); + + acRequest->Username = sanitizedUsername; + acRequest->Password.clear(); + + // check if the proper server password was supplied. + if (acRequest->Token == sPassword.value()) + { Mods& responseList = serverResponse.UserMods; auto& modsComponent = m_pWorld->ctx().at(); @@ -947,6 +1008,7 @@ void GameServer::HandleAuthenticationRequest(const ConnectionId_t aConnectionId, pPlayer->SetMods(playerMods); pPlayer->SetModIds(playerModsIds); pPlayer->SetLevel(acRequest->Level); + pPlayer->SetAvatar(storedAvatar); // this event is shit, needs to be fixed, i know auto [canceled, reason] = m_pWorld->GetScriptService().HandlePlayerJoin(aConnectionId); @@ -968,6 +1030,12 @@ void GameServer::HandleAuthenticationRequest(const ConnectionId_t aConnectionId, serverResponse.Type = AuthenticationResponse::ResponseType::kAccepted; Send(aConnectionId, serverResponse); + NotifyChatMessageBroadcast joinMessage{}; + joinMessage.MessageType = ChatMessageType::kSystemMessage; + joinMessage.PlayerName = ""; + joinMessage.ChatMessage = fmt::format("{} connected to the server.", pPlayer->GetUsername().c_str()); + SendToPlayers(joinMessage); + uint32_t startId = 0; auto initStringCache = StringCache::Get().Serialize(startId); @@ -975,6 +1043,13 @@ void GameServer::HandleAuthenticationRequest(const ConnectionId_t aConnectionId, Send(aConnectionId, initStringCache); + { + NotifyPlayerProfileImage avatarNotify{}; + avatarNotify.PlayerId = pPlayer->GetId(); + avatarNotify.Avatar = pPlayer->GetAvatar(); + Send(pPlayer->GetConnectionId(), avatarNotify); + } + for (auto* pOtherPlayer : m_pWorld->GetPlayerManager()) { if (pOtherPlayer == pPlayer) @@ -983,6 +1058,7 @@ void GameServer::HandleAuthenticationRequest(const ConnectionId_t aConnectionId, NotifyPlayerJoined notify{}; notify.PlayerId = pOtherPlayer->GetId(); notify.Username = pOtherPlayer->GetUsername(); + notify.Avatar = pOtherPlayer->GetAvatar(); auto& cellComponent = pOtherPlayer->GetCellComponent(); notify.WorldSpaceId = cellComponent.WorldSpaceId; @@ -997,18 +1073,10 @@ 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); - - m_adminSessions.insert(aConnectionId); - spdlog::warn("New admin session for {:x} '{}'", aConnectionId, remoteAddress); - } */ else { - spdlog::info("New player {:x} '{}' has a bad password, kicking.", aConnectionId, remoteAddress); - sendKick(RT::kWrongPassword); + spdlog::info("New player {:x} '{}' supplied an incorrect server password, kicking.", aConnectionId, remoteAddress); + sendKick(RT::kWrongServerPassword); } } @@ -1033,46 +1101,88 @@ GameServer::Uptime GameServer::GetUptime() const noexcept return {weeks.count(), days.count(), hours.count(), minutes.count()}; } -void GameServer::UpdateTitle() const +Console::ConsoleRegistry::ExecutionResult GameServer::ExecuteConsoleCommand(const String& aCommand) noexcept { - const auto name = m_info.name.empty() ? "Private server" : m_info.name; - const char* playerText = GetClientCount() <= 1 ? " player" : " players"; + using exr = Console::ConsoleRegistry::ExecutionResult; - const auto title = fmt::format("{} - {} {} - {} Ticks - " BUILD_BRANCH "@" BUILD_COMMIT, name.c_str(), GetClientCount(), playerText, GetTickRate()); + const TiltedPhoques::String trimmed = TrimCopy(aCommand); + if (trimmed.empty()) + return exr::kFailure; -#if TP_PLATFORM_WINDOWS - SetConsoleTitleA(title.c_str()); -#else - std::cout << "\033]0;" << title << "\007"; -#endif -} + if (trimmed.front() != '/') + { + if (m_pWorld) + m_pWorld->GetChatCommandService().BroadcastSystemMessage(trimmed); + return exr::kSuccess; + } -Player* GameServer::GetAdminByUsername(const String& acUsername) const noexcept -{ - for (auto session : m_adminSessions) + TiltedPhoques::String message; + if (ParseBroadcastCommand(trimmed, message)) { - if (auto* pPlayer = PlayerManager::Get()->GetByConnectionId(session)) + auto out = spdlog::get("ConOut"); + if (message.empty()) { - if (pPlayer->GetUsername() == acUsername) - return pPlayer; + out->error("Usage: /broadcast "); + return exr::kFailure; } + + if (m_pWorld) + m_pWorld->GetChatCommandService().BroadcastSystemMessage(message); + return exr::kSuccess; } - return nullptr; + return m_commands.TryExecuteCommand(trimmed); } -Player const* GameServer::GetAdminByUsername(const String& acUsername) noexcept +void GameServer::GetStatusSnapshot(ServerStatusSnapshot& aOutStatus) const { - for (auto session : m_adminSessions) + const auto duration = std::chrono::high_resolution_clock::now() - m_startTime; + aOutStatus.UptimeSeconds = static_cast(std::chrono::duration_cast(duration).count()); + + aOutStatus.Players.clear(); + aOutStatus.Players.reserve(m_pWorld->GetPlayerManager().Count()); + + for (Player* player : m_pWorld->GetPlayerManager()) { - if (auto const* pPlayer = PlayerManager::Get()->GetByConnectionId(session)) + ServerPlayerStatusSnapshot entry; + entry.PlayerId = player->GetId(); + entry.Username = player->GetUsername(); + + const auto& cell = player->GetCellComponent(); + entry.CellBaseId = cell.Cell.BaseId; + entry.CellModId = cell.Cell.ModId; + entry.WorldBaseId = cell.WorldSpaceId.BaseId; + entry.WorldModId = cell.WorldSpaceId.ModId; + entry.GridX = cell.CenterCoords.X; + entry.GridY = cell.CenterCoords.Y; + + if (auto character = player->GetCharacter()) { - if (pPlayer->GetUsername() == acUsername) - return pPlayer; + if (const auto* movement = m_pWorld->try_get(*character)) + { + entry.HasPosition = true; + entry.PositionX = movement->Position.x; + entry.PositionY = movement->Position.y; + entry.PositionZ = movement->Position.z; + } } + + aOutStatus.Players.push_back(std::move(entry)); } +} - return nullptr; +void GameServer::UpdateTitle() const +{ + const auto name = m_info.name.empty() ? "Private server" : m_info.name; + const char* playerText = GetClientCount() <= 1 ? " player" : " players"; + + const auto title = fmt::format("{} - {} {} - {} Ticks - " BUILD_BRANCH "@" BUILD_COMMIT, name.c_str(), GetClientCount(), playerText, GetTickRate()); + +#if TP_PLATFORM_WINDOWS + SetConsoleTitleA(title.c_str()); +#else + std::cout << "\033]0;" << title << "\007"; +#endif } String GameServer::SanitizeUsername(const String& acUsername) const noexcept diff --git a/Code/server/GameServer.h b/Code/server/GameServer.h index f7256d7a8..a29fd7214 100644 --- a/Code/server/GameServer.h +++ b/Code/server/GameServer.h @@ -1,9 +1,10 @@ #pragma once -#include #include #include #include +#include +#include using TiltedPhoques::ConnectionId_t; using TiltedPhoques::Server; @@ -55,7 +56,6 @@ struct GameServer final : Server // Packet dispatching void Send(ConnectionId_t aConnectionId, const ServerMessage& acServerMessage) const; - 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; @@ -67,12 +67,6 @@ struct GameServer final : Server bool IsRunning() const noexcept { return !m_requestStop; } bool IsPasswordProtected() const noexcept { return m_isPasswordProtected; } - template void ForEachAdmin(const T& aFunctor) - { - for (auto id : m_adminSessions) - aFunctor(id); - } - struct Uptime { int weeks; @@ -81,18 +75,11 @@ struct GameServer final : Server int minutes; }; Uptime GetUptime() const noexcept; + Console::ConsoleRegistry::ExecutionResult ExecuteConsoleCommand(const String& aCommand) noexcept; + void GetStatusSnapshot(ServerStatusSnapshot& aOutStatus) const; 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: bool ValidateAuthParams(ConnectionId_t aConnectionId, const UniquePtr& acRequest); void HandleAuthenticationRequest(ConnectionId_t aConnectionId, const UniquePtr& acRequest); @@ -111,15 +98,12 @@ struct GameServer final : Server 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]; - std::function&, ConnectionId_t)> m_adminMessageHandlers[kClientAdminOpcodeMax]; - bool m_isPasswordProtected{}; Info m_info{}; UniquePtr m_pResources; Console::ConsoleRegistry& m_commands; - TiltedPhoques::Set m_adminSessions; TiltedPhoques::Map m_connectionToEntity; UniquePtr m_pWorld; diff --git a/Code/server/Scripting/GameServer_Bindings.cpp b/Code/server/Scripting/GameServer_Bindings.cpp index 11f7797a7..e3a02f438 100644 --- a/Code/server/Scripting/GameServer_Bindings.cpp +++ b/Code/server/Scripting/GameServer_Bindings.cpp @@ -19,7 +19,7 @@ void CreateGameServerBindings(sol::state_view aState) std::regex escapeHtml{"<[^>]+>\\s+(?=<)|<[^>]+>"}; notifyMessage.MessageType = ChatMessageType::kLocalChat; - notifyMessage.PlayerName = "[Server]"; + notifyMessage.PlayerName = ""; notifyMessage.ChatMessage = std::regex_replace(acMessage, escapeHtml, ""); GameServer::Get()->Send(aConnectionId, notifyMessage); }; @@ -28,7 +28,7 @@ void CreateGameServerBindings(sol::state_view aState) std::regex escapeHtml{"<[^>]+>\\s+(?=<)|<[^>]+>"}; notifyMessage.MessageType = ChatMessageType::kGlobalChat; - notifyMessage.PlayerName = "[Server]"; + notifyMessage.PlayerName = ""; notifyMessage.ChatMessage = std::regex_replace(acMessage, escapeHtml, ""); GameServer::Get()->SendToPlayers(notifyMessage); }; diff --git a/Code/server/Services/ActorValueService.cpp b/Code/server/Services/ActorValueService.cpp index b0554a853..bbd01826c 100644 --- a/Code/server/Services/ActorValueService.cpp +++ b/Code/server/Services/ActorValueService.cpp @@ -24,9 +24,16 @@ void ActorValueService::OnActorValueChanges(const PacketEvent(); - auto it = actorValuesView.find(static_cast(message.Id)); + auto it = actorValuesView.find(*entity); if (it != actorValuesView.end()) { @@ -41,8 +48,7 @@ void ActorValueService::OnActorValueChanges(const PacketEvent(message.Id); - if (!GameServer::Get()->SendToPlayersInRange(notify, cEntity, acMessage.pPlayer)) + if (!GameServer::Get()->SendToPlayersInRange(notify, *entity, acMessage.pPlayer)) spdlog::error("{}: SendToPlayersInRange failed", __FUNCTION__); } @@ -50,9 +56,16 @@ void ActorValueService::OnActorMaxValueChanges(const PacketEvent(); - auto it = actorValuesView.find(static_cast(message.Id)); + auto it = actorValuesView.find(*entity); if (it != actorValuesView.end()) { @@ -67,8 +80,7 @@ void ActorValueService::OnActorMaxValueChanges(const PacketEvent(message.Id); - if (!GameServer::Get()->SendToPlayersInRange(notify, cEntity, acMessage.pPlayer)) + if (!GameServer::Get()->SendToPlayersInRange(notify, *entity, acMessage.pPlayer)) spdlog::error("{}: SendToPlayersInRange failed", __FUNCTION__); } @@ -76,10 +88,17 @@ void ActorValueService::OnHealthChangeBroadcast(const PacketEvent(); - auto it = actorValuesView.find(static_cast(message.Id)); + auto it = actorValuesView.find(*entity); if (it != actorValuesView.end()) { @@ -89,11 +108,10 @@ void ActorValueService::OnHealthChangeBroadcast(const PacketEvent(message.Id); - if (!GameServer::Get()->SendToPlayersInRange(notify, cEntity, acMessage.pPlayer)) + if (!GameServer::Get()->SendToPlayersInRange(notify, *entity, acMessage.pPlayer)) spdlog::error("{}: SendToPlayersInRange failed", __FUNCTION__); } @@ -101,9 +119,16 @@ void ActorValueService::OnDeathStateChange(const PacketEvent(); - const auto it = characterView.find(static_cast(message.Id)); + const auto it = characterView.find(*entity); if (it != characterView.end()) { @@ -116,7 +141,6 @@ void ActorValueService::OnDeathStateChange(const PacketEvent(message.Id); - if (!GameServer::Get()->SendToPlayersInRange(notify, cEntity, acMessage.pPlayer)) + if (!GameServer::Get()->SendToPlayersInRange(notify, *entity, acMessage.pPlayer)) spdlog::error("{}: SendToPlayersInRange failed", __FUNCTION__); } diff --git a/Code/server/Services/AdminService.cpp b/Code/server/Services/AdminService.cpp index fc95433c4..f7626010f 100644 --- a/Code/server/Services/AdminService.cpp +++ b/Code/server/Services/AdminService.cpp @@ -1,35 +1,273 @@ -#include +#include #include -#include -#include -#include +#include +#include + +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#endif + +namespace +{ +constexpr char kCreateAdminsTableSql[] = + R"SQL( + CREATE TABLE IF NOT EXISTS admins( + username TEXT PRIMARY KEY + ); +)SQL"; + +std::filesystem::path ResolveExecutableDirectory() noexcept +{ + namespace fs = std::filesystem; + +#if defined(_WIN32) + std::array buffer{}; + const DWORD length = GetModuleFileNameW(nullptr, buffer.data(), static_cast(buffer.size())); + if (length != 0 && length < buffer.size()) + { + return fs::path(buffer.data()).parent_path(); + } +#else + std::error_code ec; + auto exePath = fs::canonical("/proc/self/exe", ec); + if (!ec) + return exePath.parent_path(); +#endif + + std::error_code ec; + const auto current = fs::current_path(ec); + if (!ec) + return current; + + return {}; +} + +std::filesystem::path ResolveItemsDatabasePath() noexcept +{ + namespace fs = std::filesystem; + + if (auto exeDirectory = ResolveExecutableDirectory(); !exeDirectory.empty()) + { + return exeDirectory / "items.db"; + } -AdminService::AdminService(World& aWorld, entt::dispatcher& aDispatcher) +#if defined(_WIN32) + if (char* localAppData = nullptr; _dupenv_s(&localAppData, nullptr, "LOCALAPPDATA") == 0 && localAppData) + { + fs::path path(localAppData); + free(localAppData); + path /= "SkyrimTogether"; + path /= "Server"; + path /= "items.db"; + return path; + } +#else + if (const char* xdgDataHome = std::getenv("XDG_DATA_HOME"); xdgDataHome && xdgDataHome[0] != '\0') + { + return fs::path(xdgDataHome) / "skyrimtogether" / "server" / "items.db"; + } + + if (const char* home = std::getenv("HOME"); home && home[0] != '\0') + { + return fs::path(home) / ".local" / "share" / "skyrimtogether" / "server" / "items.db"; + } +#endif + + std::error_code ec; + const auto current = fs::current_path(ec); + if (!ec) + return current / "data" / "items.db"; + + return fs::path("items.db"); +} + +TiltedPhoques::String Normalize(const TiltedPhoques::String& value) +{ + TiltedPhoques::String normalized = value; + std::transform(normalized.begin(), normalized.end(), normalized.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + return normalized; +} +} // namespace + +AdminService::AdminService(World& aWorld, entt::dispatcher& aDispatcher) noexcept : m_world(aWorld) { - m_shutdownConnection = aDispatcher.sink>().connect<&AdminService::HandleShutdown>(this); + (void)aDispatcher; + if (!InitializeDatabase()) + spdlog::error("AdminService: failed to initialize admin database"); +} + +AdminService::~AdminService() noexcept +{ + ShutdownDatabase(); +} + +TiltedPhoques::String AdminService::NormalizeUsername(const TiltedPhoques::String& aUsername) +{ + return Normalize(aUsername); } -void AdminService::HandleShutdown(const AdminPacketEvent& acMessage) noexcept +std::filesystem::path AdminService::ResolveAdminDatabasePath() const noexcept { - spdlog::warn("Shutdown was requested by {:x}", acMessage.ConnectionId); + const auto itemsPath = ResolveItemsDatabasePath(); + if (!itemsPath.empty()) + return itemsPath.parent_path() / "admins.db"; - GameServer::Get()->Kill(); + return std::filesystem::path("admins.db"); } -void AdminService::sink_it_(const spdlog::details::log_msg& msg) +bool AdminService::InitializeDatabase() noexcept { - spdlog::memory_buf_t formatted; - formatter_->format(msg, formatted); + m_databasePath = ResolveAdminDatabasePath(); + const auto dataDirectory = m_databasePath.parent_path(); - ServerLogs logs; - logs.Logs = fmt::to_string(formatted); + std::error_code ec; + if (!dataDirectory.empty() && !std::filesystem::exists(dataDirectory, ec)) + { + std::filesystem::create_directories(dataDirectory, ec); + if (ec) + { + spdlog::error("AdminService: failed to create data directory '{}': {}", dataDirectory.string(), ec.message()); + return false; + } + } - GameServer::Get()->ForEachAdmin([&logs](ConnectionId_t aId) { GameServer::Get()->Send(aId, logs); }); + spdlog::info("AdminService: using admin database at '{}'", m_databasePath.string()); + if (sqlite3_open(m_databasePath.string().c_str(), &m_pDatabase) != SQLITE_OK) + { + spdlog::error("AdminService: unable to open admin database at '{}': {}", m_databasePath.string(), sqlite3_errmsg(m_pDatabase)); + return false; + } + + char* pError = nullptr; + const int execResult = sqlite3_exec(m_pDatabase, kCreateAdminsTableSql, nullptr, nullptr, &pError); + if (execResult != SQLITE_OK) + { + spdlog::error("AdminService: failed to initialize admin schema: {}", pError ? pError : "unknown"); + sqlite3_free(pError); + return false; + } + + return true; +} + +void AdminService::ShutdownDatabase() noexcept +{ + if (m_pDatabase) + { + sqlite3_close(m_pDatabase); + m_pDatabase = nullptr; + } +} + +bool AdminService::IsAdmin(const TiltedPhoques::String& aUsername) const noexcept +{ + if (!m_pDatabase) + return false; + + const TiltedPhoques::String normalized = NormalizeUsername(aUsername); + if (normalized.empty()) + return false; + + constexpr const char* cSelectSql = "SELECT 1 FROM admins WHERE username = ?1 LIMIT 1;"; + sqlite3_stmt* pStatement = nullptr; + if (sqlite3_prepare_v2(m_pDatabase, cSelectSql, -1, &pStatement, nullptr) != SQLITE_OK) + { + spdlog::error("AdminService: failed to prepare admin lookup: {}", sqlite3_errmsg(m_pDatabase)); + return false; + } + + sqlite3_bind_text(pStatement, 1, normalized.c_str(), -1, SQLITE_TRANSIENT); + const int stepResult = sqlite3_step(pStatement); + sqlite3_finalize(pStatement); + return stepResult == SQLITE_ROW; +} + +bool AdminService::AddAdmin(const TiltedPhoques::String& aUsername) noexcept +{ + if (!m_pDatabase) + return false; + + const TiltedPhoques::String normalized = NormalizeUsername(aUsername); + if (normalized.empty()) + return false; + + constexpr const char* cInsertSql = "INSERT OR IGNORE INTO admins(username) VALUES(?1);"; + sqlite3_stmt* pStatement = nullptr; + if (sqlite3_prepare_v2(m_pDatabase, cInsertSql, -1, &pStatement, nullptr) != SQLITE_OK) + { + spdlog::error("AdminService: failed to prepare admin insert: {}", sqlite3_errmsg(m_pDatabase)); + return false; + } + + sqlite3_bind_text(pStatement, 1, normalized.c_str(), -1, SQLITE_TRANSIENT); + const int stepResult = sqlite3_step(pStatement); + sqlite3_finalize(pStatement); + if (stepResult != SQLITE_DONE) + { + spdlog::error("AdminService: failed to insert admin '{}': {}", normalized.c_str(), sqlite3_errmsg(m_pDatabase)); + return false; + } + + return true; +} + +bool AdminService::RemoveAdmin(const TiltedPhoques::String& aUsername) noexcept +{ + if (!m_pDatabase) + return false; + + const TiltedPhoques::String normalized = NormalizeUsername(aUsername); + if (normalized.empty()) + return false; + + constexpr const char* cDeleteSql = "DELETE FROM admins WHERE username = ?1;"; + sqlite3_stmt* pStatement = nullptr; + if (sqlite3_prepare_v2(m_pDatabase, cDeleteSql, -1, &pStatement, nullptr) != SQLITE_OK) + { + spdlog::error("AdminService: failed to prepare admin delete: {}", sqlite3_errmsg(m_pDatabase)); + return false; + } + + sqlite3_bind_text(pStatement, 1, normalized.c_str(), -1, SQLITE_TRANSIENT); + const int stepResult = sqlite3_step(pStatement); + sqlite3_finalize(pStatement); + if (stepResult != SQLITE_DONE) + { + spdlog::error("AdminService: failed to delete admin '{}': {}", normalized.c_str(), sqlite3_errmsg(m_pDatabase)); + return false; + } + + return true; } -void AdminService::flush_() +void AdminService::GetAdmins(TiltedPhoques::Vector& aOutAdmins) const noexcept { + aOutAdmins.clear(); + if (!m_pDatabase) + return; + + constexpr const char* cSelectSql = "SELECT username FROM admins ORDER BY username;"; + sqlite3_stmt* pStatement = nullptr; + if (sqlite3_prepare_v2(m_pDatabase, cSelectSql, -1, &pStatement, nullptr) != SQLITE_OK) + { + spdlog::error("AdminService: failed to prepare admin list: {}", sqlite3_errmsg(m_pDatabase)); + return; + } + + while (sqlite3_step(pStatement) == SQLITE_ROW) + { + if (const auto* pText = reinterpret_cast(sqlite3_column_text(pStatement, 0))) + aOutAdmins.emplace_back(pText); + } + + sqlite3_finalize(pStatement); } diff --git a/Code/server/Services/AdminService.h b/Code/server/Services/AdminService.h index 8f323a64d..652f035e8 100644 --- a/Code/server/Services/AdminService.h +++ b/Code/server/Services/AdminService.h @@ -1,29 +1,33 @@ #pragma once -#include -#include +#include +#include + +#include + +struct sqlite3; struct World; -struct UpdateEvent; -struct AdminShutdownRequest; - -/** - * @brief Handles communication from an admin client. - * - * This service is currently not in use. - */ -class AdminService : public spdlog::sinks::base_sink + +struct AdminService { -public: - AdminService(World& aWorld, entt::dispatcher& aDispatcher); + AdminService(World& aWorld, entt::dispatcher& aDispatcher) noexcept; + ~AdminService() noexcept; -private: - void HandleShutdown(const AdminPacketEvent& aChanges) noexcept; + TP_NOCOPYMOVE(AdminService); + + bool IsAdmin(const TiltedPhoques::String& aUsername) const noexcept; + bool AddAdmin(const TiltedPhoques::String& aUsername) noexcept; + bool RemoveAdmin(const TiltedPhoques::String& aUsername) noexcept; + void GetAdmins(TiltedPhoques::Vector& aOutAdmins) const noexcept; - void sink_it_(const spdlog::details::log_msg& msg) override; - void flush_() override; +private: + static TiltedPhoques::String NormalizeUsername(const TiltedPhoques::String& aUsername); + std::filesystem::path ResolveAdminDatabasePath() const noexcept; + bool InitializeDatabase() noexcept; + void ShutdownDatabase() noexcept; - Vector m_messages; - entt::scoped_connection m_shutdownConnection; World& m_world; + sqlite3* m_pDatabase{nullptr}; + std::filesystem::path m_databasePath; }; diff --git a/Code/server/Services/CharacterService.cpp b/Code/server/Services/CharacterService.cpp index 9ec33d9ad..22da608ab 100644 --- a/Code/server/Services/CharacterService.cpp +++ b/Code/server/Services/CharacterService.cpp @@ -258,14 +258,15 @@ void CharacterService::OnOwnershipTransferRequest(const PacketEvent(message.ServerId); - - if (!m_world.valid(cEntity)) + const auto entity = m_world.TryResolveEntity(message.ServerId); + if (!entity) { spdlog::warn("Client {:X} requested ownership transfer of an entity that doesn't exist, server id: {:X}", acMessage.pPlayer->GetConnectionId(), message.ServerId); return; } + const entt::entity cEntity = *entity; + if (auto* pCharacterComponent = m_world.try_get(cEntity)) { if (pCharacterComponent->IsPlayerSummon()) @@ -337,6 +338,12 @@ void CharacterService::OnOwnershipTransferEvent(const OwnershipTransferEvent& ac ownerComponent.SetOwner(pPlayer); + // Send an authoritative snapshot before ownership changes hands so the new owner has correct visuals. + NotifySpawnData notifySpawnData{}; + notifySpawnData.Id = response.ServerId; + notifySpawnData.NewActorData = BuildActorData(acEvent.Entity); + pPlayer->Send(notifySpawnData); + pPlayer->Send(response); foundOwner = true; @@ -349,11 +356,25 @@ void CharacterService::OnOwnershipTransferEvent(const OwnershipTransferEvent& ac void CharacterService::OnCharacterRemoveEvent(const CharacterRemoveEvent& acEvent) const noexcept { + const auto entity = m_world.TryResolveEntity(acEvent.ServerId); + if (!entity) + { + spdlog::warn("Character remove event received for unknown entity {:X}", acEvent.ServerId); + return; + } + const auto view = m_world.view(); - const auto it = view.find(static_cast(acEvent.ServerId)); + const auto it = view.find(*entity); + if (it == view.end()) + { + spdlog::warn("Character remove event missing OwnerComponent for entity {:X}", acEvent.ServerId); + return; + } + const auto& characterOwnerComponent = view.get(*it); + const auto resolvedEntity = *it; - GameServer::Get()->GetWorld().GetScriptService().HandleCharacterDestoy(*it); + GameServer::Get()->GetWorld().GetScriptService().HandleCharacterDestoy(resolvedEntity); NotifyRemoveCharacter response; response.ServerId = acEvent.ServerId; @@ -366,13 +387,35 @@ void CharacterService::OnCharacterRemoveEvent(const CharacterRemoveEvent& acEven pPlayer->Send(response); } - m_world.destroy(*it); + m_world.destroy(resolvedEntity); spdlog::debug("Character destroyed {:X}", acEvent.ServerId); } void CharacterService::OnOwnershipClaimRequest(const PacketEvent& acMessage) const noexcept { - TransferOwnership(acMessage.pPlayer, acMessage.Packet.ServerId, acMessage.Packet.NewActorData); + const auto entity = m_world.TryResolveEntity(acMessage.Packet.ServerId); + if (!entity) + { + spdlog::warn("Ownership claim for unknown entity {:X} by player {:X}", acMessage.Packet.ServerId, acMessage.pPlayer->GetConnectionId()); + return; + } + + auto view = m_world.view(); + const auto it = view.find(*entity); + if (it == view.end()) + { + spdlog::warn("Ownership claim missing OwnerComponent for entity {:X} by player {:X}", acMessage.Packet.ServerId, acMessage.pPlayer->GetConnectionId()); + return; + } + + auto& ownerComponent = view.get(*it); + if (ownerComponent.GetOwner() != acMessage.pPlayer) + { + spdlog::warn("Ownership claim denied for {:X}: player {:X} not owner", acMessage.Packet.ServerId, acMessage.pPlayer->GetConnectionId()); + return; + } + + TransferOwnership(acMessage.pPlayer, acMessage.Packet.ServerId, BuildActorData(*entity)); } void CharacterService::OnCharacterSpawned(const CharacterSpawnedEvent& acEvent) const noexcept @@ -395,12 +438,20 @@ void CharacterService::OnReferencesMoveRequest(const PacketEvent(entry.first); + const auto entityId = entry.first; + const auto resolved = m_world.TryResolveEntity(entityId); + if (!resolved) + { + spdlog::debug("{:X} requested move of {:X} but entity does not exist", acMessage.pPlayer->GetConnectionId(), entityId); + continue; + } + + const auto entity = *resolved; auto itor = view.find(entity); if (itor == std::end(view)) { - spdlog::debug("{:x} requested move of {:x} but does not exist", acMessage.pPlayer->GetConnectionId(), World::ToInteger(*itor)); + spdlog::debug("{:X} requested move of {:X} but entity is not owned by them", acMessage.pPlayer->GetConnectionId(), entityId); continue; } @@ -449,10 +500,19 @@ void CharacterService::OnFactionsChanges(const PacketEvent(id)); + const auto entity = m_world.TryResolveEntity(id); + if (!entity) + { + spdlog::debug("{:X} requested faction update for unknown entity {:X}", acMessage.pPlayer->GetConnectionId(), id); + continue; + } - if (it == std::end(view) || view.get(*it).GetOwner() != acMessage.pPlayer) + auto it = view.find(*entity); + if (it == std::end(view)) + { + spdlog::debug("{:X} requested faction update without ownership for entity {:X}", acMessage.pPlayer->GetConnectionId(), id); continue; + } auto& characterComponent = view.get(*it); characterComponent.FactionsContent = factions; @@ -468,8 +528,14 @@ void CharacterService::OnMountRequest(const PacketEvent& acMessage notify.RiderId = message.RiderId; notify.MountId = message.MountId; - const entt::entity cEntity = static_cast(message.MountId); - if (!GameServer::Get()->SendToPlayersInRange(notify, cEntity, acMessage.GetSender())) + const auto entity = m_world.TryResolveEntity(message.MountId); + if (!entity) + { + spdlog::debug("{:X} requested mount broadcast for unknown entity {:X}", acMessage.pPlayer->GetConnectionId(), message.MountId); + return; + } + + if (!GameServer::Get()->SendToPlayersInRange(notify, *entity, acMessage.GetSender())) spdlog::error("{}: SendToPlayersInRange failed", __FUNCTION__); } @@ -481,25 +547,42 @@ void CharacterService::OnNewPackageRequest(const PacketEvent& notify.ActorId = message.ActorId; notify.PackageId = message.PackageId; - const entt::entity cEntity = static_cast(message.ActorId); - if (!GameServer::Get()->SendToPlayersInRange(notify, cEntity, acMessage.GetSender())) + const auto entity = m_world.TryResolveEntity(message.ActorId); + if (!entity) + { + spdlog::debug("{:X} requested package update for unknown entity {:X}", acMessage.pPlayer->GetConnectionId(), message.ActorId); + return; + } + + if (!GameServer::Get()->SendToPlayersInRange(notify, *entity, acMessage.GetSender())) spdlog::error("{}: SendToPlayersInRange failed", __FUNCTION__); } void CharacterService::OnRequestRespawn(const PacketEvent& acMessage) const noexcept { + const auto entity = m_world.TryResolveEntity(acMessage.Packet.ActorId); + if (!entity) + { + spdlog::warn("Respawn requested for unknown actor id {:X}", acMessage.Packet.ActorId); + return; + } + auto view = m_world.view(); - auto it = view.find(static_cast(acMessage.Packet.ActorId)); + auto it = view.find(*entity); if (it == view.end()) { spdlog::warn("No OwnerComponent found for actor id {:X}", acMessage.Packet.ActorId); return; } + const auto resolvedEntity = *it; auto& ownerComponent = view.get(*it); - - // Replay cache needs to be cleared when a character respawns - m_world.try_get(*it)->ActionsReplayCache.Clear(); + if (auto* pAnimationComponent = m_world.try_get(resolvedEntity)) + { + pAnimationComponent->Actions.clear(); + pAnimationComponent->CurrentAction = {}; + pAnimationComponent->ActionsReplayCache.Clear(); + } if (ownerComponent.GetOwner() == acMessage.pPlayer) { @@ -513,13 +596,13 @@ void CharacterService::OnRequestRespawn(const PacketEvent& acMes NotifyRespawn notify; notify.ActorId = acMessage.Packet.ActorId; - if (!GameServer::Get()->SendToPlayersInRange(notify, *it, acMessage.GetSender())) + if (!GameServer::Get()->SendToPlayersInRange(notify, resolvedEntity, acMessage.GetSender())) spdlog::error("{}: SendToPlayersInRange failed", __FUNCTION__); } else { CharacterSpawnRequest message; - Serialize(m_world, *it, &message); + Serialize(m_world, resolvedEntity, &message); acMessage.GetSender()->Send(message); } @@ -545,8 +628,14 @@ void CharacterService::OnDialogueRequest(const PacketEvent& acM notify.ServerId = message.ServerId; notify.SoundFilename = message.SoundFilename; - const entt::entity cEntity = static_cast(message.ServerId); - if (!GameServer::Get()->SendToPlayersInRange(notify, cEntity, acMessage.GetSender())) + const auto entity = m_world.TryResolveEntity(message.ServerId); + if (!entity) + { + spdlog::debug("{:X} requested dialogue broadcast for unknown entity {:X}", acMessage.pPlayer->GetConnectionId(), message.ServerId); + return; + } + + if (!GameServer::Get()->SendToPlayersInRange(notify, *entity, acMessage.GetSender())) spdlog::error("{}: SendToPlayersInRange failed", __FUNCTION__); } @@ -558,8 +647,14 @@ void CharacterService::OnSubtitleRequest(const PacketEvent& acM notify.ServerId = message.ServerId; notify.Text = message.Text; - const entt::entity cEntity = static_cast(message.ServerId); - if (!GameServer::Get()->SendToPlayersInRange(notify, cEntity, acMessage.GetSender())) + const auto entity = m_world.TryResolveEntity(message.ServerId); + if (!entity) + { + spdlog::debug("{:X} requested subtitle broadcast for unknown entity {:X}", acMessage.pPlayer->GetConnectionId(), message.ServerId); + return; + } + + if (!GameServer::Get()->SendToPlayersInRange(notify, *entity, acMessage.GetSender())) spdlog::error("{}: SendToPlayersInRange failed", __FUNCTION__); } @@ -655,12 +750,18 @@ void CharacterService::CreateCharacter(const PacketEvent void CharacterService::TransferOwnership(Player* apPlayer, const uint32_t acServerId, const ActorData& acActorData) const noexcept { - // const OwnerView view(m_world, acMessage.GetSender()); + const auto entity = m_world.TryResolveEntity(acServerId); + if (!entity) + { + spdlog::warn("Client {:X} requested ownership of an entity that doesn't exist ({:X})!", apPlayer->GetConnectionId(), acServerId); + return; + } + auto view = m_world.view(); - const auto it = view.find(static_cast(acServerId)); + const auto it = view.find(*entity); if (it == view.end()) { - spdlog::warn("Client {:X} requested ownership of an entity that doesn't exist ({:X})!", apPlayer->GetConnectionId(), acServerId); + spdlog::warn("Client {:X} requested ownership but OwnerComponent is missing for entity {:X}", apPlayer->GetConnectionId(), acServerId); return; } @@ -755,7 +856,7 @@ void CharacterService::ProcessFactionsChanges() const noexcept const auto characterView = m_world.view(); - TiltedPhoques::Map messages; + TiltedPhoques::Map messages; for (auto entity : characterView) { @@ -775,7 +876,7 @@ void CharacterService::ProcessFactionsChanges() const noexcept if (!cellIdComponent.IsInRange(pPlayer->GetCellComponent(), characterComponent.IsDragon())) continue; - auto& message = messages[pPlayer]; + auto& message = messages[pPlayer->GetConnectionId()]; auto& change = message.Changes[World::ToInteger(entity)]; change = characterComponent.FactionsContent; @@ -784,10 +885,13 @@ void CharacterService::ProcessFactionsChanges() const noexcept characterComponent.SetDirtyFactions(false); } - for (auto [pPlayer, message] : messages) + for (auto [connectionId, message] : messages) { if (!message.Changes.empty()) - pPlayer->Send(message); + { + if (auto* pPlayer = m_world.GetPlayerManager().GetByConnectionId(connectionId)) + pPlayer->Send(message); + } } } @@ -804,11 +908,11 @@ void CharacterService::ProcessMovementChanges() const noexcept const auto characterView = m_world.view(); - TiltedPhoques::Map messages; + TiltedPhoques::Map messages; for (auto pPlayer : m_world.GetPlayerManager()) { - auto& message = messages[pPlayer]; + auto& message = messages[pPlayer->GetConnectionId()]; message.Tick = GameServer::Get()->GetTick(); } @@ -833,7 +937,7 @@ void CharacterService::ProcessMovementChanges() const noexcept if (!cellIdComponent.IsInRange(pPlayer->GetCellComponent(), characterComponent.IsDragon())) continue; - auto& message = messages[pPlayer]; + auto& message = messages[pPlayer->GetConnectionId()]; auto& update = message.Updates[World::ToInteger(entity)]; auto& movement = update.UpdatedMovement; @@ -857,9 +961,12 @@ void CharacterService::ProcessMovementChanges() const noexcept m_world.view().each([](MovementComponent& movementComponent) { movementComponent.Sent = true; }); - for (auto& [pPlayer, message] : messages) + for (auto& [connectionId, message] : messages) { if (!message.Updates.empty()) - pPlayer->Send(message); + { + if (auto* pPlayer = m_world.GetPlayerManager().GetByConnectionId(connectionId)) + pPlayer->Send(message); + } } } diff --git a/Code/server/Services/ChatCommandService.cpp b/Code/server/Services/ChatCommandService.cpp new file mode 100644 index 000000000..e3bdf20fa --- /dev/null +++ b/Code/server/Services/ChatCommandService.cpp @@ -0,0 +1,588 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace +{ +const std::regex kEscapeHtml{"<[^>]+>\\s+(?=<)|<[^>]+>"}; + +TiltedPhoques::String ToLowerCopy(const TiltedPhoques::String& text) +{ + TiltedPhoques::String out = text; + std::transform(out.begin(), out.end(), out.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + return out; +} + +TiltedPhoques::String TrimCopy(const TiltedPhoques::String& value) +{ + TiltedPhoques::String trimmed = value; + while (!trimmed.empty() && std::isspace(static_cast(trimmed.front()))) + trimmed.erase(trimmed.begin()); + while (!trimmed.empty() && std::isspace(static_cast(trimmed.back()))) + trimmed.pop_back(); + return trimmed; +} + +bool ParseIntArg(const TiltedPhoques::String& text, int& outValue) +{ + const char* begin = text.data(); + const char* end = text.data() + text.size(); + int value = 0; + const auto result = std::from_chars(begin, end, value); + if (result.ec != std::errc() || result.ptr != end) + return false; + outValue = value; + return true; +} + +TiltedPhoques::String SanitizeChatText(const TiltedPhoques::String& text) +{ + if (text.empty()) + return text; + + return std::regex_replace(text, kEscapeHtml, ""); +} + +NotifyChatMessageBroadcast BuildChatMessage(ChatMessageType type, std::string_view playerName, std::string_view message) +{ + NotifyChatMessageBroadcast notify{}; + notify.MessageType = type; + + TiltedPhoques::String nameText(playerName.begin(), playerName.end()); + TiltedPhoques::String messageText(message.begin(), message.end()); + notify.PlayerName = SanitizeChatText(nameText); + notify.ChatMessage = SanitizeChatText(messageText); + return notify; +} + +Player* FindPlayerByUsernameInsensitive(World& world, const TiltedPhoques::String& username) +{ + if (username.empty()) + return nullptr; + + const TiltedPhoques::String needle = ToLowerCopy(username); + for (Player* player : world.GetPlayerManager()) + { + if (!player) + continue; + + if (ToLowerCopy(player->GetUsername()) == needle) + return player; + } + + return nullptr; +} + +TiltedPhoques::String JoinArgs(const TiltedPhoques::Vector& args, size_t startIndex = 0) +{ + TiltedPhoques::String joined; + if (startIndex >= args.size()) + return joined; + + for (size_t i = startIndex; i < args.size(); ++i) + { + if (i > startIndex) + joined += ' '; + joined += args[i]; + } + return joined; +} + +void SendDirectChat(Player* recipient, ChatMessageType type, std::string_view senderName, std::string_view content) +{ + if (!recipient) + return; + + const auto notifyMessage = BuildChatMessage(type, senderName, content); + recipient->Send(notifyMessage); +} +} // namespace + +class HelpCommand final : public ChatCommand +{ +public: + const char* GetName() const noexcept override { return "help"; } + const char* GetDescription() const noexcept override { return "Show available commands"; } + + bool Execute(World& world, Player* player, const ChatCommandContext& context) const override + { + (void)context; + auto& service = world.GetChatCommandService(); + const auto commands = service.BuildCommandList(player); + TiltedPhoques::String line = "Commands:"; + for (const auto& command : commands) + { + line += " "; + line += command.Name; + } + service.SendSystemMessage(player, line.c_str()); + return true; + } +}; + +class LocalCommand final : public ChatCommand +{ +public: + const char* GetName() const noexcept override { return "local"; } + const char* GetDescription() const noexcept override { return "Send a message to nearby players"; } + + bool Execute(World& world, Player* player, const ChatCommandContext& context) const override + { + const auto content = JoinArgs(context.Args); + if (content.empty()) + { + world.GetChatCommandService().SendSystemMessage(player, "Usage: /local "); + return true; + } + + world.GetChatCommandService().SendChatMessage(kLocalChat, content, player); + return true; + } +}; + +class PartyCommand final : public ChatCommand +{ +public: + const char* GetName() const noexcept override { return "party"; } + const char* GetDescription() const noexcept override { return "Send a message to your party"; } + + bool Execute(World& world, Player* player, const ChatCommandContext& context) const override + { + const auto content = JoinArgs(context.Args); + if (content.empty()) + { + world.GetChatCommandService().SendSystemMessage(player, "Usage: /party "); + return true; + } + + world.GetChatCommandService().SendChatMessage(kPartyChat, content, player); + return true; + } +}; + +class MeCommand final : public ChatCommand +{ +public: + const char* GetName() const noexcept override { return "me"; } + const char* GetDescription() const noexcept override { return "Emote a message"; } + + bool Execute(World& world, Player* player, const ChatCommandContext& context) const override + { + const auto content = JoinArgs(context.Args); + if (content.empty()) + { + world.GetChatCommandService().SendSystemMessage(player, "Usage: /me "); + return true; + } + + const TiltedPhoques::String message = TiltedPhoques::String("* ") + player->GetUsername() + " " + content; + const auto notifyMessage = BuildChatMessage(kGlobalChat, "", message); + GameServer::Get()->SendToPlayers(notifyMessage); + if (auto out = spdlog::get("ConOut")) + out->info("[Emote] {}", notifyMessage.ChatMessage.c_str()); + return true; + } +}; + +class BroadcastCommand final : public ChatCommand +{ +public: + const char* GetName() const noexcept override { return "broadcast"; } + const char* GetDescription() const noexcept override { return "Broadcast a server message"; } + bool RequiresAdmin() const noexcept override { return true; } + + bool Execute(World& world, Player* player, const ChatCommandContext& context) const override + { + const auto content = JoinArgs(context.Args); + if (content.empty()) + { + world.GetChatCommandService().SendSystemMessage(player, "Usage: /broadcast "); + return true; + } + + world.GetChatCommandService().BroadcastSystemMessage(content); + return true; + } +}; + +class DirectMessageCommand final : public ChatCommand +{ +public: + explicit DirectMessageCommand(const char* alias) + : m_alias(alias) + { + } + + const char* GetName() const noexcept override { return m_alias; } + const char* GetDescription() const noexcept override { return "Send a private message to a player"; } + + bool Execute(World& world, Player* player, const ChatCommandContext& context) const override + { + if (context.Args.size() < 2) + { + const TiltedPhoques::String usage = TiltedPhoques::String("Usage: /") + m_alias + " "; + world.GetChatCommandService().SendSystemMessage(player, usage.c_str()); + return true; + } + + const auto& targetName = context.Args[0]; + const auto content = JoinArgs(context.Args, 1); + if (content.empty()) + { + const TiltedPhoques::String usage = TiltedPhoques::String("Usage: /") + m_alias + " "; + world.GetChatCommandService().SendSystemMessage(player, usage.c_str()); + return true; + } + + Player* target = FindPlayerByUsernameInsensitive(world, targetName); + if (!target) + { + world.GetChatCommandService().SendSystemMessage(player, "Message failed: player not found."); + return true; + } + + const TiltedPhoques::String toMessage = TiltedPhoques::String("You whisper to ") + target->GetUsername() + ": " + content; + const TiltedPhoques::String fromMessage = player->GetUsername() + " whispers to you: " + content; + + SendDirectChat(player, kWhisper, "", toMessage); + if (target != player) + SendDirectChat(target, kWhisper, "", fromMessage); + + return true; + } + +private: + const char* m_alias; +}; + +class VoteTimeCommand final : public ChatCommand +{ +public: + const char* GetName() const noexcept override { return "votetime"; } + const char* GetDescription() const noexcept override { return "Start a vote to change time"; } + + bool Execute(World& world, Player* player, const ChatCommandContext& context) const override + { + (void)context; + world.ctx().at().HandleChatCommand(player, context.Raw); + return true; + } +}; + +class YesCommand final : public ChatCommand +{ +public: + const char* GetName() const noexcept override { return "yes"; } + const char* GetDescription() const noexcept override { return "Vote yes in the active vote"; } + + bool Execute(World& world, Player* player, const ChatCommandContext& context) const override + { + (void)context; + world.ctx().at().HandleChatCommand(player, "/yes"); + return true; + } +}; + +class NoCommand final : public ChatCommand +{ +public: + const char* GetName() const noexcept override { return "no"; } + const char* GetDescription() const noexcept override { return "Vote no in the active vote"; } + + bool Execute(World& world, Player* player, const ChatCommandContext& context) const override + { + (void)context; + world.ctx().at().HandleChatCommand(player, "/no"); + return true; + } +}; + +class SetTimeCommand final : public ChatCommand +{ +public: + const char* GetName() const noexcept override { return "settime"; } + const char* GetDescription() const noexcept override { return "Set the server time"; } + bool RequiresAdmin() const noexcept override { return true; } + + bool Execute(World& world, Player* player, const ChatCommandContext& context) const override + { + if (context.Args.size() < 2) + { + world.GetChatCommandService().SendSystemMessage(player, "Usage: /settime "); + return true; + } + + int hour = 0; + int minute = 0; + if (!ParseIntArg(context.Args[0], hour) || !ParseIntArg(context.Args[1], minute)) + { + world.GetChatCommandService().SendSystemMessage(player, "Usage: /settime "); + return true; + } + + const float timescale = world.GetCalendarService().GetTimeScale(); + if (!world.GetCalendarService().SetTime(hour, minute, timescale)) + { + world.GetChatCommandService().SendSystemMessage(player, "SetTime failed: hour must be 0-23 and minute 0-59."); + return true; + } + + world.GetChatCommandService().SendSystemMessage(player, "Time updated."); + return true; + } +}; + +class TeleportCommand final : public ChatCommand +{ +public: + explicit TeleportCommand(const char* alias) + : m_alias(alias) + { + } + + const char* GetName() const noexcept override { return m_alias; } + const char* GetDescription() const noexcept override { return "Teleport to a player"; } + bool RequiresAdmin() const noexcept override { return true; } + + bool Execute(World& world, Player* player, const ChatCommandContext& context) const override + { + if (context.Args.empty()) + { + world.GetChatCommandService().SendSystemMessage(player, "Usage: /teleport "); + return true; + } + + Player* target = FindPlayerByUsernameInsensitive(world, context.Args[0]); + if (!target) + { + world.GetChatCommandService().SendSystemMessage(player, "Teleport failed: player not found."); + return true; + } + + const auto character = target->GetCharacter(); + if (!character.has_value()) + { + world.GetChatCommandService().SendSystemMessage(player, "Teleport failed: target has no character."); + return true; + } + + const auto* movement = world.try_get(*character); + if (!movement) + { + world.GetChatCommandService().SendSystemMessage(player, "Teleport failed: target position unavailable."); + return true; + } + + TeleportCommandResponse response{}; + const auto& cellComponent = target->GetCellComponent(); + response.CellId = cellComponent.Cell; + response.WorldSpaceId = cellComponent.WorldSpaceId; + response.Position = movement->Position; + player->Send(response); + + world.GetChatCommandService().SendSystemMessage(player, "Teleporting..."); + return true; + } + +private: + const char* m_alias; +}; + +ChatCommandService::ChatCommandService(World& aWorld, entt::dispatcher& aDispatcher) noexcept + : m_world(aWorld) +{ + (void)aDispatcher; + RegisterCommand(TiltedPhoques::CastUnique(TiltedPhoques::MakeUnique())); + RegisterCommand(TiltedPhoques::CastUnique(TiltedPhoques::MakeUnique())); + RegisterCommand(TiltedPhoques::CastUnique(TiltedPhoques::MakeUnique())); + RegisterCommand(TiltedPhoques::CastUnique(TiltedPhoques::MakeUnique())); + RegisterCommand(TiltedPhoques::CastUnique(TiltedPhoques::MakeUnique())); + RegisterCommand(TiltedPhoques::CastUnique(TiltedPhoques::MakeUnique("message"))); + RegisterCommand(TiltedPhoques::CastUnique(TiltedPhoques::MakeUnique("whisper"))); + RegisterCommand(TiltedPhoques::CastUnique(TiltedPhoques::MakeUnique())); + RegisterCommand(TiltedPhoques::CastUnique(TiltedPhoques::MakeUnique())); + RegisterCommand(TiltedPhoques::CastUnique(TiltedPhoques::MakeUnique())); + RegisterCommand(TiltedPhoques::CastUnique(TiltedPhoques::MakeUnique())); + RegisterCommand(TiltedPhoques::CastUnique(TiltedPhoques::MakeUnique("teleport"))); + RegisterCommand(TiltedPhoques::CastUnique(TiltedPhoques::MakeUnique("tp"))); +} + +bool ChatCommandService::TryHandle(Player* player, const TiltedPhoques::String& message) const +{ + ChatCommandContext context{}; + if (!ParseCommand(message, context)) + return false; + + const auto* command = FindCommand(context.NameLower); + if (!command) + return false; + + if (command->RequiresAdmin() && !IsAdmin(player)) + { + SendSystemMessage(player, "You do not have permission to use that command."); + return true; + } + + return command->Execute(m_world, player, context); +} + +void ChatCommandService::SendCommandList(Player* player) const noexcept +{ + if (!player) + return; + + NotifyCommandList notify{}; + notify.Commands = BuildCommandList(player); + player->Send(notify); +} + +TiltedPhoques::Vector ChatCommandService::BuildCommandList(Player* player) const +{ + TiltedPhoques::Vector out; + const bool admin = IsAdmin(player); + out.reserve(m_commands.size()); + for (const auto& command : m_commands) + { + if (command->RequiresAdmin() && !admin) + continue; + NotifyCommandList::CommandEntry entry{}; + entry.Name = TiltedPhoques::String("/") + command->GetName(); + entry.Description = command->GetDescription(); + out.push_back(std::move(entry)); + } + return out; +} + +void ChatCommandService::SendSystemMessage(Player* player, std::string_view message) const noexcept +{ + if (!player) + return; + + const auto notify = BuildChatMessage(ChatMessageType::kSystemMessage, "", message); + player->Send(notify); +} + +void ChatCommandService::BroadcastSystemMessage(std::string_view message) const noexcept +{ + const auto notify = BuildChatMessage(ChatMessageType::kSystemMessage, "", message); + GameServer::Get()->SendToPlayers(notify); + if (auto out = spdlog::get("ConOut")) + out->info("[System] {}", notify.ChatMessage.c_str()); +} + +void ChatCommandService::SendChatMessage(ChatMessageType type, const TiltedPhoques::String& content, Player* sender) const noexcept +{ + if (!sender) + return; + + const auto notifyMessage = BuildChatMessage(type, sender->GetUsername(), content); + if (auto out = spdlog::get("ConOut")) + { + const char* label = "Chat"; + switch (notifyMessage.MessageType) + { + case kGlobalChat: label = "Global"; break; + case kPartyChat: label = "Party"; break; + case kLocalChat: label = "Local"; break; + case kPlayerDialogue: label = "Dialogue"; break; + default: break; + } + if (!notifyMessage.PlayerName.empty()) + out->info("[{}] {}: {}", label, notifyMessage.PlayerName.c_str(), notifyMessage.ChatMessage.c_str()); + else + out->info("[{}] {}", label, notifyMessage.ChatMessage.c_str()); + } + + auto character = sender->GetCharacter(); + + switch (notifyMessage.MessageType) + { + case kGlobalChat: GameServer::Get()->SendToPlayers(notifyMessage); break; + case kSystemMessage: break; + case kPlayerDialogue: GameServer::Get()->SendToParty(notifyMessage, sender->GetParty()); break; + case kPartyChat: GameServer::Get()->SendToParty(notifyMessage, sender->GetParty()); break; + case kLocalChat: + if (character) + { + if (!GameServer::Get()->SendToPlayersInRange(notifyMessage, *character)) + spdlog::error("{}: SendToPlayersInRange failed", __FUNCTION__); + } + break; + default: spdlog::error("{} is not a known MessageType", static_cast(notifyMessage.MessageType)); break; + } +} + +bool ChatCommandService::ParseCommand(const TiltedPhoques::String& raw, ChatCommandContext& out) const +{ + out.Raw = TrimCopy(raw); + if (out.Raw.empty() || out.Raw.front() != '/') + return false; + + TiltedPhoques::String text = out.Raw; + text.erase(text.begin()); + text = TrimCopy(text); + if (text.empty()) + return false; + + size_t pos = 0; + while (pos < text.size()) + { + while (pos < text.size() && std::isspace(static_cast(text[pos]))) + ++pos; + if (pos >= text.size()) + break; + size_t end = pos; + while (end < text.size() && !std::isspace(static_cast(text[end]))) + ++end; + out.Args.emplace_back(text.substr(pos, end - pos)); + pos = end; + } + + if (out.Args.empty()) + return false; + + out.NameLower = ToLowerCopy(out.Args[0]); + out.Args.erase(out.Args.begin()); + return true; +} + +const ChatCommand* ChatCommandService::FindCommand(std::string_view nameLower) const +{ + TiltedPhoques::String key(nameLower); + std::transform(key.begin(), key.end(), key.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + const auto it = m_commandLookup.find(key); + return it == m_commandLookup.end() ? nullptr : it->second; +} + +bool ChatCommandService::IsAdmin(Player* player) const +{ + if (!player) + return false; + return m_world.GetAdminService().IsAdmin(player->GetUsername()); +} + +void ChatCommandService::RegisterCommand(TiltedPhoques::UniquePtr command) +{ + if (!command) + return; + + const TiltedPhoques::String key = ToLowerCopy(command->GetName()); + m_commandLookup[key] = command.get(); + m_commands.push_back(std::move(command)); +} diff --git a/Code/server/Services/ChatCommandService.h b/Code/server/Services/ChatCommandService.h new file mode 100644 index 000000000..7b602580e --- /dev/null +++ b/Code/server/Services/ChatCommandService.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +#include + +#include +#include +#include + +struct World; +struct Player; + +struct ChatCommandContext +{ + TiltedPhoques::String Raw; + TiltedPhoques::String NameLower; + TiltedPhoques::Vector Args; +}; + +class ChatCommand +{ +public: + virtual ~ChatCommand() = default; + + virtual const char* GetName() const noexcept = 0; + virtual const char* GetDescription() const noexcept = 0; + virtual bool RequiresAdmin() const noexcept { return false; } + virtual bool Execute(World& world, Player* player, const ChatCommandContext& context) const = 0; +}; + +class ChatCommandService +{ +public: + ChatCommandService(World& aWorld, entt::dispatcher& aDispatcher) noexcept; + ~ChatCommandService() noexcept = default; + + TP_NOCOPYMOVE(ChatCommandService); + + bool TryHandle(Player* player, const TiltedPhoques::String& message) const; + void SendCommandList(Player* player) const noexcept; + TiltedPhoques::Vector BuildCommandList(Player* player) const; + + void SendSystemMessage(Player* player, std::string_view message) const noexcept; + void BroadcastSystemMessage(std::string_view message) const noexcept; + void SendChatMessage(ChatMessageType type, const TiltedPhoques::String& content, Player* sender) const noexcept; + +private: + bool ParseCommand(const TiltedPhoques::String& raw, ChatCommandContext& out) const; + const ChatCommand* FindCommand(std::string_view nameLower) const; + bool IsAdmin(Player* player) const; + + void RegisterCommand(TiltedPhoques::UniquePtr command); + +private: + World& m_world; + TiltedPhoques::Vector> m_commands; + std::unordered_map m_commandLookup; +}; diff --git a/Code/server/Services/CombatService.cpp b/Code/server/Services/CombatService.cpp index 6320fd9ac..f995b35eb 100644 --- a/Code/server/Services/CombatService.cpp +++ b/Code/server/Services/CombatService.cpp @@ -50,7 +50,13 @@ void CombatService::OnProjectileLaunchRequest(const PacketEvent(packet.ShooterID); - if (!GameServer::Get()->SendToPlayersInRange(notify, cShooterEntity, acMessage.GetSender())) + const auto entity = m_world.TryResolveEntity(packet.ShooterID); + if (!entity) + { + spdlog::debug("Projectile launch from unknown entity {:X}", packet.ShooterID); + return; + } + + if (!GameServer::Get()->SendToPlayersInRange(notify, *entity, acMessage.GetSender())) spdlog::error("{}: SendToPlayersInRange failed", __FUNCTION__); } diff --git a/Code/server/Services/CommandService.cpp b/Code/server/Services/CommandService.cpp index 7bb66ce86..4a3a791c1 100644 --- a/Code/server/Services/CommandService.cpp +++ b/Code/server/Services/CommandService.cpp @@ -6,8 +6,50 @@ #include #include +#include #include #include +#include + +#include +#include +#include + +namespace +{ +void SendSystemMessage(Player* apPlayer, std::string_view aMessage) noexcept +{ + if (!apPlayer) + return; + + NotifyChatMessageBroadcast notify{}; + notify.MessageType = ChatMessageType::kSystemMessage; + notify.PlayerName = ""; + notify.ChatMessage = aMessage.data(); + apPlayer->Send(notify); +} + +Player* FindPlayerByUsername(World& aWorld, const TiltedPhoques::String& aUsername) +{ + if (aUsername.empty()) + return nullptr; + + TiltedPhoques::String needle = aUsername; + std::transform(needle.begin(), needle.end(), needle.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + for (Player* pPlayer : aWorld.GetPlayerManager()) + { + if (!pPlayer) + continue; + + TiltedPhoques::String candidate = pPlayer->GetUsername(); + std::transform(candidate.begin(), candidate.end(), candidate.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (candidate == needle) + return pPlayer; + } + + return nullptr; +} +} // namespace CommandService::CommandService(World& aWorld, entt::dispatcher& aDispatcher) noexcept : m_world(aWorld) @@ -20,54 +62,61 @@ void CommandService::OnSetTimeCommand(const PacketEvent& { 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 (!m_world.GetAdminService().IsAdmin(acMessage.pPlayer->GetUsername())) { - if (PlayerManager::Get()->GetByConnectionId(session)->GetId() == cPlayerId) - { - const auto cHours = static_cast(acMessage.Packet.Hours); - const auto cMinutes = static_cast(acMessage.Packet.Minutes); + response.Result = NotifySetTimeResult::SetTimeResult::kNoPermission; + acMessage.pPlayer->Send(response); + SendSystemMessage(acMessage.pPlayer, "SetTime requires admin permissions."); + return; + } - m_world.GetCalendarService().SetTime(cHours, cMinutes, m_world.GetCalendarService().GetTimeScale()); + const int hour = static_cast(acMessage.Packet.Hours); + const int minute = static_cast(acMessage.Packet.Minutes); + const float timescale = m_world.GetCalendarService().GetTimeScale(); + const bool success = m_world.GetCalendarService().SetTime(hour, minute, timescale); - response.Result = NotifySetTimeResult::SetTimeResult::kSuccess; - acMessage.pPlayer->Send(response); + response.Result = success ? NotifySetTimeResult::SetTimeResult::kSuccess : NotifySetTimeResult::SetTimeResult::kNoPermission; + acMessage.pPlayer->Send(response); - return; - } + if (!success) + { + SendSystemMessage(acMessage.pPlayer, "SetTime failed: hour must be 0-23 and minute 0-59."); } - - response.Result = NotifySetTimeResult::SetTimeResult::kNoPermission; - acMessage.pPlayer->Send(response); } void CommandService::OnTeleportCommandRequest(const PacketEvent& acMessage) const noexcept { - Player* pTargetPlayer = nullptr; - for (Player* pPlayer : m_world.GetPlayerManager()) + if (!m_world.GetAdminService().IsAdmin(acMessage.pPlayer->GetUsername())) { - if (pPlayer->GetUsername() == acMessage.Packet.TargetPlayer) - pTargetPlayer = pPlayer; + SendSystemMessage(acMessage.pPlayer, "Teleport requires admin permissions."); + return; } - TeleportCommandResponse response{}; - if (pTargetPlayer) + Player* pTarget = FindPlayerByUsername(m_world, acMessage.Packet.TargetPlayer); + if (!pTarget) { - auto character = pTargetPlayer->GetCharacter(); - if (character) - { - const auto* pMovementComponent = m_world.try_get(*character); - if (pMovementComponent) - { - const auto& cellComponent = pTargetPlayer->GetCellComponent(); - response.CellId = cellComponent.Cell; - response.Position = pMovementComponent->Position; - response.WorldSpaceId = cellComponent.WorldSpaceId; - } - } + SendSystemMessage(acMessage.pPlayer, "Teleport failed: player not found."); + return; } + const auto character = pTarget->GetCharacter(); + if (!character.has_value()) + { + SendSystemMessage(acMessage.pPlayer, "Teleport failed: target has no character."); + return; + } + + const auto* pMovement = m_world.try_get(*character); + if (!pMovement) + { + SendSystemMessage(acMessage.pPlayer, "Teleport failed: target position unavailable."); + return; + } + + TeleportCommandResponse response{}; + const auto& cellComponent = pTarget->GetCellComponent(); + response.CellId = cellComponent.Cell; + response.WorldSpaceId = cellComponent.WorldSpaceId; + response.Position = pMovement->Position; acMessage.pPlayer->Send(response); } diff --git a/Code/server/Services/DropService.cpp b/Code/server/Services/DropService.cpp new file mode 100644 index 000000000..004d96b4c --- /dev/null +++ b/Code/server/Services/DropService.cpp @@ -0,0 +1,2208 @@ +#include "DropService.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#if defined(_WIN32) +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif +# ifndef NOMINMAX +# define NOMINMAX +# endif +# include +#else +# include +#endif + +namespace +{ +Console::Setting bEnableItemDrops{"Gameplay:bEnableItemDrops", "(Experimental) Syncs dropped items by players", true}; + +constexpr float kDropForwardOffset = 35.f; +constexpr float kDropVerticalOffset = 5.f; +constexpr double kCleanupIntervalSeconds = 60.0; +constexpr int64_t kDropExpirySeconds = 7 * 24 * 60 * 60; +constexpr int64_t kCreationEnginePickupExpirySeconds = 24 * 60 * 60; + +Vector3_NetQuantize ToNetVector(const glm::vec3& aVector) noexcept +{ + Vector3_NetQuantize value{}; + value.x = aVector.x; + value.y = aVector.y; + value.z = aVector.z; + return value; +} + +struct StatementDeleter +{ + void operator()(sqlite3_stmt* apStatement) const noexcept + { + if (apStatement) + sqlite3_finalize(apStatement); + } +}; + +using StatementPtr = std::unique_ptr; + +StatementPtr PrepareStatement(sqlite3* apDatabase, const char* acSql) noexcept +{ + sqlite3_stmt* pStatement = nullptr; + if (sqlite3_prepare_v2(apDatabase, acSql, -1, &pStatement, nullptr) != SQLITE_OK) + return nullptr; + + return StatementPtr(pStatement); +} + +void BindGuid(sqlite3_stmt* apStatement, int aIndex, const Guid& acGuid) noexcept +{ + if (acGuid.IsEmpty()) + sqlite3_bind_null(apStatement, aIndex); + else + sqlite3_bind_blob(apStatement, aIndex, acGuid.Bytes.data(), static_cast(acGuid.Bytes.size()), SQLITE_TRANSIENT); +} + +Guid GuidFromColumn(sqlite3_stmt* apStatement, int aIndex) noexcept +{ + Guid value{}; + const void* pBlob = sqlite3_column_blob(apStatement, aIndex); + const int blobSize = sqlite3_column_bytes(apStatement, aIndex); + if (pBlob && blobSize == static_cast(value.Bytes.size())) + std::memcpy(value.Bytes.data(), pBlob, value.Bytes.size()); + else + value.Clear(); + return value; +} + +bool EnsureColumnExists(sqlite3* apDatabase, const char* acSql) noexcept +{ + if (!apDatabase || !acSql) + return false; + + char* pError = nullptr; + const int result = sqlite3_exec(apDatabase, acSql, nullptr, nullptr, &pError); + if (result == SQLITE_OK) + return true; + + std::string errorMessage = pError ? pError : ""; + sqlite3_free(pError); + + if (result == SQLITE_ERROR && errorMessage.find("duplicate column name") != std::string::npos) + return true; + + spdlog::error("DropService: schema migration failed: {}", errorMessage.empty() ? "unknown error" : errorMessage); + return false; +} + +TiltedPhoques::Vector FetchCreationEnginePickupsForCell(sqlite3* apDatabase, const GameId& acCellId, bool aHasWorldFilter, const GameId& acWorldId) noexcept +{ + TiltedPhoques::Vector result{}; + if (!apDatabase || !acCellId) + return result; + + const char* sql = aHasWorldFilter + ? "SELECT engine_mod_id, engine_base_id FROM creation_engine_pickups WHERE cell_mod_id = ?1 AND cell_base_id = ?2 AND world_mod_id = ?3 AND world_base_id = ?4;" + : "SELECT engine_mod_id, engine_base_id FROM creation_engine_pickups WHERE cell_mod_id = ?1 AND cell_base_id = ?2;"; + + StatementPtr statement = PrepareStatement(apDatabase, sql); + if (!statement) + return result; + + sqlite3_bind_int(statement.get(), 1, static_cast(acCellId.ModId)); + sqlite3_bind_int(statement.get(), 2, static_cast(acCellId.BaseId)); + if (aHasWorldFilter) + { + sqlite3_bind_int(statement.get(), 3, static_cast(acWorldId.ModId)); + sqlite3_bind_int(statement.get(), 4, static_cast(acWorldId.BaseId)); + } + + while (sqlite3_step(statement.get()) == SQLITE_ROW) + { + GameId ref{}; + ref.ModId = static_cast(sqlite3_column_int64(statement.get(), 0)); + ref.BaseId = static_cast(sqlite3_column_int64(statement.get(), 1)); + if (ref) + result.push_back(ref); + } + + return result; +} + +std::string SerializeEntryBlob(const Inventory::Entry& acEntry) noexcept +{ + TiltedPhoques::Buffer buffer(1 << 12); + TiltedPhoques::Buffer::Writer writer(&buffer); + Inventory::Entry normalized = acEntry; + normalized.Serialize(writer); + const auto size = static_cast(writer.Size()); + std::string blob(size, '\0'); + std::memcpy(blob.data(), buffer.GetWriteData(), size); + return blob; +} + +Inventory::Entry DeserializeEntryBlob(const void* apBlob, int aSize) noexcept +{ + Inventory::Entry entry{}; + if (!apBlob || aSize <= 0) + return entry; + + auto* pData = const_cast(reinterpret_cast(apBlob)); + TiltedPhoques::ViewBuffer view(pData, static_cast(aSize)); + TiltedPhoques::Buffer::Reader reader(&view); + entry.Deserialize(reader); + return entry; +} + +constexpr const char* kCreateDropsTableSql = R"SQL( + CREATE TABLE IF NOT EXISTS server_drops( + server_drop_id INTEGER PRIMARY KEY AUTOINCREMENT, + server_id INTEGER NOT NULL, + actor_form_id INTEGER NOT NULL, + origin_player_id INTEGER NOT NULL, + client_drop_id BLOB NULL, + item_type INTEGER NOT NULL DEFAULT 0, + item_mod_id INTEGER NOT NULL, + item_base_id INTEGER NOT NULL, + count INTEGER NOT NULL CHECK(count > 0), + cell_mod_id INTEGER NOT NULL, + cell_base_id INTEGER NOT NULL, + world_mod_id INTEGER NOT NULL, + world_base_id INTEGER NOT NULL, + reference_mod_id INTEGER NOT NULL DEFAULT 0, + reference_base_id INTEGER NOT NULL DEFAULT 0, + has_location INTEGER NOT NULL DEFAULT 0, + pos_x REAL, + pos_y REAL, + pos_z REAL, + has_rotation INTEGER NOT NULL DEFAULT 0, + rot_x REAL, + rot_y REAL, + rot_z REAL, + is_active INTEGER NOT NULL DEFAULT 1, + version INTEGER NOT NULL DEFAULT 1, + item_blob BLOB NOT NULL, + created_at INTEGER DEFAULT (strftime('%s','now')), + updated_at INTEGER DEFAULT (strftime('%s','now')) + ); + CREATE INDEX IF NOT EXISTS idx_server_drops_cell_active ON server_drops(cell_mod_id, cell_base_id) WHERE is_active = 1; + CREATE UNIQUE INDEX IF NOT EXISTS idx_server_drops_client ON server_drops(origin_player_id, client_drop_id) WHERE client_drop_id IS NOT NULL AND is_active = 1; + + CREATE TABLE IF NOT EXISTS server_drop_history( + id INTEGER PRIMARY KEY AUTOINCREMENT, + server_drop_id INTEGER NOT NULL, + action TEXT NOT NULL, + performed_by INTEGER, + details TEXT, + created_at INTEGER DEFAULT (strftime('%s','now')), + FOREIGN KEY(server_drop_id) REFERENCES server_drops(server_drop_id) + ); + CREATE INDEX IF NOT EXISTS idx_server_drop_history_drop ON server_drop_history(server_drop_id); + + CREATE TABLE IF NOT EXISTS player_inventory( + player_id INTEGER NOT NULL, + item_mod_id INTEGER NOT NULL, + item_base_id INTEGER NOT NULL, + stack_id TEXT NOT NULL, + count INTEGER NOT NULL DEFAULT 0 CHECK(count >= 0), + entry_blob BLOB NOT NULL, + updated_at INTEGER DEFAULT (strftime('%s','now')), + PRIMARY KEY(player_id, stack_id) + ); + CREATE INDEX IF NOT EXISTS idx_player_inventory_item ON player_inventory(player_id, item_mod_id, item_base_id); + + CREATE TABLE IF NOT EXISTS drop_bindings( + server_drop_id INTEGER NOT NULL, + player_id INTEGER NOT NULL, + client_ref_handle INTEGER NOT NULL, + bound_at INTEGER DEFAULT (strftime('%s','now')), + PRIMARY KEY(server_drop_id, player_id), + FOREIGN KEY(server_drop_id) REFERENCES server_drops(server_drop_id) + ); +)SQL"; + +constexpr const char* kCreateCreationEngineTableSql = R"SQL( + CREATE TABLE IF NOT EXISTS creation_engine_pickups( + engine_mod_id INTEGER NOT NULL, + engine_base_id INTEGER NOT NULL, + cell_mod_id INTEGER NOT NULL, + cell_base_id INTEGER NOT NULL, + world_mod_id INTEGER NOT NULL DEFAULT 0, + world_base_id INTEGER NOT NULL DEFAULT 0, + picked_by INTEGER NOT NULL, + picked_at INTEGER DEFAULT (strftime('%s','now')), + PRIMARY KEY(engine_mod_id, engine_base_id) + ); + CREATE INDEX IF NOT EXISTS idx_creation_engine_pickups_cell ON creation_engine_pickups(cell_mod_id, cell_base_id); +)SQL"; + +std::filesystem::path ResolveExecutableDirectory() noexcept +{ + namespace fs = std::filesystem; + +#if defined(_WIN32) + std::array buffer{}; + const DWORD length = GetModuleFileNameW(nullptr, buffer.data(), static_cast(buffer.size())); + if (length != 0 && length < buffer.size()) + { + return fs::path(buffer.data()).parent_path(); + } +#else + std::error_code ec; + auto exePath = fs::canonical("/proc/self/exe", ec); + if (!ec) + return exePath.parent_path(); +#endif + + std::error_code ec; + const auto current = fs::current_path(ec); + if (!ec) + return current; + + return {}; +} + +std::filesystem::path ResolveItemsDatabasePath() noexcept +{ + namespace fs = std::filesystem; + + if (auto exeDirectory = ResolveExecutableDirectory(); !exeDirectory.empty()) + { + return exeDirectory / "items.db"; + } + +#if defined(_WIN32) + if (char* localAppData = nullptr; _dupenv_s(&localAppData, nullptr, "LOCALAPPDATA") == 0 && localAppData) + { + fs::path path(localAppData); + free(localAppData); + path /= "SkyrimTogether"; + path /= "Server"; + path /= "items.db"; + return path; + } +#else + if (const char* xdgDataHome = std::getenv("XDG_DATA_HOME"); xdgDataHome && xdgDataHome[0] != '\0') + { + return fs::path(xdgDataHome) / "skyrimtogether" / "server" / "items.db"; + } + + if (const char* home = std::getenv("HOME"); home && home[0] != '\0') + { + return fs::path(home) / ".local" / "share" / "skyrimtogether" / "server" / "items.db"; + } +#endif + + std::error_code ec; + const auto current = fs::current_path(ec); + if (!ec) + return current / "data" / "items.db"; + + return fs::path("items.db"); +} + +std::filesystem::path ResolveCreationEngineDatabasePath() noexcept +{ + namespace fs = std::filesystem; + + const fs::path itemsPath = ResolveItemsDatabasePath(); + if (!itemsPath.empty()) + return itemsPath.parent_path() / "items_creation_engine.db"; + + return fs::path("items_creation_engine.db"); +} +} + +DropService::DropService(World& aWorld, entt::dispatcher& aDispatcher) + : m_world(aWorld) +{ + m_requestDropConnection = aDispatcher.sink>().connect<&DropService::OnDropRequest>(this); + m_requestPickupConnection = aDispatcher.sink>().connect<&DropService::OnPickupRequest>(this); + m_requestDroppedItemsConnection = aDispatcher.sink>().connect<&DropService::OnDroppedItemsRequest>(this); + m_requestDropMoveConnection = aDispatcher.sink>().connect<&DropService::OnDropMoveRequest>(this); + m_requestDropPhysicsDisabledConnection = aDispatcher.sink>().connect<&DropService::OnDropPhysicsDisabledRequest>(this); + m_updateConnection = aDispatcher.sink().connect<&DropService::OnUpdate>(this); + + if (!InitializeDatabase()) + spdlog::error("DropService: failed to initialize drop persistence database"); + else + LoadPersistedDrops(); + + if (!InitializeCreationEngineDatabase()) + spdlog::error("DropService: failed to initialize creation engine pickup database"); +} + +DropService::~DropService() +{ + ShutdownCreationEngineDatabase(); + ShutdownDatabase(); +} + +void DropService::OnDropRequest(const PacketEvent& acMessage) noexcept +{ + if (!bEnableItemDrops) + return; + + const auto& message = acMessage.Packet; + + const auto entity = m_world.TryResolveEntity(message.ServerId); + if (!entity) + { + spdlog::warn("Drop requested for unknown entity {:X}", message.ServerId); + return; + } + + auto view = m_world.view(); + const auto it = view.find(*entity); + if (it == view.end()) + { + spdlog::warn("Drop requested for entity {:X} without OwnerComponent", message.ServerId); + return; + } + + auto& ownerComponent = view.get(*it); + if (ownerComponent.GetOwner() != acMessage.pPlayer) + { + spdlog::warn("Drop denied for {:X}: player {:X} not owner", message.ServerId, acMessage.pPlayer->GetConnectionId()); + return; + } + + Player* pPlayer = ownerComponent.GetOwner(); + if (!pPlayer) + { + spdlog::warn("Drop denied for {:X}: missing owner player", message.ServerId); + return; + } + + const FormIdComponent* pFormIdComponent = m_world.try_get(*entity); + entt::entity notifyEntity = *entity; + if (!pFormIdComponent) + { + if (auto characterEntity = acMessage.pPlayer->GetCharacter(); characterEntity && m_world.valid(*characterEntity)) + { + notifyEntity = *characterEntity; + pFormIdComponent = m_world.try_get(*characterEntity); + } + } + + uint32_t actorFormId = pFormIdComponent ? pFormIdComponent->Id : message.ActorFormId; + if (!pFormIdComponent) + spdlog::warn("DropService: drop requested for entity {:X} without FormIdComponent (using fallback {:X})", message.ServerId, actorFormId); + + auto* pCellComponent = m_world.try_get(*entity); + if (!pCellComponent) + spdlog::warn("DropService: drop requested for entity {:X} without CellIdComponent", message.ServerId); + + GameId fallbackCellId{}; + GameId fallbackWorldId{}; + const auto& playerCellComponent = acMessage.pPlayer->GetCellComponent(); + if (playerCellComponent.Cell) + fallbackCellId = playerCellComponent.Cell; + if (playerCellComponent.WorldSpaceId) + fallbackWorldId = playerCellComponent.WorldSpaceId; + + glm::vec3 authoritativePosition{}; + glm::vec3 authoritativeRotation{}; + bool hasAuthoritativePosition = false; + bool hasAuthoritativeRotation = false; + + if (auto* pMovementComponent = m_world.try_get(*entity)) + { + const glm::vec3 movementPos = pMovementComponent->Position; + const glm::vec3 movementRot = pMovementComponent->Rotation; + + const bool posFinite = std::isfinite(movementPos.x) && std::isfinite(movementPos.y) && std::isfinite(movementPos.z); + const bool rotFinite = std::isfinite(movementRot.x) && std::isfinite(movementRot.y) && std::isfinite(movementRot.z); + const float posLenSq = movementPos.x * movementPos.x + movementPos.y * movementPos.y + movementPos.z * movementPos.z; + + if (posFinite && posLenSq > 1.f) + { + const float yaw = movementRot.z; + const glm::vec3 forward{std::cos(yaw), std::sin(yaw), 0.f}; + authoritativePosition = movementPos + forward * kDropForwardOffset; + authoritativePosition.z += kDropVerticalOffset; + hasAuthoritativePosition = true; + } + + if (rotFinite) + { + authoritativeRotation = movementRot; + hasAuthoritativeRotation = true; + } + } + + if (!hasAuthoritativePosition && message.HasLocation) + { + authoritativePosition = glm::vec3(message.Location.x, message.Location.y, message.Location.z); + hasAuthoritativePosition = true; + } + + if (!hasAuthoritativeRotation && message.HasRotation) + { + authoritativeRotation = glm::vec3(message.Rotation.x, message.Rotation.y, message.Rotation.z); + hasAuthoritativeRotation = true; + } + + ActiveDrop drop{}; + drop.ServerId = notifyEntity != entt::null ? World::ToInteger(notifyEntity) : message.ServerId; + drop.ActorFormId = actorFormId; + drop.OriginPlayerId = pPlayer->GetId(); + drop.DropEntry = message.Item; + drop.PickupEntry = message.Item; + if (drop.PickupEntry.Count < 0) + drop.PickupEntry.Count = -drop.PickupEntry.Count; + drop.HasLocation = hasAuthoritativePosition; + if (drop.HasLocation) + drop.Location = ToNetVector(authoritativePosition); + drop.HasRotation = hasAuthoritativeRotation; + if (drop.HasRotation) + drop.Rotation = ToNetVector(authoritativeRotation); + if (message.CellId) + drop.CellId = message.CellId; + else if (pCellComponent && pCellComponent->Cell) + drop.CellId = pCellComponent->Cell; + else + drop.CellId = fallbackCellId; + + if (message.WorldSpaceId) + drop.WorldSpaceId = message.WorldSpaceId; + else if (pCellComponent && pCellComponent->WorldSpaceId) + drop.WorldSpaceId = pCellComponent->WorldSpaceId; + else + drop.WorldSpaceId = fallbackWorldId; + drop.ReferenceId = message.ReferenceId; + drop.ClientDropId = message.ClientDropId; + drop.Version = 1; + + const auto& ownerCell = pPlayer->GetCellComponent(); + if (ownerCell.Cell) + { + const bool cellMismatch = drop.CellId && drop.CellId != ownerCell.Cell; + if (!drop.CellId || cellMismatch) + { + if (cellMismatch) + { + spdlog::debug("DropService: correcting drop cell for player {} from {:X}:{:X} to {:X}:{:X}", pPlayer->GetId(), drop.CellId.ModId, drop.CellId.BaseId, ownerCell.Cell.ModId, + ownerCell.Cell.BaseId); + } + drop.CellId = ownerCell.Cell; + } + + if (ownerCell.WorldSpaceId) + { + if (!drop.WorldSpaceId || drop.WorldSpaceId != ownerCell.WorldSpaceId) + drop.WorldSpaceId = ownerCell.WorldSpaceId; + } + else if (drop.WorldSpaceId) + { + drop.WorldSpaceId = {}; + } + } + + const int32_t dropCount = drop.PickupEntry.Count; + if (dropCount <= 0) + { + spdlog::warn("DropService: invalid drop count {} for actor {:X}", dropCount, message.ServerId); + return; + } + + auto buildNotify = [](const ActiveDrop& acDrop) { + NotifyActorDrop notify{}; + notify.ServerId = acDrop.ServerId; + notify.ActorFormId = acDrop.ActorFormId; + notify.Item = acDrop.DropEntry; + notify.DropId = acDrop.DropId; + notify.SpawnEpoch = acDrop.SpawnEpoch; + notify.HasLocation = acDrop.HasLocation; + if (notify.HasLocation) + notify.Location = acDrop.Location; + notify.HasRotation = acDrop.HasRotation; + if (notify.HasRotation) + notify.Rotation = acDrop.Rotation; + notify.CellId = acDrop.CellId; + notify.WorldSpaceId = acDrop.WorldSpaceId; + notify.ReferenceId = acDrop.ReferenceId; + return notify; + }; + + if (!drop.ClientDropId.IsEmpty()) + { + if (auto existingDropId = FindExistingDropId(drop.OriginPlayerId, drop.ClientDropId)) + { + if (auto* pExisting = ResolveActiveDrop(*existingDropId)) + { + // Bump spawn epoch by incrementing version in DB and mirror in memory + { + constexpr const char* cBumpSql = "UPDATE server_drops SET version = version + 1, updated_at = strftime('%s','now') WHERE server_drop_id = ?1;"; + StatementPtr stmt = PrepareStatement(m_pDatabase, cBumpSql); + if (stmt) + { + sqlite3_bind_int64(stmt.get(), 1, static_cast(*existingDropId)); + sqlite3_step(stmt.get()); + } + } + pExisting->Version += 1; + pExisting->SpawnEpoch = pExisting->Version; + + NotifyActorDrop notify = buildNotify(*pExisting); + const bool broadcasted = GameServer::Get()->SendToPlayersInRange(notify, notifyEntity, nullptr); + if (!broadcasted) + { + spdlog::error("{}: SendToPlayersInRange failed (duplicate drop)", __FUNCTION__); + GameServer::Get()->SendToPlayers(notify, nullptr); + } + else + { + auto* pNotifyCell = m_world.try_get(notifyEntity); + const bool playerCellReady = static_cast(pPlayer->GetCellComponent()); + bool inRange = false; + if (pNotifyCell && playerCellReady) + { + bool isDragon = false; + if (const auto* pCharacterComponent = m_world.try_get(notifyEntity)) + isDragon = pCharacterComponent->IsDragon(); + inRange = pNotifyCell->IsInRange(pPlayer->GetCellComponent(), isDragon); + } + + if (!pNotifyCell || !playerCellReady || !inRange) + pPlayer->Send(notify); + } + } + else + { + spdlog::warn("DropService: duplicate drop {} found but failed to load active state", *existingDropId); + } + return; + } + } + + if (!BeginTransaction()) + { + spdlog::error("DropService: failed to begin transaction for actor {:X}", message.ServerId); + return; + } + + uint64_t serverDropId = 0; + if (!InsertDrop(drop, serverDropId)) + { + spdlog::error("DropService: failed to persist drop for actor {:X}", message.ServerId); + RollbackTransaction(); + return; + } + drop.DropId = serverDropId; + + std::string historyDetails; + if (drop.HasLocation) + { + historyDetails = fmt::format("cell={:X}:{:X}, world={:X}:{:X}, pos=({:.2f}, {:.2f}, {:.2f})", drop.CellId.ModId, drop.CellId.BaseId, drop.WorldSpaceId.ModId, drop.WorldSpaceId.BaseId, drop.Location.x, + drop.Location.y, drop.Location.z); + } + else + { + historyDetails = fmt::format("cell={:X}:{:X}, world={:X}:{:X}", drop.CellId.ModId, drop.CellId.BaseId, drop.WorldSpaceId.ModId, drop.WorldSpaceId.BaseId); + } + + if (!InsertDropHistory(drop.DropId, "create", drop.OriginPlayerId, historyDetails)) + { + spdlog::error("DropService: failed to insert history for drop {}", drop.DropId); + RollbackTransaction(); + return; + } + + if (!CommitTransaction()) + { + spdlog::error("DropService: commit failed for drop {}", drop.DropId); + RollbackTransaction(); + return; + } + TrackActiveDrop(drop); + + NotifyActorDrop notify = buildNotify(drop); + + const bool broadcasted = GameServer::Get()->SendToPlayersInRange(notify, notifyEntity, nullptr); + if (!broadcasted) + { + spdlog::error("{}: SendToPlayersInRange failed", __FUNCTION__); + GameServer::Get()->SendToPlayers(notify, nullptr); + } + else + { + auto* pNotifyCell = m_world.try_get(notifyEntity); + const bool playerCellReady = static_cast(pPlayer->GetCellComponent()); + bool inRange = false; + if (pNotifyCell && playerCellReady) + { + bool isDragon = false; + if (const auto* pCharacterComponent = m_world.try_get(notifyEntity)) + isDragon = pCharacterComponent->IsDragon(); + inRange = pNotifyCell->IsInRange(pPlayer->GetCellComponent(), isDragon); + } + + if (!pNotifyCell || !playerCellReady || !inRange) + pPlayer->Send(notify); + } + + spdlog::debug("DropService: drop {} tracked for actor {:X}, player {}, cell {:X}:{:X}, world {:X}:{:X}", drop.DropId, drop.ServerId, drop.OriginPlayerId, drop.CellId.ModId, drop.CellId.BaseId, + drop.WorldSpaceId.ModId, drop.WorldSpaceId.BaseId); +} + +void DropService::OnPickupRequest(const PacketEvent& acMessage) noexcept +{ + if (!bEnableItemDrops) + return; + + const auto& message = acMessage.Packet; + spdlog::debug("DropService: pickup request actor {:X} drop {} ref {:X}:{:X}", message.ServerId, message.DropId, message.ReferenceId.ModId, message.ReferenceId.BaseId); + + if (!message.DropId) + { + HandleUntrackedPickupRequest(acMessage); + return; + } + + ActiveDrop* pDrop = ResolveActiveDrop(message.DropId); + if (!pDrop) + { + spdlog::warn("DropService: pickup requested for unknown drop {}", message.DropId); + if (message.Item.BaseId) + { + Inventory::Entry correctionEntry = message.Item; + const int32_t pickedCount = correctionEntry.Count == 0 ? 1 : std::abs(correctionEntry.Count); + correctionEntry.Count = -pickedCount; + + NotifyInventoryChanges correction{}; + correction.ServerId = message.ServerId; + correction.Item = correctionEntry; + correction.Silent = true; + acMessage.pPlayer->Send(correction); + } + return; + } + + const auto pickerEntity = m_world.TryResolveEntity(message.ServerId); + if (!pickerEntity) + { + spdlog::warn("DropService: pickup requested for missing entity {:X}", message.ServerId); + return; + } + + auto view = m_world.view(); + const auto it = view.find(*pickerEntity); + if (it == view.end()) + { + spdlog::warn("DropService: pickup requested for entity {:X} without OwnerComponent", message.ServerId); + return; + } + + auto& ownerComponent = view.get(*it); + if (ownerComponent.GetOwner() != acMessage.pPlayer) + { + spdlog::warn("DropService: pickup denied for {:X}: player {:X} not owner", message.ServerId, acMessage.pPlayer->GetConnectionId()); + return; + } + + Player* pPicker = ownerComponent.GetOwner(); + if (!pPicker) + { + spdlog::warn("DropService: pickup denied for {:X}: missing owner player", message.ServerId); + return; + } + + if (!BeginTransaction()) + { + spdlog::error("DropService: failed to begin pickup transaction for drop {}", message.DropId); + if (pDrop->PickupEntry.BaseId) + { + Inventory::Entry correctionEntry = pDrop->PickupEntry; + const int32_t pickedCount = correctionEntry.Count == 0 ? 1 : std::abs(correctionEntry.Count); + correctionEntry.Count = -pickedCount; + + NotifyInventoryChanges correction{}; + correction.ServerId = message.ServerId; + correction.Item = correctionEntry; + correction.Silent = true; + acMessage.pPlayer->Send(correction); + } + return; + } + + const std::string historyDetails = fmt::format("picked_by={}, cell={:04X}:{:04X}", pPicker->GetId(), pDrop->CellId.ModId, pDrop->CellId.BaseId); + if (!InsertDropHistory(pDrop->DropId, "pickup", pPicker->GetId(), historyDetails)) + { + spdlog::error("DropService: failed to insert pickup history for drop {}", pDrop->DropId); + RollbackTransaction(); + if (pDrop->PickupEntry.BaseId) + { + Inventory::Entry correctionEntry = pDrop->PickupEntry; + const int32_t pickedCount = correctionEntry.Count == 0 ? 1 : std::abs(correctionEntry.Count); + correctionEntry.Count = -pickedCount; + + NotifyInventoryChanges correction{}; + correction.ServerId = message.ServerId; + correction.Item = correctionEntry; + correction.Silent = true; + acMessage.pPlayer->Send(correction); + } + return; + } + + if (!MarkDropInactive(pDrop->DropId)) + { + spdlog::warn("DropService: drop {} already inactive during pickup", pDrop->DropId); + RollbackTransaction(); + if (pDrop->PickupEntry.BaseId) + { + Inventory::Entry correctionEntry = pDrop->PickupEntry; + const int32_t pickedCount = correctionEntry.Count == 0 ? 1 : std::abs(correctionEntry.Count); + correctionEntry.Count = -pickedCount; + + NotifyInventoryChanges correction{}; + correction.ServerId = message.ServerId; + correction.Item = correctionEntry; + correction.Silent = true; + acMessage.pPlayer->Send(correction); + } + return; + } + + if (!CommitTransaction()) + { + spdlog::error("DropService: pickup commit failed for drop {}", pDrop->DropId); + RollbackTransaction(); + if (pDrop->PickupEntry.BaseId) + { + Inventory::Entry correctionEntry = pDrop->PickupEntry; + const int32_t pickedCount = correctionEntry.Count == 0 ? 1 : std::abs(correctionEntry.Count); + correctionEntry.Count = -pickedCount; + + NotifyInventoryChanges correction{}; + correction.ServerId = message.ServerId; + correction.Item = correctionEntry; + correction.Silent = true; + acMessage.pPlayer->Send(correction); + } + return; + } + ActiveDrop dropCopy = *pDrop; + if (dropCopy.Type == ServerItemType::CreationEngine) + { + const GameId pickupRef = message.ReferenceId ? message.ReferenceId : dropCopy.ReferenceId; + if (pickupRef && dropCopy.CellId && m_pCreationEngineDatabase) + { + if (!RecordCreationEnginePickup(pickupRef, dropCopy.CellId, dropCopy.WorldSpaceId, pPicker->GetId()) && + !IsCreationEnginePickupRecorded(pickupRef)) + { + spdlog::warn("DropService: failed to record creation engine pickup for drop {}", dropCopy.DropId); + } + } + } + RemoveActiveDrop(pDrop->DropId); + + NotifyDroppedItemPickedUp notify{}; + notify.ServerId = message.ServerId; + notify.Item = dropCopy.PickupEntry; + notify.DropId = dropCopy.DropId; + if (dropCopy.HasLocation) + { + notify.HasLocation = true; + notify.Location = dropCopy.Location; + } + if (dropCopy.HasRotation) + { + notify.HasRotation = true; + notify.Rotation = dropCopy.Rotation; + } + notify.CellId = dropCopy.CellId; + notify.WorldSpaceId = dropCopy.WorldSpaceId; + notify.ReferenceId = message.ReferenceId ? message.ReferenceId : dropCopy.ReferenceId; + + BroadcastPickup(notify); + spdlog::debug("DropService: drop {} picked up by actor {:X}", dropCopy.DropId, message.ServerId); +} + +void DropService::OnDroppedItemsRequest(const PacketEvent& acMessage) noexcept +{ + if (!bEnableItemDrops) + return; + + const auto& request = acMessage.Packet; + NotifyDroppedItems notify{}; + notify.RequestId = request.RequestId; + spdlog::debug("DropService: drop sync request {} from player {:X}, all={}, cell {:X}:{:X}, discoveries={}", request.RequestId, acMessage.GetSender()->GetConnectionId(), request.RequestAll, + request.HasCellFilter ? request.CellId.ModId : 0, request.HasCellFilter ? request.CellId.BaseId : 0, request.Discoveries.size()); + + if (!request.Discoveries.empty()) + { + const bool useTransaction = BeginTransaction(); + bool hadFailure = false; + + for (const auto& entry : request.Discoveries) + { + if (!entry.ReferenceId || !entry.Item.BaseId || !entry.CellId) + continue; + + if (IsCreationEnginePickupRecorded(entry.ReferenceId)) + continue; + + const auto existingIt = m_referenceDropIndex.find(entry.ReferenceId); + if (existingIt != m_referenceDropIndex.end()) + { + ActiveDrop* pExisting = ResolveActiveDrop(existingIt->second); + if (!pExisting || pExisting->Type != ServerItemType::CreationEngine) + continue; + + const GameId previousCell = pExisting->CellId; + pExisting->CellId = entry.CellId; + pExisting->WorldSpaceId = entry.WorldSpaceId; + if (entry.HasLocation) + { + pExisting->HasLocation = true; + pExisting->Location = entry.Location; + } + if (entry.HasRotation) + { + pExisting->HasRotation = true; + pExisting->Rotation = entry.Rotation; + } + + if (previousCell != pExisting->CellId) + { + EraseDropFromIndex(pExisting->DropId, previousCell); + IndexDrop(pExisting->DropId, pExisting->CellId); + } + + if (!UpdateDropLocation(*pExisting) && useTransaction) + hadFailure = true; + + continue; + } + + ActiveDrop drop{}; + drop.ServerId = 0; + drop.ActorFormId = 0; + drop.OriginPlayerId = acMessage.GetSender()->GetConnectionId(); + drop.Type = ServerItemType::CreationEngine; + drop.DropEntry = entry.Item; + drop.PickupEntry = entry.Item; + if (drop.PickupEntry.Count == 0) + drop.PickupEntry.Count = 1; + drop.CellId = entry.CellId; + drop.WorldSpaceId = entry.WorldSpaceId; + drop.ReferenceId = entry.ReferenceId; + drop.HasLocation = entry.HasLocation; + if (entry.HasLocation) + drop.Location = entry.Location; + drop.HasRotation = entry.HasRotation; + if (entry.HasRotation) + drop.Rotation = entry.Rotation; + + uint64_t dropId = 0; + if (!InsertDrop(drop, dropId)) + { + spdlog::warn("DropService: failed to insert creation-engine item ref {:X}:{:X}", entry.ReferenceId.ModId, entry.ReferenceId.BaseId); + if (useTransaction) + hadFailure = true; + continue; + } + + drop.DropId = dropId; + drop.Version = 1; + drop.SpawnEpoch = drop.Version; + TrackActiveDrop(drop); + } + + if (useTransaction) + { + if (!hadFailure) + hadFailure = !CommitTransaction(); + + if (hadFailure) + { + RollbackTransaction(); + LoadPersistedDrops(); + spdlog::warn("DropService: discovery transaction rolled back"); + } + } + } + + auto appendEntry = [&](const ActiveDrop& acDrop) { + if (!request.RequestAll) + { + if (request.HasWorldSpaceFilter && acDrop.WorldSpaceId != request.WorldSpaceId) + return; + } + + notify.Entries.push_back(MakeNotifyEntry(acDrop)); + }; + + if (!request.RequestAll && request.HasCellFilter) + { + auto indexIt = m_cellDropIndex.find(request.CellId); + if (indexIt != m_cellDropIndex.end()) + { + for (auto dropId : indexIt->second) + { + const auto dropIt = m_activeDrops.find(dropId); + if (dropIt == m_activeDrops.end()) + continue; + appendEntry(dropIt->second); + } + } + } + else + { + for (const auto& [dropId, drop] : m_activeDrops) + appendEntry(drop); + } + + if (!request.RequestAll && request.HasCellFilter) + notify.CreationEnginePickedUpReferences = FetchCreationEnginePickupsForCell(m_pCreationEngineDatabase, request.CellId, request.HasWorldSpaceFilter, request.WorldSpaceId); + + acMessage.pPlayer->Send(notify); + spdlog::debug("DropService: sent {} drops and {} creation-engine pickups in response to request {}", notify.Entries.size(), notify.CreationEnginePickedUpReferences.size(), request.RequestId); +} + +void DropService::OnDropMoveRequest(const PacketEvent& acMessage) noexcept +{ + if (!bEnableItemDrops) + return; + + const auto& message = acMessage.Packet; + if (!message.HasLocation && !message.HasRotation) + return; + + uint64_t resolvedDropId = message.DropId; + if (!resolvedDropId && message.ReferenceId) + { + if (const auto it = m_referenceDropIndex.find(message.ReferenceId); it != m_referenceDropIndex.end()) + resolvedDropId = it->second; + } + + NotifyDroppedItemMove notify{}; + notify.DropId = resolvedDropId; + notify.HasLocation = message.HasLocation; + if (notify.HasLocation) + notify.Location = message.Location; + notify.HasRotation = message.HasRotation; + if (notify.HasRotation) + notify.Rotation = message.Rotation; + notify.HasVelocity = message.HasVelocity; + if (notify.HasVelocity) + notify.Velocity = message.Velocity; + notify.HasAngularVelocity = message.HasAngularVelocity; + if (notify.HasAngularVelocity) + notify.AngularVelocity = message.AngularVelocity; + notify.CellId = message.CellId; + notify.WorldSpaceId = message.WorldSpaceId; + notify.ReferenceId = message.ReferenceId; + + if (resolvedDropId) + { + ActiveDrop* pDrop = ResolveActiveDrop(resolvedDropId); + if (!pDrop) + { + if (message.DropId) + { + spdlog::debug("DropService: move requested for unknown drop {}", message.DropId); + return; + } + + resolvedDropId = 0; + notify.DropId = 0; + } + else + { + const GameId previousCell = pDrop->CellId; + + if (message.CellId) + pDrop->CellId = message.CellId; + if (message.WorldSpaceId) + pDrop->WorldSpaceId = message.WorldSpaceId; + const GameId previousRef = pDrop->ReferenceId; + if (message.ReferenceId) + pDrop->ReferenceId = message.ReferenceId; + + if (message.HasLocation) + { + pDrop->HasLocation = true; + pDrop->Location = message.Location; + } + + if (message.HasRotation) + { + pDrop->HasRotation = true; + pDrop->Rotation = message.Rotation; + } + + if (message.HasVelocity) + { + pDrop->HasVelocity = true; + pDrop->Velocity = message.Velocity; + } + + if (message.HasAngularVelocity) + { + pDrop->HasAngularVelocity = true; + pDrop->AngularVelocity = message.AngularVelocity; + } + + if (previousRef && previousRef != pDrop->ReferenceId) + m_referenceDropIndex.erase(previousRef); + if (pDrop->ReferenceId) + m_referenceDropIndex[pDrop->ReferenceId] = pDrop->DropId; + + if (previousCell != pDrop->CellId) + { + EraseDropFromIndex(pDrop->DropId, previousCell); + IndexDrop(pDrop->DropId, pDrop->CellId); + } + + UpdateDropLocation(*pDrop); + + notify.DropId = pDrop->DropId; + notify.HasLocation = pDrop->HasLocation; + if (notify.HasLocation) + notify.Location = pDrop->Location; + notify.HasRotation = pDrop->HasRotation; + if (notify.HasRotation) + notify.Rotation = pDrop->Rotation; + notify.HasVelocity = pDrop->HasVelocity; + if (notify.HasVelocity) + notify.Velocity = pDrop->Velocity; + notify.HasAngularVelocity = pDrop->HasAngularVelocity; + if (notify.HasAngularVelocity) + notify.AngularVelocity = pDrop->AngularVelocity; + notify.CellId = pDrop->CellId; + notify.WorldSpaceId = pDrop->WorldSpaceId; + notify.ReferenceId = pDrop->ReferenceId; + } + } + + if (auto characterEntity = acMessage.pPlayer->GetCharacter(); characterEntity && m_world.valid(*characterEntity)) + { + if (!GameServer::Get()->SendToPlayersInRange(notify, *characterEntity, acMessage.pPlayer)) + { + spdlog::error("{}: SendToPlayersInRange failed for drop move {}", __FUNCTION__, notify.DropId); + GameServer::Get()->SendToPlayers(notify, acMessage.pPlayer); + } + } + else + { + GameServer::Get()->SendToPlayers(notify, acMessage.pPlayer); + } +} + +void DropService::OnDropPhysicsDisabledRequest(const PacketEvent& acMessage) noexcept +{ + if (!bEnableItemDrops) + return; + + const auto& message = acMessage.Packet; + + uint64_t resolvedDropId = message.DropId; + if (!resolvedDropId && message.ReferenceId) + { + if (const auto it = m_referenceDropIndex.find(message.ReferenceId); it != m_referenceDropIndex.end()) + resolvedDropId = it->second; + } + + if (!resolvedDropId) + return; + + NotifyDroppedItemPhysicsDisabled notify{}; + notify.DropId = resolvedDropId; + notify.HasLocation = message.HasLocation; + if (notify.HasLocation) + notify.Location = message.Location; + notify.HasRotation = message.HasRotation; + if (notify.HasRotation) + notify.Rotation = message.Rotation; + notify.CellId = message.CellId; + notify.WorldSpaceId = message.WorldSpaceId; + notify.ReferenceId = message.ReferenceId; + + ActiveDrop* pDrop = ResolveActiveDrop(resolvedDropId); + if (!pDrop) + { + spdlog::debug("DropService: physics disabled requested for unknown drop {}", resolvedDropId); + return; + } + + const GameId previousCell = pDrop->CellId; + + if (message.CellId) + pDrop->CellId = message.CellId; + if (message.WorldSpaceId) + pDrop->WorldSpaceId = message.WorldSpaceId; + const GameId previousRef = pDrop->ReferenceId; + if (message.ReferenceId) + pDrop->ReferenceId = message.ReferenceId; + + if (message.HasLocation) + { + pDrop->HasLocation = true; + pDrop->Location = message.Location; + } + + if (message.HasRotation) + { + pDrop->HasRotation = true; + pDrop->Rotation = message.Rotation; + } + + if (previousRef && previousRef != pDrop->ReferenceId) + m_referenceDropIndex.erase(previousRef); + if (pDrop->ReferenceId) + m_referenceDropIndex[pDrop->ReferenceId] = pDrop->DropId; + + if (previousCell != pDrop->CellId) + { + EraseDropFromIndex(pDrop->DropId, previousCell); + IndexDrop(pDrop->DropId, pDrop->CellId); + } + + UpdateDropLocation(*pDrop); + + notify.DropId = pDrop->DropId; + notify.HasLocation = pDrop->HasLocation; + if (notify.HasLocation) + notify.Location = pDrop->Location; + notify.HasRotation = pDrop->HasRotation; + if (notify.HasRotation) + notify.Rotation = pDrop->Rotation; + notify.CellId = pDrop->CellId; + notify.WorldSpaceId = pDrop->WorldSpaceId; + notify.ReferenceId = pDrop->ReferenceId; + + if (auto characterEntity = acMessage.pPlayer->GetCharacter(); characterEntity && m_world.valid(*characterEntity)) + { + if (!GameServer::Get()->SendToPlayersInRange(notify, *characterEntity, acMessage.pPlayer)) + { + spdlog::error("{}: SendToPlayersInRange failed for drop physics disabled {}", __FUNCTION__, resolvedDropId); + GameServer::Get()->SendToPlayers(notify, acMessage.pPlayer); + } + } + else + { + GameServer::Get()->SendToPlayers(notify, acMessage.pPlayer); + } +} + +void DropService::OnUpdate(const UpdateEvent& acEvent) noexcept +{ + m_cleanupAccumulator += acEvent.Delta; + if (m_cleanupAccumulator < kCleanupIntervalSeconds) + return; + + m_cleanupAccumulator = 0.0; + CleanupExpiredDrops(); + CleanupExpiredCreationEnginePickups(); +} + +bool DropService::InitializeDatabase() noexcept +{ + m_databasePath = ResolveItemsDatabasePath(); + const auto dataDirectory = m_databasePath.parent_path(); + + std::error_code ec; + if (!dataDirectory.empty() && !std::filesystem::exists(dataDirectory, ec)) + { + std::filesystem::create_directories(dataDirectory, ec); + if (ec) + { + spdlog::error("DropService: failed to create data directory '{}': {}", dataDirectory.string(), ec.message()); + return false; + } + } + + spdlog::info("DropService: using drop database at '{}'", m_databasePath.string()); + if (sqlite3_open(m_databasePath.string().c_str(), &m_pDatabase) != SQLITE_OK) + { + spdlog::error("DropService: unable to open drop database at '{}': {}", m_databasePath.string(), sqlite3_errmsg(m_pDatabase)); + return false; + } + + if (sqlite3_exec(m_pDatabase, "PRAGMA foreign_keys = ON;", nullptr, nullptr, nullptr) != SQLITE_OK) + { + spdlog::error("DropService: failed to enable foreign keys: {}", sqlite3_errmsg(m_pDatabase)); + return false; + } + + char* pError = nullptr; + const int execResult = sqlite3_exec(m_pDatabase, kCreateDropsTableSql, nullptr, nullptr, &pError); + if (execResult != SQLITE_OK) + { + spdlog::error("DropService: failed to initialize drop schema: {}", pError ? pError : "unknown"); + sqlite3_free(pError); + return false; + } + + if (!EnsureColumnExists(m_pDatabase, "ALTER TABLE server_drops ADD COLUMN item_type INTEGER NOT NULL DEFAULT 0;")) + return false; + if (!EnsureColumnExists(m_pDatabase, "ALTER TABLE server_drops ADD COLUMN reference_mod_id INTEGER NOT NULL DEFAULT 0;")) + return false; + if (!EnsureColumnExists(m_pDatabase, "ALTER TABLE server_drops ADD COLUMN reference_base_id INTEGER NOT NULL DEFAULT 0;")) + return false; + + return true; +} + +void DropService::ShutdownDatabase() noexcept +{ + if (m_pDatabase) + { + sqlite3_close(m_pDatabase); + m_pDatabase = nullptr; + } +} + +bool DropService::InitializeCreationEngineDatabase() noexcept +{ + m_creationEngineDatabasePath = ResolveCreationEngineDatabasePath(); + const auto dataDirectory = m_creationEngineDatabasePath.parent_path(); + + std::error_code ec; + if (!dataDirectory.empty() && !std::filesystem::exists(dataDirectory, ec)) + { + std::filesystem::create_directories(dataDirectory, ec); + if (ec) + { + spdlog::error("DropService: failed to create data directory '{}': {}", dataDirectory.string(), ec.message()); + return false; + } + } + + spdlog::info("DropService: using creation engine pickup database at '{}'", m_creationEngineDatabasePath.string()); + if (sqlite3_open(m_creationEngineDatabasePath.string().c_str(), &m_pCreationEngineDatabase) != SQLITE_OK) + { + spdlog::error("DropService: unable to open creation engine database at '{}': {}", m_creationEngineDatabasePath.string(), sqlite3_errmsg(m_pCreationEngineDatabase)); + return false; + } + + char* pError = nullptr; + const int execResult = sqlite3_exec(m_pCreationEngineDatabase, kCreateCreationEngineTableSql, nullptr, nullptr, &pError); + if (execResult != SQLITE_OK) + { + spdlog::error("DropService: failed to initialize creation engine schema: {}", pError ? pError : "unknown"); + sqlite3_free(pError); + return false; + } + + return true; +} + +void DropService::ShutdownCreationEngineDatabase() noexcept +{ + if (m_pCreationEngineDatabase) + { + sqlite3_close(m_pCreationEngineDatabase); + m_pCreationEngineDatabase = nullptr; + } +} + +bool DropService::IsCreationEnginePickupRecorded(const GameId& acEngineRefId) noexcept +{ + if (!m_pCreationEngineDatabase || !acEngineRefId) + return false; + + constexpr const char* cSelectSql = "SELECT 1 FROM creation_engine_pickups WHERE engine_mod_id = ?1 AND engine_base_id = ?2 LIMIT 1;"; + StatementPtr statement = PrepareStatement(m_pCreationEngineDatabase, cSelectSql); + if (!statement) + return false; + + sqlite3_bind_int(statement.get(), 1, static_cast(acEngineRefId.ModId)); + sqlite3_bind_int(statement.get(), 2, static_cast(acEngineRefId.BaseId)); + + return sqlite3_step(statement.get()) == SQLITE_ROW; +} + +bool DropService::RecordCreationEnginePickup(const GameId& acEngineRefId, const GameId& acCellId, const GameId& acWorldId, uint32_t aPickedBy) noexcept +{ + if (!m_pCreationEngineDatabase || !acEngineRefId || !acCellId) + return false; + + constexpr const char* cInsertSql = + "INSERT OR IGNORE INTO creation_engine_pickups(engine_mod_id, engine_base_id, cell_mod_id, cell_base_id, world_mod_id, world_base_id, picked_by) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7);"; + StatementPtr statement = PrepareStatement(m_pCreationEngineDatabase, cInsertSql); + if (!statement) + return false; + + sqlite3_bind_int(statement.get(), 1, static_cast(acEngineRefId.ModId)); + sqlite3_bind_int(statement.get(), 2, static_cast(acEngineRefId.BaseId)); + sqlite3_bind_int(statement.get(), 3, static_cast(acCellId.ModId)); + sqlite3_bind_int(statement.get(), 4, static_cast(acCellId.BaseId)); + sqlite3_bind_int(statement.get(), 5, static_cast(acWorldId.ModId)); + sqlite3_bind_int(statement.get(), 6, static_cast(acWorldId.BaseId)); + sqlite3_bind_int(statement.get(), 7, static_cast(aPickedBy)); + + if (sqlite3_step(statement.get()) != SQLITE_DONE) + { + spdlog::error("DropService: failed to record creation engine pickup: {}", sqlite3_errmsg(m_pCreationEngineDatabase)); + return false; + } + + return sqlite3_changes(m_pCreationEngineDatabase) > 0; +} + +void DropService::CleanupExpiredCreationEnginePickups() noexcept +{ + if (!m_pCreationEngineDatabase) + return; + + constexpr const char* cDeleteSql = "DELETE FROM creation_engine_pickups WHERE picked_at < strftime('%s','now') - ?1;"; + StatementPtr statement = PrepareStatement(m_pCreationEngineDatabase, cDeleteSql); + if (!statement) + { + spdlog::error("DropService: failed to prepare creation engine cleanup query: {}", sqlite3_errmsg(m_pCreationEngineDatabase)); + return; + } + + sqlite3_bind_int(statement.get(), 1, static_cast(kCreationEnginePickupExpirySeconds)); + + if (sqlite3_step(statement.get()) != SQLITE_DONE) + { + spdlog::error("DropService: failed to cleanup creation engine pickups: {}", sqlite3_errmsg(m_pCreationEngineDatabase)); + return; + } + + const int changes = sqlite3_changes(m_pCreationEngineDatabase); + if (changes > 0) + spdlog::info("DropService: cleaned up {} expired creation engine pickups", changes); +} + +void DropService::LoadPersistedDrops() noexcept +{ + if (!m_pDatabase) + return; + + m_activeDrops.clear(); + m_cellDropIndex.clear(); + m_referenceDropIndex.clear(); + + constexpr const char* cSelectSql = + "SELECT server_drop_id, server_id, actor_form_id, origin_player_id, client_drop_id, item_type, item_mod_id, item_base_id, count, cell_mod_id, cell_base_id, world_mod_id, world_base_id, reference_mod_id, " + "reference_base_id, has_location, pos_x, pos_y, pos_z, has_rotation, rot_x, rot_y, rot_z, version, item_blob " + "FROM server_drops WHERE is_active = 1;"; + + StatementPtr statement = PrepareStatement(m_pDatabase, cSelectSql); + if (!statement) + { + spdlog::error("DropService: failed to prepare drop select statement: {}", sqlite3_errmsg(m_pDatabase)); + return; + } + + uint32_t restoredCount = 0; + while (sqlite3_step(statement.get()) == SQLITE_ROW) + { + ActiveDrop drop{}; + drop.DropId = static_cast(sqlite3_column_int64(statement.get(), 0)); + drop.ServerId = static_cast(sqlite3_column_int64(statement.get(), 1)); + drop.ActorFormId = static_cast(sqlite3_column_int64(statement.get(), 2)); + drop.OriginPlayerId = static_cast(sqlite3_column_int64(statement.get(), 3)); + drop.ClientDropId = GuidFromColumn(statement.get(), 4); + const auto typeValue = static_cast(sqlite3_column_int64(statement.get(), 5)); + drop.Type = typeValue == static_cast(ServerItemType::CreationEngine) ? ServerItemType::CreationEngine : ServerItemType::Dropped; + drop.CellId.ModId = static_cast(sqlite3_column_int64(statement.get(), 9)); + drop.CellId.BaseId = static_cast(sqlite3_column_int64(statement.get(), 10)); + drop.WorldSpaceId.ModId = static_cast(sqlite3_column_int64(statement.get(), 11)); + drop.WorldSpaceId.BaseId = static_cast(sqlite3_column_int64(statement.get(), 12)); + drop.ReferenceId.ModId = static_cast(sqlite3_column_int64(statement.get(), 13)); + drop.ReferenceId.BaseId = static_cast(sqlite3_column_int64(statement.get(), 14)); + + drop.HasLocation = sqlite3_column_int(statement.get(), 15) != 0; + if (drop.HasLocation) + { + drop.Location.x = static_cast(sqlite3_column_double(statement.get(), 16)); + drop.Location.y = static_cast(sqlite3_column_double(statement.get(), 17)); + drop.Location.z = static_cast(sqlite3_column_double(statement.get(), 18)); + } + + drop.HasRotation = sqlite3_column_int(statement.get(), 19) != 0; + if (drop.HasRotation) + { + drop.Rotation.x = static_cast(sqlite3_column_double(statement.get(), 20)); + drop.Rotation.y = static_cast(sqlite3_column_double(statement.get(), 21)); + drop.Rotation.z = static_cast(sqlite3_column_double(statement.get(), 22)); + } + + drop.Version = static_cast(sqlite3_column_int64(statement.get(), 23)); + + const void* pBlob = sqlite3_column_blob(statement.get(), 24); + const int blobSize = sqlite3_column_bytes(statement.get(), 24); + drop.DropEntry = DeserializeEntryBlob(pBlob, blobSize); + drop.PickupEntry = drop.DropEntry; + if (drop.PickupEntry.Count < 0) + drop.PickupEntry.Count = -drop.PickupEntry.Count; + + TrackActiveDrop(drop); + ++restoredCount; + } + + spdlog::info("DropService: restored {} persisted drops", restoredCount); +} + + +NotifyDroppedItems::Entry DropService::MakeNotifyEntry(const ActiveDrop& acDrop) const noexcept +{ + NotifyDroppedItems::Entry entry{}; + entry.DropId = acDrop.DropId; + entry.ServerId = acDrop.ServerId; + entry.ActorFormId = acDrop.ActorFormId; + entry.Type = acDrop.Type; + entry.Item = acDrop.DropEntry; + entry.HasLocation = acDrop.HasLocation; + if (entry.HasLocation) + entry.Location = acDrop.Location; + entry.HasRotation = acDrop.HasRotation; + if (entry.HasRotation) + entry.Rotation = acDrop.Rotation; + entry.CellId = acDrop.CellId; + entry.WorldSpaceId = acDrop.WorldSpaceId; + entry.ReferenceId = acDrop.ReferenceId; + entry.SpawnEpoch = acDrop.SpawnEpoch; + return entry; +} + +bool DropService::BeginTransaction() noexcept +{ + if (!m_pDatabase) + return false; + + if (sqlite3_exec(m_pDatabase, "BEGIN IMMEDIATE TRANSACTION;", nullptr, nullptr, nullptr) != SQLITE_OK) + { + spdlog::error("DropService: failed to begin transaction: {}", sqlite3_errmsg(m_pDatabase)); + return false; + } + + return true; +} + +bool DropService::CommitTransaction() noexcept +{ + if (!m_pDatabase) + return false; + + if (sqlite3_exec(m_pDatabase, "COMMIT;", nullptr, nullptr, nullptr) != SQLITE_OK) + { + spdlog::error("DropService: commit failed: {}", sqlite3_errmsg(m_pDatabase)); + return false; + } + + return true; +} + +void DropService::RollbackTransaction() noexcept +{ + if (m_pDatabase) + sqlite3_exec(m_pDatabase, "ROLLBACK;", nullptr, nullptr, nullptr); +} + +std::optional DropService::FindExistingDropId(uint32_t aPlayerId, const Guid& acClientDropId) noexcept +{ + if (!m_pDatabase || acClientDropId.IsEmpty()) + return std::nullopt; + + constexpr const char* cSelectSql = "SELECT server_drop_id FROM server_drops WHERE origin_player_id = ?1 AND client_drop_id = ?2 AND is_active = 1 LIMIT 1;"; + StatementPtr statement = PrepareStatement(m_pDatabase, cSelectSql); + if (!statement) + { + spdlog::error("DropService: failed to prepare client drop lookup: {}", sqlite3_errmsg(m_pDatabase)); + return std::nullopt; + } + + sqlite3_bind_int(statement.get(), 1, static_cast(aPlayerId)); + BindGuid(statement.get(), 2, acClientDropId); + + if (sqlite3_step(statement.get()) == SQLITE_ROW) + return static_cast(sqlite3_column_int64(statement.get(), 0)); + + return std::nullopt; +} + +std::optional DropService::FetchDropFromDatabase(uint64_t aDropId) noexcept +{ + if (!m_pDatabase) + return std::nullopt; + + constexpr const char* cSelectSql = + "SELECT server_drop_id, server_id, actor_form_id, origin_player_id, client_drop_id, item_type, item_mod_id, item_base_id, count, cell_mod_id, cell_base_id, world_mod_id, world_base_id, reference_mod_id, " + "reference_base_id, has_location, pos_x, pos_y, pos_z, has_rotation, rot_x, rot_y, rot_z, version, item_blob " + "FROM server_drops WHERE server_drop_id = ?1 AND is_active = 1;"; + + StatementPtr statement = PrepareStatement(m_pDatabase, cSelectSql); + if (!statement) + { + spdlog::error("DropService: failed to prepare drop fetch: {}", sqlite3_errmsg(m_pDatabase)); + return std::nullopt; + } + + sqlite3_bind_int64(statement.get(), 1, static_cast(aDropId)); + + if (sqlite3_step(statement.get()) != SQLITE_ROW) + return std::nullopt; + + ActiveDrop drop{}; + drop.DropId = static_cast(sqlite3_column_int64(statement.get(), 0)); + drop.ServerId = static_cast(sqlite3_column_int64(statement.get(), 1)); + drop.ActorFormId = static_cast(sqlite3_column_int64(statement.get(), 2)); + drop.OriginPlayerId = static_cast(sqlite3_column_int64(statement.get(), 3)); + drop.ClientDropId = GuidFromColumn(statement.get(), 4); + const auto typeValue = static_cast(sqlite3_column_int64(statement.get(), 5)); + drop.Type = typeValue == static_cast(ServerItemType::CreationEngine) ? ServerItemType::CreationEngine : ServerItemType::Dropped; + drop.CellId.ModId = static_cast(sqlite3_column_int64(statement.get(), 9)); + drop.CellId.BaseId = static_cast(sqlite3_column_int64(statement.get(), 10)); + drop.WorldSpaceId.ModId = static_cast(sqlite3_column_int64(statement.get(), 11)); + drop.WorldSpaceId.BaseId = static_cast(sqlite3_column_int64(statement.get(), 12)); + drop.ReferenceId.ModId = static_cast(sqlite3_column_int64(statement.get(), 13)); + drop.ReferenceId.BaseId = static_cast(sqlite3_column_int64(statement.get(), 14)); + drop.HasLocation = sqlite3_column_int(statement.get(), 15) != 0; + if (drop.HasLocation) + { + drop.Location.x = static_cast(sqlite3_column_double(statement.get(), 16)); + drop.Location.y = static_cast(sqlite3_column_double(statement.get(), 17)); + drop.Location.z = static_cast(sqlite3_column_double(statement.get(), 18)); + } + drop.HasRotation = sqlite3_column_int(statement.get(), 19) != 0; + if (drop.HasRotation) + { + drop.Rotation.x = static_cast(sqlite3_column_double(statement.get(), 20)); + drop.Rotation.y = static_cast(sqlite3_column_double(statement.get(), 21)); + drop.Rotation.z = static_cast(sqlite3_column_double(statement.get(), 22)); + } + drop.Version = static_cast(sqlite3_column_int64(statement.get(), 23)); + drop.SpawnEpoch = drop.Version; + const void* pBlob = sqlite3_column_blob(statement.get(), 24); + const int blobSize = sqlite3_column_bytes(statement.get(), 24); + drop.DropEntry = DeserializeEntryBlob(pBlob, blobSize); + drop.PickupEntry = drop.DropEntry; + if (drop.PickupEntry.Count < 0) + drop.PickupEntry.Count = -drop.PickupEntry.Count; + + return drop; +} + +bool DropService::InsertDrop(const ActiveDrop& acDrop, uint64_t& aOutDropId) noexcept +{ + if (!m_pDatabase) + return false; + + constexpr const char* cInsertSql = + "INSERT INTO server_drops(server_id, actor_form_id, origin_player_id, client_drop_id, item_type, item_mod_id, item_base_id, count, cell_mod_id, cell_base_id, world_mod_id, world_base_id, reference_mod_id, " + "reference_base_id, has_location, pos_x, pos_y, pos_z, has_rotation, rot_x, rot_y, rot_z, version, item_blob) " + "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24);"; + + StatementPtr statement = PrepareStatement(m_pDatabase, cInsertSql); + if (!statement) + { + spdlog::error("DropService: failed to prepare drop insert: {}", sqlite3_errmsg(m_pDatabase)); + return false; + } + + sqlite3_bind_int(statement.get(), 1, static_cast(acDrop.ServerId)); + sqlite3_bind_int(statement.get(), 2, static_cast(acDrop.ActorFormId)); + sqlite3_bind_int(statement.get(), 3, static_cast(acDrop.OriginPlayerId)); + BindGuid(statement.get(), 4, acDrop.ClientDropId); + sqlite3_bind_int(statement.get(), 5, static_cast(acDrop.Type)); + sqlite3_bind_int(statement.get(), 6, static_cast(acDrop.PickupEntry.BaseId.ModId)); + sqlite3_bind_int(statement.get(), 7, static_cast(acDrop.PickupEntry.BaseId.BaseId)); + sqlite3_bind_int(statement.get(), 8, static_cast(acDrop.PickupEntry.Count)); + sqlite3_bind_int(statement.get(), 9, static_cast(acDrop.CellId.ModId)); + sqlite3_bind_int(statement.get(), 10, static_cast(acDrop.CellId.BaseId)); + sqlite3_bind_int(statement.get(), 11, static_cast(acDrop.WorldSpaceId.ModId)); + sqlite3_bind_int(statement.get(), 12, static_cast(acDrop.WorldSpaceId.BaseId)); + sqlite3_bind_int(statement.get(), 13, static_cast(acDrop.ReferenceId.ModId)); + sqlite3_bind_int(statement.get(), 14, static_cast(acDrop.ReferenceId.BaseId)); + sqlite3_bind_int(statement.get(), 15, acDrop.HasLocation ? 1 : 0); + if (acDrop.HasLocation) + { + sqlite3_bind_double(statement.get(), 16, acDrop.Location.x); + sqlite3_bind_double(statement.get(), 17, acDrop.Location.y); + sqlite3_bind_double(statement.get(), 18, acDrop.Location.z); + } + else + { + sqlite3_bind_null(statement.get(), 16); + sqlite3_bind_null(statement.get(), 17); + sqlite3_bind_null(statement.get(), 18); + } + sqlite3_bind_int(statement.get(), 19, acDrop.HasRotation ? 1 : 0); + if (acDrop.HasRotation) + { + sqlite3_bind_double(statement.get(), 20, acDrop.Rotation.x); + sqlite3_bind_double(statement.get(), 21, acDrop.Rotation.y); + sqlite3_bind_double(statement.get(), 22, acDrop.Rotation.z); + } + else + { + sqlite3_bind_null(statement.get(), 20); + sqlite3_bind_null(statement.get(), 21); + sqlite3_bind_null(statement.get(), 22); + } + sqlite3_bind_int(statement.get(), 23, static_cast(acDrop.Version)); + const std::string dropBlob = SerializeEntryBlob(acDrop.DropEntry); + sqlite3_bind_blob(statement.get(), 24, dropBlob.data(), static_cast(dropBlob.size()), SQLITE_TRANSIENT); + + if (sqlite3_step(statement.get()) != SQLITE_DONE) + { + spdlog::error("DropService: failed to insert drop: {}", sqlite3_errmsg(m_pDatabase)); + return false; + } + + aOutDropId = static_cast(sqlite3_last_insert_rowid(m_pDatabase)); + return true; +} + +bool DropService::InsertDropHistory(uint64_t aDropId, std::string_view aAction, uint32_t aPerformedBy, const std::string& acDetails) noexcept +{ + if (!m_pDatabase) + return false; + + constexpr const char* cInsertSql = "INSERT INTO server_drop_history(server_drop_id, action, performed_by, details) VALUES (?1, ?2, ?3, ?4);"; + StatementPtr statement = PrepareStatement(m_pDatabase, cInsertSql); + if (!statement) + { + spdlog::error("DropService: failed to prepare drop history insert: {}", sqlite3_errmsg(m_pDatabase)); + return false; + } + + sqlite3_bind_int64(statement.get(), 1, static_cast(aDropId)); + sqlite3_bind_text(statement.get(), 2, aAction.data(), static_cast(aAction.size()), SQLITE_TRANSIENT); + sqlite3_bind_int(statement.get(), 3, static_cast(aPerformedBy)); + sqlite3_bind_text(statement.get(), 4, acDetails.c_str(), -1, SQLITE_TRANSIENT); + + if (sqlite3_step(statement.get()) != SQLITE_DONE) + { + spdlog::error("DropService: failed to persist drop history: {}", sqlite3_errmsg(m_pDatabase)); + return false; + } + + return true; +} + +bool DropService::UpdateDropLocation(const ActiveDrop& acDrop) noexcept +{ + if (!m_pDatabase) + return false; + + constexpr const char* cUpdateSql = + "UPDATE server_drops SET cell_mod_id = ?2, cell_base_id = ?3, world_mod_id = ?4, world_base_id = ?5, reference_mod_id = ?6, reference_base_id = ?7, has_location = ?8, pos_x = ?9, pos_y = ?10, pos_z = ?11, " + "has_rotation = ?12, rot_x = ?13, rot_y = ?14, rot_z = ?15, updated_at = strftime('%s','now') WHERE server_drop_id = ?1 AND is_active = 1;"; + + StatementPtr statement = PrepareStatement(m_pDatabase, cUpdateSql); + if (!statement) + { + spdlog::error("DropService: failed to prepare drop location update: {}", sqlite3_errmsg(m_pDatabase)); + return false; + } + + sqlite3_bind_int64(statement.get(), 1, static_cast(acDrop.DropId)); + sqlite3_bind_int(statement.get(), 2, static_cast(acDrop.CellId.ModId)); + sqlite3_bind_int(statement.get(), 3, static_cast(acDrop.CellId.BaseId)); + sqlite3_bind_int(statement.get(), 4, static_cast(acDrop.WorldSpaceId.ModId)); + sqlite3_bind_int(statement.get(), 5, static_cast(acDrop.WorldSpaceId.BaseId)); + sqlite3_bind_int(statement.get(), 6, static_cast(acDrop.ReferenceId.ModId)); + sqlite3_bind_int(statement.get(), 7, static_cast(acDrop.ReferenceId.BaseId)); + sqlite3_bind_int(statement.get(), 8, acDrop.HasLocation ? 1 : 0); + if (acDrop.HasLocation) + { + sqlite3_bind_double(statement.get(), 9, acDrop.Location.x); + sqlite3_bind_double(statement.get(), 10, acDrop.Location.y); + sqlite3_bind_double(statement.get(), 11, acDrop.Location.z); + } + else + { + sqlite3_bind_null(statement.get(), 9); + sqlite3_bind_null(statement.get(), 10); + sqlite3_bind_null(statement.get(), 11); + } + sqlite3_bind_int(statement.get(), 12, acDrop.HasRotation ? 1 : 0); + if (acDrop.HasRotation) + { + sqlite3_bind_double(statement.get(), 13, acDrop.Rotation.x); + sqlite3_bind_double(statement.get(), 14, acDrop.Rotation.y); + sqlite3_bind_double(statement.get(), 15, acDrop.Rotation.z); + } + else + { + sqlite3_bind_null(statement.get(), 13); + sqlite3_bind_null(statement.get(), 14); + sqlite3_bind_null(statement.get(), 15); + } + + if (sqlite3_step(statement.get()) != SQLITE_DONE) + { + spdlog::error("DropService: failed to update drop location {}: {}", acDrop.DropId, sqlite3_errmsg(m_pDatabase)); + return false; + } + + return sqlite3_changes(m_pDatabase) > 0; +} + +bool DropService::MarkDropInactive(uint64_t aDropId) noexcept +{ + if (!m_pDatabase) + return false; + + constexpr const char* cUpdateSql = "UPDATE server_drops SET is_active = 0, version = version + 1, updated_at = strftime('%s','now') WHERE server_drop_id = ?1 AND is_active = 1;"; + StatementPtr statement = PrepareStatement(m_pDatabase, cUpdateSql); + if (!statement) + { + spdlog::error("DropService: failed to prepare drop deactivate: {}", sqlite3_errmsg(m_pDatabase)); + return false; + } + + sqlite3_bind_int64(statement.get(), 1, static_cast(aDropId)); + if (sqlite3_step(statement.get()) != SQLITE_DONE) + { + spdlog::error("DropService: failed to mark drop {} inactive: {}", aDropId, sqlite3_errmsg(m_pDatabase)); + return false; + } + + return sqlite3_changes(m_pDatabase) > 0; +} + +#if 0 +bool DropService::UpdateInventoryForDelta(uint32_t aPlayerId, const Inventory::Entry& acEntry, int32_t aDelta, InventoryComponent& aInventoryComponent) noexcept +{ + if (!m_pDatabase) + return false; + + Inventory::Entry signature = NormalizeEntrySignature(acEntry); + std::string stackId; + int32_t stackCount = 0; + const bool createIfMissing = aDelta > 0; + if (!EnsureStackForEntry(aPlayerId, acEntry, signature, aInventoryComponent, stackId, stackCount, createIfMissing)) + return false; + + constexpr const char* cUpdateSql = + "UPDATE player_inventory SET count = count + ?1, updated_at = strftime('%s','now') WHERE player_id = ?2 AND stack_id = ?3 AND (count + ?1) >= 0;"; + + StatementPtr statement = PrepareStatement(m_pDatabase, cUpdateSql); + if (!statement) + { + spdlog::error("DropService: failed to prepare inventory update: {}", sqlite3_errmsg(m_pDatabase)); + return false; + } + + sqlite3_bind_int(statement.get(), 1, aDelta); + sqlite3_bind_int(statement.get(), 2, static_cast(aPlayerId)); + sqlite3_bind_text(statement.get(), 3, stackId.c_str(), -1, SQLITE_TRANSIENT); + + if (sqlite3_step(statement.get()) != SQLITE_DONE) + { + spdlog::error("DropService: inventory update failed: {}", sqlite3_errmsg(m_pDatabase)); + return false; + } + + if (sqlite3_changes(m_pDatabase) == 0) + { + spdlog::warn("DropService: inventory update affected no rows for player {}", aPlayerId); + return false; + } + + return true; +} + +bool DropService::EnsureStackForEntry(uint32_t aPlayerId, const Inventory::Entry& acEntry, const Inventory::Entry& acSignature, InventoryComponent& aInventoryComponent, std::string& aOutStackId, + int32_t& aOutCount, bool aCreateIfMissing) noexcept +{ + if (!m_pDatabase) + return false; + + constexpr const char* cSelectSql = "SELECT stack_id, count, entry_blob FROM player_inventory WHERE player_id = ?1 AND item_mod_id = ?2 AND item_base_id = ?3;"; + StatementPtr statement = PrepareStatement(m_pDatabase, cSelectSql); + if (!statement) + { + spdlog::error("DropService: failed to prepare inventory lookup: {}", sqlite3_errmsg(m_pDatabase)); + return false; + } + + sqlite3_bind_int(statement.get(), 1, static_cast(aPlayerId)); + sqlite3_bind_int(statement.get(), 2, static_cast(acEntry.BaseId.ModId)); + sqlite3_bind_int(statement.get(), 3, static_cast(acEntry.BaseId.BaseId)); + + while (sqlite3_step(statement.get()) == SQLITE_ROW) + { + const char* stack = reinterpret_cast(sqlite3_column_text(statement.get(), 0)); + const int32_t count = sqlite3_column_int(statement.get(), 1); + const void* pBlob = sqlite3_column_blob(statement.get(), 2); + const int blobSize = sqlite3_column_bytes(statement.get(), 2); + Inventory::Entry storedEntry = DeserializeEntryBlob(pBlob, blobSize); + storedEntry.Count = 1; + + if (!storedEntry.CanBeMerged(acSignature)) + continue; + + aOutStackId = stack ? stack : ""; + aOutCount = count; + return true; + } + + if (!aCreateIfMissing) + { + const auto& entries = aInventoryComponent.Content.Entries; + for (const auto& runtimeEntry : entries) + { + if (!runtimeEntry.CanBeMerged(acEntry)) + continue; + + int32_t runtimeCount = runtimeEntry.Count; + if (runtimeCount < 0) + runtimeCount = -runtimeCount; + if (runtimeCount <= 0) + continue; + + const Guid stackGuid = Guid::Random(); + aOutStackId = stackGuid.ToString(); + aOutCount = runtimeCount; + const std::string blob = SerializeEntryBlob(acSignature); + + constexpr const char* cInsertSql = + "INSERT INTO player_inventory(player_id, item_mod_id, item_base_id, stack_id, count, entry_blob) VALUES (?1, ?2, ?3, ?4, ?5, ?6);"; + StatementPtr insertStatement = PrepareStatement(m_pDatabase, cInsertSql); + if (!insertStatement) + { + spdlog::error("DropService: failed to prepare inventory snapshot insert: {}", sqlite3_errmsg(m_pDatabase)); + return false; + } + + sqlite3_bind_int(insertStatement.get(), 1, static_cast(aPlayerId)); + sqlite3_bind_int(insertStatement.get(), 2, static_cast(acEntry.BaseId.ModId)); + sqlite3_bind_int(insertStatement.get(), 3, static_cast(acEntry.BaseId.BaseId)); + sqlite3_bind_text(insertStatement.get(), 4, aOutStackId.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_int(insertStatement.get(), 5, runtimeCount); + sqlite3_bind_blob(insertStatement.get(), 6, blob.data(), static_cast(blob.size()), SQLITE_TRANSIENT); + + if (sqlite3_step(insertStatement.get()) != SQLITE_DONE) + { + spdlog::error("DropService: failed to insert inventory snapshot: {}", sqlite3_errmsg(m_pDatabase)); + return false; + } + + return true; + } + + int32_t synthesizedCount = acEntry.Count; + if (synthesizedCount < 0) + synthesizedCount = -synthesizedCount; + if (synthesizedCount == 0) + synthesizedCount = 1; + + const Guid stackGuid = Guid::Random(); + aOutStackId = stackGuid.ToString(); + aOutCount = synthesizedCount; + const std::string blob = SerializeEntryBlob(acSignature); + + constexpr const char* cInsertSql = + "INSERT INTO player_inventory(player_id, item_mod_id, item_base_id, stack_id, count, entry_blob) VALUES (?1, ?2, ?3, ?4, ?5, ?6);"; + StatementPtr insertStatement = PrepareStatement(m_pDatabase, cInsertSql); + if (!insertStatement) + { + spdlog::error("DropService: failed to prepare synthesized stack insert: {}", sqlite3_errmsg(m_pDatabase)); + return false; + } + + sqlite3_bind_int(insertStatement.get(), 1, static_cast(aPlayerId)); + sqlite3_bind_int(insertStatement.get(), 2, static_cast(acEntry.BaseId.ModId)); + sqlite3_bind_int(insertStatement.get(), 3, static_cast(acEntry.BaseId.BaseId)); + sqlite3_bind_text(insertStatement.get(), 4, aOutStackId.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_int(insertStatement.get(), 5, synthesizedCount); + sqlite3_bind_blob(insertStatement.get(), 6, blob.data(), static_cast(blob.size()), SQLITE_TRANSIENT); + + if (sqlite3_step(insertStatement.get()) != SQLITE_DONE) + { + spdlog::error("DropService: failed to insert synthesized stack: {}", sqlite3_errmsg(m_pDatabase)); + return false; + } + + spdlog::warn("DropService: synthesized stack for player {} item {:X}:{:X} with count {}", aPlayerId, acEntry.BaseId.ModId, acEntry.BaseId.BaseId, synthesizedCount); + return true; + } + + const Guid stackGuid = Guid::Random(); + aOutStackId = stackGuid.ToString(); + aOutCount = 0; + const std::string blob = SerializeEntryBlob(acSignature); + constexpr const char* cInsertSql = + "INSERT INTO player_inventory(player_id, item_mod_id, item_base_id, stack_id, count, entry_blob) VALUES (?1, ?2, ?3, ?4, 0, ?5);"; + StatementPtr insertStatement = PrepareStatement(m_pDatabase, cInsertSql); + if (!insertStatement) + { + spdlog::error("DropService: failed to prepare inventory stack insert: {}", sqlite3_errmsg(m_pDatabase)); + return false; + } + + sqlite3_bind_int(insertStatement.get(), 1, static_cast(aPlayerId)); + sqlite3_bind_int(insertStatement.get(), 2, static_cast(acEntry.BaseId.ModId)); + sqlite3_bind_int(insertStatement.get(), 3, static_cast(acEntry.BaseId.BaseId)); + sqlite3_bind_text(insertStatement.get(), 4, aOutStackId.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_blob(insertStatement.get(), 5, blob.data(), static_cast(blob.size()), SQLITE_TRANSIENT); + + if (sqlite3_step(insertStatement.get()) != SQLITE_DONE) + { + spdlog::error("DropService: failed to insert inventory stack: {}", sqlite3_errmsg(m_pDatabase)); + return false; + } + + return true; +} +#endif + +DropService::ActiveDrop* DropService::ResolveActiveDrop(uint64_t aDropId) noexcept +{ + const auto it = m_activeDrops.find(aDropId); + if (it != m_activeDrops.end()) + return &it.value(); + + if (auto dropOpt = FetchDropFromDatabase(aDropId)) + { + TrackActiveDrop(*dropOpt); + const auto inserted = m_activeDrops.find(aDropId); + if (inserted != m_activeDrops.end()) + return &inserted.value(); + } + + return nullptr; +} + +void DropService::TrackActiveDrop(const ActiveDrop& acDrop) noexcept +{ + if (const auto it = m_activeDrops.find(acDrop.DropId); it != m_activeDrops.end()) + { + if (it->second.ReferenceId && it->second.ReferenceId != acDrop.ReferenceId) + m_referenceDropIndex.erase(it->second.ReferenceId); + } + + m_activeDrops[acDrop.DropId] = acDrop; + IndexDrop(acDrop.DropId, acDrop.CellId); + if (acDrop.ReferenceId) + m_referenceDropIndex[acDrop.ReferenceId] = acDrop.DropId; +} + +void DropService::RemoveActiveDrop(uint64_t aDropId) noexcept +{ + const auto it = m_activeDrops.find(aDropId); + if (it == m_activeDrops.end()) + return; + + EraseDropFromIndex(aDropId, it->second.CellId); + if (it->second.ReferenceId) + m_referenceDropIndex.erase(it->second.ReferenceId); + m_activeDrops.erase(it); +} + +void DropService::IndexDrop(uint64_t aDropId, const GameId& acCellId) noexcept +{ + m_cellDropIndex[acCellId].push_back(aDropId); +} + +void DropService::EraseDropFromIndex(uint64_t aDropId, const GameId& acCellId) noexcept +{ + auto indexIt = m_cellDropIndex.find(acCellId); + if (indexIt == m_cellDropIndex.end()) + return; + + TiltedPhoques::Vector filtered; + filtered.reserve(indexIt->second.size()); + for (const auto dropId : indexIt->second) + { + if (dropId != aDropId) + filtered.push_back(dropId); + } + + m_cellDropIndex.erase(indexIt); + if (!filtered.empty()) + m_cellDropIndex.emplace(acCellId, std::move(filtered)); +} + +void DropService::HandleUntrackedPickupRequest(const PacketEvent& acMessage) noexcept +{ + const auto& message = acMessage.Packet; + + const auto pickerEntity = m_world.TryResolveEntity(message.ServerId); + if (!pickerEntity) + { + spdlog::warn("DropService: untracked pickup requested for missing entity {:X}", message.ServerId); + return; + } + + auto view = m_world.view(); + const auto it = view.find(*pickerEntity); + if (it == view.end()) + { + spdlog::warn("DropService: untracked pickup requested for entity {:X} without OwnerComponent", message.ServerId); + return; + } + + auto& ownerComponent = view.get(*it); + if (ownerComponent.GetOwner() != acMessage.pPlayer) + { + spdlog::warn("DropService: untracked pickup denied for {:X}: player {:X} not owner", message.ServerId, acMessage.pPlayer->GetConnectionId()); + return; + } + + Player* pPicker = ownerComponent.GetOwner(); + if (!pPicker) + { + spdlog::warn("DropService: untracked pickup denied for {:X}: missing owner player", message.ServerId); + return; + } + + if (!message.Item.BaseId) + { + spdlog::warn("DropService: untracked pickup missing item data for actor {:X}", message.ServerId); + return; + } + + Inventory::Entry pickupEntry = message.Item; + if (pickupEntry.Count == 0) + pickupEntry.Count = 1; + else if (pickupEntry.Count < 0) + pickupEntry.Count = -pickupEntry.Count; + + const bool hasEngineRef = message.ReferenceId != GameId{}; + const bool hasCell = message.CellId != GameId{}; + const bool shouldTrackPickup = hasEngineRef && hasCell && m_pCreationEngineDatabase; + + if (shouldTrackPickup && IsCreationEnginePickupRecorded(message.ReferenceId)) + { + spdlog::info("DropService: rejecting duplicate creation engine pickup ref {:X}:{:X} in cell {:X}:{:X} for player {}", message.ReferenceId.ModId, message.ReferenceId.BaseId, message.CellId.ModId, message.CellId.BaseId, pPicker->GetId()); + + NotifyInventoryChanges correction{}; + correction.ServerId = message.ServerId; + correction.Item = pickupEntry; + correction.Item.Count = -pickupEntry.Count; + correction.Silent = true; + acMessage.pPlayer->Send(correction); + + NotifyDroppedItemPickedUp notify{}; + notify.ServerId = message.ServerId; + notify.Item = pickupEntry; + notify.DropId = 0; + notify.HasLocation = message.HasLocation; + if (notify.HasLocation) + notify.Location = message.Location; + notify.HasRotation = message.HasRotation; + if (notify.HasRotation) + notify.Rotation = message.Rotation; + notify.CellId = message.CellId; + notify.WorldSpaceId = message.WorldSpaceId; + notify.ReferenceId = message.ReferenceId; + + BroadcastPickup(notify); + return; + } + + if (shouldTrackPickup) + { + if (!RecordCreationEnginePickup(message.ReferenceId, message.CellId, message.WorldSpaceId, pPicker->GetId())) + { + if (IsCreationEnginePickupRecorded(message.ReferenceId)) + { + spdlog::info("DropService: rejecting creation engine pickup ref {:X}:{:X} (race/duplicate) for player {}", message.ReferenceId.ModId, message.ReferenceId.BaseId, pPicker->GetId()); + + NotifyInventoryChanges correction{}; + correction.ServerId = message.ServerId; + correction.Item = pickupEntry; + correction.Item.Count = -pickupEntry.Count; + correction.Silent = true; + acMessage.pPlayer->Send(correction); + + NotifyDroppedItemPickedUp notify{}; + notify.ServerId = message.ServerId; + notify.Item = pickupEntry; + notify.DropId = 0; + notify.HasLocation = message.HasLocation; + if (notify.HasLocation) + notify.Location = message.Location; + notify.HasRotation = message.HasRotation; + if (notify.HasRotation) + notify.Rotation = message.Rotation; + notify.CellId = message.CellId; + notify.WorldSpaceId = message.WorldSpaceId; + notify.ReferenceId = message.ReferenceId; + + BroadcastPickup(notify); + return; + } + + spdlog::warn("DropService: failed to record creation engine pickup ref {:X}:{:X} for player {}, continuing without tracking", message.ReferenceId.ModId, message.ReferenceId.BaseId, pPicker->GetId()); + } + } + + NotifyDroppedItemPickedUp notify{}; + notify.ServerId = message.ServerId; + notify.Item = pickupEntry; + notify.DropId = 0; + notify.HasLocation = message.HasLocation; + if (notify.HasLocation) + notify.Location = message.Location; + notify.HasRotation = message.HasRotation; + if (notify.HasRotation) + notify.Rotation = message.Rotation; + notify.CellId = message.CellId; + notify.WorldSpaceId = message.WorldSpaceId; + notify.ReferenceId = message.ReferenceId; + + BroadcastPickup(notify); + spdlog::debug("DropService: processed untracked pickup for actor {:X}", message.ServerId); +} + +void DropService::BroadcastPickup(const NotifyDroppedItemPickedUp& acMessage) const noexcept +{ + GameServer::Get()->SendToPlayers(acMessage, nullptr); +} + +void DropService::CleanupExpiredDrops() noexcept +{ + if (!m_pDatabase) + return; + + constexpr const char* cSelectSql = "SELECT server_drop_id FROM server_drops WHERE is_active = 1 AND created_at < strftime('%s','now') - ?1;"; + StatementPtr statement = PrepareStatement(m_pDatabase, cSelectSql); + if (!statement) + { + spdlog::error("DropService: failed to prepare cleanup query: {}", sqlite3_errmsg(m_pDatabase)); + return; + } + + sqlite3_bind_int(statement.get(), 1, static_cast(kDropExpirySeconds)); + + TiltedPhoques::Vector expiredDrops; + while (sqlite3_step(statement.get()) == SQLITE_ROW) + expiredDrops.push_back(static_cast(sqlite3_column_int64(statement.get(), 0))); + + if (expiredDrops.empty()) + return; + + for (auto dropId : expiredDrops) + { + MarkDropInactive(dropId); + RemoveActiveDrop(dropId); + } + + spdlog::info("DropService: cleaned up {} expired drops", expiredDrops.size()); +} diff --git a/Code/server/Services/DropService.h b/Code/server/Services/DropService.h new file mode 100644 index 000000000..292a4c0e7 --- /dev/null +++ b/Code/server/Services/DropService.h @@ -0,0 +1,109 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct NotifyActorDrop; +struct NotifyDroppedItemPickedUp; +struct sqlite3; + +struct World; +struct OwnerComponent; +struct Player; +struct UpdateEvent; + +class DropService +{ +public: + DropService(World& aWorld, entt::dispatcher& aDispatcher); + ~DropService(); + +private: + struct ActiveDrop + { + uint64_t DropId{}; + uint32_t ServerId{}; + uint32_t ActorFormId{}; + uint32_t OriginPlayerId{}; + ServerItemType Type{ServerItemType::Dropped}; + Inventory::Entry DropEntry{}; + Inventory::Entry PickupEntry{}; + bool HasLocation{false}; + Vector3_NetQuantize Location{}; + bool HasRotation{false}; + Vector3_NetQuantize Rotation{}; + bool HasVelocity{false}; + Vector3_NetQuantize Velocity{}; + bool HasAngularVelocity{false}; + Vector3_NetQuantize AngularVelocity{}; + GameId CellId{}; + GameId WorldSpaceId{}; + GameId ReferenceId{}; + Guid ClientDropId{}; + uint64_t SpawnEpoch{1}; + uint64_t Version{1}; + }; + + void OnDropRequest(const PacketEvent& acMessage) noexcept; + void OnPickupRequest(const PacketEvent& acMessage) noexcept; + void OnDroppedItemsRequest(const PacketEvent& acMessage) noexcept; + void OnDropMoveRequest(const PacketEvent& acMessage) noexcept; + void OnDropPhysicsDisabledRequest(const PacketEvent& acMessage) noexcept; + void OnUpdate(const UpdateEvent& acEvent) noexcept; + bool InitializeDatabase() noexcept; + void ShutdownDatabase() noexcept; + void LoadPersistedDrops() noexcept; + bool BeginTransaction() noexcept; + bool CommitTransaction() noexcept; + void RollbackTransaction() noexcept; + std::optional FindExistingDropId(uint32_t aPlayerId, const Guid& acClientDropId) noexcept; + std::optional FetchDropFromDatabase(uint64_t aDropId) noexcept; + bool InsertDrop(const ActiveDrop& acDrop, uint64_t& aOutDropId) noexcept; + bool InsertDropHistory(uint64_t aDropId, std::string_view aAction, uint32_t aPerformedBy, const std::string& acDetails) noexcept; + bool UpdateDropLocation(const ActiveDrop& acDrop) noexcept; + bool MarkDropInactive(uint64_t aDropId) noexcept; + ActiveDrop* ResolveActiveDrop(uint64_t aDropId) noexcept; + void TrackActiveDrop(const ActiveDrop& acDrop) noexcept; + void RemoveActiveDrop(uint64_t aDropId) noexcept; + void IndexDrop(uint64_t aDropId, const GameId& acCellId) noexcept; + void EraseDropFromIndex(uint64_t aDropId, const GameId& acCellId) noexcept; + NotifyDroppedItems::Entry MakeNotifyEntry(const ActiveDrop& acDrop) const noexcept; + void HandleUntrackedPickupRequest(const PacketEvent& acMessage) noexcept; + void BroadcastPickup(const NotifyDroppedItemPickedUp& acMessage) const noexcept; + void CleanupExpiredDrops() noexcept; + bool InitializeCreationEngineDatabase() noexcept; + void ShutdownCreationEngineDatabase() noexcept; + bool IsCreationEnginePickupRecorded(const GameId& acEngineRefId) noexcept; + bool RecordCreationEnginePickup(const GameId& acEngineRefId, const GameId& acCellId, const GameId& acWorldId, uint32_t aPickedBy) noexcept; + void CleanupExpiredCreationEnginePickups() noexcept; + + World& m_world; + entt::scoped_connection m_requestDropConnection; + entt::scoped_connection m_requestPickupConnection; + entt::scoped_connection m_requestDroppedItemsConnection; + entt::scoped_connection m_requestDropMoveConnection; + entt::scoped_connection m_requestDropPhysicsDisabledConnection; + entt::scoped_connection m_updateConnection; + + TiltedPhoques::Map m_activeDrops; + TiltedPhoques::Map> m_cellDropIndex; + TiltedPhoques::Map m_referenceDropIndex; + sqlite3* m_pDatabase{nullptr}; + std::filesystem::path m_databasePath; + sqlite3* m_pCreationEngineDatabase{nullptr}; + std::filesystem::path m_creationEngineDatabasePath; + double m_cleanupAccumulator{0.0}; +}; diff --git a/Code/server/Services/Generic/VoteTimeService.cpp b/Code/server/Services/Generic/VoteTimeService.cpp new file mode 100644 index 000000000..0fdc53697 --- /dev/null +++ b/Code/server/Services/Generic/VoteTimeService.cpp @@ -0,0 +1,219 @@ +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include + +#include + +using TiltedPhoques::String; + +namespace { +static bool ParseTime(const String& s, int& outH, int& outM) noexcept +{ + // Accept HH or HH:MM, range check + outH = 0; outM = 0; + int h = 0, m = 0; + size_t colon = s.find(':'); + try { + if (colon == String::npos) + { + h = std::stoi(s.c_str()); + m = 0; + } + else + { + h = std::stoi(s.substr(0, colon).c_str()); + m = std::stoi(s.substr(colon + 1).c_str()); + } + } catch (...) { return false; } + if (h < 0 || h > 23 || m < 0 || m > 59) + return false; + outH = h; outM = m; return true; +} + +static String ToLower(String s) +{ + std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return char(std::tolower(c)); }); + return s; +} +} + +VoteTimeService::VoteTimeService(World& aWorld, entt::dispatcher& aDispatcher) noexcept + : m_world(aWorld) +{ + m_chatConn = aDispatcher.sink>().connect<&VoteTimeService::OnChat>(this); + m_updateConn = aDispatcher.sink().connect<&VoteTimeService::OnUpdate>(this); + m_joinConn = aDispatcher.sink().connect<&VoteTimeService::OnPlayerJoin>(this); + m_leaveConn = aDispatcher.sink().connect<&VoteTimeService::OnPlayerLeave>(this); +} + +void VoteTimeService::BroadcastSystem(const String& text) const noexcept +{ + NotifyChatMessageBroadcast msg{}; + msg.MessageType = kSystemMessage; + msg.PlayerName = ""; + msg.ChatMessage = text; + GameServer::Get()->SendToPlayers(msg); +} + +String VoteTimeService::FormatTime(int h, int m) noexcept +{ + char buf[16]; + std::snprintf(buf, sizeof(buf), "%02d:%02d", h, m); + return String(buf); +} + +void VoteTimeService::StartVote(uint32_t starterId, int hour, int minute) noexcept +{ + m_active = true; + m_targetHour = hour; + m_targetMinute = minute; + m_starterId = starterId; + m_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(60); + + m_eligible.clear(); + m_yes.clear(); + m_no.clear(); + + // Snapshot eligible players at start + m_world.GetPlayerManager().ForEach([&](Player* p){ if (p) m_eligible.insert(p->GetId()); }); + + // Starter counts as yes + m_yes.insert(starterId); + + BroadcastSystem("Vote to set time to " + FormatTime(hour, minute) + " started. Type /yes or /no (60s timeout)."); + + CheckAndMaybeApply(); +} + +void VoteTimeService::CastYes(uint32_t id) noexcept +{ + if (!m_active) return; + if (m_eligible.find(id) == m_eligible.end()) return; + if (m_no.find(id) != m_no.end()) return; // already no + m_yes.insert(id); + CheckAndMaybeApply(); +} + +void VoteTimeService::CastNo(uint32_t id) noexcept +{ + if (!m_active) return; + if (m_eligible.find(id) == m_eligible.end()) return; + m_no.insert(id); + BroadcastSystem("Vote to set time to " + FormatTime(m_targetHour, m_targetMinute) + " failed: a player voted no."); + m_active = false; +} + +void VoteTimeService::CheckAndMaybeApply() noexcept +{ + if (!m_active) return; + + // All eligible must be in yes set + for (auto pid : m_eligible) + { + if (m_yes.find(pid) == m_yes.end()) + return; + } + + // Unanimous + const bool ok = m_world.GetCalendarService().SetTime(m_targetHour, m_targetMinute, m_world.GetCalendarService().GetTimeScale()); + if (ok) + BroadcastSystem("Time set to " + FormatTime(m_targetHour, m_targetMinute) + " by unanimous vote."); + else + BroadcastSystem("Failed to set time. (Invalid time?)"); + + m_active = false; +} + +void VoteTimeService::OnUpdate(const UpdateEvent&) noexcept +{ + if (!m_active) return; + if (std::chrono::steady_clock::now() > m_deadline) + { + BroadcastSystem("Vote to set time to " + FormatTime(m_targetHour, m_targetMinute) + " expired."); + m_active = false; + } +} + +void VoteTimeService::OnPlayerJoin(const PlayerJoinEvent&) noexcept +{ + // Do not add to eligibility after vote start (vote snapshot behavior) +} + +void VoteTimeService::OnPlayerLeave(const PlayerLeaveEvent& evt) noexcept +{ + if (!m_active) return; + const auto id = evt.pPlayer ? evt.pPlayer->GetId() : 0u; + if (id == 0) return; + // If they were eligible, remove them and re-check + if (m_eligible.erase(id) > 0) + { + m_yes.erase(id); + m_no.erase(id); + CheckAndMaybeApply(); + } +} + +void VoteTimeService::OnChat(const PacketEvent& aMsg) noexcept +{ + HandleChatCommand(aMsg.pPlayer, aMsg.Packet.ChatMessage); +} + +void VoteTimeService::HandleChatCommand(Player* player, const TiltedPhoques::String& message) noexcept +{ + const uint32_t playerId = player ? player->GetId() : 0u; + if (playerId == 0) + return; + + String text = ToLower(message); + + // Strip leading/trailing spaces + while (!text.empty() && std::isspace(static_cast(text.front()))) + text.erase(text.begin()); + while (!text.empty() && std::isspace(static_cast(text.back()))) + text.pop_back(); + + if (text.rfind("/votetime", 0) == 0) + { + // Parse argument after command + String arg; + if (text.size() > 9) + { + arg = text.substr(9); + // strip spaces + while (!arg.empty() && std::isspace(static_cast(arg.front()))) + arg.erase(arg.begin()); + } + int h = 0, m = 0; + if (!arg.empty() && ParseTime(arg, h, m)) + { + StartVote(playerId, h, m); + } + else + { + BroadcastSystem("Usage: /votetime HH[:MM]"); + } + return; + } + + if (text == "/yes") + { + CastYes(playerId); + return; + } + if (text == "/no") + { + CastNo(playerId); + return; + } +} diff --git a/Code/server/Services/InventoryService.cpp b/Code/server/Services/InventoryService.cpp index 299137853..f217e0228 100644 --- a/Code/server/Services/InventoryService.cpp +++ b/Code/server/Services/InventoryService.cpp @@ -10,12 +10,8 @@ #include #include #include +#include -#include -namespace -{ -Console::Setting bEnableItemDrops{"Gameplay:bEnableItemDrops", "(Experimental) Syncs dropped items by players", false}; -} InventoryService::InventoryService(World& aWorld, entt::dispatcher& aDispatcher) : m_world(aWorld) @@ -29,15 +25,34 @@ void InventoryService::OnInventoryChanges(const PacketEvent(); + const auto entity = m_world.TryResolveEntity(message.ServerId); + if (!entity) + { + spdlog::warn("Inventory update requested for unknown entity {:X}", message.ServerId); + return; + } + + auto view = m_world.view(); - const auto it = view.find(static_cast(message.ServerId)); + const auto it = view.find(*entity); if (it != view.end()) { + auto& ownerComponent = view.get(*it); + if (ownerComponent.GetOwner() != acMessage.pPlayer) + { + spdlog::warn("Inventory change denied for {:X}: player {:X} not owner", message.ServerId, acMessage.pPlayer->GetConnectionId()); + return; + } + auto& inventoryComponent = view.get(*it); inventoryComponent.Content.AddOrRemoveEntry(message.Item); } + else + { + spdlog::warn("Inventory change requested for entity {:X} without InventoryComponent", message.ServerId); + return; + } if (!message.UpdateClients) return; @@ -46,10 +61,7 @@ void InventoryService::OnInventoryChanges(const PacketEvent(message.ServerId); - if (!GameServer::Get()->SendToPlayersInRange(notify, cOrigin, acMessage.GetSender())) + if (!GameServer::Get()->SendToPlayersInRange(notify, *entity, acMessage.GetSender())) spdlog::error("{}: SendToPlayersInRange failed", __FUNCTION__); } @@ -57,27 +69,47 @@ void InventoryService::OnEquipmentChanges(const PacketEvent(); + const auto entity = m_world.TryResolveEntity(message.ServerId); + if (!entity) + { + spdlog::warn("Equipment update requested for unknown entity {:X}", message.ServerId); + return; + } + + auto view = m_world.view(); - const auto it = view.find(static_cast(message.ServerId)); + const auto it = view.find(*entity); if (it != view.end()) { + auto& ownerComponent = view.get(*it); + if (ownerComponent.GetOwner() != acMessage.pPlayer) + { + spdlog::warn("Equipment change denied for {:X}: player {:X} not owner", message.ServerId, acMessage.pPlayer->GetConnectionId()); + return; + } + auto& inventoryComponent = view.get(*it); inventoryComponent.Content.UpdateEquipment(message.CurrentInventory); } + else + { + spdlog::warn("Equipment change requested for entity {:X} without InventoryComponent", message.ServerId); + return; + } + + const auto effectiveCount = message.Count == 0 ? 1 : message.Count; NotifyEquipmentChanges notify; notify.ServerId = message.ServerId; notify.ItemId = message.ItemId; notify.EquipSlotId = message.EquipSlotId; - notify.Count = message.Count; + notify.Count = effectiveCount; notify.Unequip = message.Unequip; notify.IsSpell = message.IsSpell; notify.IsShout = message.IsShout; - const entt::entity cOrigin = static_cast(message.ServerId); - if (!GameServer::Get()->SendToPlayersInRange(notify, cOrigin, acMessage.GetSender())) + if (!GameServer::Get()->SendToPlayersInRange(notify, *entity, acMessage.GetSender())) spdlog::error("{}: SendToPlayersInRange failed", __FUNCTION__); } @@ -85,13 +117,27 @@ void InventoryService::OnWeaponDrawnRequest(const PacketEvent { auto& message = acMessage.Packet; + const auto entity = m_world.TryResolveEntity(message.Id); + if (!entity) + { + spdlog::debug("Weapon drawn request for unknown entity {:X}", message.Id); + return; + } + auto characterView = m_world.view(); - const auto it = characterView.find(static_cast(message.Id)); + const auto it = characterView.find(*entity); if (it != std::end(characterView) && characterView.get(*it).GetOwner() == acMessage.pPlayer) { auto& characterComponent = characterView.get(*it); characterComponent.SetWeaponDrawn(message.IsWeaponDrawn); spdlog::debug("Updating weapon drawn state {:x}:{}", message.Id, message.IsWeaponDrawn); + + NotifyDrawWeapon notify{}; + notify.Id = message.Id; + notify.IsWeaponDrawn = message.IsWeaponDrawn; + + if (!GameServer::Get()->SendToPlayersInRange(notify, *entity, acMessage.GetSender())) + spdlog::error("{}: SendToPlayersInRange failed", __FUNCTION__); } } diff --git a/Code/server/Services/LoginService.cpp b/Code/server/Services/LoginService.cpp new file mode 100644 index 000000000..6589367fb --- /dev/null +++ b/Code/server/Services/LoginService.cpp @@ -0,0 +1,313 @@ +#include + +#include +#include + +#include + +#include +#include +#include +#include + +#if defined(_WIN32) +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif +# ifndef NOMINMAX +# define NOMINMAX +# endif +# include +#else +# include +#endif + +namespace +{ +constexpr const char* kCreateUsersTableSql = R"SQL( + CREATE TABLE IF NOT EXISTS users( + username TEXT PRIMARY KEY, + password_hash TEXT NOT NULL, + password_salt TEXT NOT NULL, + avatar TEXT DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); +)SQL"; + +std::filesystem::path ResolveExecutableDirectory() noexcept +{ + namespace fs = std::filesystem; + +#if defined(_WIN32) + std::array buffer{}; + const DWORD length = GetModuleFileNameW(nullptr, buffer.data(), static_cast(buffer.size())); + if (length != 0 && length < buffer.size()) + { + return fs::path(buffer.data()).parent_path(); + } +#else + std::error_code ec; + auto exePath = fs::canonical("/proc/self/exe", ec); + if (!ec) + return exePath.parent_path(); +#endif + + std::error_code ec; + const auto current = fs::current_path(ec); + if (!ec) + return current; + + return {}; +} + +std::filesystem::path ResolveDatabasePath() noexcept +{ + namespace fs = std::filesystem; + + if (auto exeDirectory = ResolveExecutableDirectory(); !exeDirectory.empty()) + { + return exeDirectory / "logins.db"; + } + +#if defined(_WIN32) + if (const char* localAppData = std::getenv("LOCALAPPDATA"); localAppData && localAppData[0] != '\0') + { + return fs::path(localAppData) / "SkyrimTogether" / "Server" / "logins.db"; + } +#else + if (const char* xdgDataHome = std::getenv("XDG_DATA_HOME"); xdgDataHome && xdgDataHome[0] != '\0') + { + return fs::path(xdgDataHome) / "skyrimtogether" / "server" / "logins.db"; + } + + if (const char* home = std::getenv("HOME"); home && home[0] != '\0') + { + return fs::path(home) / ".local" / "share" / "skyrimtogether" / "server" / "logins.db"; + } +#endif + + std::error_code ec; + const auto current = fs::current_path(ec); + if (!ec) + return current / "data" / "logins.db"; + + return fs::path("logins.db"); +} +} // namespace + +LoginService::LoginService(World& aWorld, entt::dispatcher& aDispatcher) noexcept +{ + (void)aWorld; + (void)aDispatcher; + + const auto databasePath = ResolveDatabasePath(); + const auto dataDirectory = databasePath.parent_path(); + std::error_code ec; + if (!dataDirectory.empty() && !std::filesystem::exists(dataDirectory, ec)) + { + std::filesystem::create_directories(dataDirectory, ec); + if (ec) + { + spdlog::error("LoginService: failed to create data directory '{}': {}", dataDirectory.string(), ec.message()); + return; + } + } + else if (ec) + { + spdlog::error("LoginService: failed to access data directory '{}': {}", dataDirectory.string(), ec.message()); + return; + } + + spdlog::info("LoginService: using login database at '{}'", databasePath.string()); + if (sqlite3_open(databasePath.string().c_str(), &m_pDatabase) != SQLITE_OK) + { + spdlog::error("LoginService: unable to open database at '{}': {}", databasePath.string(), sqlite3_errmsg(m_pDatabase)); + return; + } + + if (!InitializeSchema()) + spdlog::error("LoginService: failed to initialize sqlite schema"); +} + +LoginService::~LoginService() noexcept +{ + if (m_pDatabase) + { + sqlite3_close(m_pDatabase); + m_pDatabase = nullptr; + } +} + +LoginService::LoginResult LoginService::VerifyOrCreateUser(const TiltedPhoques::String& aUsername, const TiltedPhoques::String& aPassword) noexcept +{ + if (!m_pDatabase) + return LoginResult::InternalError; + + if (aUsername.empty() || aPassword.empty()) + return LoginResult::InvalidCredentials; + + if (!Credential::LooksLikePasswordHash(aPassword.c_str())) + { + spdlog::error("LoginService: rejecting authentication for '{}' due to unhashed password payload", aUsername.c_str()); + return LoginResult::InvalidCredentials; + } + + sqlite3_stmt* pStatement = nullptr; + constexpr const char* cLookupSql = "SELECT password_hash, password_salt FROM users WHERE username = ?1;"; + if (sqlite3_prepare_v2(m_pDatabase, cLookupSql, -1, &pStatement, nullptr) != SQLITE_OK) + { + spdlog::error("LoginService: failed to prepare lookup statement: {}", sqlite3_errmsg(m_pDatabase)); + return LoginResult::InternalError; + } + + sqlite3_bind_text(pStatement, 1, aUsername.c_str(), -1, SQLITE_TRANSIENT); + + const int stepResult = sqlite3_step(pStatement); + if (stepResult == SQLITE_ROW) + { + const auto* storedHash = reinterpret_cast(sqlite3_column_text(pStatement, 0)); + const auto* storedSalt = reinterpret_cast(sqlite3_column_text(pStatement, 1)); + bool match = false; + if (storedHash && storedSalt) + { + auto derived = Credential::DeriveServerPassword(aPassword.c_str(), storedSalt); + match = derived == storedHash; + } + sqlite3_finalize(pStatement); + return match ? LoginResult::Ok : LoginResult::InvalidCredentials; + } + + sqlite3_finalize(pStatement); + + if (stepResult != SQLITE_DONE) + { + spdlog::error("LoginService: unexpected sqlite step result {}", stepResult); + return LoginResult::InternalError; + } + + return InsertUser(aUsername, aPassword); +} + +TiltedPhoques::String LoginService::GetAvatar(const TiltedPhoques::String& aUsername) const noexcept +{ + TiltedPhoques::String avatar; + if (!m_pDatabase || aUsername.empty()) + return avatar; + + sqlite3_stmt* pStatement = nullptr; + constexpr const char* cSelectSql = "SELECT avatar FROM users WHERE username = ?1;"; + if (sqlite3_prepare_v2(m_pDatabase, cSelectSql, -1, &pStatement, nullptr) != SQLITE_OK) + { + spdlog::error("LoginService: failed to prepare avatar lookup for '{}': {}", aUsername.c_str(), sqlite3_errmsg(m_pDatabase)); + return avatar; + } + + sqlite3_bind_text(pStatement, 1, aUsername.c_str(), -1, SQLITE_TRANSIENT); + + const int stepResult = sqlite3_step(pStatement); + if (stepResult == SQLITE_ROW) + { + if (const auto* pText = reinterpret_cast(sqlite3_column_text(pStatement, 0))) + avatar = pText; + } + else if (stepResult != SQLITE_DONE) + { + spdlog::error("LoginService: avatar lookup for '{}' failed with sqlite code {}", aUsername.c_str(), stepResult); + } + + sqlite3_finalize(pStatement); + return avatar; +} + +void LoginService::SetAvatar(const TiltedPhoques::String& aUsername, const TiltedPhoques::String& aAvatar) noexcept +{ + if (!m_pDatabase || aUsername.empty()) + return; + + sqlite3_stmt* pStatement = nullptr; + constexpr const char* cUpdateSql = "UPDATE users SET avatar = ?2 WHERE username = ?1;"; + if (sqlite3_prepare_v2(m_pDatabase, cUpdateSql, -1, &pStatement, nullptr) != SQLITE_OK) + { + spdlog::error("LoginService: failed to prepare avatar update for '{}': {}", aUsername.c_str(), sqlite3_errmsg(m_pDatabase)); + return; + } + + sqlite3_bind_text(pStatement, 1, aUsername.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(pStatement, 2, aAvatar.c_str(), -1, SQLITE_TRANSIENT); + + const int stepResult = sqlite3_step(pStatement); + if (stepResult != SQLITE_DONE) + spdlog::error("LoginService: failed to update avatar for '{}': {}", aUsername.c_str(), sqlite3_errmsg(m_pDatabase)); + + sqlite3_finalize(pStatement); +} + +bool LoginService::InitializeSchema() noexcept +{ + if (!m_pDatabase) + return false; + + char* pErrorMessage = nullptr; + if (sqlite3_exec(m_pDatabase, kCreateUsersTableSql, nullptr, nullptr, &pErrorMessage) != SQLITE_OK) + { + spdlog::error("LoginService: failed to create users table: {}", pErrorMessage ? pErrorMessage : "unknown error"); + sqlite3_free(pErrorMessage); + return false; + } + + pErrorMessage = nullptr; + if (sqlite3_exec(m_pDatabase, "ALTER TABLE users ADD COLUMN password_salt TEXT;", nullptr, nullptr, &pErrorMessage) != SQLITE_OK) + { + if (pErrorMessage) + { + std::string_view message(pErrorMessage); + if (message.find("duplicate column name") == std::string_view::npos) + spdlog::error("LoginService: failed to ensure password_salt column: {}", message); + sqlite3_free(pErrorMessage); + } + } + + pErrorMessage = nullptr; + if (sqlite3_exec(m_pDatabase, "ALTER TABLE users ADD COLUMN avatar TEXT DEFAULT '';", nullptr, nullptr, &pErrorMessage) != SQLITE_OK) + { + if (pErrorMessage) + { + std::string_view message(pErrorMessage); + if (message.find("duplicate column name") == std::string_view::npos) + spdlog::error("LoginService: failed to ensure avatar column: {}", message); + sqlite3_free(pErrorMessage); + } + } + + return true; +} + +LoginService::LoginResult LoginService::InsertUser(const TiltedPhoques::String& aUsername, const TiltedPhoques::String& aPasswordHash) noexcept +{ + sqlite3_stmt* pStatement = nullptr; + constexpr const char* cInsertSql = "INSERT INTO users(username, password_hash, password_salt) VALUES(?1, ?2, ?3);"; + if (sqlite3_prepare_v2(m_pDatabase, cInsertSql, -1, &pStatement, nullptr) != SQLITE_OK) + { + spdlog::error("LoginService: failed to prepare insert statement: {}", sqlite3_errmsg(m_pDatabase)); + return LoginResult::InternalError; + } + + const auto salt = Credential::GenerateSalt(); + const auto storedHash = Credential::DeriveServerPassword(aPasswordHash.c_str(), salt.c_str()); + + sqlite3_bind_text(pStatement, 1, aUsername.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(pStatement, 2, storedHash.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(pStatement, 3, salt.c_str(), -1, SQLITE_TRANSIENT); + + const int stepResult = sqlite3_step(pStatement); + sqlite3_finalize(pStatement); + + if (stepResult != SQLITE_DONE) + { + spdlog::error("LoginService: failed to insert user '{}': {}", aUsername.c_str(), sqlite3_errmsg(m_pDatabase)); + return LoginResult::InternalError; + } + + spdlog::info("LoginService: registered new user '{}'", aUsername.c_str()); + return LoginResult::Ok; +} diff --git a/Code/server/Services/LoginService.h b/Code/server/Services/LoginService.h new file mode 100644 index 000000000..816023691 --- /dev/null +++ b/Code/server/Services/LoginService.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include + +struct sqlite3; +struct World; + +/** + * @brief Persists account credentials for password-based login. + */ +class LoginService +{ +public: + enum class LoginResult + { + Ok, + InvalidCredentials, + InternalError, + }; + + LoginService(World& aWorld, entt::dispatcher& aDispatcher) noexcept; + ~LoginService() noexcept; + + TP_NOCOPYMOVE(LoginService); + + [[nodiscard]] LoginResult VerifyOrCreateUser(const TiltedPhoques::String& aUsername, const TiltedPhoques::String& aPassword) noexcept; + [[nodiscard]] TiltedPhoques::String GetAvatar(const TiltedPhoques::String& aUsername) const noexcept; + void SetAvatar(const TiltedPhoques::String& aUsername, const TiltedPhoques::String& aAvatar) noexcept; + +private: + [[nodiscard]] bool InitializeSchema() noexcept; + [[nodiscard]] LoginResult InsertUser(const TiltedPhoques::String& aUsername, const TiltedPhoques::String& aPasswordHash) noexcept; + +private: + sqlite3* m_pDatabase{nullptr}; +}; diff --git a/Code/server/Services/MagicService.cpp b/Code/server/Services/MagicService.cpp index e12262eb6..65c4b7d88 100644 --- a/Code/server/Services/MagicService.cpp +++ b/Code/server/Services/MagicService.cpp @@ -6,11 +6,15 @@ #include #include #include +#include #include #include #include #include +#include + +#include MagicService::MagicService(World& aWorld, entt::dispatcher& aDispatcher) noexcept : m_world(aWorld) @@ -19,6 +23,7 @@ MagicService::MagicService(World& aWorld, entt::dispatcher& aDispatcher) noexcep m_interruptCastConnection = aDispatcher.sink>().connect<&MagicService::OnInterruptCastRequest>(this); m_addTargetConnection = aDispatcher.sink>().connect<&MagicService::OnAddTargetRequest>(this); m_removeSpellConnection = aDispatcher.sink>().connect<&MagicService::OnRemoveSpellRequest>(this); + m_healingProximityConnection = aDispatcher.sink>().connect<&MagicService::OnHealingProximityRequest>(this); } void MagicService::OnSpellCastRequest(const PacketEvent& acMessage) const noexcept @@ -32,8 +37,14 @@ void MagicService::OnSpellCastRequest(const PacketEvent& acMes notify.IsDualCasting = message.IsDualCasting; notify.DesiredTarget = message.DesiredTarget; - const auto entity = static_cast(message.CasterId); - if (!GameServer::Get()->SendToPlayersInRange(notify, entity, acMessage.GetSender())) + const auto entity = m_world.TryResolveEntity(message.CasterId); + if (!entity) + { + spdlog::debug("Spell cast request for unknown caster {:X}", message.CasterId); + return; + } + + if (!GameServer::Get()->SendToPlayersInRange(notify, *entity, acMessage.GetSender())) spdlog::error("{}: SendToPlayersInRange failed", __FUNCTION__); } @@ -45,8 +56,14 @@ void MagicService::OnInterruptCastRequest(const PacketEvent(message.CasterId); - if (!GameServer::Get()->SendToPlayersInRange(notify, entity, acMessage.GetSender())) + const auto entity = m_world.TryResolveEntity(message.CasterId); + if (!entity) + { + spdlog::debug("Interrupt cast request for unknown caster {:X}", message.CasterId); + return; + } + + if (!GameServer::Get()->SendToPlayersInRange(notify, *entity, acMessage.GetSender())) spdlog::error("{}: SendToPlayersInRange failed", __FUNCTION__); } @@ -64,8 +81,14 @@ void MagicService::OnAddTargetRequest(const PacketEvent& acMes notify.ApplyHealPerkBonus = message.ApplyHealPerkBonus; notify.ApplyStaminaPerkBonus = message.ApplyStaminaPerkBonus; - const auto entity = static_cast(message.TargetId); - if (!GameServer::Get()->SendToPlayersInRange(notify, entity, acMessage.GetSender())) + const auto entity = m_world.TryResolveEntity(message.TargetId); + if (!entity) + { + spdlog::debug("Add target request for unknown target {:X}", message.TargetId); + return; + } + + if (!GameServer::Get()->SendToPlayersInRange(notify, *entity, acMessage.GetSender())) spdlog::error("{}: SendToPlayersInRange failed", __FUNCTION__); } @@ -79,7 +102,39 @@ void MagicService::OnRemoveSpellRequest(const PacketEvent& a //spdlog::info(__FUNCTION__ ": TargetId: {}, Spell baseId: {}", notify.TargetId, notify.SpellId.BaseId); - const auto entity = static_cast(message.TargetId); - if (!GameServer::Get()->SendToPlayersInRange(notify, entity, acMessage.GetSender())) + const auto entity = m_world.TryResolveEntity(message.TargetId); + if (!entity) + { + spdlog::debug("Remove spell request for unknown target {:X}", message.TargetId); + return; + } + + if (!GameServer::Get()->SendToPlayersInRange(notify, *entity, acMessage.GetSender())) spdlog::error("{}: SendToPlayersInRange failed", __FUNCTION__); } + +void MagicService::OnHealingProximityRequest(const PacketEvent& acMessage) const noexcept +{ + const auto& message = acMessage.Packet; + + NotifyHealingProximity notify; + notify.CasterId = message.CasterId; + notify.CasterX = message.CasterX; + notify.CasterY = message.CasterY; + notify.CasterZ = message.CasterZ; + notify.SpellFormId = message.SpellFormId; + notify.CasterRestorationLevel = message.CasterRestorationLevel; + + const auto entity = m_world.TryResolveEntity(message.CasterId); + if (!entity) + { + spdlog::debug("Healing proximity request for unknown caster {:X}", message.CasterId); + return; + } + + if (!GameServer::Get()->SendToPlayersInRange(notify, *entity, acMessage.GetSender())) + spdlog::error("{}: SendToPlayersInRange failed for healing proximity", __FUNCTION__); + + if (auto* pCaster = acMessage.GetSender()) + pCaster->Send(notify); +} diff --git a/Code/server/Services/MagicService.h b/Code/server/Services/MagicService.h index 8328211be..000b2c87d 100644 --- a/Code/server/Services/MagicService.h +++ b/Code/server/Services/MagicService.h @@ -7,6 +7,7 @@ struct World; struct SpellCastRequest; struct InterruptCastRequest; struct AddTargetRequest; +struct HealingProximityRequest; /** * @brief Relays spell casting and magic effects. @@ -20,7 +21,7 @@ struct MagicService protected: /** - * @brief Relays spell cast messages to other clients. + * @brief Relays spell cast messages to other clients and broadcasts healing proximity. */ void OnSpellCastRequest(const PacketEvent& acMessage) const noexcept; /** @@ -35,6 +36,10 @@ struct MagicService * @brief Relays spell removal messages to other clients. */ void OnRemoveSpellRequest(const PacketEvent& acMessage) const noexcept; + /** + * @brief Broadcasts healing proximity event to nearby downed players. + */ + void OnHealingProximityRequest(const PacketEvent& acMessage) const noexcept; private: @@ -44,4 +49,5 @@ struct MagicService entt::scoped_connection m_interruptCastConnection; entt::scoped_connection m_addTargetConnection; entt::scoped_connection m_removeSpellConnection; + entt::scoped_connection m_healingProximityConnection; }; diff --git a/Code/server/Services/MapService.cpp b/Code/server/Services/MapService.cpp index a0bf4e844..12b58fe6e 100644 --- a/Code/server/Services/MapService.cpp +++ b/Code/server/Services/MapService.cpp @@ -5,6 +5,10 @@ #include #include #include +#include +#include + +#include MapService::MapService(World& aWorld, entt::dispatcher& aDispatcher) noexcept : m_world(aWorld) @@ -13,6 +17,9 @@ MapService::MapService(World& aWorld, entt::dispatcher& aDispatcher) noexcept aDispatcher.sink>().connect<&MapService::OnSetWaypointRequest>(this); m_playerRemoveWaypointConnection = aDispatcher.sink>().connect<&MapService::OnRemoveWaypointRequest>(this); + m_partyFastTravelMarkersConnection = + aDispatcher.sink>().connect<&MapService::OnPartyFastTravelMarkersRequest>(this); + m_updateConnection = aDispatcher.sink().connect<&MapService::OnUpdate>(this); } void MapService::OnSetWaypointRequest(const PacketEvent& acMessage) const noexcept @@ -41,3 +48,152 @@ void MapService::OnRemoveWaypointRequest(const PacketEventSendToParty(notify, partyComponent, acMessage.GetSender()); } +void MapService::OnPartyFastTravelMarkersRequest(const PacketEvent& acMessage) noexcept +{ + // Client-side gating via ServerSettings should prevent sending/receiving when disabled. + // Keep server-side behavior permissive to avoid breaking older clients and mixed versions. + + const auto& partyComponent = acMessage.pPlayer->GetParty(); + if (!partyComponent.JoinedPartyId.has_value()) + return; + + const uint32_t partyId = *partyComponent.JoinedPartyId; + const uint32_t senderPlayerId = acMessage.pPlayer->GetId(); + + auto& state = m_partyFastTravelMarkers[partyId]; + auto* pParty = m_world.GetPartyService().GetById(partyId); + if (!pParty) + return; + if (!pParty->Options.SyncFastTravelMarkers()) + return; + + // Keep the cached state accurate for the current party membership (avoid leaking markers from players who left). + TiltedPhoques::Set memberIds{}; + memberIds.reserve(pParty->Members.size()); + for (auto* pMember : pParty->Members) + { + if (pMember) + memberIds.insert(pMember->GetId()); + } + + auto& byPlayer = state.ByPlayer; + TiltedPhoques::Vector toErase{}; + toErase.reserve(byPlayer.size()); + + for (const auto& entry : byPlayer) + { + if (!memberIds.contains(entry.first)) + toErase.push_back(entry.first); + } + + for (const auto playerId : toErase) + byPlayer.erase(playerId); + + // Rebuild union from per-player sets. + state.Union.clear(); + for (const auto& entry : byPlayer) + { + for (const auto& marker : entry.second) + state.Union.insert(marker); + } + + auto& senderSet = byPlayer[senderPlayerId]; + if (acMessage.Packet.FullSync) + senderSet.clear(); + + TiltedPhoques::Vector newToParty{}; + newToParty.reserve(acMessage.Packet.Markers.size()); + + for (const auto& marker : acMessage.Packet.Markers) + { + if (!marker) + continue; + + senderSet.insert(marker); + + if (state.Union.insert(marker).second) + newToParty.push_back(marker); + } + + if (!newToParty.empty()) + { + NotifyPartyFastTravelMarkers notify{}; + notify.Markers = newToParty; + + GameServer::Get()->SendToParty(notify, partyComponent, acMessage.GetSender()); + } + + TiltedPhoques::Vector missingForSender{}; + missingForSender.reserve(state.Union.size()); + + for (const auto& marker : state.Union) + { + if (!senderSet.contains(marker)) + missingForSender.push_back(marker); + } + + if (!missingForSender.empty()) + { + NotifyPartyFastTravelMarkers notify{}; + notify.Markers = std::move(missingForSender); + + acMessage.pPlayer->Send(notify); + } +} + +void MapService::OnUpdate(const UpdateEvent&) noexcept +{ + const auto now = GameServer::Get()->GetTick(); + if (m_nextCleanupTick > now) + return; + + // Clean up every 30 seconds. + m_nextCleanupTick = now + 30000; + + auto it = m_partyFastTravelMarkers.begin(); + while (it != m_partyFastTravelMarkers.end()) + { + const uint32_t partyId = it->first; + const auto* pParty = m_world.GetPartyService().GetById(partyId); + if (!pParty) + { + it = m_partyFastTravelMarkers.erase(it); + continue; + } + + // Prune per-player caches for players no longer in the party. + TiltedPhoques::Set memberIds{}; + memberIds.reserve(pParty->Members.size()); + for (auto* pMember : pParty->Members) + { + if (pMember) + memberIds.insert(pMember->GetId()); + } + + auto& state = const_cast(it->second); + auto& byPlayer = state.ByPlayer; + TiltedPhoques::Vector toErase{}; + toErase.reserve(byPlayer.size()); + + for (const auto& entry : byPlayer) + { + if (!memberIds.contains(entry.first)) + toErase.push_back(entry.first); + } + + for (const auto playerId : toErase) + byPlayer.erase(playerId); + + // Rebuild union from current party members to avoid keeping markers contributed only by players who left. + TiltedPhoques::Set newUnion{}; + for (const auto& entry : byPlayer) + { + const auto& markers = entry.second; + for (const auto& marker : markers) + newUnion.insert(marker); + } + state.Union = std::move(newUnion); + + ++it; + } +} diff --git a/Code/server/Services/MapService.h b/Code/server/Services/MapService.h index 964430fd6..299cac3fa 100644 --- a/Code/server/Services/MapService.h +++ b/Code/server/Services/MapService.h @@ -1,10 +1,13 @@ #pragma once #include +#include struct World; +struct UpdateEvent; struct RequestSetWaypoint; struct RequestRemoveWaypoint; +struct PartyFastTravelMarkersRequest; /** * @brief Handles player specific actions that might change the information needed by other clients about that player. @@ -19,11 +22,22 @@ struct MapService protected: void OnSetWaypointRequest(const PacketEvent& acMessage) const noexcept; void OnRemoveWaypointRequest(const PacketEvent& acMessage) const noexcept; + void OnPartyFastTravelMarkersRequest(const PacketEvent& acMessage) noexcept; + void OnUpdate(const UpdateEvent& acEvent) noexcept; private: + struct PartyFastTravelMarkerState + { + TiltedPhoques::Set Union{}; + TiltedPhoques::Map> ByPlayer{}; + }; + World& m_world; + TiltedPhoques::Map m_partyFastTravelMarkers{}; + uint64_t m_nextCleanupTick{0}; entt::scoped_connection m_playerSetWaypointConnection; entt::scoped_connection m_playerRemoveWaypointConnection; + entt::scoped_connection m_partyFastTravelMarkersConnection; entt::scoped_connection m_updateConnection; }; diff --git a/Code/server/Services/ObjectService.cpp b/Code/server/Services/ObjectService.cpp index 2bd7d154f..039e9d68c 100644 --- a/Code/server/Services/ObjectService.cpp +++ b/Code/server/Services/ObjectService.cpp @@ -124,9 +124,18 @@ void ObjectService::OnActivate(const PacketEvent& acMessage) co notifyActivate.ActivatorId = acMessage.Packet.ActivatorId; notifyActivate.PreActivationOpenState = acMessage.Packet.PreActivationOpenState; + TiltedPhoques::Vector players; + players.reserve(m_world.GetPlayerManager().Count()); + for (auto pPlayer : m_world.GetPlayerManager()) { - if (pPlayer != acMessage.pPlayer && pPlayer->GetCellComponent().Cell == acMessage.Packet.CellId) + players.push_back(pPlayer->GetConnectionId()); + } + + for (auto connectionId : players) + { + auto pPlayer = m_world.GetPlayerManager().GetByConnectionId(connectionId); + if (pPlayer && pPlayer != acMessage.pPlayer && pPlayer->GetCellComponent().Cell == acMessage.Packet.CellId) { pPlayer->Send(notifyActivate); } @@ -157,8 +166,20 @@ void ObjectService::OnLockChange(const PacketEvent& acMessage objectComponent.CurrentLockData.LockLevel = acMessage.Packet.LockLevel; } + TiltedPhoques::Vector players; + players.reserve(m_world.GetPlayerManager().Count()); + for (Player* pPlayer : m_world.GetPlayerManager()) { + players.push_back(pPlayer->GetConnectionId()); + } + + for (auto connectionId : players) + { + Player* pPlayer = m_world.GetPlayerManager().GetByConnectionId(connectionId); + if (!pPlayer) + continue; + if (pPlayer == acMessage.pPlayer) continue; @@ -176,8 +197,17 @@ void ObjectService::OnScriptAnimationRequest(const PacketEvent players; + players.reserve(m_world.GetPlayerManager().Count()); + for (Player* pPlayer : m_world.GetPlayerManager()) { - pPlayer->Send(message); + players.push_back(pPlayer->GetConnectionId()); + } + + for (auto connectionId : players) + { + if (Player* pPlayer = m_world.GetPlayerManager().GetByConnectionId(connectionId)) + pPlayer->Send(message); } } diff --git a/Code/server/Services/OverlayService.cpp b/Code/server/Services/OverlayService.cpp index df5343290..a174cdef8 100644 --- a/Code/server/Services/OverlayService.cpp +++ b/Code/server/Services/OverlayService.cpp @@ -10,21 +10,101 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + #include #include #include "Game/Player.h" +#include +#include +#include #include +#include + +#include +#include + +void sendPlayerMessage(const ChatMessageType acType, const String acContent, Player* aSendingPlayer) noexcept; + +namespace +{ +TiltedPhoques::String TrimCopy(const TiltedPhoques::String& value) +{ + TiltedPhoques::String trimmed = value; + while (!trimmed.empty() && std::isspace(static_cast(trimmed.front()))) + trimmed.erase(trimmed.begin()); + while (!trimmed.empty() && std::isspace(static_cast(trimmed.back()))) + trimmed.pop_back(); + return trimmed; +} + +void SendSystemMessage(Player* apPlayer, std::string_view aMessage) +{ + if (!apPlayer) + return; + + NotifyChatMessageBroadcast notify{}; + notify.MessageType = ChatMessageType::kSystemMessage; + notify.PlayerName = ""; + notify.ChatMessage = aMessage.data(); + apPlayer->Send(notify); +} + +bool PopulateTeleportDestination(World& aWorld, Player* apTarget, NotifyTeleport& aOutMessage) noexcept +{ + if (!apTarget) + return false; + + const auto character = apTarget->GetCharacter(); + if (!character.has_value()) + return false; + + const auto* pMovementComponent = aWorld.try_get(*character); + if (!pMovementComponent) + return false; + + const auto& cellComponent = apTarget->GetCellComponent(); + aOutMessage.CellId = cellComponent.Cell; + aOutMessage.Position = pMovementComponent->Position; + aOutMessage.WorldSpaceId = cellComponent.WorldSpaceId; + return true; +} + +constexpr float kTeleportMovementCancelDistanceSquared = 9.f; // 3 units squared +} // namespace OverlayService::OverlayService(World& aWorld, entt::dispatcher& aDispatcher) : m_world(aWorld) { m_chatMessageConnection = aDispatcher.sink>().connect<&OverlayService::HandleChatMessage>(this); + m_playerEnterWorldConnection = aDispatcher.sink().connect<&OverlayService::HandlePlayerJoin>(this); m_playerDialogueConnection = aDispatcher.sink>().connect<&OverlayService::OnPlayerDialogue>(this); m_teleportConnection = aDispatcher.sink>().connect<&OverlayService::OnTeleport>(this); + m_teleportResponseConnection = aDispatcher.sink>().connect<&OverlayService::OnTeleportResponse>(this); m_playerHealthConnection = aDispatcher.sink>().connect<&OverlayService::OnPlayerHealthUpdate>(this); + m_playerLeaveConnection = aDispatcher.sink().connect<&OverlayService::OnPlayerLeave>(this); + m_updateConnection = aDispatcher.sink().connect<&OverlayService::OnUpdate>(this); + m_playEmoteConnection = aDispatcher.sink>().connect<&OverlayService::OnPlayEmoteRequest>(this); + m_cancelEmoteConnection = aDispatcher.sink>().connect<&OverlayService::OnCancelEmoteRequest>(this); +} + +void OverlayService::SendCommandList(Player* apPlayer) const noexcept +{ + m_world.GetChatCommandService().SendCommandList(apPlayer); } void sendPlayerMessage(const ChatMessageType acType, const String acContent, Player* aSendingPlayer) noexcept @@ -36,6 +116,23 @@ void sendPlayerMessage(const ChatMessageType acType, const String acContent, Pla notifyMessage.PlayerName = std::regex_replace(aSendingPlayer->GetUsername(), escapeHtml, ""); notifyMessage.ChatMessage = std::regex_replace(acContent, escapeHtml, ""); + if (auto out = spdlog::get("ConOut")) + { + const char* label = "Chat"; + switch (notifyMessage.MessageType) + { + case kGlobalChat: label = "Global"; break; + case kPartyChat: label = "Party"; break; + case kLocalChat: label = "Local"; break; + case kPlayerDialogue: label = "Dialogue"; break; + default: break; + } + if (!notifyMessage.PlayerName.empty()) + out->info("[{}] {}: {}", label, notifyMessage.PlayerName.c_str(), notifyMessage.ChatMessage.c_str()); + else + out->info("[{}] {}", label, notifyMessage.ChatMessage.c_str()); + } + auto character = aSendingPlayer->GetCharacter(); switch (notifyMessage.MessageType) @@ -59,8 +156,23 @@ void sendPlayerMessage(const ChatMessageType acType, const String acContent, Pla } } +void OverlayService::HandlePlayerJoin(const PlayerEnterWorldEvent& acEvent) const noexcept +{ + SendCommandList(const_cast(acEvent.pPlayer)); +} + void OverlayService::HandleChatMessage(const PacketEvent& acMessage) const noexcept { + const TiltedPhoques::String trimmed = TrimCopy(acMessage.Packet.ChatMessage); + if (!trimmed.empty() && trimmed.front() == '/') + { + if (m_world.GetChatCommandService().TryHandle(acMessage.pPlayer, trimmed)) + return; + + SendSystemMessage(acMessage.pPlayer, "Unknown command. Type /help for a list of commands."); + return; + } + auto [canceled, reason] = m_world.GetScriptService().HandleChatMessage(*acMessage.pPlayer->GetCharacter(), acMessage.Packet.ChatMessage); if (canceled) return; @@ -73,28 +185,288 @@ void OverlayService::OnPlayerDialogue(const PacketEvent& sendPlayerMessage(kPlayerDialogue, acMessage.Packet.Text, acMessage.pPlayer); } -void OverlayService::OnTeleport(const PacketEvent& acMessage) const noexcept +void OverlayService::OnTeleport(const PacketEvent& acMessage) noexcept { + Player* pRequester = acMessage.pPlayer; + if (!pRequester) + return; + Player* pTargetPlayer = m_world.GetPlayerManager().GetById(acMessage.Packet.PlayerId); if (!pTargetPlayer) + { + SendSystemMessage(pRequester, "Teleport request failed: player not found."); return; + } + + if (pTargetPlayer->GetId() == pRequester->GetId()) + { + SendSystemMessage(pRequester, "You cannot request to teleport to yourself."); + return; + } + + auto& pending = m_pendingTeleportRequests[pTargetPlayer->GetId()]; + if (!pending.insert(pRequester->GetId()).second) + { + SendSystemMessage(pRequester, fmt::format("Teleport request to {} is already pending.", pTargetPlayer->GetUsername().c_str())); + return; + } + + NotifyTeleportRequest notify{}; + notify.RequesterId = static_cast(pRequester->GetId()); + notify.RequesterName = pRequester->GetUsername(); + pTargetPlayer->Send(notify); + + SendSystemMessage(pTargetPlayer, fmt::format("{} wants to teleport to you.", pRequester->GetUsername().c_str())); + SendSystemMessage(pRequester, fmt::format("Teleport request sent to {}.", pTargetPlayer->GetUsername().c_str())); +} + +void OverlayService::OnTeleportResponse(const PacketEvent& acMessage) noexcept +{ + Player* pResponder = acMessage.pPlayer; + if (!pResponder) + return; + + const uint32_t responderId = pResponder->GetId(); + const uint32_t requesterId = acMessage.Packet.RequesterId; + + auto pendingIt = m_pendingTeleportRequests.find(responderId); + if (pendingIt == m_pendingTeleportRequests.end()) + { + SendSystemMessage(pResponder, "No pending teleport request found."); + return; + } + + auto& requesters = pendingIt->second; + if (requesters.erase(requesterId) == 0) + { + SendSystemMessage(pResponder, "No pending teleport request found."); + return; + } + + if (requesters.empty()) + m_pendingTeleportRequests.erase(pendingIt); + + Player* pRequester = m_world.GetPlayerManager().GetById(requesterId); + if (!pRequester) + { + SendSystemMessage(pResponder, "Teleport requester is no longer online."); + return; + } + + if (!acMessage.Packet.Accepted) + { + SendSystemMessage(pRequester, fmt::format("{} declined your teleport request.", pResponder->GetUsername().c_str())); + SendSystemMessage(pResponder, fmt::format("You declined {}'s teleport request.", pRequester->GetUsername().c_str())); + return; + } + + NotifyTeleport teleportMessage{}; + if (!PopulateTeleportDestination(m_world, pResponder, teleportMessage)) + { + SendSystemMessage(pResponder, "Unable to locate your position for teleport."); + SendSystemMessage(pRequester, "Teleport request failed."); + return; + } + + OverlayService::PendingTeleportCountdown pending{}; + pending.RequesterId = requesterId; + pending.ResponderId = responderId; + pending.TeleportMessage = teleportMessage; + pending.TimeRemaining = 5.f; + pending.TargetName = pResponder->GetUsername(); + pending.LastAnnouncedSeconds = 5; + + if (const auto character = pRequester->GetCharacter()) + { + if (const auto* pMovement = m_world.try_get(*character)) + { + pending.InitialPosition = pMovement->Position; + pending.HasInitialPosition = true; + } + } + + m_activeTeleportCountdowns[pending.RequesterId] = pending; - NotifyTeleport response{}; + NotifyTeleportCountdown countdown{}; + countdown.TargetPlayerId = static_cast(responderId); + countdown.TargetName = pending.TargetName; + countdown.DurationSeconds = 5; + countdown.Cancelled = false; + pRequester->Send(countdown); - auto character = pTargetPlayer->GetCharacter(); - if (character) + SendSystemMessage(pRequester, fmt::format("{} accepted your teleport request. Teleporting in {} seconds. Do not move!", pending.TargetName.c_str(), countdown.DurationSeconds)); + SendSystemMessage(pResponder, fmt::format("You accepted {}'s teleport request. Teleporting them in {} seconds.", pRequester->GetUsername().c_str(), countdown.DurationSeconds)); +} + +void OverlayService::OnUpdate(const UpdateEvent& acEvent) noexcept +{ + if (m_activeTeleportCountdowns.empty()) + return; + + for (auto it = m_activeTeleportCountdowns.begin(); it != m_activeTeleportCountdowns.end();) { - const auto* pMovementComponent = m_world.try_get(*character); - if (pMovementComponent) + auto& pending = it->second; + + Player* pRequester = m_world.GetPlayerManager().GetById(pending.RequesterId); + if (!pRequester) + { + it = m_activeTeleportCountdowns.erase(it); + continue; + } + + Player* pResponder = m_world.GetPlayerManager().GetById(pending.ResponderId); + bool cancelTeleport = pResponder == nullptr; + + if (!cancelTeleport && pending.HasInitialPosition) + { + if (const auto character = pRequester->GetCharacter()) + { + if (const auto* pMovement = m_world.try_get(*character)) + { + const float distanceSquared = glm::distance2(pMovement->Position, pending.InitialPosition); + if (distanceSquared > kTeleportMovementCancelDistanceSquared) + cancelTeleport = true; + } + } + } + + if (cancelTeleport) + { + NotifyTeleportCountdown cancelMessage{}; + cancelMessage.TargetPlayerId = static_cast(pending.ResponderId); + cancelMessage.TargetName = pending.TargetName; + cancelMessage.DurationSeconds = 0; + cancelMessage.Cancelled = true; + cancelMessage.Reason = pResponder ? "Teleport cancelled: you moved." : "Teleport cancelled: target player disconnected."; + pRequester->Send(cancelMessage); + + SendSystemMessage(pRequester, cancelMessage.Reason.c_str()); + if (pResponder) + SendSystemMessage(pResponder, fmt::format("{} moved. Teleport cancelled.", pRequester->GetUsername().c_str())); + + it = m_activeTeleportCountdowns.erase(it); + continue; + } + + pending.TimeRemaining -= acEvent.Delta; + if (pending.TimeRemaining <= 0.f) + { + pRequester->Send(pending.TeleportMessage); + + NotifyTeleportCountdown clearMessage{}; + clearMessage.TargetPlayerId = static_cast(pending.ResponderId); + clearMessage.TargetName = pending.TargetName; + clearMessage.DurationSeconds = 0; + clearMessage.Cancelled = true; + clearMessage.Reason = ""; + pRequester->Send(clearMessage); + + SendSystemMessage(pRequester, fmt::format("Teleporting to {}.", pending.TargetName.c_str())); + if (pResponder) + SendSystemMessage(pResponder, fmt::format("{} is teleporting to you.", pRequester->GetUsername().c_str())); + + it = m_activeTeleportCountdowns.erase(it); + continue; + } + + const auto secondsRemaining = static_cast(std::ceil(pending.TimeRemaining)); + if (secondsRemaining != pending.LastAnnouncedSeconds && secondsRemaining > 0) { - const auto& cellComponent = pTargetPlayer->GetCellComponent(); - response.CellId = cellComponent.Cell; - response.Position = pMovementComponent->Position; - response.WorldSpaceId = cellComponent.WorldSpaceId; + NotifyTeleportCountdown update{}; + update.TargetPlayerId = static_cast(pending.ResponderId); + update.TargetName = pending.TargetName; + update.DurationSeconds = secondsRemaining; + update.Cancelled = false; + update.Reason = ""; + pRequester->Send(update); + pending.LastAnnouncedSeconds = secondsRemaining; } + + ++it; } +} + +void OverlayService::OnPlayEmoteRequest(const PacketEvent& acMessage) const noexcept +{ + const auto& message = acMessage.Packet; + const auto entity = m_world.TryResolveEntity(message.ServerId); + if (!entity) + { + spdlog::debug("Play emote request for unknown entity {:X}", message.ServerId); + return; + } + + const auto* pOwner = m_world.try_get(*entity); + if (!pOwner || pOwner->GetOwner() != acMessage.pPlayer) + { + spdlog::warn("Play emote request rejected for entity {:X}", message.ServerId); + return; + } + + NotifyPlayEmote notify{}; + notify.ServerId = message.ServerId; + notify.EventName = message.EventName; + + const auto senderCell = acMessage.pPlayer->GetCellComponent().Cell; + if (!senderCell) + return; + + TiltedPhoques::Vector players; + players.reserve(m_world.GetPlayerManager().Count()); + + for (Player* pPlayer : m_world.GetPlayerManager()) + players.push_back(pPlayer->GetConnectionId()); + + for (auto connectionId : players) + { + Player* pPlayer = m_world.GetPlayerManager().GetByConnectionId(connectionId); + if (!pPlayer || pPlayer == acMessage.pPlayer) + continue; - acMessage.pPlayer->Send(response); + if (pPlayer->GetCellComponent().Cell == senderCell) + pPlayer->Send(notify); + } +} + +void OverlayService::OnCancelEmoteRequest(const PacketEvent& acMessage) const noexcept +{ + const auto& message = acMessage.Packet; + const auto entity = m_world.TryResolveEntity(message.ServerId); + if (!entity) + { + spdlog::debug("Cancel emote request for unknown entity {:X}", message.ServerId); + return; + } + + const auto* pOwner = m_world.try_get(*entity); + if (!pOwner || pOwner->GetOwner() != acMessage.pPlayer) + { + spdlog::warn("Cancel emote request rejected for entity {:X}", message.ServerId); + return; + } + + NotifyCancelEmote notify{}; + notify.ServerId = message.ServerId; + + const auto senderCell = acMessage.pPlayer->GetCellComponent().Cell; + if (!senderCell) + return; + + TiltedPhoques::Vector players; + players.reserve(m_world.GetPlayerManager().Count()); + + for (Player* pPlayer : m_world.GetPlayerManager()) + players.push_back(pPlayer->GetConnectionId()); + + for (auto connectionId : players) + { + Player* pPlayer = m_world.GetPlayerManager().GetByConnectionId(connectionId); + if (!pPlayer || pPlayer == acMessage.pPlayer) + continue; + + if (pPlayer->GetCellComponent().Cell == senderCell) + pPlayer->Send(notify); + } } void OverlayService::OnPlayerHealthUpdate(const PacketEvent& acMessage) const noexcept @@ -105,3 +477,57 @@ void OverlayService::OnPlayerHealthUpdate(const PacketEventSendToParty(notify, acMessage.pPlayer->GetParty(), acMessage.GetSender()); } + +void OverlayService::OnPlayerLeave(const PlayerLeaveEvent& acEvent) noexcept +{ + if (!acEvent.pPlayer) + return; + + const uint32_t playerId = acEvent.pPlayer->GetId(); + m_pendingTeleportRequests.erase(playerId); + + for (auto it = m_pendingTeleportRequests.begin(); it != m_pendingTeleportRequests.end();) + { + it->second.erase(playerId); + if (it->second.empty()) + it = m_pendingTeleportRequests.erase(it); + else + ++it; + } + + for (auto it = m_activeTeleportCountdowns.begin(); it != m_activeTeleportCountdowns.end();) + { + const auto& pending = it->second; + const bool involvesPlayer = pending.RequesterId == playerId || pending.ResponderId == playerId; + if (!involvesPlayer) + { + ++it; + continue; + } + + const bool requesterLeft = pending.RequesterId == playerId; + const bool responderLeft = pending.ResponderId == playerId; + + if (!requesterLeft) + { + if (Player* pRequester = m_world.GetPlayerManager().GetById(pending.RequesterId)) + { + NotifyTeleportCountdown cancelMessage{}; + cancelMessage.TargetPlayerId = static_cast(pending.ResponderId); + cancelMessage.TargetName = pending.TargetName; + cancelMessage.DurationSeconds = 0; + cancelMessage.Cancelled = true; + cancelMessage.Reason = responderLeft ? "Teleport cancelled: target player disconnected." : "Teleport cancelled."; + pRequester->Send(cancelMessage); + + SendSystemMessage(pRequester, cancelMessage.Reason.c_str()); + } + } + else if (Player* pResponder = m_world.GetPlayerManager().GetById(pending.ResponderId)) + { + SendSystemMessage(pResponder, fmt::format("{} disconnected. Teleport request cancelled.", acEvent.pPlayer->GetUsername().c_str())); + } + + it = m_activeTeleportCountdowns.erase(it); + } +} diff --git a/Code/server/Services/OverlayService.h b/Code/server/Services/OverlayService.h index 29783a748..bbd818bb8 100644 --- a/Code/server/Services/OverlayService.h +++ b/Code/server/Services/OverlayService.h @@ -4,12 +4,25 @@ #include #include +#include + +#include +#include + +#include + +#include struct World; struct PlayerDialogueRequest; struct TeleportRequest; struct RequestPlayerHealthUpdate; +struct TeleportResponse; +struct PlayEmoteRequest; +struct CancelEmoteRequest; +struct PlayerLeaveEvent; +struct Player; /** * @brief Dispatches UI events that modify the UI view of other cients. @@ -18,22 +31,48 @@ class OverlayService { public: OverlayService(World& aWorld, entt::dispatcher& aDispatcher); + void SendCommandList(Player* apPlayer) const noexcept; protected: /** * @brief Applies regex on chat message and relays it to other clients. */ void HandleChatMessage(const PacketEvent& acMessage) const noexcept; - void HandlePlayerJoin(const PlayerEnterWorldEvent& acEvent) const noexcept; void OnPlayerDialogue(const PacketEvent& acMessage) const noexcept; - void OnTeleport(const PacketEvent& acMessage) const noexcept; + void OnTeleport(const PacketEvent& acMessage) noexcept; + void OnTeleportResponse(const PacketEvent& acMessage) noexcept; void OnPlayerHealthUpdate(const PacketEvent& acMessage) const noexcept; + void HandlePlayerJoin(const PlayerEnterWorldEvent& acEvent) const noexcept; + void OnPlayerLeave(const PlayerLeaveEvent& acEvent) noexcept; + void OnUpdate(const UpdateEvent& acEvent) noexcept; + void OnPlayEmoteRequest(const PacketEvent& acMessage) const noexcept; + void OnCancelEmoteRequest(const PacketEvent& acMessage) const noexcept; private: + struct PendingTeleportCountdown + { + uint32_t RequesterId{}; + uint32_t ResponderId{}; + NotifyTeleport TeleportMessage{}; + glm::vec3 InitialPosition{}; + bool HasInitialPosition{false}; + float TimeRemaining{}; + TiltedPhoques::String TargetName{}; + uint16_t LastAnnouncedSeconds{}; + }; + World& m_world; entt::scoped_connection m_chatMessageConnection; entt::scoped_connection m_playerDialogueConnection; entt::scoped_connection m_teleportConnection; + entt::scoped_connection m_teleportResponseConnection; entt::scoped_connection m_playerHealthConnection; + entt::scoped_connection m_playerEnterWorldConnection; + entt::scoped_connection m_playerLeaveConnection; + entt::scoped_connection m_updateConnection; + entt::scoped_connection m_playEmoteConnection; + entt::scoped_connection m_cancelEmoteConnection; + std::unordered_map> m_pendingTeleportRequests; + std::unordered_map m_activeTeleportCountdowns; }; diff --git a/Code/server/Services/PartyService.cpp b/Code/server/Services/PartyService.cpp index deabdd974..f13662811 100644 --- a/Code/server/Services/PartyService.cpp +++ b/Code/server/Services/PartyService.cpp @@ -18,11 +18,22 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include namespace { Console::Setting bAutoPartyJoin{"Gameplay:bAutoPartyJoin", "Join parties automatically, as long as there is only one party in the server", true}; +Console::Setting uPartyCellLockCountdown{"Gameplay:uPartyCellLockCountdown", "Seconds before party members are teleported to the leader's new cell", 3u}; +Console::Setting uPartyCellLockSnapshotDelay{"Gameplay:uPartyCellLockSnapshotDelay", "Milliseconds to wait before capturing leader position for party cell lock", 3000u}; } PartyService::PartyService(World& aWorld, entt::dispatcher& aDispatcher) noexcept @@ -36,6 +47,10 @@ PartyService::PartyService(World& aWorld, entt::dispatcher& aDispatcher) noexcep , m_partyCreateConnection(aDispatcher.sink>().connect<&PartyService::OnPartyCreate>(this)) , m_partyChangeLeaderConnection(aDispatcher.sink>().connect<&PartyService::OnPartyChangeLeader>(this)) , m_partyKickConnection(aDispatcher.sink>().connect<&PartyService::OnPartyKick>(this)) + , m_partyPositionUpdateConnection(aDispatcher.sink>().connect<&PartyService::OnPartyPositionUpdate>(this)) + , m_partyPositionsRequestConnection(aDispatcher.sink>().connect<&PartyService::OnPartyPositionsRequest>(this)) + , m_partyActorNamesRequestConnection(aDispatcher.sink>().connect<&PartyService::OnPartyActorNamesRequest>(this)) + , m_partyOptionsUpdateConnection(aDispatcher.sink>().connect<&PartyService::OnPartyOptionsUpdate>(this)) { } @@ -79,6 +94,113 @@ PartyService::Party* PartyService::GetPlayerParty(Player* const apPlayer) noexce void PartyService::OnUpdate(const UpdateEvent& acEvent) noexcept { const auto cCurrentTick = GameServer::Get()->GetTick(); + + for (auto it = m_parties.begin(); it != m_parties.end(); ++it) + { + auto& party = const_cast(it->second); + if (!party.PendingCellLockNotify || cCurrentTick < party.PendingCellLockNotifyAt) + continue; + + party.PendingCellLockNotify = false; + + if (!party.Options.LockPartyToLeaderCell()) + continue; + + Player* pLeader = m_world.GetPlayerManager().GetById(party.PendingCellLockLeaderId); + if (!pLeader) + continue; + + UpdateLeaderCellSnapshot(party, pLeader); + NotifyPartyLeaderCellLock(party, pLeader, false); + } + + // Periodic broadcast of party member positions (every ~500ms) + if (m_nextPositionsBroadcast <= cCurrentTick) + { + m_nextPositionsBroadcast = cCurrentTick + 500; + + for (auto& [partyId, party] : m_parties) + { + NotifyPartyPositions msg{}; + // Build one message containing all members' positions and cells + for (auto* pPlayer : party.Members) + { + NotifyPartyPositions::Entry e{}; + e.PlayerId = pPlayer->GetId(); + + // Position from MovementComponent if character exists + bool hasMovement = false; + bool isInterior = false; + if (auto optChar = pPlayer->GetCharacter()) + { + if (m_world.valid(*optChar) && m_world.any_of(*optChar)) + { + const auto& move = m_world.get(*optChar); + e.Position.x = move.Position.x; + e.Position.y = move.Position.y; + e.Position.z = move.Position.z; + hasMovement = true; + + Vector3_NetQuantize pos{}; + pos.x = move.Position.x; + pos.y = move.Position.y; + pos.z = move.Position.z; + m_world.GetPlayerLocationService().UpdateLocation(pPlayer, pos, pPlayer->GetCellComponent().WorldSpaceId, + pPlayer->GetCellComponent().Cell, PlayerLocation::Source::Movement); + } + } + // Worldspace / Cell + const auto& cell = pPlayer->GetCellComponent(); + e.WorldSpaceId = cell.WorldSpaceId; + e.CellId = cell.Cell; + isInterior = !cell.WorldSpaceId; + + PlayerLocation location{}; + if (m_world.GetPlayerLocationService().TryGetLocation(pPlayer->GetId(), location)) + { + if (!hasMovement && location.HasPosition) + { + e.Position = location.Position; + if (location.WorldSpaceId) + e.WorldSpaceId = location.WorldSpaceId; + if (location.CellId) + e.CellId = location.CellId; + hasMovement = true; + isInterior = !location.WorldSpaceId; + } + + if (location.HasExterior && (!e.WorldSpaceId || !hasMovement)) + { + e.Position = location.LastExteriorPosition; + e.WorldSpaceId = location.LastExteriorWorldSpaceId; + e.CellId = location.LastExteriorCellId; + hasMovement = true; + // Keep interior flag from actual state when using exterior fallback. + } + } + + e.IsInterior = isInterior; + if (hasMovement) + msg.Entries.push_back(e); + } + + // Send to each party member + TiltedPhoques::Vector members; + members.reserve(party.Members.size()); + for (auto* pPlayer : party.Members) + { + if (pPlayer) + members.push_back(pPlayer->GetConnectionId()); + } + + for (auto connectionId : members) + { + if (auto* pPlayer = m_world.GetPlayerManager().GetByConnectionId(connectionId)) + pPlayer->Send(msg); + } + } + } + if (m_nextInvitationExpire > cCurrentTick) return; @@ -104,6 +226,135 @@ void PartyService::OnUpdate(const UpdateEvent& acEvent) noexcept } } + + +void PartyService::OnPartyPositionUpdate(const PacketEvent& acPacket) noexcept +{ + Player* const pSender = acPacket.pPlayer; + auto* pParty = GetPlayerParty(pSender); + if (!pParty) + return; + + const auto& msgIn = acPacket.Packet; + + m_world.GetPlayerLocationService().UpdateLocation(pSender, msgIn.Position, msgIn.WorldSpaceId, msgIn.CellId, + PlayerLocation::Source::ClientReport); + + NotifyPartyPositions out{}; + NotifyPartyPositions::Entry e{}; + e.PlayerId = pSender->GetId(); + e.Position = msgIn.Position; + e.WorldSpaceId = msgIn.WorldSpaceId; + e.CellId = msgIn.CellId; + out.Entries.push_back(e); + + for (auto* pMember : pParty->Members) + { + if (pMember == pSender) + continue; // don't need to echo to the sender + pMember->Send(out); + } +} + +void PartyService::OnPartyPositionsRequest(const PacketEvent& acPacket) noexcept +{ + Player* const pSender = acPacket.pPlayer; + auto* pParty = GetPlayerParty(pSender); + if (!pParty) + return; + + NotifyPartyPositions msg{}; + for (auto* pPlayer : pParty->Members) + { + NotifyPartyPositions::Entry e{}; + e.PlayerId = pPlayer->GetId(); + bool hasPosition = false; + bool isInterior = false; + + const auto& cell = pPlayer->GetCellComponent(); + e.WorldSpaceId = cell.WorldSpaceId; + e.CellId = cell.Cell; + isInterior = !cell.WorldSpaceId; + + PlayerLocation location{}; + if (m_world.GetPlayerLocationService().TryGetLocation(pPlayer->GetId(), location)) + { + if (location.HasPosition) + { + e.Position = location.Position; + if (location.WorldSpaceId) + e.WorldSpaceId = location.WorldSpaceId; + if (location.CellId) + e.CellId = location.CellId; + hasPosition = true; + isInterior = !location.WorldSpaceId; + } + + if (location.HasExterior && (!e.WorldSpaceId || !hasPosition)) + { + e.Position = location.LastExteriorPosition; + e.WorldSpaceId = location.LastExteriorWorldSpaceId; + e.CellId = location.LastExteriorCellId; + hasPosition = true; + // Keep interior flag from actual state when using exterior fallback. + } + } + e.IsInterior = isInterior; + if (hasPosition) + msg.Entries.push_back(e); + } + + pSender->Send(msg); +} + +void PartyService::OnPartyActorNamesRequest(const PacketEvent& acPacket) noexcept +{ + Player* const pSender = acPacket.pPlayer; + auto* pParty = GetPlayerParty(pSender); + if (!pParty) + return; + + for (auto* pMember : pParty->Members) + { + const auto& actorName = pMember->GetActorName(); + if (actorName.empty()) + continue; + + NotifyPlayerActorName notify{}; + notify.PlayerId = pMember->GetId(); + notify.ActorName = actorName; + pSender->Send(notify); + } +} + +void PartyService::OnPartyOptionsUpdate(const PacketEvent& acPacket) noexcept +{ + Player* const pSender = acPacket.pPlayer; + auto* pParty = GetPlayerParty(pSender); + if (!pParty) + return; + + if (pParty->LeaderPlayerId != pSender->GetId()) + return; + + const bool wasCellLockEnabled = pParty->Options.LockPartyToLeaderCell(); + pParty->Options = acPacket.Packet.Options; + + NotifyPartyOptions notify{}; + notify.Options = pParty->Options; + GameServer::Get()->SendToParty(notify, pSender->GetParty()); + + if (wasCellLockEnabled && !pParty->Options.LockPartyToLeaderCell()) + { + pParty->PendingCellLockNotify = false; + NotifyPartyLeaderCellLock(*pParty, pSender, true); + } + else if (!wasCellLockEnabled && pParty->Options.LockPartyToLeaderCell()) + { + ScheduleLeaderCellLockNotify(*pParty, pSender); + } +} + void PartyService::OnPartyCreate(const PacketEvent& acPacket) noexcept { Player* const player = acPacket.pPlayer; @@ -124,9 +375,17 @@ void PartyService::OnPartyCreate(const PacketEvent& acPacket if (m_parties.size() == 1 && bAutoPartyJoin) { + TiltedPhoques::Vector otherPlayers; + otherPlayers.reserve(m_world.GetPlayerManager().Count()); for (Player* otherPlayer : m_world.GetPlayerManager()) { - if (otherPlayer->GetId() != player->GetId()) + otherPlayers.push_back(otherPlayer->GetConnectionId()); + } + + for (auto connectionId : otherPlayers) + { + Player* otherPlayer = m_world.GetPlayerManager().GetByConnectionId(connectionId); + if (otherPlayer && otherPlayer->GetId() != player->GetId()) { party.Members.push_back(otherPlayer); otherPlayer->GetParty().JoinedPartyId = partyId; @@ -208,6 +467,7 @@ void PartyService::OnPlayerJoin(const PlayerJoinEvent& acEvent) noexcept NotifyPlayerJoined notify{}; notify.PlayerId = acEvent.pPlayer->GetId(); notify.Username = acEvent.pPlayer->GetUsername(); + notify.Avatar = acEvent.pPlayer->GetAvatar(); notify.WorldSpaceId = acEvent.WorldSpaceId; notify.CellId = acEvent.CellId; @@ -218,6 +478,18 @@ void PartyService::OnPlayerJoin(const PlayerJoinEvent& acEvent) noexcept GameServer::Get()->SendToPlayers(notify, acEvent.pPlayer); + for (Player* player : m_world.GetPlayerManager()) + { + const auto& actorName = player->GetActorName(); + if (actorName.empty()) + continue; + + NotifyPlayerActorName actorNotify{}; + actorNotify.PlayerId = player->GetId(); + actorNotify.ActorName = actorName; + acEvent.pPlayer->Send(actorNotify); + } + if (m_parties.size() == 1 && bAutoPartyJoin) { for (Player* player : m_world.GetPlayerManager()) @@ -237,7 +509,7 @@ void PartyService::OnPlayerJoin(const PlayerJoinEvent& acEvent) noexcept break; } } - + } } @@ -390,9 +662,18 @@ void PartyService::RemovePlayerFromParty(Player* apPlayer) noexcept void PartyService::BroadcastPlayerList(Player* apPlayer) const noexcept { auto pIgnoredPlayer = apPlayer; + TiltedPhoques::Vector players; + players.reserve(m_world.GetPlayerManager().Count()); + for (auto pSelf : m_world.GetPlayerManager()) { - if (pIgnoredPlayer == pSelf) + players.push_back(pSelf->GetConnectionId()); + } + + for (auto selfId : players) + { + auto pSelf = m_world.GetPlayerManager().GetByConnectionId(selfId); + if (!pSelf || pIgnoredPlayer == pSelf) continue; NotifyPlayerList playerList; @@ -404,7 +685,10 @@ void PartyService::BroadcastPlayerList(Player* apPlayer) const noexcept if (pIgnoredPlayer == pPlayer) continue; - playerList.Players[pPlayer->GetId()] = pPlayer->GetUsername(); + NotifyPlayerList::PlayerListEntry entry{}; + entry.Name = pPlayer->GetUsername(); + entry.Avatar = pPlayer->GetAvatar(); + playerList.Players[pPlayer->GetId()] = std::move(entry); } pSelf->Send(playerList); @@ -428,10 +712,21 @@ void PartyService::BroadcastPartyInfo(uint32_t aPartyId) const noexcept message.PlayerIds.push_back(pPlayer->GetId()); } + TiltedPhoques::Vector memberIds; + memberIds.reserve(members.size()); for (auto pPlayer : members) { - message.IsLeader = pPlayer->GetId() == party.LeaderPlayerId; - pPlayer->Send(message); + if (pPlayer) + memberIds.push_back(pPlayer->GetConnectionId()); + } + + for (auto connectionId : memberIds) + { + if (auto* pPlayer = m_world.GetPlayerManager().GetByConnectionId(connectionId)) + { + message.IsLeader = pPlayer->GetId() == party.LeaderPlayerId; + pPlayer->Send(message); + } } } @@ -446,4 +741,104 @@ void PartyService::SendPartyJoinedEvent(Party& aParty, Player* aPlayer) noexcept } spdlog::debug("[PartyService]: Sending party join event to player"); aPlayer->Send(joinedMessage); + + NotifyPartyOptions optionsMessage{}; + optionsMessage.Options = aParty.Options; + aPlayer->Send(optionsMessage); + + if (aParty.Options.LockPartyToLeaderCell() && aParty.LeaderCell.HasLocation && aParty.LeaderPlayerId != aPlayer->GetId()) + { + ::NotifyPartyLeaderCellLock notify{}; + notify.WorldSpaceId = aParty.LeaderCell.WorldSpaceId; + notify.CellId = aParty.LeaderCell.CellId; + notify.Position = aParty.LeaderCell.Position; + notify.CountdownSeconds = uPartyCellLockCountdown.value_as(); + notify.Cancelled = false; + aPlayer->Send(notify); + } +} + +void PartyService::UpdateLeaderCellSnapshot(Party& aParty, Player* apLeader) noexcept +{ + if (!apLeader) + return; + + const auto& cellComponent = apLeader->GetCellComponent(); + if (!cellComponent.Cell && !cellComponent.WorldSpaceId) + return; + + Vector3_NetQuantize position{}; + bool hasPosition = false; + + if (auto optCharacter = apLeader->GetCharacter()) + { + if (m_world.valid(*optCharacter) && m_world.any_of(*optCharacter)) + { + const auto& move = m_world.get(*optCharacter); + position.x = move.Position.x; + position.y = move.Position.y; + position.z = move.Position.z; + hasPosition = true; + } + } + + if (!hasPosition) + { + PlayerLocation location{}; + if (m_world.GetPlayerLocationService().TryGetLocation(apLeader->GetId(), location)) + { + if (location.HasPosition) + { + position = location.Position; + hasPosition = true; + } + else if (location.HasExterior) + { + position = location.LastExteriorPosition; + hasPosition = true; + } + } + } + + if (!hasPosition) + return; + + aParty.LeaderCell.WorldSpaceId = cellComponent.WorldSpaceId; + aParty.LeaderCell.CellId = cellComponent.Cell; + aParty.LeaderCell.Position = position; + aParty.LeaderCell.HasLocation = true; +} + +void PartyService::ScheduleLeaderCellLockNotify(Party& aParty, Player* apLeader) noexcept +{ + if (!apLeader) + return; + + aParty.PendingCellLockNotify = true; + aParty.PendingCellLockNotifyAt = + GameServer::Get()->GetTick() + uPartyCellLockSnapshotDelay.value_as(); + aParty.PendingCellLockLeaderId = apLeader->GetId(); +} + +void PartyService::NotifyPartyLeaderCellLock(Party& aParty, Player* apLeader, bool aCancelled) noexcept +{ + if (!aParty.LeaderCell.HasLocation && !aCancelled) + return; + + ::NotifyPartyLeaderCellLock notify{}; + notify.WorldSpaceId = aParty.LeaderCell.WorldSpaceId; + notify.CellId = aParty.LeaderCell.CellId; + notify.Position = aParty.LeaderCell.Position; + notify.CountdownSeconds = uPartyCellLockCountdown.value_as(); + notify.Cancelled = aCancelled; + + for (auto* pMember : aParty.Members) + { + if (!pMember) + continue; + if (apLeader && pMember->GetId() == apLeader->GetId()) + continue; + + pMember->Send(notify); + } } diff --git a/Code/server/Services/PartyService.h b/Code/server/Services/PartyService.h index fbb0f7f7d..081e5e455 100644 --- a/Code/server/Services/PartyService.h +++ b/Code/server/Services/PartyService.h @@ -1,6 +1,8 @@ #pragma once #include +#include +#include struct World; struct UpdateEvent; @@ -13,6 +15,10 @@ struct NotifyPartyInfo; struct PartyCreateRequest; struct PartyChangeLeaderRequest; struct PartyKickRequest; +struct PartyPositionUpdateRequest; +struct PartyPositionsRequest; +struct PartyActorNamesRequest; +struct PartyOptionsUpdateRequest; /** * @brief Manages every party in the server. @@ -24,6 +30,17 @@ struct PartyService uint32_t LeaderPlayerId; Vector Members; GameId CachedWeather{}; + PartyOptions Options{}; + struct LeaderCellSnapshot + { + GameId WorldSpaceId{}; + GameId CellId{}; + Vector3_NetQuantize Position{}; + bool HasLocation{false}; + } LeaderCell{}; + bool PendingCellLockNotify{false}; + uint64_t PendingCellLockNotifyAt{0}; + uint32_t PendingCellLockLeaderId{0}; }; PartyService(World& aWorld, entt::dispatcher& aDispatcher) noexcept; @@ -35,6 +52,9 @@ struct PartyService bool IsPlayerInParty(Player* const apPlayer) const noexcept; bool IsPlayerLeader(Player* const apPlayer) noexcept; Party* GetPlayerParty(Player* const apPlayer) noexcept; + void UpdateLeaderCellSnapshot(Party& aParty, Player* apLeader) noexcept; + void NotifyPartyLeaderCellLock(Party& aParty, Player* apLeader, bool aCancelled) noexcept; + void ScheduleLeaderCellLockNotify(Party& aParty, Player* apLeader) noexcept; protected: void OnUpdate(const UpdateEvent& acEvent) noexcept; @@ -46,6 +66,10 @@ struct PartyService void OnPartyCreate(const PacketEvent& acPacket) noexcept; void OnPartyChangeLeader(const PacketEvent& acPacket) noexcept; void OnPartyKick(const PacketEvent& acPacket) noexcept; + void OnPartyPositionUpdate(const PacketEvent& acPacket) noexcept; + void OnPartyPositionsRequest(const PacketEvent& acPacket) noexcept; + void OnPartyActorNamesRequest(const PacketEvent& acPacket) noexcept; + void OnPartyOptionsUpdate(const PacketEvent& acPacket) noexcept; void RemovePlayerFromParty(Player* apPlayer) noexcept; void BroadcastPlayerList(Player* apPlayer = nullptr) const noexcept; @@ -57,6 +81,7 @@ struct PartyService TiltedPhoques::Map m_parties; uint32_t m_nextId{0}; uint64_t m_nextInvitationExpire{0}; + uint64_t m_nextPositionsBroadcast{0}; entt::scoped_connection m_updateEvent; entt::scoped_connection m_playerJoinConnection; @@ -67,6 +92,10 @@ struct PartyService entt::scoped_connection m_partyCreateConnection; entt::scoped_connection m_partyChangeLeaderConnection; entt::scoped_connection m_partyKickConnection; + entt::scoped_connection m_partyPositionUpdateConnection; + entt::scoped_connection m_partyPositionsRequestConnection; + entt::scoped_connection m_partyActorNamesRequestConnection; + entt::scoped_connection m_partyOptionsUpdateConnection; void SendPartyJoinedEvent(Party& aParty, Player* aPlayer) noexcept; }; diff --git a/Code/server/Services/PlayerLocationService.cpp b/Code/server/Services/PlayerLocationService.cpp new file mode 100644 index 000000000..b422a3c5d --- /dev/null +++ b/Code/server/Services/PlayerLocationService.cpp @@ -0,0 +1,446 @@ +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#endif + +namespace +{ +constexpr char kCreateLocationsTableSql[] = + R"SQL( + CREATE TABLE IF NOT EXISTS player_locations( + username TEXT PRIMARY KEY, + player_id INTEGER NOT NULL DEFAULT 0, + endpoint TEXT, + pos_x REAL NOT NULL DEFAULT 0, + pos_y REAL NOT NULL DEFAULT 0, + pos_z REAL NOT NULL DEFAULT 0, + cell_mod_id INTEGER NOT NULL DEFAULT 0, + cell_base_id INTEGER NOT NULL DEFAULT 0, + world_mod_id INTEGER NOT NULL DEFAULT 0, + world_base_id INTEGER NOT NULL DEFAULT 0, + last_seen INTEGER NOT NULL DEFAULT 0, + source INTEGER NOT NULL DEFAULT 0, + has_position INTEGER NOT NULL DEFAULT 0, + ext_pos_x REAL NOT NULL DEFAULT 0, + ext_pos_y REAL NOT NULL DEFAULT 0, + ext_pos_z REAL NOT NULL DEFAULT 0, + ext_cell_mod_id INTEGER NOT NULL DEFAULT 0, + ext_cell_base_id INTEGER NOT NULL DEFAULT 0, + ext_world_mod_id INTEGER NOT NULL DEFAULT 0, + ext_world_base_id INTEGER NOT NULL DEFAULT 0, + ext_last_seen INTEGER NOT NULL DEFAULT 0, + has_exterior INTEGER NOT NULL DEFAULT 0 + ); +)SQL"; + +std::filesystem::path ResolveExecutableDirectory() noexcept +{ + namespace fs = std::filesystem; + +#if defined(_WIN32) + std::array buffer{}; + const DWORD length = GetModuleFileNameW(nullptr, buffer.data(), static_cast(buffer.size())); + if (length != 0 && length < buffer.size()) + { + return fs::path(buffer.data()).parent_path(); + } +#else + std::error_code ec; + auto exePath = fs::canonical("/proc/self/exe", ec); + if (!ec) + return exePath.parent_path(); +#endif + + std::error_code ec; + const auto current = fs::current_path(ec); + if (!ec) + return current; + + return {}; +} + +std::filesystem::path ResolveLocationsDatabasePath() noexcept +{ + namespace fs = std::filesystem; + + if (auto exeDirectory = ResolveExecutableDirectory(); !exeDirectory.empty()) + { + return exeDirectory / "locations.db"; + } + +#if defined(_WIN32) + if (char* localAppData = nullptr; _dupenv_s(&localAppData, nullptr, "LOCALAPPDATA") == 0 && localAppData) + { + fs::path path(localAppData); + free(localAppData); + path /= "SkyrimTogether"; + path /= "Server"; + path /= "locations.db"; + return path; + } +#else + if (const char* xdgDataHome = std::getenv("XDG_DATA_HOME"); xdgDataHome && xdgDataHome[0] != '\0') + { + return fs::path(xdgDataHome) / "skyrimtogether" / "server" / "locations.db"; + } + + if (const char* home = std::getenv("HOME"); home && home[0] != '\0') + { + return fs::path(home) / ".local" / "share" / "skyrimtogether" / "server" / "locations.db"; + } +#endif + + std::error_code ec; + const auto current = fs::current_path(ec); + if (!ec) + return current / "data" / "locations.db"; + + return fs::path("locations.db"); +} + +uint64_t GetEpochSeconds() noexcept +{ + using clock = std::chrono::system_clock; + return static_cast(std::chrono::duration_cast(clock::now().time_since_epoch()).count()); +} + +bool EnsureColumnExists(sqlite3* apDatabase, const char* acSql) noexcept +{ + if (!apDatabase || !acSql) + return false; + + char* pError = nullptr; + const int result = sqlite3_exec(apDatabase, acSql, nullptr, nullptr, &pError); + if (result == SQLITE_OK) + return true; + + std::string_view message = pError ? pError : ""; + if (!message.empty()) + { + const bool duplicate = message.find("duplicate column") != std::string_view::npos || + message.find("already exists") != std::string_view::npos; + sqlite3_free(pError); + return duplicate; + } + + sqlite3_free(pError); + return false; +} +} // namespace + +PlayerLocationService::PlayerLocationService(World& aWorld, entt::dispatcher& aDispatcher) noexcept + : m_world(aWorld) +{ + m_playerJoinConnection = aDispatcher.sink().connect<&PlayerLocationService::OnPlayerJoin>(this); + m_playerLeaveConnection = aDispatcher.sink().connect<&PlayerLocationService::OnPlayerLeave>(this); + m_updateConnection = aDispatcher.sink().connect<&PlayerLocationService::OnUpdate>(this); + + if (!InitializeDatabase()) + spdlog::error("PlayerLocationService: failed to initialize locations database"); +} + +PlayerLocationService::~PlayerLocationService() noexcept +{ + ShutdownDatabase(); +} + +void PlayerLocationService::UpdateLocation(Player* apPlayer, const Vector3_NetQuantize& acPos, const GameId& acWorldSpaceId, const GameId& acCellId, + PlayerLocation::Source aSource, bool aForcePersist) noexcept +{ + if (!apPlayer) + return; + + const uint32_t playerId = apPlayer->GetId(); + auto& location = m_locations[playerId]; + location.Position = acPos; + location.WorldSpaceId = acWorldSpaceId; + location.CellId = acCellId; + location.LastSeenEpoch = GetEpochSeconds(); + location.SourceTag = aSource; + location.HasPosition = true; + location.Username = apPlayer->GetUsername(); + location.Endpoint = apPlayer->GetEndPoint(); + if (acWorldSpaceId) + { + location.LastExteriorPosition = acPos; + location.LastExteriorWorldSpaceId = acWorldSpaceId; + location.LastExteriorCellId = acCellId; + location.LastExteriorEpoch = location.LastSeenEpoch; + location.HasExterior = true; + } + + PersistLocation(location, playerId, aForcePersist ? "force" : "update"); +} + +void PlayerLocationService::UpdateCell(Player* apPlayer, const GameId& acWorldSpaceId, const GameId& acCellId, PlayerLocation::Source aSource, + bool aForcePersist) noexcept +{ + if (!apPlayer) + return; + + const uint32_t playerId = apPlayer->GetId(); + auto it = m_locations.find(playerId); + if (it != m_locations.end()) + { + auto& location = it->second; + location.WorldSpaceId = acWorldSpaceId; + location.CellId = acCellId; + location.LastSeenEpoch = GetEpochSeconds(); + location.SourceTag = aSource; + location.Endpoint = apPlayer->GetEndPoint(); + if (location.HasPosition) + PersistLocation(location, playerId, aForcePersist ? "force-cell" : "cell"); + return; + } + + PlayerLocation location{}; + location.Username = apPlayer->GetUsername(); + location.Endpoint = apPlayer->GetEndPoint(); + location.WorldSpaceId = acWorldSpaceId; + location.CellId = acCellId; + location.LastSeenEpoch = GetEpochSeconds(); + location.SourceTag = aSource; + location.HasPosition = false; + location.HasExterior = false; + m_locations.emplace(playerId, std::move(location)); +} + +bool PlayerLocationService::TryGetLocation(uint32_t aPlayerId, PlayerLocation& aOut) const noexcept +{ + const auto it = m_locations.find(aPlayerId); + if (it == m_locations.end()) + return false; + + aOut = it->second; + return true; +} + +void PlayerLocationService::OnPlayerJoin(const PlayerJoinEvent& aEvent) noexcept +{ + LoadFromDatabase(aEvent.pPlayer); +} + +void PlayerLocationService::OnPlayerLeave(const PlayerLeaveEvent& aEvent) noexcept +{ + if (!aEvent.pPlayer) + return; + + const uint32_t playerId = aEvent.pPlayer->GetId(); + auto it = m_locations.find(playerId); + if (it != m_locations.end()) + { + PersistLocation(it->second, playerId, "leave"); + m_locations.erase(it); + } +} + +void PlayerLocationService::OnUpdate(const UpdateEvent&) noexcept +{ + // No periodic tasks yet; reserved for future throttling if needed. +} + +bool PlayerLocationService::InitializeDatabase() noexcept +{ + m_databasePath = ResolveLocationsDatabasePath(); + const auto dataDirectory = m_databasePath.parent_path(); + + std::error_code ec; + if (!dataDirectory.empty() && !std::filesystem::exists(dataDirectory, ec)) + { + std::filesystem::create_directories(dataDirectory, ec); + if (ec) + { + spdlog::error("PlayerLocationService: failed to create data directory '{}': {}", dataDirectory.string(), ec.message()); + return false; + } + } + + spdlog::info("PlayerLocationService: using locations database at '{}'", m_databasePath.string()); + if (sqlite3_open(m_databasePath.string().c_str(), &m_pDatabase) != SQLITE_OK) + { + spdlog::error("PlayerLocationService: unable to open database at '{}': {}", m_databasePath.string(), sqlite3_errmsg(m_pDatabase)); + return false; + } + + char* pError = nullptr; + const int execResult = sqlite3_exec(m_pDatabase, kCreateLocationsTableSql, nullptr, nullptr, &pError); + if (execResult != SQLITE_OK) + { + spdlog::error("PlayerLocationService: failed to initialize schema: {}", pError ? pError : "unknown"); + sqlite3_free(pError); + return false; + } + + EnsureColumnExists(m_pDatabase, "ALTER TABLE player_locations ADD COLUMN has_position INTEGER NOT NULL DEFAULT 0;"); + EnsureColumnExists(m_pDatabase, "ALTER TABLE player_locations ADD COLUMN ext_pos_x REAL NOT NULL DEFAULT 0;"); + EnsureColumnExists(m_pDatabase, "ALTER TABLE player_locations ADD COLUMN ext_pos_y REAL NOT NULL DEFAULT 0;"); + EnsureColumnExists(m_pDatabase, "ALTER TABLE player_locations ADD COLUMN ext_pos_z REAL NOT NULL DEFAULT 0;"); + EnsureColumnExists(m_pDatabase, "ALTER TABLE player_locations ADD COLUMN ext_cell_mod_id INTEGER NOT NULL DEFAULT 0;"); + EnsureColumnExists(m_pDatabase, "ALTER TABLE player_locations ADD COLUMN ext_cell_base_id INTEGER NOT NULL DEFAULT 0;"); + EnsureColumnExists(m_pDatabase, "ALTER TABLE player_locations ADD COLUMN ext_world_mod_id INTEGER NOT NULL DEFAULT 0;"); + EnsureColumnExists(m_pDatabase, "ALTER TABLE player_locations ADD COLUMN ext_world_base_id INTEGER NOT NULL DEFAULT 0;"); + EnsureColumnExists(m_pDatabase, "ALTER TABLE player_locations ADD COLUMN ext_last_seen INTEGER NOT NULL DEFAULT 0;"); + EnsureColumnExists(m_pDatabase, "ALTER TABLE player_locations ADD COLUMN has_exterior INTEGER NOT NULL DEFAULT 0;"); + + return true; +} + +void PlayerLocationService::ShutdownDatabase() noexcept +{ + if (m_pDatabase) + { + sqlite3_close(m_pDatabase); + m_pDatabase = nullptr; + } +} + +void PlayerLocationService::LoadFromDatabase(Player* apPlayer) noexcept +{ + if (!apPlayer || !m_pDatabase) + return; + + const auto& username = apPlayer->GetUsername(); + if (username.empty()) + return; + + constexpr const char* cSelectSql = + "SELECT player_id, endpoint, pos_x, pos_y, pos_z, cell_mod_id, cell_base_id, world_mod_id, world_base_id, last_seen, source, has_position, " + "ext_pos_x, ext_pos_y, ext_pos_z, ext_cell_mod_id, ext_cell_base_id, ext_world_mod_id, ext_world_base_id, ext_last_seen, has_exterior " + "FROM player_locations WHERE username = ?1 LIMIT 1;"; + + sqlite3_stmt* pStatement = nullptr; + if (sqlite3_prepare_v2(m_pDatabase, cSelectSql, -1, &pStatement, nullptr) != SQLITE_OK) + { + spdlog::error("PlayerLocationService: failed to prepare location lookup: {}", sqlite3_errmsg(m_pDatabase)); + return; + } + + sqlite3_bind_text(pStatement, 1, username.c_str(), -1, SQLITE_TRANSIENT); + if (sqlite3_step(pStatement) == SQLITE_ROW) + { + PlayerLocation location{}; + location.Username = username; + location.Endpoint = apPlayer->GetEndPoint(); + location.Position.x = static_cast(sqlite3_column_double(pStatement, 2)); + location.Position.y = static_cast(sqlite3_column_double(pStatement, 3)); + location.Position.z = static_cast(sqlite3_column_double(pStatement, 4)); + location.CellId.ModId = static_cast(sqlite3_column_int64(pStatement, 5)); + location.CellId.BaseId = static_cast(sqlite3_column_int64(pStatement, 6)); + location.WorldSpaceId.ModId = static_cast(sqlite3_column_int64(pStatement, 7)); + location.WorldSpaceId.BaseId = static_cast(sqlite3_column_int64(pStatement, 8)); + location.LastSeenEpoch = static_cast(sqlite3_column_int64(pStatement, 9)); + location.SourceTag = static_cast(sqlite3_column_int64(pStatement, 10)); + const bool storedHasPosition = sqlite3_column_int64(pStatement, 11) != 0; + const bool hasWorld = location.WorldSpaceId || location.CellId; + const bool hasPosData = (location.Position.x != 0.0f || location.Position.y != 0.0f || location.Position.z != 0.0f); + location.HasPosition = storedHasPosition || hasWorld || hasPosData; + location.LastExteriorPosition.x = static_cast(sqlite3_column_double(pStatement, 12)); + location.LastExteriorPosition.y = static_cast(sqlite3_column_double(pStatement, 13)); + location.LastExteriorPosition.z = static_cast(sqlite3_column_double(pStatement, 14)); + location.LastExteriorCellId.ModId = static_cast(sqlite3_column_int64(pStatement, 15)); + location.LastExteriorCellId.BaseId = static_cast(sqlite3_column_int64(pStatement, 16)); + location.LastExteriorWorldSpaceId.ModId = static_cast(sqlite3_column_int64(pStatement, 17)); + location.LastExteriorWorldSpaceId.BaseId = static_cast(sqlite3_column_int64(pStatement, 18)); + location.LastExteriorEpoch = static_cast(sqlite3_column_int64(pStatement, 19)); + const bool storedHasExterior = sqlite3_column_int64(pStatement, 20) != 0; + const bool hasExtWorld = location.LastExteriorWorldSpaceId || location.LastExteriorCellId; + const bool hasExtPos = (location.LastExteriorPosition.x != 0.0f || location.LastExteriorPosition.y != 0.0f || location.LastExteriorPosition.z != 0.0f); + location.HasExterior = storedHasExterior || hasExtWorld || hasExtPos; + if (!location.HasExterior && location.HasPosition && location.WorldSpaceId) + { + location.HasExterior = true; + location.LastExteriorPosition = location.Position; + location.LastExteriorWorldSpaceId = location.WorldSpaceId; + location.LastExteriorCellId = location.CellId; + location.LastExteriorEpoch = location.LastSeenEpoch; + } + + m_locations[apPlayer->GetId()] = std::move(location); + } + + sqlite3_finalize(pStatement); +} + +void PlayerLocationService::PersistLocation(PlayerLocation& aLocation, uint32_t aPlayerId, const char* apReason) noexcept +{ + if (!m_pDatabase || aLocation.Username.empty()) + return; + + constexpr auto cPersistInterval = std::chrono::seconds(5); + const auto now = std::chrono::steady_clock::now(); + if (apReason && std::string_view(apReason) != "force" && std::string_view(apReason) != "force-cell") + { + if (aLocation.LastPersist.time_since_epoch().count() != 0 && (now - aLocation.LastPersist) < cPersistInterval) + return; + } + + constexpr const char* cUpsertSql = + "INSERT INTO player_locations(username, player_id, endpoint, pos_x, pos_y, pos_z, cell_mod_id, cell_base_id, world_mod_id, world_base_id, last_seen, source, has_position, " + "ext_pos_x, ext_pos_y, ext_pos_z, ext_cell_mod_id, ext_cell_base_id, ext_world_mod_id, ext_world_base_id, ext_last_seen, has_exterior) " + "VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22) " + "ON CONFLICT(username) DO UPDATE SET " + "player_id = excluded.player_id, endpoint = excluded.endpoint, pos_x = excluded.pos_x, pos_y = excluded.pos_y, pos_z = excluded.pos_z, " + "cell_mod_id = excluded.cell_mod_id, cell_base_id = excluded.cell_base_id, world_mod_id = excluded.world_mod_id, world_base_id = excluded.world_base_id, " + "last_seen = excluded.last_seen, source = excluded.source, has_position = excluded.has_position, " + "ext_pos_x = excluded.ext_pos_x, ext_pos_y = excluded.ext_pos_y, ext_pos_z = excluded.ext_pos_z, " + "ext_cell_mod_id = excluded.ext_cell_mod_id, ext_cell_base_id = excluded.ext_cell_base_id, " + "ext_world_mod_id = excluded.ext_world_mod_id, ext_world_base_id = excluded.ext_world_base_id, " + "ext_last_seen = excluded.ext_last_seen, has_exterior = excluded.has_exterior;"; + + sqlite3_stmt* pStatement = nullptr; + if (sqlite3_prepare_v2(m_pDatabase, cUpsertSql, -1, &pStatement, nullptr) != SQLITE_OK) + { + spdlog::error("PlayerLocationService: failed to prepare location upsert: {}", sqlite3_errmsg(m_pDatabase)); + return; + } + + sqlite3_bind_text(pStatement, 1, aLocation.Username.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_int64(pStatement, 2, static_cast(aPlayerId)); + if (!aLocation.Endpoint.empty()) + sqlite3_bind_text(pStatement, 3, aLocation.Endpoint.c_str(), -1, SQLITE_TRANSIENT); + else + sqlite3_bind_null(pStatement, 3); + sqlite3_bind_double(pStatement, 4, aLocation.Position.x); + sqlite3_bind_double(pStatement, 5, aLocation.Position.y); + sqlite3_bind_double(pStatement, 6, aLocation.Position.z); + sqlite3_bind_int(pStatement, 7, static_cast(aLocation.CellId.ModId)); + sqlite3_bind_int(pStatement, 8, static_cast(aLocation.CellId.BaseId)); + sqlite3_bind_int(pStatement, 9, static_cast(aLocation.WorldSpaceId.ModId)); + sqlite3_bind_int(pStatement, 10, static_cast(aLocation.WorldSpaceId.BaseId)); + sqlite3_bind_int64(pStatement, 11, static_cast(aLocation.LastSeenEpoch)); + sqlite3_bind_int(pStatement, 12, static_cast(aLocation.SourceTag)); + sqlite3_bind_int(pStatement, 13, aLocation.HasPosition ? 1 : 0); + sqlite3_bind_double(pStatement, 14, aLocation.LastExteriorPosition.x); + sqlite3_bind_double(pStatement, 15, aLocation.LastExteriorPosition.y); + sqlite3_bind_double(pStatement, 16, aLocation.LastExteriorPosition.z); + sqlite3_bind_int(pStatement, 17, static_cast(aLocation.LastExteriorCellId.ModId)); + sqlite3_bind_int(pStatement, 18, static_cast(aLocation.LastExteriorCellId.BaseId)); + sqlite3_bind_int(pStatement, 19, static_cast(aLocation.LastExteriorWorldSpaceId.ModId)); + sqlite3_bind_int(pStatement, 20, static_cast(aLocation.LastExteriorWorldSpaceId.BaseId)); + sqlite3_bind_int64(pStatement, 21, static_cast(aLocation.LastExteriorEpoch)); + sqlite3_bind_int(pStatement, 22, aLocation.HasExterior ? 1 : 0); + + if (sqlite3_step(pStatement) != SQLITE_DONE) + { + spdlog::error("PlayerLocationService: failed to persist location for {}: {}", aLocation.Username.c_str(), sqlite3_errmsg(m_pDatabase)); + } + + sqlite3_finalize(pStatement); + aLocation.LastPersist = now; +} diff --git a/Code/server/Services/PlayerLocationService.h b/Code/server/Services/PlayerLocationService.h new file mode 100644 index 000000000..e9899f1b4 --- /dev/null +++ b/Code/server/Services/PlayerLocationService.h @@ -0,0 +1,82 @@ +#pragma once + +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include + +struct Player; +struct PlayerJoinEvent; +struct PlayerLeaveEvent; +struct UpdateEvent; +struct World; + +struct PlayerLocation +{ + enum class Source : uint8_t + { + Unknown = 0, + Movement = 1, + ClientReport = 2, + CellChange = 3, + Respawn = 4, + Teleport = 5 + }; + + Vector3_NetQuantize Position{}; + GameId WorldSpaceId{}; + GameId CellId{}; + uint64_t LastSeenEpoch{0}; + Source SourceTag{Source::Unknown}; + bool HasPosition{false}; + Vector3_NetQuantize LastExteriorPosition{}; + GameId LastExteriorWorldSpaceId{}; + GameId LastExteriorCellId{}; + uint64_t LastExteriorEpoch{0}; + bool HasExterior{false}; + TiltedPhoques::String Username{}; + TiltedPhoques::String Endpoint{}; + + std::chrono::steady_clock::time_point LastPersist{}; +}; + +struct PlayerLocationService +{ + PlayerLocationService(World& aWorld, entt::dispatcher& aDispatcher) noexcept; + ~PlayerLocationService() noexcept; + + TP_NOCOPYMOVE(PlayerLocationService); + + void UpdateLocation(Player* apPlayer, const Vector3_NetQuantize& acPos, const GameId& acWorldSpaceId, const GameId& acCellId, + PlayerLocation::Source aSource, bool aForcePersist = false) noexcept; + void UpdateCell(Player* apPlayer, const GameId& acWorldSpaceId, const GameId& acCellId, PlayerLocation::Source aSource, bool aForcePersist = false) noexcept; + bool TryGetLocation(uint32_t aPlayerId, PlayerLocation& aOut) const noexcept; + +private: + void OnPlayerJoin(const PlayerJoinEvent& aEvent) noexcept; + void OnPlayerLeave(const PlayerLeaveEvent& aEvent) noexcept; + void OnUpdate(const UpdateEvent& aEvent) noexcept; + + bool InitializeDatabase() noexcept; + void ShutdownDatabase() noexcept; + void LoadFromDatabase(Player* apPlayer) noexcept; + void PersistLocation(PlayerLocation& aLocation, uint32_t aPlayerId, const char* apReason) noexcept; + + World& m_world; + sqlite3* m_pDatabase{nullptr}; + std::filesystem::path m_databasePath{}; + + std::unordered_map m_locations; + + entt::scoped_connection m_playerJoinConnection; + entt::scoped_connection m_playerLeaveConnection; + entt::scoped_connection m_updateConnection; +}; diff --git a/Code/server/Services/PlayerService.cpp b/Code/server/Services/PlayerService.cpp index eefdb1321..c018e1fcc 100644 --- a/Code/server/Services/PlayerService.cpp +++ b/Code/server/Services/PlayerService.cpp @@ -3,9 +3,13 @@ #include "Events/PlayerLeaveCellEvent.h" #include + +#include + #include #include + #include #include #include @@ -17,6 +21,14 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include #include namespace @@ -31,6 +43,9 @@ PlayerService::PlayerService(World& aWorld, entt::dispatcher& aDispatcher) noexc , m_exteriorCellEnterConnection(aDispatcher.sink>().connect<&PlayerService::HandleExteriorCellEnter>(this)) , m_playerRespawnConnection(aDispatcher.sink>().connect<&PlayerService::OnPlayerRespawnRequest>(this)) , m_playerLevelConnection(aDispatcher.sink>().connect<&PlayerService::OnPlayerLevelRequest>(this)) + , m_partyMemberDownedConnection(aDispatcher.sink>().connect<&PlayerService::OnPartyMemberDownedRequest>(this)) + , m_playerProfileImageUpdateConnection(aDispatcher.sink>().connect<&PlayerService::OnPlayerProfileImageUpdate>(this)) + , m_playerActorNameUpdateConnection(aDispatcher.sink>().connect<&PlayerService::OnPlayerActorNameUpdate>(this)) { } @@ -56,6 +71,7 @@ void PlayerService::HandleGridCellShift(const PacketEvent& CellIdComponent cell = CellIdComponent{message.PlayerCell, message.WorldSpaceId, message.CenterCoords}; pPlayer->SetCellComponent(cell); + m_world.GetPlayerLocationService().UpdateCell(pPlayer, cell.WorldSpaceId, cell.Cell, PlayerLocation::Source::CellChange); m_world.GetDispatcher().trigger(PlayerLeaveCellEvent(oldCell)); @@ -99,8 +115,18 @@ void PlayerService::HandleExteriorCellEnter(const PacketEventSetCellComponent(cell); + m_world.GetPlayerLocationService().UpdateCell(pPlayer, cell.WorldSpaceId, cell.Cell, PlayerLocation::Source::CellChange); SendPlayerCellChanged(pPlayer); + + if (auto* pParty = m_world.GetPartyService().GetPlayerParty(pPlayer)) + { + if (pParty->LeaderPlayerId == pPlayer->GetId()) + { + if (pParty->Options.LockPartyToLeaderCell()) + m_world.GetPartyService().ScheduleLeaderCellLockNotify(*pParty, pPlayer); + } + } } } @@ -114,6 +140,7 @@ void PlayerService::HandleInteriorCellEnter(const PacketEventSetCellComponent(cell); + m_world.GetPlayerLocationService().UpdateCell(pPlayer, cell.WorldSpaceId, cell.Cell, PlayerLocation::Source::CellChange); m_world.GetDispatcher().trigger(PlayerLeaveCellEvent(oldCell)); @@ -145,6 +172,15 @@ void PlayerService::HandleInteriorCellEnter(const PacketEventLeaderPlayerId == pPlayer->GetId()) + { + if (pParty->Options.LockPartyToLeaderCell()) + m_world.GetPartyService().ScheduleLeaderCellLockNotify(*pParty, pPlayer); + } + } } void PlayerService::OnPlayerRespawnRequest(const PacketEvent& acMessage) const noexcept @@ -155,9 +191,23 @@ void PlayerService::OnPlayerRespawnRequest(const PacketEvent(*entity)) + { + pAnimationComponent->Actions.clear(); + pAnimationComponent->CurrentAction = {}; + pAnimationComponent->ActionsReplayCache.Clear(); + } + auto view = m_world.view(); - const auto it = view.find(static_cast(*character)); + const auto it = view.find(*entity); if (it != view.end()) { @@ -176,12 +226,11 @@ void PlayerService::OnPlayerRespawnRequest(const PacketEventSendToPlayersInRange(notifyInventoryChanges, *character, acMessage.GetSender())) + if (!GameServer::Get()->SendToPlayersInRange(notifyInventoryChanges, *entity, acMessage.GetSender())) spdlog::error("{}: SendToPlayersInRange failed", __FUNCTION__); // ...and instead, send NotifyPlayerRespawn so that the client can print a message. @@ -193,9 +242,9 @@ void PlayerService::OnPlayerRespawnRequest(const PacketEventSendToPlayersInRange(notifyRespawn, *character, acMessage.GetSender())) + if (!GameServer::Get()->SendToPlayersInRange(notifyRespawn, *entity, acMessage.GetSender())) spdlog::error("{}: SendToPlayersInRange failed", __FUNCTION__); } } @@ -210,3 +259,104 @@ void PlayerService::OnPlayerLevelRequest(const PacketEvent& GameServer::Get()->SendToPlayers(notify, acMessage.pPlayer); } + +void PlayerService::OnPartyMemberDownedRequest(const PacketEvent& acMessage) const noexcept +{ + auto* pPlayer = acMessage.pPlayer; + if (!pPlayer) + return; + + NotifyPartyMemberDowned notify{}; + notify.PlayerId = pPlayer->GetId(); + notify.IsDowned = acMessage.Packet.IsDowned; + + if (auto character = pPlayer->GetCharacter()) + { + notify.ServerId = World::ToInteger(*character); + if (const auto* pMovement = m_world.try_get(*character)) + { + notify.PositionX = pMovement->Position.x; + notify.PositionY = pMovement->Position.y; + notify.PositionZ = pMovement->Position.z; + } + } + else + { + notify.ServerId = 0; + } + + const auto& cellComponent = pPlayer->GetCellComponent(); + notify.WorldSpaceId = cellComponent.WorldSpaceId; + notify.CellId = cellComponent.Cell; + + GameServer::Get()->SendToParty(notify, pPlayer->GetParty(), acMessage.GetSender()); +} + +void PlayerService::OnPlayerProfileImageUpdate(const PacketEvent& acMessage) const noexcept +{ + auto* pPlayer = acMessage.pPlayer; + if (!pPlayer) + return; + + const auto& imageData = acMessage.Packet.ImageData; + + constexpr size_t cMaxAvatarBytes = 256u * 1024u; + if (imageData.size() > cMaxAvatarBytes) + { + spdlog::warn("[PlayerService] Avatar upload from player {} exceeded {} bytes ({} received)", pPlayer->GetId(), cMaxAvatarBytes, imageData.size()); + return; + } + + auto sanitized = imageData; + if (!sanitized.empty()) + { + if (sanitized.rfind("data:image", 0) != 0) + { + spdlog::warn("[PlayerService] Avatar upload from player {} rejected due to invalid data URI prefix", pPlayer->GetId()); + return; + } + } + + pPlayer->SetAvatar(std::move(sanitized)); + + NotifyPlayerProfileImage notify{}; + notify.PlayerId = pPlayer->GetId(); + notify.Avatar = pPlayer->GetAvatar(); + + GameServer::Get()->SendToPlayers(notify); + + if (m_world.ctx().contains()) + { + auto& loginService = m_world.ctx().at(); + loginService.SetAvatar(pPlayer->GetUsername(), pPlayer->GetAvatar()); + } +} + +void PlayerService::OnPlayerActorNameUpdate(const PacketEvent& acMessage) const noexcept +{ + auto* pPlayer = acMessage.pPlayer; + if (!pPlayer) + return; + + const auto& actorName = acMessage.Packet.ActorName; + if (actorName.empty()) + return; + + constexpr size_t kMaxActorName = 128u; + if (actorName.size() > kMaxActorName) + { + spdlog::warn("[PlayerService] Actor name update from player {} exceeded {} bytes ({} received)", pPlayer->GetId(), kMaxActorName, actorName.size()); + return; + } + + if (actorName == pPlayer->GetActorName()) + return; + + pPlayer->SetActorName(actorName); + + NotifyPlayerActorName notify{}; + notify.PlayerId = pPlayer->GetId(); + notify.ActorName = pPlayer->GetActorName(); + + GameServer::Get()->SendToPlayers(notify); +} diff --git a/Code/server/Services/PlayerService.h b/Code/server/Services/PlayerService.h index 9ac735176..e280e9697 100644 --- a/Code/server/Services/PlayerService.h +++ b/Code/server/Services/PlayerService.h @@ -8,6 +8,9 @@ struct EnterInteriorCellRequest; struct EnterExteriorCellRequest; struct PlayerRespawnRequest; struct PlayerLevelRequest; +struct PartyMemberDownedRequest; +struct PlayerProfileImageUpdateRequest; +struct PlayerActorNameUpdateRequest; /** * @brief Handles player specific actions that might change the information needed by other clients about that player. @@ -25,6 +28,9 @@ struct PlayerService void HandleInteriorCellEnter(const PacketEvent& acMessage) const noexcept; void OnPlayerRespawnRequest(const PacketEvent& acMessage) const noexcept; void OnPlayerLevelRequest(const PacketEvent& acMessage) const noexcept; + void OnPartyMemberDownedRequest(const PacketEvent& acMessage) const noexcept; + void OnPlayerProfileImageUpdate(const PacketEvent& acMessage) const noexcept; + void OnPlayerActorNameUpdate(const PacketEvent& acMessage) const noexcept; private: World& m_world; @@ -34,4 +40,7 @@ struct PlayerService entt::scoped_connection m_interiorCellEnterConnection; entt::scoped_connection m_playerRespawnConnection; entt::scoped_connection m_playerLevelConnection; + entt::scoped_connection m_partyMemberDownedConnection; + entt::scoped_connection m_playerProfileImageUpdateConnection; + entt::scoped_connection m_playerActorNameUpdateConnection; }; diff --git a/Code/server/Services/StringCacheService.cpp b/Code/server/Services/StringCacheService.cpp index 629d41e3c..effeae1e0 100644 --- a/Code/server/Services/StringCacheService.cpp +++ b/Code/server/Services/StringCacheService.cpp @@ -27,8 +27,20 @@ void StringCacheService::HandleUpdate(const UpdateEvent&) const noexcept if (!stringCache.ProcessDirty()) return; + TiltedPhoques::Vector players; + players.reserve(m_world.GetPlayerManager().Count()); + for (const auto pPlayer : m_world.GetPlayerManager()) { + players.push_back(pPlayer->GetConnectionId()); + } + + for (auto connectionId : players) + { + auto pPlayer = m_world.GetPlayerManager().GetByConnectionId(connectionId); + if (!pPlayer) + continue; + auto startId = pPlayer->GetStringCacheId(); auto update = stringCache.Serialize(startId); pPlayer->SetStringCacheId(startId); diff --git a/Code/server/Services/SyncModeService.cpp b/Code/server/Services/SyncModeService.cpp new file mode 100644 index 000000000..0af95e6c2 --- /dev/null +++ b/Code/server/Services/SyncModeService.cpp @@ -0,0 +1,71 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +SyncModeService::SyncModeService(World& aWorld, entt::dispatcher& aDispatcher) noexcept + : m_world(aWorld) +{ + m_playerJoinConnection = aDispatcher.sink().connect<&SyncModeService::OnPlayerJoin>(this); + m_playerLeaveConnection = aDispatcher.sink().connect<&SyncModeService::OnPlayerLeave>(this); + m_requestConnection = aDispatcher.sink>().connect<&SyncModeService::OnRequestSetSyncMode>(this); +} + +void SyncModeService::OnPlayerJoin(const PlayerJoinEvent& acEvent) noexcept +{ + SendSnapshot(acEvent.pPlayer); + + // Inform other players about the newcomer (default mode is Normal unless overridden later). + BroadcastMode(acEvent.pPlayer->GetId(), acEvent.pPlayer->GetSyncMode(), acEvent.pPlayer); +} + +void SyncModeService::OnPlayerLeave(const PlayerLeaveEvent&) noexcept +{ + // No state to cleanup yet; sync mode lives on the Player object. +} + +void SyncModeService::OnRequestSetSyncMode(const PacketEvent& acEvent) noexcept +{ + Player* const pPlayer = acEvent.pPlayer; + if (!pPlayer) + return; + + const SyncMode newMode = acEvent.Packet.Mode; + if (pPlayer->GetSyncMode() == newMode) + return; + + pPlayer->SetSyncMode(newMode); + + BroadcastMode(pPlayer->GetId(), newMode); +} + +void SyncModeService::BroadcastMode(const uint32_t aPlayerId, const SyncMode aMode, Player* apIgnoredPlayer) const noexcept +{ + NotifyPlayerSyncMode notify{}; + notify.PlayerId = aPlayerId; + notify.Mode = aMode; + + GameServer::Get()->SendToPlayers(notify, apIgnoredPlayer); +} + +void SyncModeService::SendSnapshot(Player* apPlayer) const noexcept +{ + if (!apPlayer) + return; + + NotifyPlayerSyncMode notify{}; + + for (Player* pPlayer : m_world.GetPlayerManager()) + { + notify.PlayerId = pPlayer->GetId(); + notify.Mode = pPlayer->GetSyncMode(); + apPlayer->Send(notify); + } +} diff --git a/Code/server/Services/SyncModeService.h b/Code/server/Services/SyncModeService.h new file mode 100644 index 000000000..980810d30 --- /dev/null +++ b/Code/server/Services/SyncModeService.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include + +struct World; +struct Player; +struct PlayerJoinEvent; +struct PlayerLeaveEvent; +struct RequestSetSyncMode; + +/** + * @brief Tracks player sync modes (Normal/Ghost) and relays updates to other clients. + */ +struct SyncModeService +{ + SyncModeService(World& aWorld, entt::dispatcher& aDispatcher) noexcept; + ~SyncModeService() noexcept = default; + + TP_NOCOPYMOVE(SyncModeService); + +private: + void OnPlayerJoin(const PlayerJoinEvent& acEvent) noexcept; + void OnPlayerLeave(const PlayerLeaveEvent& acEvent) noexcept; + void OnRequestSetSyncMode(const PacketEvent& acEvent) noexcept; + + void BroadcastMode(uint32_t aPlayerId, SyncMode aMode, Player* apIgnoredPlayer = nullptr) const noexcept; + void SendSnapshot(Player* apPlayer) const noexcept; + + World& m_world; + + entt::scoped_connection m_playerJoinConnection; + entt::scoped_connection m_playerLeaveConnection; + entt::scoped_connection m_requestConnection; +}; diff --git a/Code/server/Services/TradeService.cpp b/Code/server/Services/TradeService.cpp new file mode 100644 index 000000000..b92fdd15d --- /dev/null +++ b/Code/server/Services/TradeService.cpp @@ -0,0 +1,698 @@ +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace +{ +constexpr uint64_t kTradeInviteDurationMs = 15000; +constexpr uint32_t kTradeFinalizeCountdownMs = 4000; +constexpr uint64_t kCountdownBroadcastIntervalMs = 200; +} + +TradeService::TradeService(World& aWorld, entt::dispatcher& aDispatcher) noexcept + : m_world(aWorld) +{ + m_updateConnection = aDispatcher.sink().connect<&TradeService::OnUpdate>(this); + m_playerLeaveConnection = aDispatcher.sink().connect<&TradeService::OnPlayerLeave>(this); + m_tradeInviteConnection = aDispatcher.sink>().connect<&TradeService::OnTradeInvite>(this); + m_tradeInviteResponseConnection = aDispatcher.sink>().connect<&TradeService::OnTradeInviteResponse>(this); + m_tradeOfferUpdateConnection = aDispatcher.sink>().connect<&TradeService::OnTradeOfferUpdate>(this); + m_tradeSetReadyConnection = aDispatcher.sink>().connect<&TradeService::OnTradeSetReady>(this); + m_tradeCancelConnection = aDispatcher.sink>().connect<&TradeService::OnTradeCancel>(this); +} + +void TradeService::OnUpdate(const UpdateEvent&) noexcept +{ + const auto cCurrentTick = GameServer::Get()->GetTick(); + + auto it = std::begin(m_pendingInvites); + while (it != std::end(m_pendingInvites)) + { + if (it->second.ExpiryTick <= cCurrentTick) + { + auto* pTarget = it->first; + auto* pRequester = it->second.Requester; + + spdlog::debug("[TradeService]: Invite between {} and {} expired", pRequester ? pRequester->GetId() : 0, pTarget ? pTarget->GetId() : 0); + + if (pRequester) + SendTradeCancelled(pRequester, pTarget, TradeCancelReason::Timeout, true); + if (pTarget) + SendTradeCancelled(pTarget, pRequester, TradeCancelReason::Timeout, false); + + it = m_pendingInvites.erase(it); + } + else + { + ++it; + } + } + + TiltedPhoques::Vector finalizeSessions; + for (auto it = m_sessions.begin(); it != m_sessions.end(); ++it) + { + const uint32_t id = it->first; + auto* pSession = const_cast(&it->second); + if (!pSession || !pSession->CountdownActive) + continue; + + const uint32_t remaining = GetRemainingCountdownMs(*pSession, cCurrentTick); + if (remaining == 0) + { + pSession->CountdownActive = false; + finalizeSessions.push_back(id); + continue; + } + + if (cCurrentTick - pSession->LastCountdownBroadcastTick >= kCountdownBroadcastIntervalMs) + { + pSession->LastCountdownBroadcastTick = cCurrentTick; + SendStateUpdate(*pSession); + } + } + + for (const auto sessionId : finalizeSessions) + { + auto sessionIt = m_sessions.find(sessionId); + if (sessionIt == std::end(m_sessions)) + continue; + + FinalizeTrade(const_cast(sessionIt->second)); + } +} + +void TradeService::OnPlayerLeave(const PlayerLeaveEvent& acEvent) noexcept +{ + auto* pPlayer = acEvent.pPlayer; + if (!pPlayer) + return; + + if (auto* pSession = GetSession(pPlayer)) + { + spdlog::debug("[TradeService]: Player {} left during trade, cancelling session {}", pPlayer->GetId(), pSession->Id); + CancelSession(*pSession, TradeCancelReason::PlayerLeft, pPlayer); + } + + RemoveInviteFor(pPlayer, TradeCancelReason::PlayerLeft, pPlayer); +} + +void TradeService::OnTradeInvite(const PacketEvent& acPacket) noexcept +{ + Player* pRequester = acPacket.pPlayer; + if (!pRequester) + return; + + const auto& message = acPacket.Packet; + + Player* pTarget = m_world.GetPlayerManager().GetById(message.TargetPlayerId); + if (!pTarget || pTarget == pRequester) + { + SendTradeCancelled(pRequester, pTarget, TradeCancelReason::FailedValidation, true); + return; + } + + if (IsPlayerBusy(pRequester)) + { + SendTradeCancelled(pRequester, pTarget, TradeCancelReason::SelfBusy, true); + return; + } + + if (IsPlayerBusy(pTarget) || m_pendingInvites.find(pTarget) != std::end(m_pendingInvites)) + { + SendTradeCancelled(pRequester, pTarget, TradeCancelReason::PartnerBusy, true); + return; + } + + if (HasOutgoingInvite(pRequester)) + { + SendTradeCancelled(pRequester, pTarget, TradeCancelReason::SelfBusy, true); + return; + } + + const auto cExpiryTick = GameServer::Get()->GetTick() + kTradeInviteDurationMs; + m_pendingInvites[pTarget] = PendingInvite{pRequester, cExpiryTick}; + + spdlog::debug("[TradeService]: Player {} invited {} to trade", pRequester->GetId(), pTarget->GetId()); + + NotifyTradeInvite notify; + notify.InviterPlayerId = pRequester->GetId(); + notify.ExpiryTick = cExpiryTick; + pTarget->Send(notify); +} + +void TradeService::OnTradeInviteResponse(const PacketEvent& acPacket) noexcept +{ + Player* pResponder = acPacket.pPlayer; + if (!pResponder) + return; + + const auto& message = acPacket.Packet; + + auto it = m_pendingInvites.find(pResponder); + if (it == std::end(m_pendingInvites)) + { + SendTradeCancelled(pResponder, nullptr, TradeCancelReason::FailedValidation, false); + return; + } + + Player* pRequester = it->second.Requester; + if (!pRequester || pRequester->GetId() != message.RequesterPlayerId) + { + SendTradeCancelled(pResponder, pRequester, TradeCancelReason::FailedValidation, false); + if (pRequester) + SendTradeCancelled(pRequester, pResponder, TradeCancelReason::FailedValidation, true); + m_pendingInvites.erase(it); + return; + } + + PendingInvite invite = it->second; + m_pendingInvites.erase(it); + + if (!message.Accept) + { + spdlog::debug("[TradeService]: Player {} declined trade invite from {}", pResponder->GetId(), pRequester->GetId()); + SendTradeCancelled(pRequester, pResponder, TradeCancelReason::Declined, true); + SendTradeCancelled(pResponder, pRequester, TradeCancelReason::Declined, false); + return; + } + + if (IsPlayerBusy(pRequester) || IsPlayerBusy(pResponder)) + { + spdlog::warn("[TradeService]: Trade invite accept failed because one of the players is busy"); + SendTradeCancelled(pRequester, pResponder, TradeCancelReason::PartnerBusy, true); + SendTradeCancelled(pResponder, pRequester, TradeCancelReason::PartnerBusy, false); + return; + } + + auto* pSession = CreateSession(pRequester, pResponder); + if (!pSession) + { + spdlog::error("[TradeService]: Failed to create trade session between {} and {}", pRequester->GetId(), pResponder->GetId()); + SendTradeCancelled(pRequester, pResponder, TradeCancelReason::FailedValidation, true); + SendTradeCancelled(pResponder, pRequester, TradeCancelReason::FailedValidation, false); + return; + } + + spdlog::debug("[TradeService]: Created trade session {} between {} and {}", pSession->Id, pRequester->GetId(), pResponder->GetId()); + + SendTradeStarted(*pSession); + SendStateUpdate(*pSession); +} + +void TradeService::OnTradeOfferUpdate(const PacketEvent& acPacket) noexcept +{ + Player* pPlayer = acPacket.pPlayer; + if (!pPlayer) + return; + + TradeSession* pSession = GetSession(pPlayer); + if (!pSession) + return; + + const int32_t cIndex = GetSessionIndex(*pSession, pPlayer); + if (cIndex < 0) + return; + + const auto& message = acPacket.Packet; + + TiltedPhoques::Vector sanitized; + sanitized.reserve(message.Items.size()); + for (const auto& entry : message.Items) + { + if (entry.Count <= 0 || entry.IsQuestItem) + continue; + sanitized.push_back(entry); + } + + pSession->Offers[cIndex].Items = std::move(sanitized); + pSession->Offers[cIndex].Ready = false; + pSession->Offers[1 - cIndex].Ready = false; + ResetCountdown(*pSession); + + spdlog::debug("[TradeService]: Updated offer for player {} in session {}", pPlayer->GetId(), pSession->Id); + + SendStateUpdate(*pSession); +} + +void TradeService::OnTradeSetReady(const PacketEvent& acPacket) noexcept +{ + Player* pPlayer = acPacket.pPlayer; + if (!pPlayer) + return; + + TradeSession* pSession = GetSession(pPlayer); + if (!pSession) + return; + + const int32_t cIndex = GetSessionIndex(*pSession, pPlayer); + if (cIndex < 0) + return; + + const auto& message = acPacket.Packet; + + if (!message.Ready) + { + pSession->Offers[cIndex].Ready = false; + ResetCountdown(*pSession); + SendStateUpdate(*pSession); + return; + } + + if (!ValidateOffer(pPlayer, pSession->Offers[cIndex].Items)) + { + spdlog::warn("[TradeService]: Player {} attempted to ready invalid offer", pPlayer->GetId()); + CancelSession(*pSession, TradeCancelReason::FailedValidation, pPlayer); + return; + } + + pSession->Offers[cIndex].Ready = true; + + const bool cBothReady = pSession->Offers[0].Ready && pSession->Offers[1].Ready; + if (cBothReady) + StartCountdown(*pSession); + else + ResetCountdown(*pSession); + + SendStateUpdate(*pSession); +} + +void TradeService::OnTradeCancel(const PacketEvent& acPacket) noexcept +{ + Player* pPlayer = acPacket.pPlayer; + if (!pPlayer) + return; + + if (auto* pSession = GetSession(pPlayer)) + { + spdlog::debug("[TradeService]: Player {} cancelled trade session {}", pPlayer->GetId(), pSession->Id); + CancelSession(*pSession, TradeCancelReason::Cancelled, pPlayer); + return; + } + + RemoveInviteFor(pPlayer, TradeCancelReason::Cancelled, pPlayer); +} + +TradeService::TradeSession* TradeService::GetSession(Player* apPlayer) noexcept +{ + if (!apPlayer) + return nullptr; + + auto it = m_playerToSession.find(apPlayer); + if (it == std::end(m_playerToSession)) + return nullptr; + + auto sessionIt = m_sessions.find(it->second); + if (sessionIt == std::end(m_sessions)) + return nullptr; + + return const_cast(&sessionIt->second); +} + +const TradeService::TradeSession* TradeService::GetSession(const Player* apPlayer) const noexcept +{ + if (!apPlayer) + return nullptr; + + auto it = m_playerToSession.find(const_cast(apPlayer)); + if (it == std::end(m_playerToSession)) + return nullptr; + + auto sessionIt = m_sessions.find(it->second); + if (sessionIt == std::end(m_sessions)) + return nullptr; + + return &sessionIt->second; +} + +int32_t TradeService::GetSessionIndex(const TradeSession& aSession, const Player* apPlayer) const noexcept +{ + if (apPlayer == aSession.Players[0]) + return 0; + if (apPlayer == aSession.Players[1]) + return 1; + return -1; +} + +TradeService::TradeSession* TradeService::CreateSession(Player* apInitiator, Player* apPartner) noexcept +{ + if (!apInitiator || !apPartner) + return nullptr; + + const uint32_t cSessionId = m_nextSessionId++; + auto result = m_sessions.insert({cSessionId, TradeSession{}}); + if (!result.second) + return nullptr; + + auto* pSession = const_cast(&result.first->second); + pSession->Id = cSessionId; + pSession->Players[0] = apInitiator; + pSession->Players[1] = apPartner; + pSession->Initiator = apInitiator; + pSession->CountdownActive = false; + pSession->CountdownStartTick = 0; + pSession->CountdownDurationMs = kTradeFinalizeCountdownMs; + pSession->LastCountdownBroadcastTick = 0; + + m_playerToSession[apInitiator] = cSessionId; + m_playerToSession[apPartner] = cSessionId; + + return pSession; +} + +void TradeService::DestroySession(TradeSession& aSession) noexcept +{ + for (auto* pPlayer : aSession.Players) + { + if (pPlayer) + m_playerToSession.erase(pPlayer); + } + + m_sessions.erase(aSession.Id); +} + +void TradeService::CancelSession(TradeSession& aSession, TradeCancelReason aReason, Player* apOriginator) noexcept +{ + ResetCountdown(aSession); + TradeSession sessionCopy = aSession; + DestroySession(aSession); + + for (auto* pPlayer : sessionCopy.Players) + { + if (!pPlayer) + continue; + + Player* pPartner = (pPlayer == sessionCopy.Players[0]) ? sessionCopy.Players[1] : sessionCopy.Players[0]; + const bool cWasInitiator = (pPlayer == sessionCopy.Initiator); + SendTradeCancelled(pPlayer, pPartner, aReason, cWasInitiator); + } +} + +void TradeService::SendStateUpdate(const TradeSession& aSession) const noexcept +{ + const auto currentTick = GameServer::Get()->GetTick(); + const uint32_t remaining = GetRemainingCountdownMs(aSession, currentTick); + const uint32_t total = aSession.CountdownActive ? aSession.CountdownDurationMs : 0; + + for (int i = 0; i < 2; ++i) + { + Player* pPlayer = aSession.Players[i]; + if (!pPlayer) + continue; + + Player* pPartner = aSession.Players[1 - i]; + + NotifyTradeState notify; + notify.PartnerPlayerId = pPartner ? pPartner->GetId() : 0; + notify.SelfReady = aSession.Offers[i].Ready; + notify.PartnerReady = aSession.Offers[1 - i].Ready; + notify.SelfItems = aSession.Offers[i].Items; + notify.PartnerItems = aSession.Offers[1 - i].Items; + notify.CountdownTotalMs = total; + notify.CountdownMs = remaining; + notify.SelfInventory.clear(); + if (auto* pInventory = GetInventoryFor(pPlayer)) + notify.SelfInventory = pInventory->Entries; + + pPlayer->Send(notify); + } +} + +void TradeService::SendTradeStarted(const TradeSession& aSession) const noexcept +{ + for (int i = 0; i < 2; ++i) + { + Player* pPlayer = aSession.Players[i]; + if (!pPlayer) + continue; + + Player* pPartner = aSession.Players[1 - i]; + + NotifyTradeStarted notify; + notify.PartnerPlayerId = pPartner ? pPartner->GetId() : 0; + notify.InitiatedBySelf = (pPlayer == aSession.Initiator); + pPlayer->Send(notify); + } +} + +void TradeService::SendTradeCancelled(Player* apRecipient, Player* apPartner, TradeCancelReason aReason, bool aWasInitiator) const noexcept +{ + if (!apRecipient) + return; + + NotifyTradeCancel notify; + notify.PartnerPlayerId = apPartner ? apPartner->GetId() : 0; + notify.Reason = aReason; + notify.WasInitiator = aWasInitiator; + + apRecipient->Send(notify); +} + +void TradeService::FinalizeTrade(TradeSession& aSession) noexcept +{ + TradeSession sessionCopy = aSession; + + if (!ValidateOffer(sessionCopy.Players[0], sessionCopy.Offers[0].Items) || !ValidateOffer(sessionCopy.Players[1], sessionCopy.Offers[1].Items)) + { + spdlog::warn("[TradeService]: Validation failed at finalize for session {}", sessionCopy.Id); + CancelSession(aSession, TradeCancelReason::FailedValidation); + return; + } + + auto transferItems = [&](Player* apFrom, Player* apTo, const TiltedPhoques::Vector& aItems) + { + if (!apFrom || !apTo) + return false; + + auto fromCharacter = apFrom->GetCharacter(); + auto toCharacter = apTo->GetCharacter(); + if (!fromCharacter || !toCharacter) + return false; + + if (!m_world.valid(*fromCharacter) || !m_world.valid(*toCharacter)) + return false; + + if (!m_world.any_of(*fromCharacter) || !m_world.any_of(*toCharacter)) + return false; + + auto& fromInventory = m_world.get(*fromCharacter).Content; + auto& toInventory = m_world.get(*toCharacter).Content; + + for (const auto& item : aItems) + { + Inventory::Entry removal = item; + removal.Count = -std::abs(item.Count); + fromInventory.AddOrRemoveEntry(removal); + + NotifyInventoryChanges notifyRemoval; + notifyRemoval.ServerId = World::ToInteger(*fromCharacter); + notifyRemoval.Item = removal; + notifyRemoval.Silent = true; + if (!GameServer::Get()->SendToPlayersInRange(notifyRemoval, *fromCharacter, apFrom)) + spdlog::error("[TradeService]: Failed to broadcast inventory removal for {}", apFrom->GetId()); + apFrom->Send(notifyRemoval); + Inventory::Entry addition = item; + addition.Count = std::abs(item.Count); + toInventory.AddOrRemoveEntry(addition); + + NotifyInventoryChanges notifyAddition; + notifyAddition.ServerId = World::ToInteger(*toCharacter); + notifyAddition.Item = addition; + notifyAddition.Silent = true; + if (!GameServer::Get()->SendToPlayersInRange(notifyAddition, *toCharacter, apTo)) + spdlog::error("[TradeService]: Failed to broadcast inventory addition for {}", apTo->GetId()); + apTo->Send(notifyAddition); + } + + return true; + }; + + if (!transferItems(sessionCopy.Players[0], sessionCopy.Players[1], sessionCopy.Offers[0].Items) || + !transferItems(sessionCopy.Players[1], sessionCopy.Players[0], sessionCopy.Offers[1].Items)) + { + spdlog::error("[TradeService]: Failed to transfer items for session {}", sessionCopy.Id); + CancelSession(aSession, TradeCancelReason::FailedValidation); + return; + } + + DestroySession(aSession); + + for (int i = 0; i < 2; ++i) + { + Player* pPlayer = sessionCopy.Players[i]; + if (!pPlayer) + continue; + + Player* pPartner = sessionCopy.Players[1 - i]; + + NotifyTradeComplete notify; + notify.PartnerPlayerId = pPartner ? pPartner->GetId() : 0; + pPlayer->Send(notify); + } +} + +bool TradeService::ValidateOffer(Player* apPlayer, const TiltedPhoques::Vector& aItems) const noexcept +{ + if (aItems.empty()) + return true; + + for (const auto& item : aItems) + { + if (item.Count <= 0 || item.IsQuestItem) + return false; + + if (!HasItems(apPlayer, item)) + return false; + } + + return true; +} + +bool TradeService::HasItems(Player* apPlayer, const Inventory::Entry& aItem) const noexcept +{ + auto* pInventory = GetInventoryFor(apPlayer); + if (!pInventory) + return false; + + const auto requested = std::abs(aItem.Count); + + for (const auto& existing : pInventory->Entries) + { + if (existing.CanBeMerged(aItem) && existing.Count >= requested) + return true; + } + + return false; +} + +Inventory* TradeService::GetInventoryFor(Player* apPlayer) const noexcept +{ + if (!apPlayer) + return nullptr; + + auto character = apPlayer->GetCharacter(); + if (!character) + return nullptr; + + if (!m_world.valid(*character) || !m_world.any_of(*character)) + return nullptr; + + auto& component = m_world.get(*character); + return const_cast(&component.Content); +} + +bool TradeService::IsPlayerBusy(Player* apPlayer) const noexcept +{ + if (!apPlayer) + return false; + + if (m_playerToSession.find(const_cast(apPlayer)) != std::end(m_playerToSession)) + return true; + + if (m_pendingInvites.find(const_cast(apPlayer)) != std::end(m_pendingInvites)) + return true; + + if (HasOutgoingInvite(apPlayer)) + return true; + + return false; +} + +bool TradeService::HasOutgoingInvite(Player* apPlayer) const noexcept +{ + if (!apPlayer) + return false; + + for (const auto& [target, invite] : m_pendingInvites) + { + if (invite.Requester == apPlayer) + return true; + } + + return false; +} + +void TradeService::RemoveInviteFor(Player* apPlayer, TradeCancelReason aReason, Player* apOriginator) noexcept +{ + if (!apPlayer) + return; + + auto targetIt = m_pendingInvites.find(apPlayer); + if (targetIt != std::end(m_pendingInvites)) + { + Player* pRequester = targetIt->second.Requester; + m_pendingInvites.erase(targetIt); + + if (pRequester) + SendTradeCancelled(pRequester, apPlayer, aReason, true); + SendTradeCancelled(apPlayer, pRequester, aReason, false); + return; + } + + auto it = std::begin(m_pendingInvites); + while (it != std::end(m_pendingInvites)) + { + if (it->second.Requester == apPlayer) + { + Player* pTarget = it->first; + SendTradeCancelled(pTarget, apPlayer, aReason, false); + SendTradeCancelled(apPlayer, pTarget, aReason, true); + it = m_pendingInvites.erase(it); + } + else + { + ++it; + } + } +} + +void TradeService::StartCountdown(TradeSession& aSession) noexcept +{ + const uint64_t currentTick = GameServer::Get()->GetTick(); + aSession.CountdownActive = true; + aSession.CountdownStartTick = currentTick; + if (aSession.CountdownDurationMs == 0) + aSession.CountdownDurationMs = kTradeFinalizeCountdownMs; + aSession.LastCountdownBroadcastTick = currentTick; +} + +void TradeService::ResetCountdown(TradeSession& aSession) noexcept +{ + aSession.CountdownActive = false; + aSession.CountdownStartTick = 0; + aSession.LastCountdownBroadcastTick = 0; +} + +uint32_t TradeService::GetRemainingCountdownMs(const TradeSession& aSession, uint64_t aCurrentTick) const noexcept +{ + if (!aSession.CountdownActive || aSession.CountdownDurationMs == 0) + return 0; + + const uint64_t endTick = aSession.CountdownStartTick + aSession.CountdownDurationMs; + if (aCurrentTick >= endTick) + return 0; + + return static_cast(endTick - aCurrentTick); +} diff --git a/Code/server/Services/TradeService.h b/Code/server/Services/TradeService.h new file mode 100644 index 000000000..cd066a5bd --- /dev/null +++ b/Code/server/Services/TradeService.h @@ -0,0 +1,98 @@ +#pragma once + +#include + +#include +#include + +struct World; +struct UpdateEvent; +struct PlayerLeaveEvent; +struct TradeInviteRequest; +struct TradeInviteResponseRequest; +struct TradeOfferUpdateRequest; +struct TradeSetReadyRequest; +struct TradeCancelRequest; + +struct Player; + +/** + * @brief Handles player to player trading sessions. + */ +class TradeService +{ +public: + TradeService(World& aWorld, entt::dispatcher& aDispatcher) noexcept; + ~TradeService() noexcept = default; + + TP_NOCOPYMOVE(TradeService); + +protected: + void OnUpdate(const UpdateEvent& acEvent) noexcept; + void OnPlayerLeave(const PlayerLeaveEvent& acEvent) noexcept; + void OnTradeInvite(const PacketEvent& acPacket) noexcept; + void OnTradeInviteResponse(const PacketEvent& acPacket) noexcept; + void OnTradeOfferUpdate(const PacketEvent& acPacket) noexcept; + void OnTradeSetReady(const PacketEvent& acPacket) noexcept; + void OnTradeCancel(const PacketEvent& acPacket) noexcept; + +private: + struct TradeOffer + { + TiltedPhoques::Vector Items; + bool Ready{false}; + }; + + struct TradeSession + { + uint32_t Id{0}; + Player* Players[2]{nullptr, nullptr}; + Player* Initiator{nullptr}; + TradeOffer Offers[2]; + bool CountdownActive{false}; + uint64_t CountdownStartTick{0}; + uint32_t CountdownDurationMs{0}; + uint64_t LastCountdownBroadcastTick{0}; + }; + + struct PendingInvite + { + Player* Requester{nullptr}; + uint64_t ExpiryTick{0}; + }; + + TradeSession* GetSession(Player* apPlayer) noexcept; + const TradeSession* GetSession(const Player* apPlayer) const noexcept; + int32_t GetSessionIndex(const TradeSession& aSession, const Player* apPlayer) const noexcept; + TradeSession* CreateSession(Player* apInitiator, Player* apPartner) noexcept; + void DestroySession(TradeSession& aSession) noexcept; + void CancelSession(TradeSession& aSession, TradeCancelReason aReason, Player* apOriginator = nullptr) noexcept; + void SendStateUpdate(const TradeSession& aSession) const noexcept; + void SendTradeStarted(const TradeSession& aSession) const noexcept; + void SendTradeCancelled(Player* apRecipient, Player* apPartner, TradeCancelReason aReason, bool aWasInitiator) const noexcept; + void FinalizeTrade(TradeSession& aSession) noexcept; + bool ValidateOffer(Player* apPlayer, const TiltedPhoques::Vector& aItems) const noexcept; + bool HasItems(Player* apPlayer, const Inventory::Entry& aItem) const noexcept; + Inventory* GetInventoryFor(Player* apPlayer) const noexcept; + bool IsPlayerBusy(Player* apPlayer) const noexcept; + bool HasOutgoingInvite(Player* apPlayer) const noexcept; + void RemoveInviteFor(Player* apPlayer, TradeCancelReason aReason, Player* apOriginator = nullptr) noexcept; + void StartCountdown(TradeSession& aSession) noexcept; + void ResetCountdown(TradeSession& aSession) noexcept; + uint32_t GetRemainingCountdownMs(const TradeSession& aSession, uint64_t aCurrentTick) const noexcept; + + World& m_world; + + TiltedPhoques::Map m_sessions; + TiltedPhoques::Map m_playerToSession; + TiltedPhoques::Map m_pendingInvites; + uint32_t m_nextSessionId{1}; + + entt::scoped_connection m_updateConnection; + entt::scoped_connection m_playerLeaveConnection; + entt::scoped_connection m_tradeInviteConnection; + entt::scoped_connection m_tradeInviteResponseConnection; + entt::scoped_connection m_tradeOfferUpdateConnection; + entt::scoped_connection m_tradeSetReadyConnection; + entt::scoped_connection m_tradeCancelConnection; +}; diff --git a/Code/server/Services/VoteTimeService.h b/Code/server/Services/VoteTimeService.h new file mode 100644 index 000000000..47f06fe8b --- /dev/null +++ b/Code/server/Services/VoteTimeService.h @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +struct World; +struct SendChatMessageRequest; +struct Player; + +// Simple unanimous vote-to-set-time feature, initiated via chat command. +// Commands (global/party/local chat all acceptable): +// /votetime HH[:MM] -> starts a vote +// /yes -> vote yes on the active vote +// /no -> vote no (aborts) +// Vote requires 100% Yes from the players online at initiation time. +// Vote times out after a short period (default 60s). +class VoteTimeService +{ +public: + VoteTimeService(World& aWorld, entt::dispatcher& aDispatcher) noexcept; + void HandleChatCommand(Player* player, const TiltedPhoques::String& message) noexcept; + +private: + void OnChat(const PacketEvent& aMsg) noexcept; + void OnUpdate(const UpdateEvent&) noexcept; + void OnPlayerJoin(const PlayerJoinEvent&) noexcept; // ignored for eligibility + void OnPlayerLeave(const PlayerLeaveEvent& aEvt) noexcept; + + void StartVote(uint32_t aStarterId, int aHour, int aMinute) noexcept; + void CastYes(uint32_t aPlayerId) noexcept; + void CastNo(uint32_t aPlayerId) noexcept; + void CheckAndMaybeApply() noexcept; + void BroadcastSystem(const TiltedPhoques::String& aText) const noexcept; + static TiltedPhoques::String FormatTime(int h, int m) noexcept; + + World& m_world; + + bool m_active{false}; + int m_targetHour{0}; + int m_targetMinute{0}; + uint32_t m_starterId{0}; + std::chrono::steady_clock::time_point m_deadline{}; + + // Snapshot of eligible players (IDs) at start time + std::unordered_set m_eligible{}; + std::unordered_set m_yes{}; + std::unordered_set m_no{}; + + entt::scoped_connection m_chatConn; + entt::scoped_connection m_updateConn; + entt::scoped_connection m_joinConn; + entt::scoped_connection m_leaveConn; +}; diff --git a/Code/server/World.cpp b/Code/server/World.cpp index f622b9771..9223222aa 100644 --- a/Code/server/World.cpp +++ b/Code/server/World.cpp @@ -6,23 +6,28 @@ #include #include #include -#include #include +#include #include +#include +#include #include #include #include #include +#include #include #include #include +#include +#include +#include +#include #include World::World() { - m_spAdminService = std::make_shared(*this, m_dispatcher); - spdlog::default_logger()->sinks().push_back(std::static_pointer_cast(m_spAdminService)); ctx().emplace(*this, m_dispatcher); ctx().emplace(*this, m_dispatcher); @@ -32,15 +37,23 @@ World::World() ctx().emplace(*this, m_dispatcher); ctx().emplace(*this, m_dispatcher); ctx().emplace(*this, m_dispatcher); + ctx().emplace(*this, m_dispatcher); ctx().emplace(*this, m_dispatcher); ctx().emplace(*this, m_dispatcher); + ctx().emplace(*this, m_dispatcher); ctx().emplace(*this, m_dispatcher); + ctx().emplace(*this, m_dispatcher); + ctx().emplace(*this, m_dispatcher); + ctx().emplace(*this, m_dispatcher); ctx().emplace(*this, m_dispatcher); ctx().emplace(*this, m_dispatcher); ctx().emplace(*this, m_dispatcher); + ctx().emplace(*this, m_dispatcher); ctx().emplace(*this, m_dispatcher); ctx().emplace(*this, m_dispatcher); ctx().emplace(*this, m_dispatcher); + ctx().emplace(*this, m_dispatcher); + ctx().emplace(*this, m_dispatcher); ESLoader::ESLoader loader; // emplace loaded mods into modscomponent. @@ -58,3 +71,23 @@ World::~World() { m_pScriptService.reset(); } + +std::optional World::TryResolveEntity(uint32_t aServerId) noexcept +{ + const auto entity = static_cast(aServerId); + + if (!valid(entity)) + return std::nullopt; + + return entity; +} + +std::optional World::TryResolveEntity(uint32_t aServerId) const noexcept +{ + const auto entity = static_cast(aServerId); + + if (!valid(entity)) + return std::nullopt; + + return entity; +} diff --git a/Code/server/World.h b/Code/server/World.h index 2766ac48a..b7ea7e845 100644 --- a/Code/server/World.h +++ b/Code/server/World.h @@ -1,16 +1,22 @@ #pragma once -#include "Services/AdminService.h" - #include #include #include #include #include +#include #include +#include +#include +#include + +#include #include "Game/PlayerManager.h" +#include + namespace ESLoader { struct RecordCollection; @@ -29,8 +35,16 @@ struct World : entt::registry const CharacterService& GetCharacterService() const noexcept { return ctx().at(); } PlayerService& GetPlayerService() noexcept { return ctx().at(); } const PlayerService& GetPlayerService() const noexcept { return ctx().at(); } + AdminService& GetAdminService() noexcept { return ctx().at(); } + const AdminService& GetAdminService() const noexcept { return ctx().at(); } + ChatCommandService& GetChatCommandService() noexcept { return ctx().at(); } + const ChatCommandService& GetChatCommandService() const noexcept { return ctx().at(); } + PlayerLocationService& GetPlayerLocationService() noexcept { return ctx().at(); } + const PlayerLocationService& GetPlayerLocationService() const noexcept { return ctx().at(); } PartyService& GetPartyService() noexcept { return ctx().at(); } const PartyService& GetPartyService() const noexcept { return ctx().at(); } + TradeService& GetTradeService() noexcept { return ctx().at(); } + const TradeService& GetTradeService() const noexcept { return ctx().at(); } CalendarService& GetCalendarService() noexcept { return ctx().at(); } const CalendarService& GetCalendarService() const noexcept { return ctx().at(); } QuestService& GetQuestService() noexcept { return ctx().at(); } @@ -45,11 +59,12 @@ struct World : entt::registry const ESLoader::RecordCollection* GetRecordCollection() const noexcept { return m_recordCollection.get(); } [[nodiscard]] static uint32_t ToInteger(entt::entity aEntity) { return to_integral(aEntity); } + [[nodiscard]] std::optional TryResolveEntity(uint32_t aServerId) noexcept; + [[nodiscard]] std::optional TryResolveEntity(uint32_t aServerId) const noexcept; private: entt::dispatcher m_dispatcher; - TiltedPhoques::SharedPtr m_spAdminService; TiltedPhoques::UniquePtr m_pScriptService; PlayerManager m_playerManager; UniquePtr m_recordCollection; diff --git a/Code/server/main.cpp b/Code/server/main.cpp index 10c0ef950..94adc65a0 100644 --- a/Code/server/main.cpp +++ b/Code/server/main.cpp @@ -2,6 +2,8 @@ #include "GameServer.h" #include +#include +#include #ifdef _WIN32 #define GS_EXPORT __declspec(dllexport) @@ -12,6 +14,39 @@ namespace { constexpr char kBuildTag[]{BUILD_BRANCH "@" BUILD_COMMIT}; + +using UiLogCallback = void (*)(const char*); +UiLogCallback s_uiLogCallback = nullptr; + +class UiCallbackSink : public spdlog::sinks::base_sink +{ +public: + explicit UiCallbackSink(UiLogCallback aCallback) + : m_callback(aCallback) + { + set_pattern("[%l] %v"); + set_level(spdlog::level::trace); + } + +protected: + void sink_it_(const spdlog::details::log_msg& msg) override + { + if (!m_callback) + return; + + spdlog::memory_buf_t formatted; + formatter_->format(msg, formatted); + + m_callback(fmt::to_string(formatted).c_str()); + } + + void flush_() override {} + +private: + UiLogCallback m_callback; +}; + +spdlog::sink_ptr s_uiSink; } // namespace struct GameServerInstance final : IGameServerInstance @@ -30,6 +65,8 @@ struct GameServerInstance final : IGameServerInstance bool IsListening() override; bool IsRunning() override; void Update() override; + Console::ConsoleRegistry::ExecutionResult ExecuteConsoleCommand(const TiltedPhoques::String& aCommand) override; + void GetStatus(ServerStatusSnapshot& aOutStatus) const override; private: GameServer m_gameServer; @@ -61,6 +98,17 @@ void GameServerInstance::Update() m_gameServer.Update(); } +Console::ConsoleRegistry::ExecutionResult GameServerInstance::ExecuteConsoleCommand(const TiltedPhoques::String& aCommand) +{ + return m_gameServer.ExecuteConsoleCommand(aCommand); +} + +void GameServerInstance::GetStatus(ServerStatusSnapshot& aOutStatus) const +{ + m_gameServer.GetStatusSnapshot(aOutStatus); +} + + // NOTE(Vince): For now we use this to compare the dll to the server. GS_EXPORT const char* GetBuildTag() { @@ -106,6 +154,24 @@ GS_EXPORT void RegisterLogger(std::shared_ptr aLogger) // #endif } +GS_EXPORT void SetUiLogCallback(void (*aCallback)(const char*)) +{ + s_uiLogCallback = aCallback; + if (!s_uiLogCallback) + return; + + if (!s_uiSink) + s_uiSink = std::static_pointer_cast(std::make_shared(s_uiLogCallback)); + + spdlog::apply_all([](const std::shared_ptr& logger) + { + if (!logger) + return; + logger->flush_on(logger->level()); + logger->sinks().push_back(s_uiSink); + }); +} + #ifdef _WIN32 // Before you think about moving logic in here... // There are significant limits on what you can safely do in a DLL entry point. See General Best Practices for specific diff --git a/Code/server/xmake.lua b/Code/server/xmake.lua index 654b0e42d..89e689038 100644 --- a/Code/server/xmake.lua +++ b/Code/server/xmake.lua @@ -1,6 +1,8 @@ local function istable(t) return type(t) == 'table' end add_requires("sol2 v3.3.0", {configs = {lua = "lua"}}) +add_requires("sqlite3") +add_requires("fmt") local function build_server() set_kind("shared") @@ -24,7 +26,6 @@ local function build_server() "ESLoader", "CrashHandler", "BaseLib", - "AdminProtocol", "TiltedConnect" ) add_packages( @@ -32,6 +33,7 @@ local function build_server() "spdlog", "hopscotch-map", "sqlite3", + "fmt", "lua", "sol2", "glm", diff --git a/Code/server_runner/DediRunner.cpp b/Code/server_runner/DediRunner.cpp index 96be083b7..4f8b46f97 100644 --- a/Code/server_runner/DediRunner.cpp +++ b/Code/server_runner/DediRunner.cpp @@ -3,9 +3,18 @@ #include +#include #include +#include #include +#include #include +#include +#include + +#ifdef _WIN32 +#include +#endif namespace { @@ -14,11 +23,12 @@ constexpr char kSettingsFileName[] = "STServer.ini"; DediRunner* s_pRunner{nullptr}; } // namespace -// imports -GS_IMPORT TiltedPhoques::UniquePtr CreateGameServer(Console::ConsoleRegistry& conReg, const std::function& aCallback); // needs to be global Console::Setting bConsole{"bConsole", "Enable the console", true}; +// imports +GS_IMPORT TiltedPhoques::UniquePtr CreateGameServer(Console::ConsoleRegistry& conReg, const std::function& aCallback); + DediRunner* GetDediRunner() noexcept { return s_pRunner; @@ -30,6 +40,7 @@ DediRunner::DediRunner(int argc, char** argv) s_pRunner = this; uv_loop_init(&m_loop); + m_startTime = std::chrono::steady_clock::now(); m_pServerInstance = std::move(CreateGameServer(m_console, [this, argc, argv]() { LoadSettings(argc, argv); })); @@ -40,6 +51,7 @@ DediRunner::DediRunner(int argc, char** argv) DediRunner::~DediRunner() { + m_consoleRunning.store(false); if (m_useIni) SaveSettingsToIni(m_console, m_SettingsPath); uv_loop_close(&m_loop); @@ -118,15 +130,20 @@ void DediRunner::PrintExecutorArrowHack() void DediRunner::RunGSThread() { - while (m_pServerInstance->IsListening()) + if (!m_pServerInstance) + return; + + while (m_pServerInstance->IsRunning()) { + m_tickCounter.fetch_add(1, std::memory_order_relaxed); m_pServerInstance->Update(); - if (bConsole) - { - uv_run(&m_loop, UV_RUN_NOWAIT); - if (m_console.Update()) - PrintExecutorArrowHack(); - } +#ifdef _WIN32 + ProcessQueuedCommands(); +#else + uv_run(&m_loop, UV_RUN_NOWAIT); +#endif + if (m_console.Update()) + PrintExecutorArrowHack(); } } @@ -135,6 +152,34 @@ void DediRunner::StartTerminalIO() spdlog::get("ConOut")->info("Server started, type /help for a list of commands."); PrintExecutorArrowHack(); +#ifdef _WIN32 + const auto inputHandle = GetStdHandle(STD_INPUT_HANDLE); + if (inputHandle != INVALID_HANDLE_VALUE) + { + DWORD mode = 0; + if (GetConsoleMode(inputHandle, &mode)) + { + mode |= ENABLE_EXTENDED_FLAGS | ENABLE_INSERT_MODE | ENABLE_QUICK_EDIT_MODE | ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT; + mode &= ~ENABLE_VIRTUAL_TERMINAL_INPUT; + SetConsoleMode(inputHandle, mode); + } + } + + m_consoleRunning.store(true); + m_consoleThread = std::thread([this]() + { + std::string line; + while (m_consoleRunning.load() && std::getline(std::cin, line)) + { + if (line.empty()) + continue; + + std::lock_guard lock(m_consoleMutex); + m_consoleQueue.emplace(line.c_str()); + } + }); + m_consoleThread.detach(); +#else uv_tty_init(&m_loop, &m_tty, 0, 1); uv_tty_set_mode(&m_tty, UV_TTY_MODE_NORMAL); @@ -144,6 +189,7 @@ void DediRunner::StartTerminalIO() m_tty.data = &ctx; uv_read_start(reinterpret_cast(&m_tty), AllocateBuffer, ReadStdin); +#endif } void DediRunner::RequestKill() @@ -162,14 +208,206 @@ void DediRunner::RequestKill() #endif } +void DediRunner::GetStatus(ServerStatusSnapshot& aOutStatus) const +{ + if (m_pServerInstance) + m_pServerInstance->GetStatus(aOutStatus); + else + { + aOutStatus.UptimeSeconds = 0; + aOutStatus.Players.clear(); + } +} + +void DediRunner::GetSettingsSnapshot(TiltedPhoques::Vector& aOut) +{ + aOut.clear(); + m_console.ForAllSettings( + [&](Console::SettingBase* setting) + { + if (!setting) + return; + + SettingSnapshot snapshot{}; + snapshot.Name = setting->name; + snapshot.Description = setting->desc; + snapshot.Type = setting->type; + snapshot.Flags = setting->flags; + + switch (setting->type) + { + case Console::SettingBase::Type::kBoolean: snapshot.Value = setting->data.as_boolean ? "true" : "false"; break; + case Console::SettingBase::Type::kInt: snapshot.Value = std::to_string(setting->data.as_int32); break; + case Console::SettingBase::Type::kUInt: snapshot.Value = std::to_string(setting->data.as_uint32); break; + case Console::SettingBase::Type::kInt64: snapshot.Value = std::to_string(setting->data.as_int64); break; + case Console::SettingBase::Type::kUInt64: snapshot.Value = std::to_string(setting->data.as_uint64); break; + case Console::SettingBase::Type::kFloat: + { + std::ostringstream stream; + stream << setting->data.as_float; + snapshot.Value = stream.str(); + break; + } + case Console::SettingBase::Type::kString: snapshot.Value = setting->c_str(); break; + default: snapshot.Value = ""; break; + } + + aOut.push_back(std::move(snapshot)); + }); +} + +bool DediRunner::GetSettingValue(const TiltedPhoques::String& aName, TiltedPhoques::String& aOut) +{ + auto* setting = m_console.FindSetting(aName.c_str()); + if (!setting) + return false; + + switch (setting->type) + { + case Console::SettingBase::Type::kBoolean: aOut = setting->data.as_boolean ? "true" : "false"; break; + case Console::SettingBase::Type::kInt: aOut = std::to_string(setting->data.as_int32); break; + case Console::SettingBase::Type::kUInt: aOut = std::to_string(setting->data.as_uint32); break; + case Console::SettingBase::Type::kInt64: aOut = std::to_string(setting->data.as_int64); break; + case Console::SettingBase::Type::kUInt64: aOut = std::to_string(setting->data.as_uint64); break; + case Console::SettingBase::Type::kFloat: + { + std::ostringstream stream; + stream << setting->data.as_float; + aOut = stream.str(); + break; + } + case Console::SettingBase::Type::kString: aOut = setting->c_str(); break; + default: aOut.clear(); break; + } + + return true; +} + +bool DediRunner::SetSettingValue(const TiltedPhoques::String& aName, const TiltedPhoques::String& aValue, TiltedPhoques::String* apError) +{ + auto* setting = m_console.FindSetting(aName.c_str()); + if (!setting) + { + if (apError) + *apError = "Setting not found."; + return false; + } + + if (setting->IsLocked()) + { + if (apError) + *apError = "Setting is locked."; + return false; + } + + switch (setting->type) + { + case Console::SettingBase::Type::kBoolean: + { + TiltedPhoques::String lowered = aValue; + std::transform(lowered.begin(), lowered.end(), lowered.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (lowered == "true" || lowered == "1") + setting->data.as_boolean = true; + else if (lowered == "false" || lowered == "0") + setting->data.as_boolean = false; + else + { + if (apError) + *apError = "Invalid boolean value."; + return false; + } + break; + } + case Console::SettingBase::Type::kInt: setting->data.as_int32 = Console::ConvertStringValue(aValue.c_str(), setting->data.as_int32); break; + case Console::SettingBase::Type::kUInt: setting->data.as_uint32 = Console::ConvertStringValue(aValue.c_str(), setting->data.as_uint32); break; + case Console::SettingBase::Type::kInt64: setting->data.as_int64 = Console::ConvertStringValue(aValue.c_str(), setting->data.as_int64); break; + case Console::SettingBase::Type::kUInt64: setting->data.as_uint64 = Console::ConvertStringValue(aValue.c_str(), setting->data.as_uint64); break; + case Console::SettingBase::Type::kFloat: setting->data.as_float = Console::ConvertStringValue(aValue.c_str(), setting->data.as_float); break; + case Console::SettingBase::Type::kString: + static_cast(setting)->StoreValue(*setting, aValue.c_str()); + break; + default: + if (apError) + *apError = "Unsupported setting type."; + return false; + } + + m_console.MarkDirty(); + if (m_useIni) + SaveSettingsToIni(m_console, m_SettingsPath); + + if (aName == "sLogLevel") + { + const auto level = spdlog::level::from_str(aValue.c_str()); + spdlog::set_level(level); + spdlog::apply_all([level](const std::shared_ptr& logger) + { + if (!logger) + return; + logger->set_level(level); + logger->flush_on(level); + }); + } + + return true; +} + +void DediRunner::QueueConsoleCommand(const TiltedPhoques::String& acCommand) +{ + std::lock_guard lock(m_consoleMutex); + m_consoleQueue.emplace(acCommand); +} + +uint32_t DediRunner::GetUptimeSeconds() const noexcept +{ + const auto elapsed = std::chrono::steady_clock::now() - m_startTime; + return static_cast(std::chrono::duration_cast(elapsed).count()); +} + +bool DediRunner::IsRunning() const noexcept +{ + return m_pServerInstance ? m_pServerInstance->IsRunning() : false; +} + +bool DediRunner::IsListening() const noexcept +{ + return m_pServerInstance ? m_pServerInstance->IsListening() : false; +} + void DediRunner::HandleConsole(const TiltedPhoques::String& acCommand) { using exr = Console::ConsoleRegistry::ExecutionResult; - exr r = m_console.TryExecuteCommand(acCommand); + if (auto conOut = spdlog::get(KCompilerStopThisBullshit)) + conOut->info("> {}", acCommand.c_str()); + + exr r = m_pServerInstance ? m_pServerInstance->ExecuteConsoleCommand(acCommand) : exr::kFailure; + if (auto conOut = spdlog::get(KCompilerStopThisBullshit)) + conOut->info("Command executed."); PrintExecutorArrowHack(); + if (r == exr::kFailure) + { + if (auto conOut = spdlog::get(KCompilerStopThisBullshit)) + conOut->error("Command failed: {}", acCommand.c_str()); + } + if (r == exr::kDirty && m_useIni) SaveSettingsToIni(m_console, m_SettingsPath); } + +void DediRunner::ProcessQueuedCommands() +{ + std::queue pending; + { + std::lock_guard lock(m_consoleMutex); + pending.swap(m_consoleQueue); + } + + while (!pending.empty()) + { + HandleConsole(pending.front()); + pending.pop(); + } +} diff --git a/Code/server_runner/DediRunner.h b/Code/server_runner/DediRunner.h index a62fbaeb3..3d68b32bd 100644 --- a/Code/server_runner/DediRunner.h +++ b/Code/server_runner/DediRunner.h @@ -3,13 +3,18 @@ // spdlog #include +#include +#include +#include #include +#include #include #include #include #include #include +#include #ifdef _WIN32 #define GS_IMPORT extern __declspec(dllimport) @@ -33,12 +38,31 @@ struct DediRunner void StartTerminalIO(); void RequestKill(); void HandleConsole(const TiltedPhoques::String& acCommand); + void GetStatus(ServerStatusSnapshot& aOutStatus) const; + struct SettingSnapshot + { + TiltedPhoques::String Name; + TiltedPhoques::String Description; + Console::SettingBase::Type Type; + Console::SettingsFlags Flags; + TiltedPhoques::String Value; + }; + void GetSettingsSnapshot(TiltedPhoques::Vector& aOut); + bool GetSettingValue(const TiltedPhoques::String& aName, TiltedPhoques::String& aOut); + bool SetSettingValue(const TiltedPhoques::String& aName, const TiltedPhoques::String& aValue, TiltedPhoques::String* apError = nullptr); + void QueueConsoleCommand(const TiltedPhoques::String& acCommand); + bool IsRunning() const noexcept; + bool IsListening() const noexcept; + uint64_t GetTickCounter() const noexcept { return m_tickCounter.load(); } + uint32_t GetUptimeSeconds() const noexcept; private: static void PrintExecutorArrowHack(); void LoadSettings(int argc, char** argv); + void ProcessQueuedCommands(); + static void ReadStdin(uv_stream_t* apStream, ssize_t aRead, const uv_buf_t* acpBuffer); static void AllocateBuffer(uv_handle_t* apHandle, size_t aSuggestedSize, uv_buf_t* apBuffer); @@ -51,6 +75,12 @@ struct DediRunner bool m_useIni{false}; Console::ConsoleRegistry m_console; TiltedPhoques::UniquePtr m_pServerInstance; + std::atomic m_consoleRunning{false}; + std::atomic m_tickCounter{0}; + std::chrono::steady_clock::time_point m_startTime; + std::mutex m_consoleMutex; + std::queue m_consoleQueue; + std::thread m_consoleThread; }; DediRunner* GetDediRunner() noexcept; diff --git a/Code/server_runner/ServerLogSink.cpp b/Code/server_runner/ServerLogSink.cpp new file mode 100644 index 000000000..c03b7489a --- /dev/null +++ b/Code/server_runner/ServerLogSink.cpp @@ -0,0 +1,101 @@ +#include "ServerLogSink.h" + +#include + +namespace +{ +constexpr size_t kMaxLogLines = 2000; + +bool HasSink(const std::shared_ptr& logger, const std::shared_ptr& sink) +{ + if (!logger || !sink) + return false; + + for (const auto& existing : logger->sinks()) + { + if (existing.get() == sink.get()) + return true; + } + return false; +} +} // namespace + +ServerLogSink::ServerLogSink(size_t aMaxLines) + : m_maxLines(aMaxLines) +{ + set_level(spdlog::level::trace); + set_pattern("%v"); +} + +void ServerLogSink::ConsumeLines(TiltedPhoques::Vector& aOut) +{ + std::lock_guard lock(mutex_); + for (; m_readIndex < m_lines.size(); ++m_readIndex) + aOut.push_back(m_lines[m_readIndex]); +} + +void ServerLogSink::PushExternalLine(const char* aLine) +{ + if (!aLine || aLine[0] == '\0') + return; + + std::lock_guard lock(mutex_); + m_lines.emplace_back(aLine); + if (m_lines.size() > m_maxLines) + { + const size_t trim = m_lines.size() - m_maxLines; + m_lines.erase(m_lines.begin(), m_lines.begin() + trim); + if (m_readIndex >= trim) + m_readIndex -= trim; + else + m_readIndex = 0; + } +} + +void ServerLogSink::sink_it_(const spdlog::details::log_msg& msg) +{ + spdlog::memory_buf_t formatted; + formatter_->format(msg, formatted); + + TiltedPhoques::String line = TiltedPhoques::String(fmt::to_string(formatted).c_str()); + + std::lock_guard lock(mutex_); + m_lines.emplace_back(std::move(line)); + if (m_lines.size() > m_maxLines) + { + const size_t trim = m_lines.size() - m_maxLines; + m_lines.erase(m_lines.begin(), m_lines.begin() + trim); + if (m_readIndex >= trim) + m_readIndex -= trim; + else + m_readIndex = 0; + } +} + +std::shared_ptr GetServerLogSink() +{ + static auto sink = std::make_shared(kMaxLogLines); + return sink; +} + +void AttachServerLogSinkToLogger(const std::shared_ptr& aLogger) +{ + const auto sink = GetServerLogSink(); + if (!aLogger || !sink) + return; + + if (HasSink(aLogger, sink)) + return; + + aLogger->set_level(spdlog::level::trace); + aLogger->flush_on(spdlog::level::trace); + aLogger->sinks().push_back(sink); +} + +void AttachServerLogSinkToAllLoggers() +{ + spdlog::apply_all([](const std::shared_ptr& logger) + { + AttachServerLogSinkToLogger(logger); + }); +} diff --git a/Code/server_runner/ServerLogSink.h b/Code/server_runner/ServerLogSink.h new file mode 100644 index 000000000..b122d6490 --- /dev/null +++ b/Code/server_runner/ServerLogSink.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include + +#include +#include + +struct ServerLogSink : spdlog::sinks::base_sink +{ + explicit ServerLogSink(size_t aMaxLines); + + void ConsumeLines(TiltedPhoques::Vector& aOut); + void PushExternalLine(const char* aLine); + +protected: + void sink_it_(const spdlog::details::log_msg& msg) override; + void flush_() override {} + +private: + size_t m_maxLines; + TiltedPhoques::Vector m_lines; + size_t m_readIndex{0}; +}; + +std::shared_ptr GetServerLogSink(); +void AttachServerLogSinkToLogger(const std::shared_ptr& aLogger); +void AttachServerLogSinkToAllLoggers(); diff --git a/Code/server_runner/ServerUi.cpp b/Code/server_runner/ServerUi.cpp new file mode 100644 index 000000000..ddd2dfcdb --- /dev/null +++ b/Code/server_runner/ServerUi.cpp @@ -0,0 +1,868 @@ +#include "ServerUi.h" + +#include "DediRunner.h" +#include "ServerLogSink.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#pragma comment(lib, "d3d11.lib") +#pragma comment(lib, "dxgi.lib") + +// According to imgui documentation we have to do it this way in order to avoid link conflicts with windows.h +extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); + + +namespace +{ +constexpr float kClearColor[4] = {0.08f, 0.09f, 0.11f, 1.0f}; +constexpr const char* kLogLevelOptions[] = {"trace", "debug", "info", "warn", "error", "critical", "off"}; + +struct LogColor +{ + bool Use{false}; + ImVec4 Color{}; +}; + +bool WantsQuit(const MSG& msg) +{ + return msg.message == WM_QUIT; +} + +LogColor GetLogColor(std::string_view line) +{ + size_t levelStart = std::string_view::npos; + size_t levelEnd = std::string_view::npos; + + if (!line.empty() && line[0] == '[') + { + levelStart = 1; + levelEnd = line.find(']', levelStart); + } + else + { + const size_t marker = line.find("] ["); + if (marker != std::string_view::npos) + { + levelStart = marker + 3; + levelEnd = line.find(']', levelStart); + } + } + + if (levelEnd == std::string_view::npos || levelEnd <= levelStart) + return {}; + + std::string level(line.substr(levelStart, levelEnd - levelStart)); + for (auto& c : level) + c = static_cast(std::tolower(c)); + + if (level == "error" || level == "critical") + return {true, ImVec4(0.96f, 0.35f, 0.30f, 1.0f)}; + if (level == "warning" || level == "warn") + return {true, ImVec4(0.98f, 0.76f, 0.35f, 1.0f)}; + if (level == "info") + return {true, ImVec4(0.85f, 0.90f, 0.98f, 1.0f)}; + if (level == "debug") + return {true, ImVec4(0.55f, 0.82f, 0.90f, 1.0f)}; + if (level == "trace") + return {true, ImVec4(0.62f, 0.65f, 0.72f, 1.0f)}; + + return {}; +} + +TiltedPhoques::String ToLowerCopy(std::string_view text) +{ + TiltedPhoques::String out(text); + std::transform(out.begin(), out.end(), out.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + return out; +} + +int FindLogLevelIndex(std::string_view level) +{ + TiltedPhoques::String lowered = ToLowerCopy(level); + for (int i = 0; i < IM_ARRAYSIZE(kLogLevelOptions); ++i) + { + if (lowered == kLogLevelOptions[i]) + return i; + } + return 2; +} + +bool MatchesFilter(std::string_view value, const TiltedPhoques::String& filterLower) +{ + if (filterLower.empty()) + return true; + return ToLowerCopy(value).find(filterLower) != TiltedPhoques::String::npos; +} + +TiltedPhoques::String GetSettingGroup(const TiltedPhoques::String& name) +{ + const auto pos = name.find(':'); + if (pos == TiltedPhoques::String::npos || pos == 0) + return "General"; + return name.substr(0, pos); +} + +TiltedPhoques::String GetSettingLabel(const TiltedPhoques::String& name) +{ + const auto pos = name.find(':'); + if (pos == TiltedPhoques::String::npos || pos + 1 >= name.size()) + return name; + return name.substr(pos + 1); +} + +int32_t ParseInt32(const TiltedPhoques::String& text, int32_t fallback) +{ + char* end = nullptr; + const long value = std::strtol(text.c_str(), &end, 10); + if (!end || end == text.c_str()) + return fallback; + return static_cast(value); +} + +int64_t ParseInt64(const TiltedPhoques::String& text, int64_t fallback) +{ + char* end = nullptr; + const long long value = std::strtoll(text.c_str(), &end, 10); + if (!end || end == text.c_str()) + return fallback; + return static_cast(value); +} + +uint32_t ParseUInt32(const TiltedPhoques::String& text, uint32_t fallback) +{ + char* end = nullptr; + const unsigned long value = std::strtoul(text.c_str(), &end, 10); + if (!end || end == text.c_str()) + return fallback; + return static_cast(value); +} + +uint64_t ParseUInt64(const TiltedPhoques::String& text, uint64_t fallback) +{ + char* end = nullptr; + const unsigned long long value = std::strtoull(text.c_str(), &end, 10); + if (!end || end == text.c_str()) + return fallback; + return static_cast(value); +} + +float ParseFloat(const TiltedPhoques::String& text, float fallback) +{ + char* end = nullptr; + const float value = std::strtof(text.c_str(), &end); + if (!end || end == text.c_str()) + return fallback; + return value; +} + +} // namespace + +ServerUi::ServerUi(DediRunner& aRunner) + : m_runner(aRunner) +{ +} + +ServerUi::~ServerUi() +{ + ImGui_ImplDX11_Shutdown(); + ImGui_ImplWin32_Shutdown(); + CleanupRenderTarget(); + CleanupDevice(); +} + +bool ServerUi::Initialize() +{ + WNDCLASSEXW wc = { + sizeof(WNDCLASSEXW), CS_CLASSDC, WndProc, 0L, 0L, GetModuleHandleW(nullptr), nullptr, nullptr, nullptr, nullptr, L"STServerUI", nullptr}; + if (!RegisterClassExW(&wc)) + { + return false; + } + + m_hwnd = CreateWindowW(wc.lpszClassName, L"Skyrim Together Server", WS_OVERLAPPEDWINDOW, 100, 100, 1280, 720, nullptr, nullptr, wc.hInstance, this); + if (!m_hwnd) + { + return false; + } + + SetWindowLongPtrW(m_hwnd, GWLP_USERDATA, reinterpret_cast(this)); + + if (!CreateDevice()) + { + return false; + } + + ShowWindow(m_hwnd, SW_SHOWDEFAULT); + UpdateWindow(m_hwnd); + + m_imguiDriver.Initialize(m_hwnd); + if (!ImGui_ImplWin32_Init(m_hwnd)) + { + return false; + } + + ImGui_ImplDX11_Init(m_device, m_deviceContext); + + ImGuiStyle& style = ImGui::GetStyle(); + style.WindowRounding = 8.0f; + style.ChildRounding = 8.0f; + style.FrameRounding = 6.0f; + style.ScrollbarRounding = 8.0f; + style.WindowPadding = ImVec2(16, 16); + style.FramePadding = ImVec2(10, 6); + style.ItemSpacing = ImVec2(12, 10); + style.ItemInnerSpacing = ImVec2(8, 6); + + ImVec4* colors = style.Colors; + colors[ImGuiCol_WindowBg] = ImVec4(0.07f, 0.08f, 0.10f, 1.00f); + colors[ImGuiCol_ChildBg] = ImVec4(0.10f, 0.11f, 0.14f, 1.00f); + colors[ImGuiCol_Border] = ImVec4(0.18f, 0.20f, 0.24f, 1.00f); + colors[ImGuiCol_TitleBg] = ImVec4(0.08f, 0.09f, 0.12f, 1.00f); + colors[ImGuiCol_TitleBgActive] = ImVec4(0.10f, 0.11f, 0.14f, 1.00f); + colors[ImGuiCol_Text] = ImVec4(0.92f, 0.93f, 0.95f, 1.00f); + colors[ImGuiCol_TextDisabled] = ImVec4(0.55f, 0.58f, 0.63f, 1.00f); + colors[ImGuiCol_FrameBg] = ImVec4(0.12f, 0.13f, 0.16f, 1.00f); + colors[ImGuiCol_FrameBgHovered] = ImVec4(0.16f, 0.18f, 0.22f, 1.00f); + colors[ImGuiCol_FrameBgActive] = ImVec4(0.18f, 0.20f, 0.24f, 1.00f); + colors[ImGuiCol_Button] = ImVec4(0.12f, 0.18f, 0.22f, 1.00f); + colors[ImGuiCol_ButtonHovered] = ImVec4(0.17f, 0.26f, 0.32f, 1.00f); + colors[ImGuiCol_ButtonActive] = ImVec4(0.15f, 0.22f, 0.28f, 1.00f); + colors[ImGuiCol_ScrollbarBg] = ImVec4(0.08f, 0.09f, 0.11f, 1.00f); + colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.20f, 0.23f, 0.28f, 1.00f); + colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.26f, 0.30f, 0.36f, 1.00f); + colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.30f, 0.34f, 0.41f, 1.00f); + m_logSink = GetServerLogSink(); + AttachServerLogSinkToAllLoggers(); + + if (auto conOut = spdlog::get(KCompilerStopThisBullshit)) + { + conOut->info("Server UI attached."); + conOut->info("Server ready. Type /help for commands."); + } + else + { + spdlog::info("Server UI attached."); + spdlog::info("Server ready. Type /help for commands."); + } + + m_logLines.emplace_back("Server UI attached (local)."); + m_logLines.emplace_back("Server ready. Type /help for commands."); + return true; +} + +void ServerUi::Run() +{ + MSG msg{}; + while (m_running.load()) + { + while (PeekMessageW(&msg, nullptr, 0U, 0U, PM_REMOVE)) + { + TranslateMessage(&msg); + DispatchMessageW(&msg); + if (WantsQuit(msg)) + { + m_running.store(false); + break; + } + } + + if (!m_running.load()) + break; + + RenderFrame(); + } +} + +void ServerUi::RequestClose() +{ + m_running.store(false); +} + +void ServerUi::RenderFrame() +{ + ImGui_ImplDX11_NewFrame(); + ImGui_ImplWin32_NewFrame(); + ImGui::NewFrame(); + + DrawUi(); + + ImGui::Render(); + const float clearColor[4] = {kClearColor[0], kClearColor[1], kClearColor[2], kClearColor[3]}; + m_deviceContext->OMSetRenderTargets(1, &m_renderTargetView, nullptr); + m_deviceContext->ClearRenderTargetView(m_renderTargetView, clearColor); + ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); + m_swapChain->Present(1, 0); +} + +void ServerUi::DrawUi() +{ + const ImVec2 display = ImGui::GetIO().DisplaySize; + ImGui::SetNextWindowPos(ImVec2(0, 0)); + ImGui::SetNextWindowSize(display); + + if (!ImGui::Begin("Server Console", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar)) + { + ImGui::End(); + return; + } + + ServerStatusSnapshot snapshot; + m_runner.GetStatus(snapshot); + + const uint32_t totalSeconds = m_runner.GetUptimeSeconds(); + const uint32_t days = totalSeconds / 86400; + const uint32_t hours = (totalSeconds % 86400) / 3600; + const uint32_t minutes = (totalSeconds % 3600) / 60; + const uint32_t seconds = totalSeconds % 60; + + if (ImGui::BeginChild("TopBar", ImVec2(0, 64), true)) + { + ImGui::TextUnformatted("Skyrim Together Server"); + ImGui::SameLine(); + ImGui::TextDisabled("Uptime %ud %02uh %02um %02us", days, hours, minutes, seconds); + ImGui::SameLine(); + ImGui::TextDisabled("Players %zu", snapshot.Players.size()); + ImGui::SameLine(); + ImGui::TextDisabled("|"); + ImGui::SameLine(); + ImGui::TextUnformatted(m_runner.IsRunning() ? "Running" : "Stopped"); + } + ImGui::EndChild(); + + const float sidebarWidth = 220.0f; + if (ImGui::BeginChild("Body", ImVec2(0, 0), false)) + { + if (ImGui::BeginChild("Sidebar", ImVec2(sidebarWidth, 0), true)) + { + ImGui::TextUnformatted("Control Panel"); + ImGui::Separator(); + auto drawSidebarButton = [&](const char* label, View view) + { + const bool selected = m_activeView == view; + if (selected) + { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.18f, 0.22f, 0.28f, 1.00f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.20f, 0.26f, 0.32f, 1.00f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.22f, 0.30f, 0.38f, 1.00f)); + } + + if (ImGui::Button(label, ImVec2(-FLT_MIN, 0))) + m_activeView = view; + + if (selected) + ImGui::PopStyleColor(3); + }; + + drawSidebarButton("Console", View::Console); + drawSidebarButton("Players", View::Players); + drawSidebarButton("Settings", View::Settings); + } + ImGui::EndChild(); + + ImGui::SameLine(); + + if (ImGui::BeginChild("Main", ImVec2(0, 0), false)) + { + switch (m_activeView) + { + case View::Console: DrawConsoleView(display); break; + case View::Players: DrawPlayersView(snapshot); break; + case View::Settings: DrawSettingsView(); break; + default: DrawConsoleView(display); break; + } + } + ImGui::EndChild(); + } + ImGui::EndChild(); + ImGui::End(); +} + +void ServerUi::DrawConsoleView(const ImVec2& display) +{ + (void)display; + if (ImGui::BeginChild("ConsoleCard", ImVec2(0, 0), true)) + { + ImGui::TextUnformatted("Console"); + ImGui::Separator(); + + ImGui::Checkbox("Auto-scroll", &m_autoScroll); + ImGui::SameLine(); + if (ImGui::Button("Clear Output")) + m_logLines.clear(); + ImGui::SameLine(); + TiltedPhoques::String logLevelValue; + int logLevelIndex = 2; + if (m_runner.GetSettingValue("sLogLevel", logLevelValue)) + logLevelIndex = FindLogLevelIndex(logLevelValue); + ImGui::SetNextItemWidth(140.0f); + if (ImGui::Combo("Log level", &logLevelIndex, kLogLevelOptions, IM_ARRAYSIZE(kLogLevelOptions))) + { + ApplySettingValue("sLogLevel", kLogLevelOptions[logLevelIndex]); + } + + ImGui::BeginChild("LogOutput", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), true); + { + if (m_logSink) + m_logSink->ConsumeLines(m_logLines); + + ImGuiListClipper clipper; + const int lineCount = m_logLines.size() > static_cast(std::numeric_limits::max()) + ? std::numeric_limits::max() + : static_cast(m_logLines.size()); + clipper.Begin(lineCount); + while (clipper.Step()) + { + for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; ++i) + { + const auto& line = m_logLines[i]; + const LogColor color = GetLogColor(line.c_str()); + if (color.Use) + ImGui::PushStyleColor(ImGuiCol_Text, color.Color); + + const bool isSelected = (m_selectedLogIndex >= 0 && m_selectionAnchor >= 0) + ? (i >= std::min(m_selectedLogIndex, m_selectionAnchor) && i <= std::max(m_selectedLogIndex, m_selectionAnchor)) + : (m_selectedLogIndex == i); + + ImGui::PushID(i); + if (ImGui::Selectable(line.c_str(), isSelected, ImGuiSelectableFlags_AllowDoubleClick)) + { + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) + { + m_selectedLogIndex = -1; + m_selectionAnchor = -1; + } + else if (ImGui::GetIO().KeyShift && m_selectedLogIndex >= 0) + { + m_selectionAnchor = m_selectedLogIndex; + } + else + { + m_selectionAnchor = -1; + } + m_selectedLogIndex = i; + } + ImGui::PopID(); + + if (color.Use) + ImGui::PopStyleColor(); + } + } + + if (m_selectedLogIndex >= 0 && m_selectedLogIndex < static_cast(m_logLines.size())) + { + if (ImGui::IsKeyPressed(ImGuiKey_C) && ImGui::GetIO().KeyCtrl) + { + int start = m_selectedLogIndex; + int end = m_selectedLogIndex; + if (m_selectionAnchor >= 0) + { + start = std::min(m_selectionAnchor, m_selectedLogIndex); + end = std::max(m_selectionAnchor, m_selectedLogIndex); + } + + std::string combined; + for (int i = start; i <= end && i < static_cast(m_logLines.size()); ++i) + { + combined.append(m_logLines[i].c_str()); + if (i != end) + combined.append("\n"); + } + ImGui::SetClipboardText(combined.c_str()); + } + } + + if (m_autoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) + ImGui::SetScrollHereY(1.0f); + } + ImGui::EndChild(); + + ImGui::SetNextItemWidth(-FLT_MIN); + if (ImGui::InputTextWithHint("##CommandInput", "Type a command (/help) or server message", m_commandBuffer, sizeof(m_commandBuffer), + ImGuiInputTextFlags_EnterReturnsTrue)) + { + TiltedPhoques::String command = m_commandBuffer; + if (!command.empty()) + { + m_runner.QueueConsoleCommand(command); + TiltedPhoques::String echo = "> "; + echo += command; + m_logLines.emplace_back(std::move(echo)); + } + m_commandBuffer[0] = '\0'; + } + } + ImGui::EndChild(); +} + +void ServerUi::DrawPlayersView(const ServerStatusSnapshot& snapshot) +{ + if (ImGui::BeginChild("PlayersCard", ImVec2(0, 0), true)) + { + ImGui::TextUnformatted("Players"); + ImGui::SameLine(); + ImGui::TextDisabled("(%zu online)", snapshot.Players.size()); + ImGui::Separator(); + + if (snapshot.Players.empty()) + { + ImGui::TextDisabled("No players connected."); + } + else if (ImGui::BeginTable("Players", 4, ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_ScrollY)) + { + ImGui::TableSetupColumn("Name"); + ImGui::TableSetupColumn("Cell"); + ImGui::TableSetupColumn("Grid"); + ImGui::TableSetupColumn("Pos"); + ImGui::TableHeadersRow(); + + for (const auto& player : snapshot.Players) + { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextUnformatted(player.Username.c_str()); + + ImGui::TableSetColumnIndex(1); + ImGui::Text("%08X:%08X", player.CellModId, player.CellBaseId); + if (player.WorldBaseId || player.WorldModId) + { + ImGui::SameLine(); + ImGui::TextDisabled("ws %08X:%08X", player.WorldModId, player.WorldBaseId); + } + + ImGui::TableSetColumnIndex(2); + ImGui::Text("%d, %d", player.GridX, player.GridY); + + ImGui::TableSetColumnIndex(3); + if (player.HasPosition) + ImGui::Text("%.1f %.1f %.1f", player.PositionX, player.PositionY, player.PositionZ); + else + ImGui::TextUnformatted("--"); + } + + ImGui::EndTable(); + } + } + ImGui::EndChild(); +} + +void ServerUi::DrawSettingsView() +{ + if (ImGui::BeginChild("SettingsCard", ImVec2(0, 0), true)) + { + ImGui::TextUnformatted("Server Settings"); + ImGui::Separator(); + + ImGui::SetNextItemWidth(-FLT_MIN); + ImGui::InputTextWithHint("##SettingSearch", "Search settings (name or description)", m_settingsSearch, sizeof(m_settingsSearch)); + + if (!m_settingsError.empty()) + { + ImGui::TextColored(ImVec4(0.96f, 0.35f, 0.30f, 1.0f), "%s", m_settingsError.c_str()); + } + else + { + ImGui::TextDisabled("Changes apply immediately. Locked settings are read-only."); + } + + TiltedPhoques::Vector settings; + m_runner.GetSettingsSnapshot(settings); + std::sort(settings.begin(), settings.end(), [](const auto& lhs, const auto& rhs) { return lhs.Name < rhs.Name; }); + + const TiltedPhoques::String filterLower = ToLowerCopy(m_settingsSearch); + + if (ImGui::BeginTable("SettingsTable", 3, ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_ScrollY)) + { + ImGui::TableSetupColumn("Setting"); + ImGui::TableSetupColumn("Value"); + ImGui::TableSetupColumn("Description"); + ImGui::TableHeadersRow(); + + TiltedPhoques::String currentGroup; + for (const auto& setting : settings) + { + if (setting.Flags & Console::SettingsFlags::kHidden) + continue; + + const auto groupName = GetSettingGroup(setting.Name); + const auto label = GetSettingLabel(setting.Name); + + if (!MatchesFilter(setting.Name, filterLower) && !MatchesFilter(setting.Description, filterLower) && !MatchesFilter(label, filterLower)) + continue; + + if (groupName != currentGroup) + { + currentGroup = groupName; + ImGui::TableNextRow(ImGuiTableRowFlags_Headers); + ImGui::TableSetColumnIndex(0); + ImGui::TextDisabled("%s", currentGroup.c_str()); + ImGui::TableSetColumnIndex(1); + ImGui::TableSetColumnIndex(2); + } + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextUnformatted(label.c_str()); + if (setting.Flags & Console::SettingsFlags::kLocked) + { + ImGui::SameLine(); + ImGui::TextDisabled("(locked)"); + } + + ImGui::TableSetColumnIndex(1); + ImGui::PushID(setting.Name.c_str()); + ImGui::SetNextItemWidth(-FLT_MIN); + + auto& state = m_settingState[setting.Name]; + if (!state.initialized || state.type != setting.Type) + { + state.type = setting.Type; + state.initialized = true; + state.lastValue = setting.Value; + state.textValue = setting.Value; + std::snprintf(state.textBuffer.data(), state.textBuffer.size(), "%s", state.textValue.c_str()); + state.intValue = ParseInt32(setting.Value, state.intValue); + state.int64Value = ParseInt64(setting.Value, state.int64Value); + state.uintValue = ParseUInt32(setting.Value, state.uintValue); + state.uint64Value = ParseUInt64(setting.Value, state.uint64Value); + state.floatValue = ParseFloat(setting.Value, state.floatValue); + } + else if (!state.editing && state.lastValue != setting.Value) + { + state.lastValue = setting.Value; + state.textValue = setting.Value; + std::snprintf(state.textBuffer.data(), state.textBuffer.size(), "%s", state.textValue.c_str()); + state.intValue = ParseInt32(setting.Value, state.intValue); + state.int64Value = ParseInt64(setting.Value, state.int64Value); + state.uintValue = ParseUInt32(setting.Value, state.uintValue); + state.uint64Value = ParseUInt64(setting.Value, state.uint64Value); + state.floatValue = ParseFloat(setting.Value, state.floatValue); + } + + const bool isLocked = setting.Flags & Console::SettingsFlags::kLocked; + if (isLocked) + ImGui::BeginDisabled(true); + + bool apply = false; + TiltedPhoques::String newValue = setting.Value; + switch (setting.Type) + { + case Console::SettingBase::Type::kBoolean: + { + bool value = setting.Value == "true" || setting.Value == "1"; + if (ImGui::Checkbox("##bool", &value)) + { + newValue = value ? "true" : "false"; + apply = true; + } + break; + } + case Console::SettingBase::Type::kInt: + if (ImGui::InputInt("##int", &state.intValue, 1, 10, ImGuiInputTextFlags_EnterReturnsTrue)) + newValue = std::to_string(state.intValue); + apply = ImGui::IsItemDeactivatedAfterEdit(); + break; + case Console::SettingBase::Type::kUInt: + if (ImGui::InputScalar("##uint", ImGuiDataType_U32, &state.uintValue, nullptr, nullptr, nullptr, ImGuiInputTextFlags_EnterReturnsTrue)) + newValue = std::to_string(state.uintValue); + apply = ImGui::IsItemDeactivatedAfterEdit(); + break; + case Console::SettingBase::Type::kInt64: + if (ImGui::InputScalar("##int64", ImGuiDataType_S64, &state.int64Value, nullptr, nullptr, nullptr, ImGuiInputTextFlags_EnterReturnsTrue)) + newValue = std::to_string(state.int64Value); + apply = ImGui::IsItemDeactivatedAfterEdit(); + break; + case Console::SettingBase::Type::kUInt64: + if (ImGui::InputScalar("##uint64", ImGuiDataType_U64, &state.uint64Value, nullptr, nullptr, nullptr, ImGuiInputTextFlags_EnterReturnsTrue)) + newValue = std::to_string(state.uint64Value); + apply = ImGui::IsItemDeactivatedAfterEdit(); + break; + case Console::SettingBase::Type::kFloat: + if (ImGui::InputFloat("##float", &state.floatValue, 0.0f, 0.0f, "%.3f", ImGuiInputTextFlags_EnterReturnsTrue)) + { + std::ostringstream stream; + stream << state.floatValue; + newValue = stream.str(); + } + apply = ImGui::IsItemDeactivatedAfterEdit(); + break; + case Console::SettingBase::Type::kString: + if (ImGui::InputText("##string", state.textBuffer.data(), state.textBuffer.size(), ImGuiInputTextFlags_EnterReturnsTrue)) + { + state.textValue = state.textBuffer.data(); + newValue = state.textValue; + apply = true; + } + if (ImGui::IsItemDeactivatedAfterEdit()) + { + state.textValue = state.textBuffer.data(); + newValue = state.textValue; + apply = true; + } + break; + default: + ImGui::TextDisabled("--"); + break; + } + + state.editing = ImGui::IsItemActive(); + + if (isLocked) + ImGui::EndDisabled(); + + if (apply && !isLocked && newValue != setting.Value) + { + if (!ApplySettingValue(setting.Name, newValue)) + { + state.textValue = setting.Value; + std::snprintf(state.textBuffer.data(), state.textBuffer.size(), "%s", state.textValue.c_str()); + } + } + + ImGui::PopID(); + + ImGui::TableSetColumnIndex(2); + ImGui::TextWrapped("%s", setting.Description.c_str()); + } + + ImGui::EndTable(); + } + } + ImGui::EndChild(); +} + +bool ServerUi::ApplySettingValue(const TiltedPhoques::String& name, const TiltedPhoques::String& value) +{ + TiltedPhoques::String error; + if (!m_runner.SetSettingValue(name, value, &error)) + { + m_settingsError = error; + return false; + } + + m_settingsError.clear(); + return true; +} + +bool ServerUi::CreateDevice() +{ + DXGI_SWAP_CHAIN_DESC sd{}; + sd.BufferCount = 2; + sd.BufferDesc.Width = 0; + sd.BufferDesc.Height = 0; + sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + sd.BufferDesc.RefreshRate.Numerator = 60; + sd.BufferDesc.RefreshRate.Denominator = 1; + sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; + sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + sd.OutputWindow = m_hwnd; + sd.SampleDesc.Count = 1; + sd.SampleDesc.Quality = 0; + sd.Windowed = TRUE; + sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD; + + UINT createDeviceFlags = 0; + D3D_FEATURE_LEVEL featureLevel; + const D3D_FEATURE_LEVEL featureLevelArray[2] = {D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_0}; + const HRESULT hr = D3D11CreateDeviceAndSwapChain( + nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, createDeviceFlags, featureLevelArray, 2, D3D11_SDK_VERSION, &sd, &m_swapChain, &m_device, + &featureLevel, &m_deviceContext); + if (FAILED(hr)) + return false; + + CreateRenderTarget(); + return true; +} + +void ServerUi::CleanupDevice() +{ + CleanupRenderTarget(); + if (m_swapChain) + { + m_swapChain->Release(); + m_swapChain = nullptr; + } + if (m_deviceContext) + { + m_deviceContext->Release(); + m_deviceContext = nullptr; + } + if (m_device) + { + m_device->Release(); + m_device = nullptr; + } +} + +void ServerUi::CreateRenderTarget() +{ + ID3D11Texture2D* backBuffer = nullptr; + m_swapChain->GetBuffer(0, IID_PPV_ARGS(&backBuffer)); + if (backBuffer) + { + m_device->CreateRenderTargetView(backBuffer, nullptr, &m_renderTargetView); + backBuffer->Release(); + } +} + +void ServerUi::CleanupRenderTarget() +{ + if (m_renderTargetView) + { + m_renderTargetView->Release(); + m_renderTargetView = nullptr; + } +} + +LRESULT WINAPI ServerUi::WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) +{ + if (ImGui_ImplWin32_WndProcHandler(hWnd, msg, wParam, lParam)) + return true; + + switch (msg) + { + case WM_SIZE: + if (wParam != SIZE_MINIMIZED) + { + auto* ui = reinterpret_cast(GetWindowLongPtrW(hWnd, GWLP_USERDATA)); + if (ui && ui->m_swapChain) + { + ui->CleanupRenderTarget(); + ui->m_swapChain->ResizeBuffers(0, (UINT)LOWORD(lParam), (UINT)HIWORD(lParam), DXGI_FORMAT_UNKNOWN, 0); + ui->CreateRenderTarget(); + } + } + return 0; + case WM_CLOSE: + if (auto* ui = reinterpret_cast(GetWindowLongPtrW(hWnd, GWLP_USERDATA))) + { + ui->RequestClose(); + ui->m_runner.RequestKill(); + } + DestroyWindow(hWnd); + return 0; + case WM_DESTROY: + if (auto* ui = reinterpret_cast(GetWindowLongPtrW(hWnd, GWLP_USERDATA))) + { + ui->RequestClose(); + ui->m_runner.RequestKill(); + } + PostQuitMessage(0); + return 0; + default: break; + } + + return DefWindowProcW(hWnd, msg, wParam, lParam); +} diff --git a/Code/server_runner/ServerUi.h b/Code/server_runner/ServerUi.h new file mode 100644 index 000000000..e748569b2 --- /dev/null +++ b/Code/server_runner/ServerUi.h @@ -0,0 +1,99 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef _WINSOCKAPI_ +#define _WINSOCKAPI_ +#endif +#include +#endif + +struct DediRunner; +struct ID3D11Device; +struct ID3D11DeviceContext; +struct IDXGISwapChain; +struct ID3D11RenderTargetView; +struct ImVec2; + +struct ServerLogSink; + +struct ServerUi +{ + explicit ServerUi(DediRunner& aRunner); + ~ServerUi(); + + bool Initialize(); + void Run(); + void RequestClose(); + +private: + enum class View + { + Console, + Players, + Settings + }; + + struct SettingUiState + { + Console::SettingBase::Type type{Console::SettingBase::Type::kNone}; + bool initialized{false}; + bool editing{false}; + TiltedPhoques::String lastValue; + TiltedPhoques::String textValue; + std::array textBuffer{}; + int32_t intValue{0}; + int64_t int64Value{0}; + uint32_t uintValue{0}; + uint64_t uint64Value{0}; + float floatValue{0.0f}; + }; + + bool CreateDevice(); + void CleanupDevice(); + void CreateRenderTarget(); + void CleanupRenderTarget(); + void RenderFrame(); + void DrawUi(); + void DrawConsoleView(const ImVec2& display); + void DrawPlayersView(const ServerStatusSnapshot& snapshot); + void DrawSettingsView(); + bool ApplySettingValue(const TiltedPhoques::String& name, const TiltedPhoques::String& value); + + static LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); + +private: + DediRunner& m_runner; + HWND m_hwnd{}; + ID3D11Device* m_device{nullptr}; + ID3D11DeviceContext* m_deviceContext{nullptr}; + IDXGISwapChain* m_swapChain{nullptr}; + ID3D11RenderTargetView* m_renderTargetView{nullptr}; + + ImGuiImpl::ImGuiDriver m_imguiDriver; + std::shared_ptr m_logSink; + + std::atomic m_running{true}; + TiltedPhoques::Vector m_logLines; + int m_selectedLogIndex{-1}; + int m_selectionAnchor{-1}; + char m_commandBuffer[4096]{}; + bool m_autoScroll{true}; + View m_activeView{View::Console}; + char m_settingsSearch[256]{}; + TiltedPhoques::String m_settingsError; + std::unordered_map m_settingState; +}; diff --git a/Code/server_runner/main.cpp b/Code/server_runner/main.cpp index 5916f71f0..af9a9afa1 100644 --- a/Code/server_runner/main.cpp +++ b/Code/server_runner/main.cpp @@ -2,7 +2,6 @@ #include #include #include -#include #include #include @@ -14,9 +13,12 @@ #include #include "DediRunner.h" +#include "ServerLogSink.h" +#include "ServerUi.h" #include #ifdef _WIN32 +#include #include #pragma comment(lib, "Comctl32.lib") #elif defined(__linux__) @@ -45,6 +47,8 @@ extern Console::Setting bConsole; GS_IMPORT void SetDefaultLogger(std::shared_ptr aLogger); GS_IMPORT void RegisterLogger(std::shared_ptr aLogger); +GS_IMPORT bool CheckBuildTag(const char* apBuildTag); +GS_IMPORT void SetUiLogCallback(void (*aCallback)(const char*)); struct LogInstance { @@ -54,19 +58,27 @@ struct LogInstance { using namespace spdlog; +#ifdef _WIN32 + SetConsoleOutputCP(CP_UTF8); +#endif + std::error_code ec; - fs::create_directory("logs", ec); + const auto logPath = TiltedPhoques::GetPath() / "logs"; + fs::create_directory(logPath, ec); + + auto fileOut = std::make_shared((logPath / kLogFileName).string(), kLogFileSizeCap, 3); + auto consoleSink = std::make_shared(); + consoleSink->set_pattern("%^[%H:%M:%S.%e] [%l] [tid %t] %$ %v"); + + auto consoleOut = std::make_shared(KCompilerStopThisBullshit, sinks_init_list{consoleSink, fileOut}); + consoleOut->set_level(level::from_str(sLogLevel.value())); - auto consoleOut = spdlog::stdout_color_mt(KCompilerStopThisBullshit); - consoleOut->set_pattern(">%$ %v"); + if (!spdlog::get(KCompilerStopThisBullshit)) + spdlog::register_logger(consoleOut); - // make the client aware of this logger. RegisterLogger(consoleOut); - auto fileOut = std::make_shared(std::string("logs/") + kLogFileName, kLogFileSizeCap, 3); - auto serverOut = std::make_shared(); - serverOut->set_pattern("%^[%H:%M:%S.%e] [%l] [tid %t] %$ %v"); - auto globalOut = std::make_shared("", sinks_init_list{serverOut, fileOut}); + auto globalOut = std::make_shared("", sinks_init_list{consoleSink, fileOut}); globalOut->set_level(level::from_str(sLogLevel.value())); // as the library is compiled into the client + server we have to do this twice @@ -74,6 +86,8 @@ struct LogInstance // also make the client aware of the file loggers SetDefaultLogger(globalOut); + + AttachServerLogSinkToAllLoggers(); } ~LogInstance() { spdlog::shutdown(); } @@ -143,11 +157,21 @@ static bool IsEULAAccepted() const auto path = fs::current_path() / kConfigPathName / kEULAName; bool preAccept = false; - if (char* pValue = std::getenv("TILTED_ACCEPT_EULA")) + char* pValue = nullptr; +#ifdef _WIN32 + size_t envLength = 0; + if (_dupenv_s(&pValue, &envLength, "TILTED_ACCEPT_EULA") == 0 && pValue) +#else + if ((pValue = std::getenv("TILTED_ACCEPT_EULA")) != nullptr) +#endif { std::string_view env(pValue); preAccept = env == "true" || env == "1" || env == "TRUE"; } +#ifdef _WIN32 + if (pValue) + free(pValue); +#endif auto saveFile = [&]() { @@ -190,7 +214,14 @@ static bool IsEULAAccepted() return true; } -GS_IMPORT bool CheckBuildTag(const char* apBuildTag); +static void UiLogCallback(const char* aLine) +{ + if (!aLine) + return; + + if (auto sink = GetServerLogSink()) + sink->PushExternalLine(aLine); +} void ConfigureConsoleMode() { @@ -205,14 +236,25 @@ int main(int argc, char** argv) { ConfigureConsoleMode(); + LogInstance logger; + (void)logger; + // the binaries are not from the same commit. if (!CheckBuildTag(kBuildTag)) + { +#ifdef _WIN32 + MessageBoxW(nullptr, L"Server runner and server DLL build tags do not match.", L"SkyrimTogetherServer", MB_OK | MB_ICONERROR); +#endif return 1; + } + SetUiLogCallback(&UiLogCallback); Base::SetCurrentThreadName("ServerRunnerMain"); - LogInstance logger; - (void)logger; +#ifdef _WIN32 + if (auto* hwnd = GetConsoleWindow()) + ShowWindow(hwnd, SW_HIDE); +#endif // Disabled EULA check since we have no EULA contents yet /* @@ -230,12 +272,37 @@ int main(int argc, char** argv) // Keep stack free. const auto cpRunner{std::make_unique(argc, argv)}; - if (bConsole) + +#ifdef _WIN32 + ServerUi ui(*cpRunner); + if (ui.Initialize()) + { +#ifdef _WIN32 + // Console input is hidden; UI handles commands. + std::thread serverThread([&]() { +#if defined(_WIN32) + __try + { + cpRunner->RunGSThread(); + } + __except (EXCEPTION_EXECUTE_HANDLER) + { + } +#else + cpRunner->RunGSThread(); +#endif + }); +#endif + ui.Run(); + serverThread.join(); + } + else { - cpRunner->StartTerminalIO(); + MessageBoxW(nullptr, L"Server UI failed to initialize.", L"SkyrimTogetherServer", MB_OK | MB_ICONERROR); } +#endif - cpRunner->RunGSThread(); + cpRunner->RequestKill(); return 0; } diff --git a/Code/server_runner/xmake.lua b/Code/server_runner/xmake.lua index df4cbc469..0df3d5250 100644 --- a/Code/server_runner/xmake.lua +++ b/Code/server_runner/xmake.lua @@ -26,13 +26,16 @@ local function build_runner() add_deps( "CommonLib", "Console", - "BaseLib") + "BaseLib", + "ImGuiImpl") add_packages( "tiltedcore", "spdlog", "hopscotch-map", "sentry-native", - "libuv") + "libuv", + "imgui") + add_syslinks("d3d11", "dxgi") add_defines("SPDLOG_HEADER_ONLY") end diff --git a/Code/skyrim_ui/angular.json b/Code/skyrim_ui/angular.json index 8dffe30e9..cede53874 100644 --- a/Code/skyrim_ui/angular.json +++ b/Code/skyrim_ui/angular.json @@ -28,25 +28,22 @@ "optimization": false, "namedChunks": true, "stylePreprocessorOptions": { - "includePaths": [ - "src", - "src/styles" - ] + "includePaths": ["src", "src/styles"] }, "tsConfig": "tsconfig.app.json", "assets": [ "src/favicon.ico", "src/assets/sounds/", - "src/assets/i18n/" - ], - "styles": [ - "src/styles.scss" + "src/assets/i18n/", + { + "glob": "avatar-placeholder.png", + "input": "src/assets/images/group", + "output": "assets/images/group" + } ], + "styles": ["src/styles.scss"], "scripts": [], - "allowedCommonJsDependencies": [ - "flat", - "events" - ] + "allowedCommonJsDependencies": ["flat", "events"] }, "configurations": { "production": { @@ -138,9 +135,7 @@ "tsconfig.spec.json", "e2e/tsconfig.json" ], - "exclude": [ - "**/node_modules/**" - ] + "exclude": ["**/node_modules/**"] } }, "e2e": { diff --git a/Code/skyrim_ui/pnpm-lock.yaml b/Code/skyrim_ui/pnpm-lock.yaml old mode 100644 new mode 100755 index 792de9402..bb0d256ed --- a/Code/skyrim_ui/pnpm-lock.yaml +++ b/Code/skyrim_ui/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.0' +lockfileVersion: '9.0' settings: autoInstallPeers: true @@ -7,156 +7,156 @@ settings: overrides: uuid@<7: '>=7' -dependencies: - '@angular/animations': - specifier: ^16.1.2 - version: 16.1.2(@angular/core@16.1.2) - '@angular/cdk': - specifier: ^16.1.2 - version: 16.1.2(@angular/common@16.1.2)(@angular/core@16.1.2)(rxjs@7.8.1) - '@angular/common': - specifier: ^16.1.2 - version: 16.1.2(@angular/core@16.1.2)(rxjs@7.8.1) - '@angular/compiler': - specifier: ^16.1.2 - version: 16.1.2(@angular/core@16.1.2) - '@angular/core': - specifier: ^16.1.2 - version: 16.1.2(rxjs@7.8.1)(zone.js@0.13.1) - '@angular/forms': - specifier: ^16.1.2 - version: 16.1.2(@angular/common@16.1.2)(@angular/core@16.1.2)(@angular/platform-browser@16.1.2)(rxjs@7.8.1) - '@angular/platform-browser': - specifier: ^16.1.2 - version: 16.1.2(@angular/animations@16.1.2)(@angular/common@16.1.2)(@angular/core@16.1.2) - '@angular/platform-browser-dynamic': - specifier: ^16.1.2 - version: 16.1.2(@angular/common@16.1.2)(@angular/compiler@16.1.2)(@angular/core@16.1.2)(@angular/platform-browser@16.1.2) - '@angular/router': - specifier: ^16.1.2 - version: 16.1.2(@angular/common@16.1.2)(@angular/core@16.1.2)(@angular/platform-browser@16.1.2)(rxjs@7.8.1) - '@fortawesome/angular-fontawesome': - specifier: ^0.13.0 - version: 0.13.0(@angular/core@16.1.2)(@fortawesome/fontawesome-svg-core@6.4.0) - '@fortawesome/fontawesome-svg-core': - specifier: ^6.4.0 - version: 6.4.0 - '@fortawesome/free-regular-svg-icons': - specifier: ^6.4.0 - version: 6.4.0 - '@fortawesome/free-solid-svg-icons': - specifier: ^6.4.0 - version: 6.4.0 - '@ngneat/elf': - specifier: ^2.3.2 - version: 2.3.2(rxjs@7.8.1) - '@ngneat/elf-devtools': - specifier: ^1.3.0 - version: 1.3.0 - '@ngneat/elf-entities': - specifier: ^4.4.4 - version: 4.4.4 - '@ngneat/loadoff': - specifier: ^2.1.0 - version: 2.1.0 - '@ngneat/reactive-forms': - specifier: ^5.0.2 - version: 5.0.2(@angular/forms@16.1.2) - '@ngneat/transloco': - specifier: 4.3.0 - version: 4.3.0(@angular/core@16.1.2)(fs-extra@11.1.1)(glob@10.3.0)(rxjs@7.8.1) - rxjs: - specifier: ~7.8.1 - version: 7.8.1 - tslib: - specifier: ^2.5.3 - version: 2.5.3 - zone.js: - specifier: ~0.13.1 - version: 0.13.1 - -devDependencies: - '@angular-devkit/build-angular': - specifier: ^16.1.1 - version: 16.1.1(@angular/compiler-cli@16.1.2)(@types/node@18.0.0)(typescript@5.1.3) - '@angular-eslint/eslint-plugin': - specifier: ^16.0.3 - version: 16.0.3(eslint@8.43.0)(typescript@5.1.3) - '@angular-eslint/eslint-plugin-template': - specifier: ^16.0.3 - version: 16.0.3(eslint@8.43.0)(typescript@5.1.3) - '@angular-eslint/template-parser': - specifier: ^16.0.3 - version: 16.0.3(eslint@8.43.0)(typescript@5.1.3) - '@angular/cli': - specifier: ^16.1.1 - version: 16.1.1 - '@angular/compiler-cli': - specifier: ^16.1.2 - version: 16.1.2(@angular/compiler@16.1.2)(typescript@5.1.3) - '@ngx-playwright/test': - specifier: ^0.4.2 - version: 0.4.2(@angular/cdk@16.1.2)(@playwright/test@1.35.1)(typescript@5.1.3) - '@types/events': - specifier: ^3.0.0 - version: 3.0.0 - '@types/node': - specifier: 18.x - version: 18.0.0 - '@typescript-eslint/eslint-plugin': - specifier: ^5.60.0 - version: 5.60.0(@typescript-eslint/parser@5.60.0)(eslint@8.43.0)(typescript@5.1.3) - '@typescript-eslint/parser': - specifier: ^5.60.0 - version: 5.60.0(eslint@8.43.0)(typescript@5.1.3) - eslint: - specifier: ^8.43.0 - version: 8.43.0 - events: - specifier: ^3.3.0 - version: 3.3.0 - prettier: - specifier: ^2.8.8 - version: 2.8.8 - ts-node: - specifier: ~10.9.1 - version: 10.9.1(@types/node@18.0.0)(typescript@5.1.3) - typescript: - specifier: ~5.1.3 - version: 5.1.3 +importers: + + .: + dependencies: + '@angular/animations': + specifier: ^16.1.2 + version: 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)) + '@angular/cdk': + specifier: ^16.1.2 + version: 16.2.14(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2) + '@angular/common': + specifier: ^16.1.2 + version: 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2) + '@angular/compiler': + specifier: ^16.1.2 + version: 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)) + '@angular/core': + specifier: ^16.1.2 + version: 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + '@angular/forms': + specifier: ^16.1.2 + version: 16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(rxjs@7.8.2) + '@angular/platform-browser': + specifier: ^16.1.2 + version: 16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)) + '@angular/platform-browser-dynamic': + specifier: ^16.1.2 + version: 16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/compiler@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))) + '@angular/router': + specifier: ^16.1.2 + version: 16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(rxjs@7.8.2) + '@fortawesome/angular-fontawesome': + specifier: ^0.13.0 + version: 0.13.0(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@fortawesome/fontawesome-svg-core@6.7.2) + '@fortawesome/fontawesome-svg-core': + specifier: ^6.4.0 + version: 6.7.2 + '@fortawesome/free-regular-svg-icons': + specifier: ^6.4.0 + version: 6.7.2 + '@fortawesome/free-solid-svg-icons': + specifier: ^6.4.0 + version: 6.7.2 + '@ngneat/elf': + specifier: ^2.3.2 + version: 2.5.1(rxjs@7.8.2) + '@ngneat/elf-devtools': + specifier: ^1.3.0 + version: 1.3.0 + '@ngneat/elf-entities': + specifier: ^4.4.4 + version: 4.6.0 + '@ngneat/loadoff': + specifier: ^2.1.0 + version: 2.1.0 + '@ngneat/reactive-forms': + specifier: ^5.0.2 + version: 5.0.2(@angular/forms@16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(rxjs@7.8.2)) + '@ngneat/transloco': + specifier: 4.3.0 + version: 4.3.0(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(fs-extra@11.1.1)(glob@10.4.5)(rxjs@7.8.2)(typescript@5.1.6) + rxjs: + specifier: ~7.8.1 + version: 7.8.2 + tslib: + specifier: ^2.5.3 + version: 2.8.1 + zone.js: + specifier: ~0.13.1 + version: 0.13.3 + devDependencies: + '@angular-devkit/build-angular': + specifier: ^16.1.1 + version: 16.2.16(@angular/compiler-cli@16.2.12(@angular/compiler@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(typescript@5.1.6))(@types/node@18.19.130)(typescript@5.1.6) + '@angular-eslint/eslint-plugin': + specifier: ^16.0.3 + version: 16.3.1(eslint@8.57.1)(typescript@5.1.6) + '@angular-eslint/eslint-plugin-template': + specifier: ^16.0.3 + version: 16.3.1(eslint@8.57.1)(typescript@5.1.6) + '@angular-eslint/template-parser': + specifier: ^16.0.3 + version: 16.3.1(eslint@8.57.1)(typescript@5.1.6) + '@angular/cli': + specifier: ^16.1.1 + version: 16.2.16(chokidar@3.5.3) + '@angular/compiler-cli': + specifier: ^16.1.2 + version: 16.2.12(@angular/compiler@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(typescript@5.1.6) + '@ngx-playwright/test': + specifier: ^0.4.2 + version: 0.4.3(@angular-devkit/core@17.3.17(chokidar@3.5.3))(@angular/cdk@16.2.14(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@playwright/test@1.56.1)(chokidar@3.5.3)(typescript@5.1.6) + '@types/events': + specifier: ^3.0.0 + version: 3.0.3 + '@types/node': + specifier: 18.x + version: 18.19.130 + '@typescript-eslint/eslint-plugin': + specifier: ^5.60.0 + version: 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.1.6))(eslint@8.57.1)(typescript@5.1.6) + '@typescript-eslint/parser': + specifier: ^5.60.0 + version: 5.62.0(eslint@8.57.1)(typescript@5.1.6) + eslint: + specifier: ^8.43.0 + version: 8.57.1 + events: + specifier: ^3.3.0 + version: 3.3.0 + prettier: + specifier: ^2.8.8 + version: 2.8.8 + ts-node: + specifier: ~10.9.1 + version: 10.9.2(@types/node@18.19.130)(typescript@5.1.6) + typescript: + specifier: ~5.1.3 + version: 5.1.6 packages: - /@ampproject/remapping@2.2.1: + '@ampproject/remapping@2.2.1': resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} engines: {node: '>=6.0.0'} - dependencies: - '@jridgewell/gen-mapping': 0.3.2 - '@jridgewell/trace-mapping': 0.3.18 - dev: true - /@angular-devkit/architect@0.1601.1(chokidar@3.5.3): - resolution: {integrity: sha512-LUHaxdAZrvh++7/R+/hzVY5moEVVTjd30b25SNNYcNJsWox1Yh9idu1AvtEuZR/A8Jj+sbHnuw0176GsJ78stg==} + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@angular-devkit/architect@0.1602.16': + resolution: {integrity: sha512-aWEeGU4UlbrSKpcAZsldVNxNXAWEeu9hM2BPk77GftbRC8PBMWpgYyrJWTz2ryn8aSmGKT3T8OyBH4gZA/667w==} engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} - dependencies: - '@angular-devkit/core': 16.1.1(chokidar@3.5.3) - rxjs: 7.8.1 - transitivePeerDependencies: - - chokidar - dev: true - /@angular-devkit/build-angular@16.1.1(@angular/compiler-cli@16.1.2)(@types/node@18.0.0)(typescript@5.1.3): - resolution: {integrity: sha512-k+9hQLGgGWHj7NJ7Fm6nwCDsvseKlipKE6Me9ydliNKclwJ0l+ScpXS/iZA1+Geud7IX3McGWf2QemWj1Nds9g==} + '@angular-devkit/architect@0.1703.17': + resolution: {integrity: sha512-LD6po8lGP2FI7WbnsSxtvpiIi+FYL0aNfteunkT+7po9jUNflBEYHA64UWNO56u7ryKNdbuiN8/TEh7FEUnmCw==} + engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + + '@angular-devkit/build-angular@16.2.16': + resolution: {integrity: sha512-gEni21kza41xaRnVWP1sMuiWHS/rdoym5FEEGDo9PG60LwRC4lekIgT09GpTlmMu007UEfo0ccQnGroD6+MqWg==} engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} peerDependencies: - '@angular/compiler-cli': ^16.1.0 - '@angular/localize': ^16.1.0 - '@angular/platform-server': ^16.1.0 - '@angular/service-worker': ^16.1.0 + '@angular/compiler-cli': ^16.0.0 + '@angular/localize': ^16.0.0 + '@angular/platform-server': ^16.0.0 + '@angular/service-worker': ^16.0.0 jest: ^29.5.0 jest-environment-jsdom: ^29.5.0 karma: ^6.3.0 - ng-packagr: ^16.1.0 + ng-packagr: ^16.0.0 protractor: ^7.0.0 tailwindcss: ^2.0.0 || ^3.0.0 typescript: '>=4.9.3 <5.2' @@ -179,2544 +179,1237 @@ packages: optional: true tailwindcss: optional: true - dependencies: - '@ampproject/remapping': 2.2.1 - '@angular-devkit/architect': 0.1601.1(chokidar@3.5.3) - '@angular-devkit/build-webpack': 0.1601.1(chokidar@3.5.3)(webpack-dev-server@4.15.0)(webpack@5.86.0) - '@angular-devkit/core': 16.1.1(chokidar@3.5.3) - '@angular/compiler-cli': 16.1.2(@angular/compiler@16.1.2)(typescript@5.1.3) - '@babel/core': 7.22.5 - '@babel/generator': 7.22.5 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.5 - '@babel/plugin-proposal-async-generator-functions': 7.20.7(@babel/core@7.22.5) - '@babel/plugin-transform-async-to-generator': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-runtime': 7.22.5(@babel/core@7.22.5) - '@babel/preset-env': 7.22.5(@babel/core@7.22.5) - '@babel/runtime': 7.22.5 - '@babel/template': 7.22.5 - '@discoveryjs/json-ext': 0.5.7 - '@ngtools/webpack': 16.1.1(@angular/compiler-cli@16.1.2)(typescript@5.1.3)(webpack@5.86.0) - '@vitejs/plugin-basic-ssl': 1.0.1(vite@4.3.9) - ansi-colors: 4.1.3 - autoprefixer: 10.4.14(postcss@8.4.24) - babel-loader: 9.1.2(@babel/core@7.22.5)(webpack@5.86.0) - babel-plugin-istanbul: 6.1.1 - browserslist: 4.21.5 - cacache: 17.1.3 - chokidar: 3.5.3 - copy-webpack-plugin: 11.0.0(webpack@5.86.0) - critters: 0.0.19 - css-loader: 6.8.1(webpack@5.86.0) - esbuild-wasm: 0.17.19 - fast-glob: 3.2.12 - https-proxy-agent: 5.0.1 - inquirer: 8.2.4 - jsonc-parser: 3.2.0 - karma-source-map-support: 1.4.0 - less: 4.1.3 - less-loader: 11.1.0(less@4.1.3)(webpack@5.86.0) - license-webpack-plugin: 4.0.2(webpack@5.86.0) - loader-utils: 3.2.1 - magic-string: 0.30.0 - mini-css-extract-plugin: 2.7.6(webpack@5.86.0) - mrmime: 1.0.1 - open: 8.4.2 - ora: 5.4.1 - parse5-html-rewriting-stream: 7.0.0 - picomatch: 2.3.1 - piscina: 3.2.0 - postcss: 8.4.24 - postcss-loader: 7.3.2(postcss@8.4.24)(webpack@5.86.0) - resolve-url-loader: 5.0.0 - rxjs: 7.8.1 - sass: 1.63.2 - sass-loader: 13.3.1(sass@1.63.2)(webpack@5.86.0) - semver: 7.5.1 - source-map-loader: 4.0.1(webpack@5.86.0) - source-map-support: 0.5.21 - terser: 5.17.7 - text-table: 0.2.0 - tree-kill: 1.2.2 - tslib: 2.5.3 - typescript: 5.1.3 - vite: 4.3.9(@types/node@18.0.0)(less@4.1.3)(sass@1.63.2)(terser@5.17.7) - webpack: 5.86.0(esbuild@0.17.19) - webpack-dev-middleware: 6.1.1(webpack@5.86.0) - webpack-dev-server: 4.15.0(webpack@5.86.0) - webpack-merge: 5.9.0 - webpack-subresource-integrity: 5.1.0(webpack@5.86.0) - optionalDependencies: - esbuild: 0.17.19 - transitivePeerDependencies: - - '@swc/core' - - '@types/node' - - bufferutil - - debug - - fibers - - html-webpack-plugin - - node-sass - - sass-embedded - - stylus - - sugarss - - supports-color - - uglify-js - - utf-8-validate - - webpack-cli - dev: true - /@angular-devkit/build-webpack@0.1601.1(chokidar@3.5.3)(webpack-dev-server@4.15.0)(webpack@5.86.0): - resolution: {integrity: sha512-J1O+5qiwCYNCFTKtczhWC3wycuks/3GitmbYv3GMSBz2HJU5XSTtGdlkSvkwCocLHa8ggPlnquLsQNjwcd+EQg==} + '@angular-devkit/build-webpack@0.1602.16': + resolution: {integrity: sha512-b99Sj0btI0C2GIfzoyP8epDMIOLqSTqXOxw6klGtBLaGZfM5KAxqFzekXh8cAnHxWCj20WdNhezS1eUTLOkaIA==} engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} peerDependencies: webpack: ^5.30.0 webpack-dev-server: ^4.0.0 - dependencies: - '@angular-devkit/architect': 0.1601.1(chokidar@3.5.3) - rxjs: 7.8.1 - webpack: 5.86.0(esbuild@0.17.19) - webpack-dev-server: 4.15.0(webpack@5.86.0) - transitivePeerDependencies: - - chokidar - dev: true - /@angular-devkit/core@16.1.1(chokidar@3.5.3): - resolution: {integrity: sha512-rhyY/N4iKbpfKmErmNmAfBLMrc1H8u8NlfcU6lwN6kbBbM8BfvLk9b7g4JXOfiOQfp4BnQ8CFf7xcIUy4++Tog==} + '@angular-devkit/core@16.2.16': + resolution: {integrity: sha512-5xHs9JFmp78sydrOAg0UGErxfMVv5c2f3RXoikS7eBOOXTWEi5pmnOkOvSJ3loQFGVs3Y7i+u02G3VrF5ZxOrA==} engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} peerDependencies: chokidar: ^3.5.2 peerDependenciesMeta: chokidar: optional: true - dependencies: - ajv: 8.12.0 - ajv-formats: 2.1.1(ajv@8.12.0) - chokidar: 3.5.3 - jsonc-parser: 3.2.0 - rxjs: 7.8.1 - source-map: 0.7.4 - dev: true - /@angular-devkit/schematics@16.1.1: - resolution: {integrity: sha512-s8LFr0m4ILEpJuQj78fCWKocnRleA3MWJU1Q5LZloCcUB8fdDvaPNCt5s0VWC2Sp+4OCxJaSN3kjjcFbCYFvTA==} + '@angular-devkit/core@17.3.17': + resolution: {integrity: sha512-7aNVqS3rOGsSZYAOO44xl2KURwaoOP+EJhJs+LqOGOFpok2kd8YLf4CAMUossMF4H7HsJpgKwYqGrV5eXunrpw==} + engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^3.5.2 + peerDependenciesMeta: + chokidar: + optional: true + + '@angular-devkit/schematics@16.2.16': + resolution: {integrity: sha512-pF6fdtJh6yLmgA7Gs45JIdxPl2MsTAhYcZIMrX1a6ID64dfwtF0MP8fDE6vrWInV1zXbzzf7l7PeKuqVtTSzKg==} engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} - dependencies: - '@angular-devkit/core': 16.1.1(chokidar@3.5.3) - jsonc-parser: 3.2.0 - magic-string: 0.30.0 - ora: 5.4.1 - rxjs: 7.8.1 - transitivePeerDependencies: - - chokidar - dev: true - /@angular-eslint/bundled-angular-compiler@16.0.3: - resolution: {integrity: sha512-8zwY6ustiPXBEF3+jELKVwGk6j2HJn7GHbqAhDFR02YiE27iRMSGTHIAWGs6ZI7F1JgfrIsOHrUgzC1x95K6rg==} - dev: true + '@angular-devkit/schematics@17.3.17': + resolution: {integrity: sha512-ZXsIJXZm0I0dNu1BqmjfEtQhnzqoupUHHZb4GHm5NeQHBFZctQlkkNxLUU27GVeBUwFgEmP7kFgSLlMPTGSL5g==} + engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} - /@angular-eslint/eslint-plugin-template@16.0.3(eslint@8.43.0)(typescript@5.1.3): - resolution: {integrity: sha512-OKTMWOjC7F5tdv7gm2tlmgyr/uVyS1RWJZn4X/6D6p0kOpiDXmajtbYHD5tzbshX2Ep62Nt+rg8+1XGHrU0ScA==} + '@angular-eslint/bundled-angular-compiler@16.3.1': + resolution: {integrity: sha512-m4WP1xwS9XLcC/3n6lIcG5HZoai/5eb5W3xm48GVcv//0qE2p7S96RSgKPgGHvif5pF8O9xAqEWs3gDEG45+7A==} + + '@angular-eslint/eslint-plugin-template@16.3.1': + resolution: {integrity: sha512-+RcFEWqNiRt3+5jXvmlIDlXtP9+vjdmgmVL6tt8yDbqdjBOewtyMu4pE4YaR4sFboyxgME9PbO2WrOyPXh6xjg==} peerDependencies: eslint: ^7.20.0 || ^8.0.0 typescript: '*' - dependencies: - '@angular-eslint/bundled-angular-compiler': 16.0.3 - '@angular-eslint/utils': 16.0.3(eslint@8.43.0)(typescript@5.1.3) - '@typescript-eslint/type-utils': 5.59.7(eslint@8.43.0)(typescript@5.1.3) - '@typescript-eslint/utils': 5.59.7(eslint@8.43.0)(typescript@5.1.3) - aria-query: 5.1.3 - axobject-query: 3.1.1 - eslint: 8.43.0 - typescript: 5.1.3 - transitivePeerDependencies: - - supports-color - dev: true - /@angular-eslint/eslint-plugin@16.0.3(eslint@8.43.0)(typescript@5.1.3): - resolution: {integrity: sha512-1c+dFytcQDOA2wJ8/rtydMV6UYq1BgVfOcBXOr0WJxC9g8Cad9czcUOkW41WGrTp5kICMliV0ypH5eEaCM2WDQ==} + '@angular-eslint/eslint-plugin@16.3.1': + resolution: {integrity: sha512-kSc8ESfoy8TUSthbq0Lpq9e17I+3Smy4rHoNpKCFEGuJgPs0+OssZMxB6a5EawGbv2EKTPEtrxzFm1WsLR0U9Q==} peerDependencies: eslint: ^7.20.0 || ^8.0.0 typescript: '*' - dependencies: - '@angular-eslint/utils': 16.0.3(eslint@8.43.0)(typescript@5.1.3) - '@typescript-eslint/utils': 5.59.7(eslint@8.43.0)(typescript@5.1.3) - eslint: 8.43.0 - typescript: 5.1.3 - transitivePeerDependencies: - - supports-color - dev: true - /@angular-eslint/template-parser@16.0.3(eslint@8.43.0)(typescript@5.1.3): - resolution: {integrity: sha512-IAWdwp/S9QC3EMiVxSS0E3ABy9PSidN3PW0Ll2EtM3mzXMYlpZXmxqd+B1xV/xKWzhk1Mp04QX8hHfG6Vq+qaQ==} + '@angular-eslint/template-parser@16.3.1': + resolution: {integrity: sha512-9+SxUtxB2iOnm0ldS2ow0stMxe02rB/TxeMIe8fxsLFHZdw8RQvs/p3HLvVHXzv6gUblMHebIb/ubUmwEVb2SA==} peerDependencies: eslint: ^7.20.0 || ^8.0.0 typescript: '*' - dependencies: - '@angular-eslint/bundled-angular-compiler': 16.0.3 - eslint: 8.43.0 - eslint-scope: 7.2.0 - typescript: 5.1.3 - dev: true - /@angular-eslint/utils@16.0.3(eslint@8.43.0)(typescript@5.1.3): - resolution: {integrity: sha512-QsbUVHJLk+fE08/D4y3wOyGk1iX2LVSygw+uzilbaAXfjD5/c0Ei5FbVx2mMYPk+aOl4yrvGQW3dmetMiAR0MQ==} + '@angular-eslint/utils@16.3.1': + resolution: {integrity: sha512-tEBcce0rG+DmcPO8jhRffUFDioGw3G4cUAE15XlRctY1J3QzOBH9HdUOTDt0mMjBgpWCzh0YVT1Moh2bPXU9Xg==} peerDependencies: eslint: ^7.20.0 || ^8.0.0 typescript: '*' - dependencies: - '@angular-eslint/bundled-angular-compiler': 16.0.3 - '@typescript-eslint/utils': 5.59.7(eslint@8.43.0)(typescript@5.1.3) - eslint: 8.43.0 - typescript: 5.1.3 - transitivePeerDependencies: - - supports-color - dev: true - /@angular/animations@16.1.2(@angular/core@16.1.2): - resolution: {integrity: sha512-Q2hdXYKaNGCRQf1G2E1L/P0lw5thAkC0g/L2GdmB+bgyPCTTk1B7WxDN/SVUCfdz1nReZiLepL3Y24RKeQ6Blw==} + '@angular/animations@16.2.12': + resolution: {integrity: sha512-MD0ElviEfAJY8qMOd6/jjSSvtqER2RDAi0lxe6EtUacC1DHCYkaPrKW4vLqY+tmZBg1yf+6n+uS77pXcHHcA3w==} engines: {node: ^16.14.0 || >=18.10.0} peerDependencies: - '@angular/core': 16.1.2 - dependencies: - '@angular/core': 16.1.2(rxjs@7.8.1)(zone.js@0.13.1) - tslib: 2.5.3 - dev: false + '@angular/core': 16.2.12 - /@angular/cdk@16.1.2(@angular/common@16.1.2)(@angular/core@16.1.2)(rxjs@7.8.1): - resolution: {integrity: sha512-sUiaY6QgiplSuhzN7Fp5HUW3HPyz3g8ET45W0I2bSiSmSaDEATLWwVXfxSC4/ZL+YdZHoC9LkYKDSktPO2EDaA==} + '@angular/cdk@16.2.14': + resolution: {integrity: sha512-n6PrGdiVeSTEmM/HEiwIyg6YQUUymZrb5afaNLGFRM5YL0Y8OBqd+XhCjb0OfD/AfgCUtedVEPwNqrfW8KzgGw==} peerDependencies: '@angular/common': ^16.0.0 || ^17.0.0 '@angular/core': ^16.0.0 || ^17.0.0 rxjs: ^6.5.3 || ^7.4.0 - dependencies: - '@angular/common': 16.1.2(@angular/core@16.1.2)(rxjs@7.8.1) - '@angular/core': 16.1.2(rxjs@7.8.1)(zone.js@0.13.1) - rxjs: 7.8.1 - tslib: 2.5.3 - optionalDependencies: - parse5: 7.1.2 - /@angular/cli@16.1.1: - resolution: {integrity: sha512-QrTgMqMnamteZu2x3JhLMo6wBjI05zMr9RQfMHWq4UrUpTqBcHAMqJIKSSbvrtuRbolLrQyLorwxzlmEOfEmbQ==} + '@angular/cli@16.2.16': + resolution: {integrity: sha512-aqfNYZ45ndrf36i+7AhQ9R8BCm025j7TtYaUmvvjT4LwiUg6f6KtlZPB/ivBlXmd1g9oXqW4advL0AIi8A/Ozg==} engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} hasBin: true - dependencies: - '@angular-devkit/architect': 0.1601.1(chokidar@3.5.3) - '@angular-devkit/core': 16.1.1(chokidar@3.5.3) - '@angular-devkit/schematics': 16.1.1 - '@schematics/angular': 16.1.1 - '@yarnpkg/lockfile': 1.1.0 - ansi-colors: 4.1.3 - ini: 4.1.1 - inquirer: 8.2.4 - jsonc-parser: 3.2.0 - npm-package-arg: 10.1.0 - npm-pick-manifest: 8.0.1 - open: 8.4.2 - ora: 5.4.1 - pacote: 15.2.0 - resolve: 1.22.2 - semver: 7.5.1 - symbol-observable: 4.0.0 - yargs: 17.7.2 - transitivePeerDependencies: - - bluebird - - chokidar - - supports-color - dev: true - /@angular/common@16.1.2(@angular/core@16.1.2)(rxjs@7.8.1): - resolution: {integrity: sha512-MrJ1CUKg4H8fH0Jc771gLceVcawBPjIM6TSLEM7DMeX7SZ6eSU9HAOCTHXBg7Kmb7ZS19G1BQBD/tkjivszk+Q==} + '@angular/common@16.2.12': + resolution: {integrity: sha512-B+WY/cT2VgEaz9HfJitBmgdk4I333XG/ybC98CMC4Wz8E49T8yzivmmxXB3OD6qvjcOB6ftuicl6WBqLbZNg2w==} engines: {node: ^16.14.0 || >=18.10.0} peerDependencies: - '@angular/core': 16.1.2 + '@angular/core': 16.2.12 rxjs: ^6.5.3 || ^7.4.0 - dependencies: - '@angular/core': 16.1.2(rxjs@7.8.1)(zone.js@0.13.1) - rxjs: 7.8.1 - tslib: 2.5.3 - /@angular/compiler-cli@16.1.2(@angular/compiler@16.1.2)(typescript@5.1.3): - resolution: {integrity: sha512-7Z4qPNv6zRTF8U0CEAqJMF2kzyImilOgGwp92PqwhycZIj/MQyN7Txv8uv2rghhCr9jt9bL2vrHPJNGD2UNLxw==} + '@angular/compiler-cli@16.2.12': + resolution: {integrity: sha512-pWSrr152562ujh6lsFZR8NfNc5Ljj+zSTQO44DsuB0tZjwEpnRcjJEgzuhGXr+CoiBf+jTSPZKemtSktDk5aaA==} engines: {node: ^16.14.0 || >=18.10.0} hasBin: true peerDependencies: - '@angular/compiler': 16.1.2 + '@angular/compiler': 16.2.12 typescript: '>=4.9.3 <5.2' - dependencies: - '@angular/compiler': 16.1.2(@angular/core@16.1.2) - '@babel/core': 7.21.8 - '@jridgewell/sourcemap-codec': 1.4.14 - chokidar: 3.5.3 - convert-source-map: 1.8.0 - reflect-metadata: 0.1.13 - semver: 7.3.8 - tslib: 2.5.3 - typescript: 5.1.3 - yargs: 17.6.2 - transitivePeerDependencies: - - supports-color - dev: true - /@angular/compiler@16.1.2(@angular/core@16.1.2): - resolution: {integrity: sha512-uLUyesolGL8sEb03GfwSFbtTa++XPQyabAUi53Nz5jzRhFzxCa6cO7w4DCxp7yzQM+8jiPKAuw9bQDNv1eYR7A==} + '@angular/compiler@16.2.12': + resolution: {integrity: sha512-6SMXUgSVekGM7R6l1Z9rCtUGtlg58GFmgbpMCsGf+VXxP468Njw8rjT2YZkf5aEPxEuRpSHhDYjqz7n14cwCXQ==} engines: {node: ^16.14.0 || >=18.10.0} peerDependencies: - '@angular/core': 16.1.2 + '@angular/core': 16.2.12 peerDependenciesMeta: '@angular/core': optional: true - dependencies: - '@angular/core': 16.1.2(rxjs@7.8.1)(zone.js@0.13.1) - tslib: 2.5.3 - /@angular/core@16.1.2(rxjs@7.8.1)(zone.js@0.13.1): - resolution: {integrity: sha512-1w8DcSY/ZJT6qBaZWZGNQ50YQ5Cffm7xXSIcjIZeQwQKRAZNlZ1O/pFEer1kvxkAbVsQH3Nf6kB6t8PkNFCWMg==} + '@angular/core@16.2.12': + resolution: {integrity: sha512-GLLlDeke/NjroaLYOks0uyzFVo6HyLl7VOm0K1QpLXnYvW63W9Ql/T3yguRZa7tRkOAeFZ3jw+1wnBD4O8MoUA==} engines: {node: ^16.14.0 || >=18.10.0} peerDependencies: rxjs: ^6.5.3 || ^7.4.0 zone.js: ~0.13.0 - dependencies: - rxjs: 7.8.1 - tslib: 2.5.3 - zone.js: 0.13.1 - /@angular/forms@16.1.2(@angular/common@16.1.2)(@angular/core@16.1.2)(@angular/platform-browser@16.1.2)(rxjs@7.8.1): - resolution: {integrity: sha512-OyAQRYE6B2NbucoE5KzmxkQqFFlcSnB8/LWS/J9KHGB1QkPdYJCuWSccypeo4YxgVjYCSvHDnO6jBiHDfwDBuQ==} + '@angular/forms@16.2.12': + resolution: {integrity: sha512-1Eao89hlBgLR3v8tU91vccn21BBKL06WWxl7zLpQmG6Hun+2jrThgOE4Pf3os4fkkbH4Apj0tWL2fNIWe/blbw==} engines: {node: ^16.14.0 || >=18.10.0} peerDependencies: - '@angular/common': 16.1.2 - '@angular/core': 16.1.2 - '@angular/platform-browser': 16.1.2 + '@angular/common': 16.2.12 + '@angular/core': 16.2.12 + '@angular/platform-browser': 16.2.12 rxjs: ^6.5.3 || ^7.4.0 - dependencies: - '@angular/common': 16.1.2(@angular/core@16.1.2)(rxjs@7.8.1) - '@angular/core': 16.1.2(rxjs@7.8.1)(zone.js@0.13.1) - '@angular/platform-browser': 16.1.2(@angular/animations@16.1.2)(@angular/common@16.1.2)(@angular/core@16.1.2) - rxjs: 7.8.1 - tslib: 2.5.3 - dev: false - /@angular/platform-browser-dynamic@16.1.2(@angular/common@16.1.2)(@angular/compiler@16.1.2)(@angular/core@16.1.2)(@angular/platform-browser@16.1.2): - resolution: {integrity: sha512-gBPvQ2tAGSUK8oCnL15rCzO9/gyzYR8pf6a7kQdDA1Vr3Aj02UCsJOZvedoMxbnGpLtw3JGWMqZ+bvp4/tCw1g==} + '@angular/platform-browser-dynamic@16.2.12': + resolution: {integrity: sha512-ya54jerNgreCVAR278wZavwjrUWImMr2F8yM5n9HBvsMBbFaAQ83anwbOEiHEF2BlR+gJiEBLfpuPRMw20pHqw==} engines: {node: ^16.14.0 || >=18.10.0} peerDependencies: - '@angular/common': 16.1.2 - '@angular/compiler': 16.1.2 - '@angular/core': 16.1.2 - '@angular/platform-browser': 16.1.2 - dependencies: - '@angular/common': 16.1.2(@angular/core@16.1.2)(rxjs@7.8.1) - '@angular/compiler': 16.1.2(@angular/core@16.1.2) - '@angular/core': 16.1.2(rxjs@7.8.1)(zone.js@0.13.1) - '@angular/platform-browser': 16.1.2(@angular/animations@16.1.2)(@angular/common@16.1.2)(@angular/core@16.1.2) - tslib: 2.5.3 - dev: false + '@angular/common': 16.2.12 + '@angular/compiler': 16.2.12 + '@angular/core': 16.2.12 + '@angular/platform-browser': 16.2.12 - /@angular/platform-browser@16.1.2(@angular/animations@16.1.2)(@angular/common@16.1.2)(@angular/core@16.1.2): - resolution: {integrity: sha512-jFelRYaVaD2F2ph+Z4tsQUxpw1gGiOU0t5i77srrHDtm+KXpERyYbnXQTog+6q+ScNtsB++0JNkiPQV6oOjtIw==} + '@angular/platform-browser@16.2.12': + resolution: {integrity: sha512-NnH7ju1iirmVEsUq432DTm0nZBGQsBrU40M3ZeVHMQ2subnGiyUs3QyzDz8+VWLL/T5xTxWLt9BkDn65vgzlIQ==} engines: {node: ^16.14.0 || >=18.10.0} peerDependencies: - '@angular/animations': 16.1.2 - '@angular/common': 16.1.2 - '@angular/core': 16.1.2 + '@angular/animations': 16.2.12 + '@angular/common': 16.2.12 + '@angular/core': 16.2.12 peerDependenciesMeta: '@angular/animations': optional: true - dependencies: - '@angular/animations': 16.1.2(@angular/core@16.1.2) - '@angular/common': 16.1.2(@angular/core@16.1.2)(rxjs@7.8.1) - '@angular/core': 16.1.2(rxjs@7.8.1)(zone.js@0.13.1) - tslib: 2.5.3 - dev: false - /@angular/router@16.1.2(@angular/common@16.1.2)(@angular/core@16.1.2)(@angular/platform-browser@16.1.2)(rxjs@7.8.1): - resolution: {integrity: sha512-0ZiEtJNwYUQewMXxMf2cNHl6N8ZroxC+WS7I6uq3QhPxhbiF0mThRyhRd158rfSVSLEFWKlyZbDQh1ECMSWhDQ==} + '@angular/router@16.2.12': + resolution: {integrity: sha512-aU6QnYSza005V9P3W6PpkieL56O0IHps96DjqI1RS8yOJUl3THmokqYN4Fm5+HXy4f390FN9i6ftadYQDKeWmA==} engines: {node: ^16.14.0 || >=18.10.0} peerDependencies: - '@angular/common': 16.1.2 - '@angular/core': 16.1.2 - '@angular/platform-browser': 16.1.2 + '@angular/common': 16.2.12 + '@angular/core': 16.2.12 + '@angular/platform-browser': 16.2.12 rxjs: ^6.5.3 || ^7.4.0 - dependencies: - '@angular/common': 16.1.2(@angular/core@16.1.2)(rxjs@7.8.1) - '@angular/core': 16.1.2(rxjs@7.8.1)(zone.js@0.13.1) - '@angular/platform-browser': 16.1.2(@angular/animations@16.1.2)(@angular/common@16.1.2)(@angular/core@16.1.2) - rxjs: 7.8.1 - tslib: 2.5.3 - dev: false - /@arcanis/slice-ansi@1.1.1: + '@arcanis/slice-ansi@1.1.1': resolution: {integrity: sha512-xguP2WR2Dv0gQ7Ykbdb7BNCnPnIPB94uTi0Z2NvkRBEnhbwjOQ7QyQKJXrVQg4qDpiD9hA5l5cCwy/z2OXgc3w==} - dependencies: - grapheme-splitter: 1.0.4 - dev: true - /@assemblyscript/loader@0.10.1: + '@assemblyscript/loader@0.10.1': resolution: {integrity: sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==} - dev: true - /@babel/code-frame@7.22.5: - resolution: {integrity: sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==} + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/highlight': 7.22.5 - /@babel/compat-data@7.22.5: - resolution: {integrity: sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==} + '@babel/compat-data@7.28.5': + resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} engines: {node: '>=6.9.0'} - dev: true - /@babel/core@7.21.8: - resolution: {integrity: sha512-YeM22Sondbo523Sz0+CirSPnbj9bG3P0CdHcBZdqUuaeOaYEFbOLoGU7lebvGP6P5J/WE9wOn7u7C4J9HvS1xQ==} + '@babel/core@7.22.9': + resolution: {integrity: sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==} engines: {node: '>=6.9.0'} - dependencies: - '@ampproject/remapping': 2.2.1 - '@babel/code-frame': 7.22.5 - '@babel/generator': 7.22.5 - '@babel/helper-compilation-targets': 7.22.5(@babel/core@7.21.8) - '@babel/helper-module-transforms': 7.22.5 - '@babel/helpers': 7.22.5 - '@babel/parser': 7.22.5 - '@babel/template': 7.22.5 - '@babel/traverse': 7.22.5 - '@babel/types': 7.22.5 - convert-source-map: 1.8.0 - debug: 4.3.4 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/core@7.22.5: - resolution: {integrity: sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==} + '@babel/core@7.23.2': + resolution: {integrity: sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==} engines: {node: '>=6.9.0'} - dependencies: - '@ampproject/remapping': 2.2.1 - '@babel/code-frame': 7.22.5 - '@babel/generator': 7.22.5 - '@babel/helper-compilation-targets': 7.22.5(@babel/core@7.22.5) - '@babel/helper-module-transforms': 7.22.5 - '@babel/helpers': 7.22.5 - '@babel/parser': 7.22.5 - '@babel/template': 7.22.5 - '@babel/traverse': 7.22.5 - '@babel/types': 7.22.5 - convert-source-map: 1.8.0 - debug: 4.3.4 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/generator@7.22.5: - resolution: {integrity: sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==} + '@babel/generator@7.22.9': + resolution: {integrity: sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.5': + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.22.5 - '@jridgewell/gen-mapping': 0.3.2 - '@jridgewell/trace-mapping': 0.3.18 - jsesc: 2.5.2 - dev: true - /@babel/helper-annotate-as-pure@7.22.5: + '@babel/helper-annotate-as-pure@7.22.5': resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.22.5 - dev: true - /@babel/helper-builder-binary-assignment-operator-visitor@7.22.5: - resolution: {integrity: sha512-m1EP3lVOPptR+2DwD125gziZNcmoNSHGmJROKoy87loWUQyJaVXDgpmruWqDARZSmtYQ+Dl25okU8+qhVzuykw==} + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.22.5 - dev: true - /@babel/helper-compilation-targets@7.22.5(@babel/core@7.21.8): - resolution: {integrity: sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==} + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/compat-data': 7.22.5 - '@babel/core': 7.21.8 - '@babel/helper-validator-option': 7.22.5 - browserslist: 4.21.5 - lru-cache: 5.1.1 - semver: 6.3.0 - dev: true - /@babel/helper-compilation-targets@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==} + '@babel/helper-create-class-features-plugin@7.28.5': + resolution: {integrity: sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/compat-data': 7.22.5 - '@babel/core': 7.22.5 - '@babel/helper-validator-option': 7.22.5 - browserslist: 4.21.5 - lru-cache: 5.1.1 - semver: 6.3.0 - dev: true - /@babel/helper-create-class-features-plugin@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-xkb58MyOYIslxu3gKmVXmjTtUPvBU4odYzbiIQbWwLKIHCsx6UGZGX6F1IznMFVnDdirseUZopzN+ZRt8Xb33Q==} + '@babel/helper-create-regexp-features-plugin@7.28.5': + resolution: {integrity: sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-environment-visitor': 7.22.5 - '@babel/helper-function-name': 7.22.5 - '@babel/helper-member-expression-to-functions': 7.22.5 - '@babel/helper-optimise-call-expression': 7.22.5 - '@babel/helper-replace-supers': 7.22.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.5 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/helper-create-regexp-features-plugin@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-1VpEFOIbMRaXyDeUwUfmTIxExLwQ+zkW+Bh5zXpApA3oQedBx9v/updixWxnx/bZpKw7u8VxWjb/qWpIcmPq8A==} - engines: {node: '>=6.9.0'} + '@babel/helper-define-polyfill-provider@0.4.4': + resolution: {integrity: sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA==} peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-annotate-as-pure': 7.22.5 - regexpu-core: 5.3.2 - semver: 6.3.0 - dev: true + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - /@babel/helper-define-polyfill-provider@0.4.0(@babel/core@7.22.5): - resolution: {integrity: sha512-RnanLx5ETe6aybRi1cO/edaRH+bNYWaryCEmjDDYyNr4wnSzyOp8T0dWipmqVHKEY3AbVKUom50AKSlj1zmKbg==} + '@babel/helper-define-polyfill-provider@0.5.0': + resolution: {integrity: sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==} peerDependencies: - '@babel/core': ^7.4.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-compilation-targets': 7.22.5(@babel/core@7.22.5) - '@babel/helper-plugin-utils': 7.22.5 - debug: 4.3.4 - lodash.debounce: 4.0.8 - resolve: 1.22.2 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: true + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - /@babel/helper-environment-visitor@7.22.5: - resolution: {integrity: sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==} - engines: {node: '>=6.9.0'} - dev: true + '@babel/helper-define-polyfill-provider@0.6.5': + resolution: {integrity: sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - /@babel/helper-function-name@7.22.5: - resolution: {integrity: sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==} + '@babel/helper-environment-visitor@7.24.7': + resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.22.5 - '@babel/types': 7.22.5 - dev: true - /@babel/helper-hoist-variables@7.22.5: - resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.22.5 - dev: true - /@babel/helper-member-expression-to-functions@7.22.5: - resolution: {integrity: sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==} + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.22.5 - dev: true - /@babel/helper-module-imports@7.22.5: - resolution: {integrity: sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==} + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.22.5 - dev: true - /@babel/helper-module-transforms@7.22.5: - resolution: {integrity: sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==} + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-environment-visitor': 7.22.5 - '@babel/helper-module-imports': 7.22.5 - '@babel/helper-simple-access': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.5 - '@babel/helper-validator-identifier': 7.22.5 - '@babel/template': 7.22.5 - '@babel/traverse': 7.22.5 - '@babel/types': 7.22.5 - transitivePeerDependencies: - - supports-color - dev: true + peerDependencies: + '@babel/core': ^7.0.0 - /@babel/helper-optimise-call-expression@7.22.5: - resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.22.5 - dev: true - /@babel/helper-plugin-utils@7.22.5: - resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} engines: {node: '>=6.9.0'} - dev: true - /@babel/helper-remap-async-to-generator@7.18.9(@babel/core@7.22.5): - resolution: {integrity: sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==} + '@babel/helper-remap-async-to-generator@7.27.1': + resolution: {integrity: sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-environment-visitor': 7.22.5 - '@babel/helper-wrap-function': 7.18.9 - '@babel/types': 7.22.5 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/helper-remap-async-to-generator@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-cU0Sq1Rf4Z55fgz7haOakIyM7+x/uCFwXpLPaeRzfoUtAEAuUZjZvFPjL/rk5rW693dIgn2hng1W7xbT7lWT4g==} + '@babel/helper-replace-supers@7.27.1': + resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-environment-visitor': 7.22.5 - '@babel/helper-wrap-function': 7.22.5 - '@babel/types': 7.22.5 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/helper-replace-supers@7.22.5: - resolution: {integrity: sha512-aLdNM5I3kdI/V9xGNyKSF3X/gTyMUBohTZ+/3QdQKAA9vxIiy12E+8E2HoOP1/DjeqU+g6as35QHJNMDDYpuCg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-environment-visitor': 7.22.5 - '@babel/helper-member-expression-to-functions': 7.22.5 - '@babel/helper-optimise-call-expression': 7.22.5 - '@babel/template': 7.22.5 - '@babel/traverse': 7.22.5 - '@babel/types': 7.22.5 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/helper-simple-access@7.22.5: - resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.22.5 - dev: true - - /@babel/helper-skip-transparent-expression-wrappers@7.22.5: - resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.22.5 - dev: true - - /@babel/helper-split-export-declaration@7.22.5: - resolution: {integrity: sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.22.5 - dev: true - /@babel/helper-string-parser@7.22.5: - resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} engines: {node: '>=6.9.0'} - dev: true - /@babel/helper-validator-identifier@7.22.5: - resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==} + '@babel/helper-split-export-declaration@7.22.6': + resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} engines: {node: '>=6.9.0'} - /@babel/helper-validator-option@7.22.5: - resolution: {integrity: sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==} + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} - dev: true - /@babel/helper-wrap-function@7.18.9: - resolution: {integrity: sha512-cG2ru3TRAL6a60tfQflpEfs4ldiPwF6YW3zfJiRgmoFVIaC1vGnBBgatfec+ZUziPHkHSaXAuEck3Cdkf3eRpQ==} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-function-name': 7.22.5 - '@babel/template': 7.22.5 - '@babel/traverse': 7.22.5 - '@babel/types': 7.22.5 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/helper-wrap-function@7.22.5: - resolution: {integrity: sha512-bYqLIBSEshYcYQyfks8ewYA8S30yaGSeRslcvKMvoUk6HHPySbxHq9YRi6ghhzEU+yhQv9bP/jXnygkStOcqZw==} + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-function-name': 7.22.5 - '@babel/template': 7.22.5 - '@babel/traverse': 7.22.5 - '@babel/types': 7.22.5 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/helpers@7.22.5: - resolution: {integrity: sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==} + '@babel/helper-wrap-function@7.28.3': + resolution: {integrity: sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.22.5 - '@babel/traverse': 7.22.5 - '@babel/types': 7.22.5 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/highlight@7.22.5: - resolution: {integrity: sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==} + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-validator-identifier': 7.22.5 - chalk: 2.4.2 - js-tokens: 4.0.0 - /@babel/parser@7.22.5: - resolution: {integrity: sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==} + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} engines: {node: '>=6.0.0'} hasBin: true - dependencies: - '@babel/types': 7.22.5 - dev: true - /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==} + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1': + resolution: {integrity: sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g==} + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1': + resolution: {integrity: sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.13.0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/plugin-transform-optional-chaining': 7.22.5(@babel/core@7.22.5) - dev: true - /@babel/plugin-proposal-async-generator-functions@7.20.7(@babel/core@7.22.5): + '@babel/plugin-proposal-async-generator-functions@7.20.7': resolution: {integrity: sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead. peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-environment-visitor': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-remap-async-to-generator': 7.18.9(@babel/core@7.22.5) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.22.5) - transitivePeerDependencies: - - supports-color - dev: true - /@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.22.5): + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - dev: true - /@babel/plugin-proposal-unicode-property-regex@7.18.6(@babel/core@7.22.5): + '@babel/plugin-proposal-unicode-property-regex@7.18.6': resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} engines: {node: '>=4'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead. peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-create-regexp-features-plugin': 7.22.5(@babel/core@7.22.5) - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.22.5): + '@babel/plugin-syntax-async-generators@7.8.4': resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.22.5): + '@babel/plugin-syntax-class-properties@7.12.13': resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.22.5): + '@babel/plugin-syntax-class-static-block@7.14.5': resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.22.5): + '@babel/plugin-syntax-dynamic-import@7.8.3': resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.22.5): + '@babel/plugin-syntax-export-namespace-from@7.8.3': resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-import-assertions@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==} + '@babel/plugin-syntax-import-assertions@7.27.1': + resolution: {integrity: sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-import-attributes@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==} + '@babel/plugin-syntax-import-attributes@7.27.1': + resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.22.5): + '@babel/plugin-syntax-import-meta@7.10.4': resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.22.5): + '@babel/plugin-syntax-json-strings@7.8.3': resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.22.5): + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.22.5): + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.22.5): + '@babel/plugin-syntax-numeric-separator@7.10.4': resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.22.5): + '@babel/plugin-syntax-object-rest-spread@7.8.3': resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.22.5): + '@babel/plugin-syntax-optional-catch-binding@7.8.3': resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.22.5): + '@babel/plugin-syntax-optional-chaining@7.8.3': resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.22.5): + '@babel/plugin-syntax-private-property-in-object@7.14.5': resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.22.5): + '@babel/plugin-syntax-top-level-await@7.14.5': resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.22.5): + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-create-regexp-features-plugin': 7.22.5(@babel/core@7.22.5) - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-arrow-functions@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==} + '@babel/plugin-transform-arrow-functions@7.27.1': + resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-async-generator-functions@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-gGOEvFzm3fWoyD5uZq7vVTD57pPJ3PczPUD/xCFGjzBpUosnklmXyKnGQbbbGs1NPNPskFex0j93yKbHt0cHyg==} + '@babel/plugin-transform-async-generator-functions@7.28.0': + resolution: {integrity: sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-environment-visitor': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-remap-async-to-generator': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.22.5) - transitivePeerDependencies: - - supports-color - dev: true - /@babel/plugin-transform-async-to-generator@7.22.5(@babel/core@7.22.5): + '@babel/plugin-transform-async-to-generator@7.22.5': resolution: {integrity: sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-module-imports': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-remap-async-to-generator': 7.22.5(@babel/core@7.22.5) - transitivePeerDependencies: - - supports-color - dev: true - /@babel/plugin-transform-block-scoped-functions@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==} + '@babel/plugin-transform-block-scoped-functions@7.27.1': + resolution: {integrity: sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-block-scoping@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-EcACl1i5fSQ6bt+YGuU/XGCeZKStLmyVGytWkpyhCLeQVA0eu6Wtiw92V+I1T/hnezUv7j74dA/Ro69gWcU+hg==} + '@babel/plugin-transform-block-scoping@7.28.5': + resolution: {integrity: sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-class-properties@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==} + '@babel/plugin-transform-class-properties@7.27.1': + resolution: {integrity: sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-create-class-features-plugin': 7.22.5(@babel/core@7.22.5) - '@babel/helper-plugin-utils': 7.22.5 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/plugin-transform-class-static-block@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-SPToJ5eYZLxlnp1UzdARpOGeC2GbHvr9d/UV0EukuVx8atktg194oe+C5BqQ8jRTkgLRVOPYeXRSBg1IlMoVRA==} + '@babel/plugin-transform-class-static-block@7.28.3': + resolution: {integrity: sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.12.0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-create-class-features-plugin': 7.22.5(@babel/core@7.22.5) - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.22.5) - transitivePeerDependencies: - - supports-color - dev: true - /@babel/plugin-transform-classes@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-2edQhLfibpWpsVBx2n/GKOz6JdGQvLruZQfGr9l1qes2KQaWswjBzhQF7UDUZMNaMMQeYnQzxwOMPsbYF7wqPQ==} + '@babel/plugin-transform-classes@7.28.4': + resolution: {integrity: sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-compilation-targets': 7.22.5(@babel/core@7.22.5) - '@babel/helper-environment-visitor': 7.22.5 - '@babel/helper-function-name': 7.22.5 - '@babel/helper-optimise-call-expression': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-replace-supers': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.5 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/plugin-transform-computed-properties@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==} + '@babel/plugin-transform-computed-properties@7.27.1': + resolution: {integrity: sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/template': 7.22.5 - dev: true - /@babel/plugin-transform-destructuring@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-GfqcFuGW8vnEqTUBM7UtPd5A4q797LTvvwKxXTgRsFjoqaJiEg9deBG6kWeQYkVEL569NpnmpC0Pkr/8BLKGnQ==} + '@babel/plugin-transform-destructuring@7.28.5': + resolution: {integrity: sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-dotall-regex@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==} + '@babel/plugin-transform-dotall-regex@7.27.1': + resolution: {integrity: sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-create-regexp-features-plugin': 7.22.5(@babel/core@7.22.5) - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-duplicate-keys@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==} + '@babel/plugin-transform-duplicate-keys@7.27.1': + resolution: {integrity: sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-dynamic-import@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-0MC3ppTB1AMxd8fXjSrbPa7LT9hrImt+/fcj+Pg5YMD7UQyWp/02+JWpdnCymmsXwIx5Z+sYn1bwCn4ZJNvhqQ==} + '@babel/plugin-transform-dynamic-import@7.27.1': + resolution: {integrity: sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.22.5) - dev: true - /@babel/plugin-transform-exponentiation-operator@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==} + '@babel/plugin-transform-exponentiation-operator@7.28.5': + resolution: {integrity: sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-builder-binary-assignment-operator-visitor': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-export-namespace-from@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-X4hhm7FRnPgd4nDA4b/5V280xCx6oL7Oob5+9qVS5C13Zq4bh1qq7LU0GgRU6b5dBWBvhGaXYVB4AcN6+ol6vg==} + '@babel/plugin-transform-export-namespace-from@7.27.1': + resolution: {integrity: sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.22.5) - dev: true - /@babel/plugin-transform-for-of@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A==} + '@babel/plugin-transform-for-of@7.27.1': + resolution: {integrity: sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-function-name@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==} + '@babel/plugin-transform-function-name@7.27.1': + resolution: {integrity: sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-compilation-targets': 7.22.5(@babel/core@7.22.5) - '@babel/helper-function-name': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-json-strings@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-DuCRB7fu8MyTLbEQd1ew3R85nx/88yMoqo2uPSjevMj3yoN7CDM8jkgrY0wmVxfJZyJ/B9fE1iq7EQppWQmR5A==} + '@babel/plugin-transform-json-strings@7.27.1': + resolution: {integrity: sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.22.5) - dev: true - /@babel/plugin-transform-literals@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==} + '@babel/plugin-transform-literals@7.27.1': + resolution: {integrity: sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-logical-assignment-operators@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-MQQOUW1KL8X0cDWfbwYP+TbVbZm16QmQXJQ+vndPtH/BoO0lOKpVoEDMI7+PskYxH+IiE0tS8xZye0qr1lGzSA==} + '@babel/plugin-transform-logical-assignment-operators@7.28.5': + resolution: {integrity: sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.22.5) - dev: true - /@babel/plugin-transform-member-expression-literals@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==} + '@babel/plugin-transform-member-expression-literals@7.27.1': + resolution: {integrity: sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-modules-amd@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==} + '@babel/plugin-transform-modules-amd@7.27.1': + resolution: {integrity: sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-module-transforms': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/plugin-transform-modules-commonjs@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==} + '@babel/plugin-transform-modules-commonjs@7.27.1': + resolution: {integrity: sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-module-transforms': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-simple-access': 7.22.5 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/plugin-transform-modules-systemjs@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ==} + '@babel/plugin-transform-modules-systemjs@7.28.5': + resolution: {integrity: sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-module-transforms': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-validator-identifier': 7.22.5 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/plugin-transform-modules-umd@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==} + '@babel/plugin-transform-modules-umd@7.27.1': + resolution: {integrity: sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-module-transforms': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==} + '@babel/plugin-transform-named-capturing-groups-regex@7.27.1': + resolution: {integrity: sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-create-regexp-features-plugin': 7.22.5(@babel/core@7.22.5) - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-new-target@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==} + '@babel/plugin-transform-new-target@7.27.1': + resolution: {integrity: sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-nullish-coalescing-operator@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-6CF8g6z1dNYZ/VXok5uYkkBBICHZPiGEl7oDnAx2Mt1hlHVHOSIKWJaXHjQJA5VB43KZnXZDIexMchY4y2PGdA==} + '@babel/plugin-transform-nullish-coalescing-operator@7.27.1': + resolution: {integrity: sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.22.5) - dev: true - /@babel/plugin-transform-numeric-separator@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g==} + '@babel/plugin-transform-numeric-separator@7.27.1': + resolution: {integrity: sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.22.5) - dev: true - /@babel/plugin-transform-object-rest-spread@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-Kk3lyDmEslH9DnvCDA1s1kkd3YWQITiBOHngOtDL9Pt6BZjzqb6hiOlb8VfjiiQJ2unmegBqZu0rx5RxJb5vmQ==} + '@babel/plugin-transform-object-rest-spread@7.28.4': + resolution: {integrity: sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/compat-data': 7.22.5 - '@babel/core': 7.22.5 - '@babel/helper-compilation-targets': 7.22.5(@babel/core@7.22.5) - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.22.5) - '@babel/plugin-transform-parameters': 7.22.5(@babel/core@7.22.5) - dev: true - /@babel/plugin-transform-object-super@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==} + '@babel/plugin-transform-object-super@7.27.1': + resolution: {integrity: sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-replace-supers': 7.22.5 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/plugin-transform-optional-catch-binding@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-pH8orJahy+hzZje5b8e2QIlBWQvGpelS76C63Z+jhZKsmzfNaPQ+LaW6dcJ9bxTpo1mtXbgHwy765Ro3jftmUg==} + '@babel/plugin-transform-optional-catch-binding@7.27.1': + resolution: {integrity: sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.22.5) - dev: true - /@babel/plugin-transform-optional-chaining@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-AconbMKOMkyG+xCng2JogMCDcqW8wedQAqpVIL4cOSescZ7+iW8utC6YDZLMCSUIReEA733gzRSaOSXMAt/4WQ==} + '@babel/plugin-transform-optional-chaining@7.28.5': + resolution: {integrity: sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.22.5) - dev: true - /@babel/plugin-transform-parameters@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg==} + '@babel/plugin-transform-parameters@7.27.7': + resolution: {integrity: sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-private-methods@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==} + '@babel/plugin-transform-private-methods@7.27.1': + resolution: {integrity: sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-create-class-features-plugin': 7.22.5(@babel/core@7.22.5) - '@babel/helper-plugin-utils': 7.22.5 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/plugin-transform-private-property-in-object@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-/9xnaTTJcVoBtSSmrVyhtSvO3kbqS2ODoh2juEU72c3aYonNF0OMGiaz2gjukyKM2wBBYJP38S4JiE0Wfb5VMQ==} + '@babel/plugin-transform-private-property-in-object@7.27.1': + resolution: {integrity: sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-create-class-features-plugin': 7.22.5(@babel/core@7.22.5) - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.22.5) - transitivePeerDependencies: - - supports-color - dev: true - /@babel/plugin-transform-property-literals@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==} + '@babel/plugin-transform-property-literals@7.27.1': + resolution: {integrity: sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-regenerator@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-rR7KePOE7gfEtNTh9Qw+iO3Q/e4DEsoQ+hdvM6QUDH7JRJ5qxq5AA52ZzBWbI5i9lfNuvySgOGP8ZN7LAmaiPw==} + '@babel/plugin-transform-regenerator@7.28.4': + resolution: {integrity: sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - regenerator-transform: 0.15.1 - dev: true - /@babel/plugin-transform-reserved-words@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==} + '@babel/plugin-transform-reserved-words@7.27.1': + resolution: {integrity: sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-runtime@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-bg4Wxd1FWeFx3daHFTWk1pkSWK/AyQuiyAoeZAOkAOUBjnZPH6KT7eMxouV47tQ6hl6ax2zyAWBdWZXbrvXlaw==} + '@babel/plugin-transform-runtime@7.22.9': + resolution: {integrity: sha512-9KjBH61AGJetCPYp/IEyLEp47SyybZb0nDRpBvmtEkm+rUIwxdlKpyNHI1TmsGkeuLclJdleQHRZ8XLBnnh8CQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-module-imports': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - babel-plugin-polyfill-corejs2: 0.4.3(@babel/core@7.22.5) - babel-plugin-polyfill-corejs3: 0.8.1(@babel/core@7.22.5) - babel-plugin-polyfill-regenerator: 0.5.0(@babel/core@7.22.5) - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/plugin-transform-shorthand-properties@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==} + '@babel/plugin-transform-shorthand-properties@7.27.1': + resolution: {integrity: sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-spread@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==} + '@babel/plugin-transform-spread@7.27.1': + resolution: {integrity: sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - dev: true - /@babel/plugin-transform-sticky-regex@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==} + '@babel/plugin-transform-sticky-regex@7.27.1': + resolution: {integrity: sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-template-literals@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==} + '@babel/plugin-transform-template-literals@7.27.1': + resolution: {integrity: sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-typeof-symbol@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==} + '@babel/plugin-transform-typeof-symbol@7.27.1': + resolution: {integrity: sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-unicode-escapes@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-biEmVg1IYB/raUO5wT1tgfacCef15Fbzhkx493D3urBI++6hpJ+RFG4SrWMn0NEZLfvilqKf3QDrRVZHo08FYg==} + '@babel/plugin-transform-unicode-escapes@7.27.1': + resolution: {integrity: sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-unicode-property-regex@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==} + '@babel/plugin-transform-unicode-property-regex@7.27.1': + resolution: {integrity: sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-create-regexp-features-plugin': 7.22.5(@babel/core@7.22.5) - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-unicode-regex@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==} + '@babel/plugin-transform-unicode-regex@7.27.1': + resolution: {integrity: sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-create-regexp-features-plugin': 7.22.5(@babel/core@7.22.5) - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-transform-unicode-sets-regex@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==} + '@babel/plugin-transform-unicode-sets-regex@7.27.1': + resolution: {integrity: sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-create-regexp-features-plugin': 7.22.5(@babel/core@7.22.5) - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/preset-env@7.22.5(@babel/core@7.22.5): - resolution: {integrity: sha512-fj06hw89dpiZzGZtxn+QybifF07nNiZjZ7sazs2aVDcysAZVGjW7+7iFYxg6GLNM47R/thYfLdrXc+2f11Vi9A==} + '@babel/preset-env@7.22.9': + resolution: {integrity: sha512-wNi5H/Emkhll/bqPjsjQorSykrlfY5OWakd6AulLvMEytpKasMVUpVy8RL4qBIBs5Ac6/5i0/Rv0b/Fg6Eag/g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/compat-data': 7.22.5 - '@babel/core': 7.22.5 - '@babel/helper-compilation-targets': 7.22.5(@babel/core@7.22.5) - '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-validator-option': 7.22.5 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.22.5) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.22.5) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.22.5) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.22.5) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.22.5) - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.22.5) - '@babel/plugin-syntax-import-assertions': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-syntax-import-attributes': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.22.5) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.22.5) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.22.5) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.22.5) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.22.5) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.22.5) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.22.5) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.22.5) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.22.5) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.22.5) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.22.5) - '@babel/plugin-transform-arrow-functions': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-async-generator-functions': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-async-to-generator': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-block-scoped-functions': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-block-scoping': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-class-properties': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-class-static-block': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-classes': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-computed-properties': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-destructuring': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-dotall-regex': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-duplicate-keys': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-dynamic-import': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-exponentiation-operator': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-export-namespace-from': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-for-of': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-function-name': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-json-strings': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-literals': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-logical-assignment-operators': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-member-expression-literals': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-modules-amd': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-modules-commonjs': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-modules-systemjs': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-modules-umd': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-new-target': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-nullish-coalescing-operator': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-numeric-separator': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-object-rest-spread': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-object-super': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-optional-catch-binding': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-optional-chaining': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-parameters': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-private-methods': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-private-property-in-object': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-property-literals': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-regenerator': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-reserved-words': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-shorthand-properties': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-spread': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-sticky-regex': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-template-literals': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-typeof-symbol': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-unicode-escapes': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-unicode-property-regex': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-unicode-regex': 7.22.5(@babel/core@7.22.5) - '@babel/plugin-transform-unicode-sets-regex': 7.22.5(@babel/core@7.22.5) - '@babel/preset-modules': 0.1.5(@babel/core@7.22.5) - '@babel/types': 7.22.5 - babel-plugin-polyfill-corejs2: 0.4.3(@babel/core@7.22.5) - babel-plugin-polyfill-corejs3: 0.8.1(@babel/core@7.22.5) - babel-plugin-polyfill-regenerator: 0.5.0(@babel/core@7.22.5) - core-js-compat: 3.31.0 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/preset-modules@0.1.5(@babel/core@7.22.5): - resolution: {integrity: sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==} + '@babel/preset-modules@0.1.6': + resolution: {integrity: sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg==} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.22.5) - '@babel/plugin-transform-dotall-regex': 7.22.5(@babel/core@7.22.5) - '@babel/types': 7.22.5 - esutils: 2.0.3 - dev: true - - /@babel/regjsgen@0.8.0: - resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==} - dev: true + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 - /@babel/runtime@7.22.5: - resolution: {integrity: sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==} + '@babel/runtime@7.22.6': + resolution: {integrity: sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==} engines: {node: '>=6.9.0'} - dependencies: - regenerator-runtime: 0.13.11 - dev: true - /@babel/template@7.22.5: + '@babel/template@7.22.5': resolution: {integrity: sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/code-frame': 7.22.5 - '@babel/parser': 7.22.5 - '@babel/types': 7.22.5 - dev: true - /@babel/traverse@7.22.5: - resolution: {integrity: sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==} + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/code-frame': 7.22.5 - '@babel/generator': 7.22.5 - '@babel/helper-environment-visitor': 7.22.5 - '@babel/helper-function-name': 7.22.5 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.5 - '@babel/parser': 7.22.5 - '@babel/types': 7.22.5 - debug: 4.3.4 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/types@7.22.5: - resolution: {integrity: sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==} + '@babel/traverse@7.28.5': + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-string-parser': 7.22.5 - '@babel/helper-validator-identifier': 7.22.5 - to-fast-properties: 2.0.0 - dev: true - /@bgotink/kdl@0.1.6: - resolution: {integrity: sha512-7xLM6wg76hnMdLi+mBZe1KcAwav2Q4U4XGSbS9fAHHmRvK0ntQTqm4ryYXrWjSncYIfz2u1mLMvMQQ1+EcgKLA==} - dev: true + '@bgotink/kdl@0.1.7': + resolution: {integrity: sha512-pjvuPTvyxFxZh1VO3UYTNOjkfMS13c1XF9qtbwNBu2S1/QfmPlPbXz+MLc5is2isS0sJHSrFkfLtIjs4g1lKOA==} - /@cspotcode/source-map-support@0.8.1: + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - dependencies: - '@jridgewell/trace-mapping': 0.3.9 - dev: true - /@discoveryjs/json-ext@0.5.7: + '@discoveryjs/json-ext@0.5.7': resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} - dev: true - /@esbuild/android-arm64@0.17.19: - resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} + '@esbuild/android-arm64@0.18.17': + resolution: {integrity: sha512-9np+YYdNDed5+Jgr1TdWBsozZ85U1Oa3xW0c7TWqH0y2aGghXtZsuT8nYRbzOMcl0bXZXjOGbksoTtVOlWrRZg==} engines: {node: '>=12'} cpu: [arm64] os: [android] - requiresBuild: true - dev: true - optional: true - /@esbuild/android-arm@0.17.19: - resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} + '@esbuild/android-arm@0.18.17': + resolution: {integrity: sha512-wHsmJG/dnL3OkpAcwbgoBTTMHVi4Uyou3F5mf58ZtmUyIKfcdA7TROav/6tCzET4A3QW2Q2FC+eFneMU+iyOxg==} engines: {node: '>=12'} cpu: [arm] os: [android] - requiresBuild: true - dev: true - optional: true - /@esbuild/android-x64@0.17.19: - resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} + '@esbuild/android-x64@0.18.17': + resolution: {integrity: sha512-O+FeWB/+xya0aLg23hHEM2E3hbfwZzjqumKMSIqcHbNvDa+dza2D0yLuymRBQQnC34CWrsJUXyH2MG5VnLd6uw==} engines: {node: '>=12'} cpu: [x64] os: [android] - requiresBuild: true - dev: true - optional: true - /@esbuild/darwin-arm64@0.17.19: - resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} + '@esbuild/darwin-arm64@0.18.17': + resolution: {integrity: sha512-M9uJ9VSB1oli2BE/dJs3zVr9kcCBBsE883prage1NWz6pBS++1oNn/7soPNS3+1DGj0FrkSvnED4Bmlu1VAE9g==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] - requiresBuild: true - dev: true - optional: true - /@esbuild/darwin-x64@0.17.19: - resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} + '@esbuild/darwin-x64@0.18.17': + resolution: {integrity: sha512-XDre+J5YeIJDMfp3n0279DFNrGCXlxOuGsWIkRb1NThMZ0BsrWXoTg23Jer7fEXQ9Ye5QjrvXpxnhzl3bHtk0g==} engines: {node: '>=12'} cpu: [x64] os: [darwin] - requiresBuild: true - dev: true - optional: true - /@esbuild/freebsd-arm64@0.17.19: - resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} + '@esbuild/freebsd-arm64@0.18.17': + resolution: {integrity: sha512-cjTzGa3QlNfERa0+ptykyxs5A6FEUQQF0MuilYXYBGdBxD3vxJcKnzDlhDCa1VAJCmAxed6mYhA2KaJIbtiNuQ==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/freebsd-x64@0.17.19: - resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} + '@esbuild/freebsd-x64@0.18.17': + resolution: {integrity: sha512-sOxEvR8d7V7Kw8QqzxWc7bFfnWnGdaFBut1dRUYtu+EIRXefBc/eIsiUiShnW0hM3FmQ5Zf27suDuHsKgZ5QrA==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-arm64@0.17.19: - resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} + '@esbuild/linux-arm64@0.18.17': + resolution: {integrity: sha512-c9w3tE7qA3CYWjT+M3BMbwMt+0JYOp3vCMKgVBrCl1nwjAlOMYzEo+gG7QaZ9AtqZFj5MbUc885wuBBmu6aADQ==} engines: {node: '>=12'} cpu: [arm64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-arm@0.17.19: - resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} + '@esbuild/linux-arm@0.18.17': + resolution: {integrity: sha512-2d3Lw6wkwgSLC2fIvXKoMNGVaeY8qdN0IC3rfuVxJp89CRfA3e3VqWifGDfuakPmp90+ZirmTfye1n4ncjv2lg==} engines: {node: '>=12'} cpu: [arm] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-ia32@0.17.19: - resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} + '@esbuild/linux-ia32@0.18.17': + resolution: {integrity: sha512-1DS9F966pn5pPnqXYz16dQqWIB0dmDfAQZd6jSSpiT9eX1NzKh07J6VKR3AoXXXEk6CqZMojiVDSZi1SlmKVdg==} engines: {node: '>=12'} cpu: [ia32] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-loong64@0.17.19: - resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} + '@esbuild/linux-loong64@0.18.17': + resolution: {integrity: sha512-EvLsxCk6ZF0fpCB6w6eOI2Fc8KW5N6sHlIovNe8uOFObL2O+Mr0bflPHyHwLT6rwMg9r77WOAWb2FqCQrVnwFg==} engines: {node: '>=12'} cpu: [loong64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-mips64el@0.17.19: - resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} + '@esbuild/linux-mips64el@0.18.17': + resolution: {integrity: sha512-e0bIdHA5p6l+lwqTE36NAW5hHtw2tNRmHlGBygZC14QObsA3bD4C6sXLJjvnDIjSKhW1/0S3eDy+QmX/uZWEYQ==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-ppc64@0.17.19: - resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} + '@esbuild/linux-ppc64@0.18.17': + resolution: {integrity: sha512-BAAilJ0M5O2uMxHYGjFKn4nJKF6fNCdP1E0o5t5fvMYYzeIqy2JdAP88Az5LHt9qBoUa4tDaRpfWt21ep5/WqQ==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-riscv64@0.17.19: - resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} + '@esbuild/linux-riscv64@0.18.17': + resolution: {integrity: sha512-Wh/HW2MPnC3b8BqRSIme/9Zhab36PPH+3zam5pqGRH4pE+4xTrVLx2+XdGp6fVS3L2x+DrsIcsbMleex8fbE6g==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-s390x@0.17.19: - resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} + '@esbuild/linux-s390x@0.18.17': + resolution: {integrity: sha512-j/34jAl3ul3PNcK3pfI0NSlBANduT2UO5kZ7FCaK33XFv3chDhICLY8wJJWIhiQ+YNdQ9dxqQctRg2bvrMlYgg==} engines: {node: '>=12'} cpu: [s390x] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-x64@0.17.19: - resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} + '@esbuild/linux-x64@0.18.17': + resolution: {integrity: sha512-QM50vJ/y+8I60qEmFxMoxIx4de03pGo2HwxdBeFd4nMh364X6TIBZ6VQ5UQmPbQWUVWHWws5MmJXlHAXvJEmpQ==} engines: {node: '>=12'} cpu: [x64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/netbsd-x64@0.17.19: - resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} + '@esbuild/netbsd-x64@0.18.17': + resolution: {integrity: sha512-/jGlhWR7Sj9JPZHzXyyMZ1RFMkNPjC6QIAan0sDOtIo2TYk3tZn5UDrkE0XgsTQCxWTTOcMPf9p6Rh2hXtl5TQ==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/openbsd-x64@0.17.19: - resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} + '@esbuild/openbsd-x64@0.18.17': + resolution: {integrity: sha512-rSEeYaGgyGGf4qZM2NonMhMOP/5EHp4u9ehFiBrg7stH6BYEEjlkVREuDEcQ0LfIl53OXLxNbfuIj7mr5m29TA==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/sunos-x64@0.17.19: - resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==} + '@esbuild/sunos-x64@0.18.17': + resolution: {integrity: sha512-Y7ZBbkLqlSgn4+zot4KUNYst0bFoO68tRgI6mY2FIM+b7ZbyNVtNbDP5y8qlu4/knZZ73fgJDlXID+ohY5zt5g==} engines: {node: '>=12'} cpu: [x64] os: [sunos] - requiresBuild: true - dev: true - optional: true - /@esbuild/win32-arm64@0.17.19: - resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} + '@esbuild/win32-arm64@0.18.17': + resolution: {integrity: sha512-bwPmTJsEQcbZk26oYpc4c/8PvTY3J5/QK8jM19DVlEsAB41M39aWovWoHtNm78sd6ip6prilxeHosPADXtEJFw==} engines: {node: '>=12'} cpu: [arm64] os: [win32] - requiresBuild: true - dev: true - optional: true - /@esbuild/win32-ia32@0.17.19: - resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} + '@esbuild/win32-ia32@0.18.17': + resolution: {integrity: sha512-H/XaPtPKli2MhW+3CQueo6Ni3Avggi6hP/YvgkEe1aSaxw+AeO8MFjq8DlgfTd9Iz4Yih3QCZI6YLMoyccnPRg==} engines: {node: '>=12'} cpu: [ia32] os: [win32] - requiresBuild: true - dev: true - optional: true - /@esbuild/win32-x64@0.17.19: - resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} + '@esbuild/win32-x64@0.18.17': + resolution: {integrity: sha512-fGEb8f2BSA3CW7riJVurug65ACLuQAzKq0SSqkY2b2yHHH0MzDfbLyKIGzHwOI/gkHcxM/leuSW6D5w/LMNitA==} engines: {node: '>=12'} cpu: [x64] os: [win32] - requiresBuild: true - dev: true - optional: true - /@eslint-community/eslint-utils@4.4.0(eslint@8.43.0): - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - dependencies: - eslint: 8.43.0 - eslint-visitor-keys: 3.4.1 - dev: true - /@eslint-community/regexpp@4.5.1: - resolution: {integrity: sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==} + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - dev: true - /@eslint/eslintrc@2.0.3: - resolution: {integrity: sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==} + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - ajv: 6.12.6 - debug: 4.3.4 - espree: 9.5.2 - globals: 13.20.0 - ignore: 5.2.4 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - dev: true - /@eslint/js@8.43.0: - resolution: {integrity: sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg==} + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - /@fortawesome/angular-fontawesome@0.13.0(@angular/core@16.1.2)(@fortawesome/fontawesome-svg-core@6.4.0): + '@fortawesome/angular-fontawesome@0.13.0': resolution: {integrity: sha512-gzSPRdveOXNO7NIiMgTyB46aiHG0i98KinnAEqHXi8qzraM/kCcHn/0y3f4MhemX6kftwsFli0IU8RyHmtXlSQ==} peerDependencies: '@angular/core': ^16.0.0 '@fortawesome/fontawesome-svg-core': ~1.2.27 || ~1.3.0-beta2 || ^6.1.0 - dependencies: - '@angular/core': 16.1.2(rxjs@7.8.1)(zone.js@0.13.1) - '@fortawesome/fontawesome-svg-core': 6.4.0 - tslib: 2.5.3 - dev: false - /@fortawesome/fontawesome-common-types@6.4.0: - resolution: {integrity: sha512-HNii132xfomg5QVZw0HwXXpN22s7VBHQBv9CeOu9tfJnhsWQNd2lmTNi8CSrnw5B+5YOmzu1UoPAyxaXsJ6RgQ==} + '@fortawesome/fontawesome-common-types@6.7.2': + resolution: {integrity: sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==} engines: {node: '>=6'} - requiresBuild: true - dev: false - /@fortawesome/fontawesome-svg-core@6.4.0: - resolution: {integrity: sha512-Bertv8xOiVELz5raB2FlXDPKt+m94MQ3JgDfsVbrqNpLU9+UE2E18GKjLKw+d3XbeYPqg1pzyQKGsrzbw+pPaw==} + '@fortawesome/fontawesome-svg-core@6.7.2': + resolution: {integrity: sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==} engines: {node: '>=6'} - requiresBuild: true - dependencies: - '@fortawesome/fontawesome-common-types': 6.4.0 - dev: false - /@fortawesome/free-regular-svg-icons@6.4.0: - resolution: {integrity: sha512-ZfycI7D0KWPZtf7wtMFnQxs8qjBXArRzczABuMQqecA/nXohquJ5J/RCR77PmY5qGWkxAZDxpnUFVXKwtY/jPw==} + '@fortawesome/free-regular-svg-icons@6.7.2': + resolution: {integrity: sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==} engines: {node: '>=6'} - requiresBuild: true - dependencies: - '@fortawesome/fontawesome-common-types': 6.4.0 - dev: false - /@fortawesome/free-solid-svg-icons@6.4.0: - resolution: {integrity: sha512-kutPeRGWm8V5dltFP1zGjQOEAzaLZj4StdQhWVZnfGFCvAPVvHh8qk5bRrU4KXnRRRNni5tKQI9PBAdI6MP8nQ==} + '@fortawesome/free-solid-svg-icons@6.7.2': + resolution: {integrity: sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==} engines: {node: '>=6'} - requiresBuild: true - dependencies: - '@fortawesome/fontawesome-common-types': 6.4.0 - dev: false - /@gar/promisify@1.1.3: + '@gar/promisify@1.1.3': resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} - dev: true - /@humanwhocodes/config-array@0.11.10: - resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==} + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} - dependencies: - '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.4 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - dev: true + deprecated: Use @eslint/config-array instead - /@humanwhocodes/module-importer@1.0.1: + '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - dev: true - /@humanwhocodes/object-schema@1.2.1: - resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} - dev: true + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead - /@isaacs/cliui@8.0.2: + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} - dependencies: - string-width: 5.1.2 - string-width-cjs: /string-width@4.2.3 - strip-ansi: 7.1.0 - strip-ansi-cjs: /strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: /wrap-ansi@7.0.0 - /@istanbuljs/load-nyc-config@1.1.0: + '@istanbuljs/load-nyc-config@1.1.0': resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} - dependencies: - camelcase: 5.3.1 - find-up: 4.1.0 - get-package-type: 0.1.0 - js-yaml: 3.14.1 - resolve-from: 5.0.0 - dev: true - /@istanbuljs/schema@0.1.3: + '@istanbuljs/schema@0.1.3': resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} - dev: true - /@jridgewell/gen-mapping@0.3.2: - resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} - engines: {node: '>=6.0.0'} - dependencies: - '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.18 - dev: true - - /@jridgewell/resolve-uri@3.1.0: - resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} - engines: {node: '>=6.0.0'} - dev: true + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - /@jridgewell/set-array@1.1.2: - resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - dev: true - /@jridgewell/source-map@0.3.3: - resolution: {integrity: sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==} - dependencies: - '@jridgewell/gen-mapping': 0.3.2 - '@jridgewell/trace-mapping': 0.3.18 - dev: true - - /@jridgewell/sourcemap-codec@1.4.14: - resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} - dev: true + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} - /@jridgewell/sourcemap-codec@1.4.15: - resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - dev: true + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - /@jridgewell/trace-mapping@0.3.18: - resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} - dependencies: - '@jridgewell/resolve-uri': 3.1.0 - '@jridgewell/sourcemap-codec': 1.4.14 - dev: true + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - /@jridgewell/trace-mapping@0.3.9: + '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - dependencies: - '@jridgewell/resolve-uri': 3.1.0 - '@jridgewell/sourcemap-codec': 1.4.15 - dev: true - /@leichtgewicht/ip-codec@2.0.4: - resolution: {integrity: sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==} - dev: true + '@leichtgewicht/ip-codec@2.0.5': + resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} - /@ngneat/elf-devtools@1.3.0: + '@ngneat/elf-devtools@1.3.0': resolution: {integrity: sha512-J9+4Vk/S/nKFGnXGZUDYrIx/K8Jfv4TLpzR3voBSOtSeq+c8Q0hdmo8iW7oaK6y5FWFDk0VJktlEh9cjylYxFg==} - dev: false - /@ngneat/elf-entities@4.4.4: - resolution: {integrity: sha512-KHIVkfM672Js66YrpUm81wcYGYJDJ6N5DBwkXYiPV3oSlNETsXfBjvrUmo+9uHyFpnIzCPo39XuFIJb/bf/bwA==} - dev: false + '@ngneat/elf-entities@4.6.0': + resolution: {integrity: sha512-J8tkdyiGKatNZ8KewgIiaZqh2ypgPM9OM8ZufT/jyClIqv6OJj0ajmoHSOJ4r4JFk+nwXAih74oe/D5ifZD9/w==} - /@ngneat/elf@2.3.2(rxjs@7.8.1): - resolution: {integrity: sha512-BMVJos82l9/l00U4G+fjTb1k85SiNDnpn2TbfFq6AWssJO6TmU4Y8SptSEjoYDKIXCne9XW+Ou/jufV6IEINIQ==} + '@ngneat/elf@2.5.1': + resolution: {integrity: sha512-13BItNZFgHglTiXuP9XhisNczwQ5QSzH+imAv9nAPsdbCq/3ortqkIYRnlxB8DGPVcuIjLujQ4OcZa+9QWgZtw==} peerDependencies: rxjs: '>=7.0.0' - dependencies: - rxjs: 7.8.1 - dev: false - /@ngneat/loadoff@2.1.0: + '@ngneat/loadoff@2.1.0': resolution: {integrity: sha512-N2Kfu7LaDu5t94nSNJU1T6/Uq4k2EAm5I0GHmglFR4dZRQAFGg1WNLT/t6zivcZiqN5wtNBZJS+FLvYKRuZGeA==} - dependencies: - tslib: 2.5.3 - dev: false - /@ngneat/reactive-forms@5.0.2(@angular/forms@16.1.2): + '@ngneat/reactive-forms@5.0.2': resolution: {integrity: sha512-9y9N55bUS9IWi0i5KnhFl0ZBStAi0qahv22LKK1QQYYldfEVvHh8keryxuGTUTnCIDfzIiZbbbmtRpTucdN1yQ==} peerDependencies: '@angular/forms': '>= 14.0.0' - dependencies: - '@angular/forms': 16.1.2(@angular/common@16.1.2)(@angular/core@16.1.2)(@angular/platform-browser@16.1.2)(rxjs@7.8.1) - tslib: 2.5.3 - dev: false - /@ngneat/transloco-utils@3.0.5: + '@ngneat/transloco-utils@3.0.5': resolution: {integrity: sha512-Xn9GaLUocXSPMhErNHbUyoloDm9sb+JaYszZJFL9F8em6frPQDSJxcYk9pV0caWpAU8INlksJSYgx1LXAH18mw==} - dependencies: - cosmiconfig: 8.2.0 - tslib: 2.5.3 - dev: false + deprecated: 'NOTICE: Transloco has moved to a new scope, this package will no longer receive updates. please use @jsverse/transloco-utils instead.' - /@ngneat/transloco@4.3.0(@angular/core@16.1.2)(fs-extra@11.1.1)(glob@10.3.0)(rxjs@7.8.1): + '@ngneat/transloco@4.3.0': resolution: {integrity: sha512-KUhGvp1ki+jvrM2PO27Tgzme1HkFmvDgS+7VyGxHta35wZEyoH6/r/EAXvfurPeYgaP6IaEMhUvAVT1WDgYwUg==} + deprecated: 'NOTICE: Transloco has moved to a new scope, this package will no longer receive updates. please use @jsverse/transloco instead.' peerDependencies: '@angular/core': '>=13.0.0' fs-extra: '>=9.1.0' glob: '>=7.1.7' rxjs: '>=6.0.0' - dependencies: - '@angular/core': 16.1.2(rxjs@7.8.1)(zone.js@0.13.1) - '@ngneat/transloco-utils': 3.0.5 - flat: 5.0.2 - fs-extra: 11.1.1 - glob: 10.3.0 - lodash.kebabcase: 4.1.1 - ora: 5.4.1 - replace-in-file: 6.3.5 - rxjs: 7.8.1 - tslib: 2.5.3 - dev: false - /@ngtools/webpack@16.1.1(@angular/compiler-cli@16.1.2)(typescript@5.1.3)(webpack@5.86.0): - resolution: {integrity: sha512-ItW8Hhokk4bQuV8qMpPeNCj0f3LDpddJpd5DwryKb0sSNacGlVff/0nCiKjJFPoCmMSg6ivpkZfqbIyL9RGYXw==} + '@ngtools/webpack@16.2.16': + resolution: {integrity: sha512-4gm2allK0Pjy/Lxb9IGRnhEZNEOJSOTWwy09VOdHouV2ODRK7Tto2LgteaFJUUSLkuvWRsI7pfuA6yrz8KDfHw==} engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} peerDependencies: - '@angular/compiler-cli': ^16.1.0 + '@angular/compiler-cli': ^16.0.0 typescript: '>=4.9.3 <5.2' webpack: ^5.54.0 - dependencies: - '@angular/compiler-cli': 16.1.2(@angular/compiler@16.1.2)(typescript@5.1.3) - typescript: 5.1.3 - webpack: 5.86.0(esbuild@0.17.19) - dev: true - /@ngx-playwright/harness@0.10.1(@angular/cdk@16.1.2)(@playwright/test@1.35.1): - resolution: {integrity: sha512-Xayz4eI9EUQHISzhv+kod4M6frnjbaaSn3qjZUpu1qf2bGrl4kA1CpMyx1wvB94av+LiOA/BA4qn6fXlc/2TAw==} + '@ngx-playwright/harness@0.10.2': + resolution: {integrity: sha512-l1iotI0r/0VXXAcITuWlDKmFSGfcZlQmqxjD0H35VRpeDITkJ0mibzLK0E224xeRMiidvIG3jJjd2ce+m3WkWQ==} peerDependencies: - '@angular/cdk': ^14.0.0-rc.0 || ^15.0.0-rc.0 || ^16.0.0-rc.0 + '@angular/cdk': ^14.0.0-rc.0 || ^15.0.0-rc.0 || ^16.0.0-rc.0 || ^17.0.0 '@playwright/test': ^1.20.2 - dependencies: - '@angular/cdk': 16.1.2(@angular/common@16.1.2)(@angular/core@16.1.2)(rxjs@7.8.1) - '@playwright/test': 1.35.1 - dev: true - /@ngx-playwright/test@0.4.2(@angular/cdk@16.1.2)(@playwright/test@1.35.1)(typescript@5.1.3): - resolution: {integrity: sha512-m1RkpI/cVdccA26KJXxBFL/DwzHrxOZd65wHQiQnNAohAwy97zkVBcnhWadHxVU5SUk4ue/47a7Lpkib6fCFtA==} + '@ngx-playwright/test@0.4.3': + resolution: {integrity: sha512-69dKUc1l2xoX2yKdoiM9tkR8dPSWZFgyaW/0UhSjsTX9ZOGL0ragyHBGYl3Qs/24FguQUzuBVyVgPZkRlSJvzA==} hasBin: true peerDependencies: - '@angular/cdk': ^13.3.2 || ^14.0.0-rc.0 || ^15.0.0-rc.0 || ^16.0.0-rc.0 + '@angular/cdk': ^13.3.2 || ^14.0.0-rc.0 || ^15.0.0-rc.0 || ^16.0.0-rc.0 || ^17.0.0 '@playwright/test': ^1.20.2 - dependencies: - '@angular-devkit/schematics': 16.1.1 - '@angular/cdk': 16.1.2(@angular/common@16.1.2)(@angular/core@16.1.2)(rxjs@7.8.1) - '@ngx-playwright/harness': 0.10.1(@angular/cdk@16.1.2)(@playwright/test@1.35.1) - '@playwright/test': 1.35.1 - '@snuggery/architect': 0.8.0 - '@snuggery/schematics': 0.7.1(@angular-devkit/schematics@16.1.1)(typescript@5.1.3) - '@snuggery/snuggery': 0.10.0 - transitivePeerDependencies: - - chokidar - - typescript - dev: true - /@nodelib/fs.scandir@2.1.5: + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - dev: true - /@nodelib/fs.stat@2.0.5: + '@nodelib/fs.stat@2.0.5': resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} engines: {node: '>= 8'} - dev: true - /@nodelib/fs.walk@1.2.8: + '@nodelib/fs.walk@1.2.8': resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.15.0 - dev: true - /@npmcli/fs@2.1.1: - resolution: {integrity: sha512-1Q0uzx6c/NVNGszePbr5Gc2riSU1zLpNlo/1YWntH+eaPmMgBssAW0qXofCVkpdj3ce4swZtlDYQu+NKiYcptg==} + '@npmcli/fs@2.1.2': + resolution: {integrity: sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - dependencies: - '@gar/promisify': 1.1.3 - semver: 7.5.3 - dev: true - /@npmcli/fs@3.1.0: - resolution: {integrity: sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==} + '@npmcli/fs@3.1.1': + resolution: {integrity: sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - semver: 7.5.3 - dev: true - /@npmcli/git@4.1.0: + '@npmcli/git@4.1.0': resolution: {integrity: sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - '@npmcli/promise-spawn': 6.0.2 - lru-cache: 7.13.1 - npm-pick-manifest: 8.0.1 - proc-log: 3.0.0 - promise-inflight: 1.0.1 - promise-retry: 2.0.1 - semver: 7.5.3 - which: 3.0.1 - transitivePeerDependencies: - - bluebird - dev: true - /@npmcli/installed-package-contents@2.0.2: - resolution: {integrity: sha512-xACzLPhnfD51GKvTOOuNX2/V4G4mz9/1I2MfDoye9kBM3RYe5g2YbscsaGoTlaWqkxeiapBWyseULVKpSVHtKQ==} + '@npmcli/installed-package-contents@2.1.0': + resolution: {integrity: sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} hasBin: true - dependencies: - npm-bundled: 3.0.0 - npm-normalize-package-bin: 3.0.1 - dev: true - /@npmcli/move-file@2.0.0: - resolution: {integrity: sha512-UR6D5f4KEGWJV6BGPH3Qb2EtgH+t+1XQ1Tt85c7qicN6cezzuHPdZwwAxqZr4JLtnQu0LZsTza/5gmNmSl8XLg==} + '@npmcli/move-file@2.0.1': + resolution: {integrity: sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} deprecated: This functionality has been moved to @npmcli/fs - dependencies: - mkdirp: 1.0.4 - rimraf: 3.0.2 - dev: true - /@npmcli/node-gyp@3.0.0: + '@npmcli/node-gyp@3.0.0': resolution: {integrity: sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dev: true - /@npmcli/promise-spawn@6.0.2: + '@npmcli/promise-spawn@6.0.2': resolution: {integrity: sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - which: 3.0.1 - dev: true - /@npmcli/run-script@6.0.2: + '@npmcli/run-script@6.0.2': resolution: {integrity: sha512-NCcr1uQo1k5U+SYlnIrbAh3cxy+OQT1VtqiAbxdymSlptbzBb62AjH2xXgjNCoP073hoa1CfCAcwoZ8k96C4nA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - '@npmcli/node-gyp': 3.0.0 - '@npmcli/promise-spawn': 6.0.2 - node-gyp: 9.1.0 - read-package-json-fast: 3.0.2 - which: 3.0.1 - transitivePeerDependencies: - - bluebird - - supports-color - dev: true - /@pkgjs/parseargs@0.11.0: + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - requiresBuild: true - optional: true - /@playwright/test@1.35.1: - resolution: {integrity: sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==} - engines: {node: '>=16'} + '@playwright/test@1.56.1': + resolution: {integrity: sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==} + engines: {node: '>=18'} hasBin: true - dependencies: - '@types/node': 18.0.0 - playwright-core: 1.35.1 - optionalDependencies: - fsevents: 2.3.2 - dev: true - /@schematics/angular@16.1.1: - resolution: {integrity: sha512-mJo7FxH3dekG7m4hHW5PyWbiCUaU+DSW93j+cikEksda+Qt6NaEX0hM0W3DjH7O+BnEg6dbAEd2GDSN/0XQghw==} + '@schematics/angular@16.2.16': + resolution: {integrity: sha512-V4cE4R5MbusKaNW9DWsisiSRUoQzbAaBIeJh42yCkg5H/lUdf18hUB7DG6Pl7yH6/tjzzz4SqIVD7N64uCDC2A==} engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} - dependencies: - '@angular-devkit/core': 16.1.1(chokidar@3.5.3) - '@angular-devkit/schematics': 16.1.1 - jsonc-parser: 3.2.0 - transitivePeerDependencies: - - chokidar - dev: true - /@sigstore/protobuf-specs@0.1.0: - resolution: {integrity: sha512-a31EnjuIDSX8IXBUib3cYLDRlPMU36AWX4xS8ysLaNu4ZzUesDiPt83pgrW2X1YLMe5L2HbDyaKK5BrL4cNKaQ==} + '@sigstore/bundle@1.1.0': + resolution: {integrity: sha512-PFutXEy0SmQxYI4texPw3dd2KewuNqv7OuK1ZFtY2fM754yhvG2KdgwIhRnoEE2uHdtdGNQ8s0lb94dW9sELog==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dev: true - /@sigstore/tuf@1.0.0: - resolution: {integrity: sha512-bLzi9GeZgMCvjJeLUIfs8LJYCxrPRA8IXQkzUtaFKKVPTz0mucRyqFcV2U20yg9K+kYAD0YSitzGfRZCFLjdHQ==} + '@sigstore/protobuf-specs@0.2.1': + resolution: {integrity: sha512-XTWVxnWJu+c1oCshMLwnKvz8ZQJJDVOlciMfgpJBQbThVjKTCG8dwyhgLngBD2KN0ap9F/gOV8rFDEx8uh7R2A==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - '@sigstore/protobuf-specs': 0.1.0 - make-fetch-happen: 11.1.1 - tuf-js: 1.1.7 - transitivePeerDependencies: - - supports-color - dev: true - /@snuggery/architect@0.8.0: - resolution: {integrity: sha512-2IkPVuP7L/bbcPN9Sdu1P4MXJdI8nX53GpIW+Z8E8ZWkc7hePidBfvYuKAO2x9F9m73tHvd5b7jiIZh2r1JIzA==} - dependencies: - '@angular-devkit/architect': 0.1601.1(chokidar@3.5.3) - '@angular-devkit/core': 16.1.1(chokidar@3.5.3) - '@snuggery/core': 0.6.1 - fs-extra: 11.1.1 - glob: 10.2.1 - rxjs: 7.8.1 - transitivePeerDependencies: - - chokidar - dev: true + '@sigstore/sign@1.0.0': + resolution: {integrity: sha512-INxFVNQteLtcfGmcoldzV6Je0sbbfh9I16DM4yJPw3j5+TFP8X6uIiA18mvpEa9yyeycAKgPmOA3X9hVdVTPUA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - /@snuggery/core@0.6.1: - resolution: {integrity: sha512-AlFBGZSJjuWMFp/ZvereWcycY1TFZQip6M7jJ0FvbdHvCcFdc6vXB1Mj6HK/c1ijsxTnxa6yxscVFDW9KVcsDg==} - dependencies: - '@angular-devkit/core': 16.1.1(chokidar@3.5.3) - '@bgotink/kdl': 0.1.6 - jsonc-parser: 3.2.0 - micromatch: 4.0.5 - typanion: 3.12.1 - yaml: 2.3.1 - transitivePeerDependencies: - - chokidar - dev: true + '@sigstore/tuf@1.0.3': + resolution: {integrity: sha512-2bRovzs0nJZFlCN3rXirE4gwxCn97JNjMmwpecqlbgV9WcxX7WRuIrgzx/X7Ib7MYRbyUTpBYE0s2x6AmZXnlg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + '@snuggery/architect@0.9.1': + resolution: {integrity: sha512-G9tFMZUIqShAiDUCmJuK5wdAndsBgfagSCGlECp10g11KglV1b6yq/teRIdZJxUn21cbaIcUzjqeE8F09ZP56g==} - /@snuggery/schematics@0.7.1(@angular-devkit/schematics@16.1.1)(typescript@5.1.3): - resolution: {integrity: sha512-rd0S3Ignj/LejslRH+LVcSzI6vkeu+yG1xCKLIUCh7ntE2Yo1Tv4jWen6IJp5+jf3Tdur9c8mS+XccdyZXU7tQ==} + '@snuggery/core@0.7.1': + resolution: {integrity: sha512-ltW2uMC6ZVS4f64TrIopRpyA69WJA9znTgS080/QmHtJk/3A+LxblfbLRetv12Decrejod5c79oCDaCEW+R0HQ==} + + '@snuggery/schematics@0.9.1': + resolution: {integrity: sha512-2ujgPhcwv6u1SVBowRpWumSw+EtiIhokxz6uGmNh69gqS6i95fSoWVwMmAncYg4RIA7M+KTPRkikotSqCc/mLQ==} peerDependencies: - '@angular-devkit/schematics': ^16.0.0-rc.0 - typescript: '>= 4.0.0 < 5.1.0' + '@angular-devkit/core': ^17.0.0 + '@angular-devkit/schematics': ^17.0.0 + typescript: '>= 4.0.0 < 5.4.0' peerDependenciesMeta: typescript: optional: true - dependencies: - '@angular-devkit/schematics': 16.1.1 - '@snuggery/core': 0.6.1 - typescript: 5.1.3 - transitivePeerDependencies: - - chokidar - dev: true - /@snuggery/snuggery@0.10.0: - resolution: {integrity: sha512-qFyX46Xf+mvtxXRwfkxGLCW1oCSLgG01lg5hMfRNU3WVRiRTQKbYnMpWUorolaG88pDPxHESSuOXo33YVV6uBg==} + '@snuggery/snuggery@0.11.1': + resolution: {integrity: sha512-IHNvXW4MnScSam8Vs3MIN9W2F0F/9HGv5thCEy7w5pQ9lvCNdKhE3FlTE1I0eXI7v0MQTEJDZV6TYjCZZZC3QQ==} hasBin: true - dependencies: - '@angular-devkit/architect': 0.1601.1(chokidar@3.5.3) - '@angular-devkit/core': 16.1.1(chokidar@3.5.3) - '@angular-devkit/schematics': 16.1.1 - '@arcanis/slice-ansi': 1.1.1 - '@snuggery/architect': 0.8.0 - '@snuggery/core': 0.6.1 - ajv: 8.12.0 - ajv-formats: 2.1.1(ajv@8.12.0) - clipanion: 3.2.0(typanion@3.12.1) - json5: 2.2.3 - kleur: 4.1.5 - prompts: 2.4.2 - rxjs: 7.8.1 - semver: 7.5.3 - strip-ansi: 6.0.1 - typanion: 3.12.1 - which-pm-runs: 1.1.0 - transitivePeerDependencies: - - chokidar - dev: true - /@tootallnate/once@2.0.0: + '@tootallnate/once@1.1.2': + resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} + engines: {node: '>= 6'} + + '@tootallnate/once@2.0.0': resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} - dev: true - /@tsconfig/node10@1.0.9: - resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} - dev: true + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} - /@tsconfig/node12@1.0.11: + '@tsconfig/node12@1.0.11': resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} - dev: true - /@tsconfig/node14@1.0.3: + '@tsconfig/node14@1.0.3': resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} - dev: true - /@tsconfig/node16@1.0.3: - resolution: {integrity: sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==} - dev: true + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - /@tufjs/canonical-json@1.0.0: + '@tufjs/canonical-json@1.0.0': resolution: {integrity: sha512-QTnf++uxunWvG2z3UFNzAoQPHxnSXOwtaI3iJ+AohhV+5vONuArPjJE7aPXPVXfXJsqrVbZBu9b81AJoSd09IQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dev: true - /@tufjs/models@1.0.4: + '@tufjs/models@1.0.4': resolution: {integrity: sha512-qaGV9ltJP0EO25YfFUPhxRVK0evXFIAGicsVXuRim4Ed9cjPxYhNnNJ49SFmbeLgtxpslIkX317IgpfcHPVj/A==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - '@tufjs/canonical-json': 1.0.0 - minimatch: 9.0.2 - dev: true - /@types/body-parser@1.19.2: - resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} - dependencies: - '@types/connect': 3.4.35 - '@types/node': 18.0.0 - dev: true + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} - /@types/bonjour@3.5.10: - resolution: {integrity: sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==} - dependencies: - '@types/node': 18.0.0 - dev: true + '@types/bonjour@3.5.13': + resolution: {integrity: sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==} - /@types/connect-history-api-fallback@1.3.5: - resolution: {integrity: sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==} - dependencies: - '@types/express-serve-static-core': 4.17.29 - '@types/node': 18.0.0 - dev: true + '@types/connect-history-api-fallback@1.5.4': + resolution: {integrity: sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==} - /@types/connect@3.4.35: - resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} - dependencies: - '@types/node': 18.0.0 - dev: true + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - /@types/eslint-scope@3.7.4: - resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} - dependencies: - '@types/eslint': 8.4.5 - '@types/estree': 1.0.1 - dev: true + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - /@types/eslint@8.4.5: - resolution: {integrity: sha512-dhsC09y1gpJWnK+Ff4SGvCuSnk9DaU0BJZSzOwa6GVSg65XtTugLBITDAAzRU5duGBoXBHpdR/9jHGxJjNflJQ==} - dependencies: - '@types/estree': 1.0.1 - '@types/json-schema': 7.0.12 - dev: true + '@types/events@3.0.3': + resolution: {integrity: sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==} - /@types/estree@1.0.1: - resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} - dev: true + '@types/express-serve-static-core@4.19.7': + resolution: {integrity: sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==} - /@types/events@3.0.0: - resolution: {integrity: sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==} - dev: true + '@types/express-serve-static-core@5.1.0': + resolution: {integrity: sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==} - /@types/express-serve-static-core@4.17.29: - resolution: {integrity: sha512-uMd++6dMKS32EOuw1Uli3e3BPgdLIXmezcfHv7N4c1s3gkhikBplORPpMq3fuWkxncZN1reb16d5n8yhQ80x7Q==} - dependencies: - '@types/node': 18.0.0 - '@types/qs': 6.9.7 - '@types/range-parser': 1.2.4 - dev: true + '@types/express@4.17.25': + resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} - /@types/express@4.17.13: - resolution: {integrity: sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==} - dependencies: - '@types/body-parser': 1.19.2 - '@types/express-serve-static-core': 4.17.29 - '@types/qs': 6.9.7 - '@types/serve-static': 1.13.10 - dev: true + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} - /@types/http-proxy@1.17.9: - resolution: {integrity: sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==} - dependencies: - '@types/node': 18.0.0 - dev: true + '@types/http-proxy@1.17.17': + resolution: {integrity: sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - /@types/json-schema@7.0.12: - resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} - dev: true + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - /@types/mime@1.3.2: - resolution: {integrity: sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==} - dev: true + '@types/node-forge@1.3.14': + resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==} - /@types/node@18.0.0: - resolution: {integrity: sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==} - dev: true + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} - /@types/qs@6.9.7: - resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==} - dev: true + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} - /@types/range-parser@1.2.4: - resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} - dev: true + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - /@types/retry@0.12.0: + '@types/retry@0.12.0': resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} - dev: true - /@types/semver@7.5.0: - resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==} - dev: true + '@types/semver@7.7.1': + resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} - /@types/serve-index@1.9.1: - resolution: {integrity: sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==} - dependencies: - '@types/express': 4.17.13 - dev: true + '@types/send@0.17.6': + resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} - /@types/serve-static@1.13.10: - resolution: {integrity: sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==} - dependencies: - '@types/mime': 1.3.2 - '@types/node': 18.0.0 - dev: true + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} - /@types/sockjs@0.3.33: - resolution: {integrity: sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==} - dependencies: - '@types/node': 18.0.0 - dev: true + '@types/serve-index@1.9.4': + resolution: {integrity: sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==} - /@types/ws@8.5.3: - resolution: {integrity: sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==} - dependencies: - '@types/node': 18.0.0 - dev: true + '@types/serve-static@1.15.10': + resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + + '@types/sockjs@0.3.36': + resolution: {integrity: sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - /@typescript-eslint/eslint-plugin@5.60.0(@typescript-eslint/parser@5.60.0)(eslint@8.43.0)(typescript@5.1.3): - resolution: {integrity: sha512-78B+anHLF1TI8Jn/cD0Q00TBYdMgjdOn980JfAVa9yw5sop8nyTfVOQAv6LWywkOGLclDBtv5z3oxN4w7jxyNg==} + '@typescript-eslint/eslint-plugin@5.62.0': + resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: '@typescript-eslint/parser': ^5.0.0 @@ -2725,26 +1418,9 @@ packages: peerDependenciesMeta: typescript: optional: true - dependencies: - '@eslint-community/regexpp': 4.5.1 - '@typescript-eslint/parser': 5.60.0(eslint@8.43.0)(typescript@5.1.3) - '@typescript-eslint/scope-manager': 5.60.0 - '@typescript-eslint/type-utils': 5.60.0(eslint@8.43.0)(typescript@5.1.3) - '@typescript-eslint/utils': 5.60.0(eslint@8.43.0)(typescript@5.1.3) - debug: 4.3.4 - eslint: 8.43.0 - grapheme-splitter: 1.0.4 - ignore: 5.2.4 - natural-compare-lite: 1.4.0 - semver: 7.5.3 - tsutils: 3.21.0(typescript@5.1.3) - typescript: 5.1.3 - transitivePeerDependencies: - - supports-color - dev: true - /@typescript-eslint/parser@5.60.0(eslint@8.43.0)(typescript@5.1.3): - resolution: {integrity: sha512-jBONcBsDJ9UoTWrARkRRCgDz6wUggmH5RpQVlt7BimSwaTkTjwypGzKORXbR4/2Hqjk9hgwlon2rVQAjWNpkyQ==} + '@typescript-eslint/parser@5.62.0': + resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -2752,55 +1428,13 @@ packages: peerDependenciesMeta: typescript: optional: true - dependencies: - '@typescript-eslint/scope-manager': 5.60.0 - '@typescript-eslint/types': 5.60.0 - '@typescript-eslint/typescript-estree': 5.60.0(typescript@5.1.3) - debug: 4.3.4 - eslint: 8.43.0 - typescript: 5.1.3 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/scope-manager@5.59.7: - resolution: {integrity: sha512-FL6hkYWK9zBGdxT2wWEd2W8ocXMu3K94i3gvMrjXpx+koFYdYV7KprKfirpgY34vTGzEPPuKoERpP8kD5h7vZQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - '@typescript-eslint/types': 5.59.7 - '@typescript-eslint/visitor-keys': 5.59.7 - dev: true - - /@typescript-eslint/scope-manager@5.60.0: - resolution: {integrity: sha512-hakuzcxPwXi2ihf9WQu1BbRj1e/Pd8ZZwVTG9kfbxAMZstKz8/9OoexIwnmLzShtsdap5U/CoQGRCWlSuPbYxQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - '@typescript-eslint/types': 5.60.0 - '@typescript-eslint/visitor-keys': 5.60.0 - dev: true - /@typescript-eslint/type-utils@5.59.7(eslint@8.43.0)(typescript@5.1.3): - resolution: {integrity: sha512-ozuz/GILuYG7osdY5O5yg0QxXUAEoI4Go3Do5xeu+ERH9PorHBPSdvD3Tjp2NN2bNLh1NJQSsQu2TPu/Ly+HaQ==} + '@typescript-eslint/scope-manager@5.62.0': + resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: '*' - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/typescript-estree': 5.59.7(typescript@5.1.3) - '@typescript-eslint/utils': 5.59.7(eslint@8.43.0)(typescript@5.1.3) - debug: 4.3.4 - eslint: 8.43.0 - tsutils: 3.21.0(typescript@5.1.3) - typescript: 5.1.3 - transitivePeerDependencies: - - supports-color - dev: true - /@typescript-eslint/type-utils@5.60.0(eslint@8.43.0)(typescript@5.1.3): - resolution: {integrity: sha512-X7NsRQddORMYRFH7FWo6sA9Y/zbJ8s1x1RIAtnlj6YprbToTiQnM6vxcMu7iYhdunmoC0rUWlca13D5DVHkK2g==} + '@typescript-eslint/type-utils@5.62.0': + resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: '*' @@ -2808,3874 +1442,2198 @@ packages: peerDependenciesMeta: typescript: optional: true - dependencies: - '@typescript-eslint/typescript-estree': 5.60.0(typescript@5.1.3) - '@typescript-eslint/utils': 5.60.0(eslint@8.43.0)(typescript@5.1.3) - debug: 4.3.4 - eslint: 8.43.0 - tsutils: 3.21.0(typescript@5.1.3) - typescript: 5.1.3 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/types@5.59.7: - resolution: {integrity: sha512-UnVS2MRRg6p7xOSATscWkKjlf/NDKuqo5TdbWck6rIRZbmKpVNTLALzNvcjIfHBE7736kZOFc/4Z3VcZwuOM/A==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - /@typescript-eslint/types@5.60.0: - resolution: {integrity: sha512-ascOuoCpNZBccFVNJRSC6rPq4EmJ2NkuoKnd6LDNyAQmdDnziAtxbCGWCbefG1CNzmDvd05zO36AmB7H8RzKPA==} + '@typescript-eslint/types@5.62.0': + resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - - /@typescript-eslint/typescript-estree@5.59.7(typescript@5.1.3): - resolution: {integrity: sha512-4A1NtZ1I3wMN2UGDkU9HMBL+TIQfbrh4uS0WDMMpf3xMRursDbqEf1ahh6vAAe3mObt8k3ZATnezwG4pdtWuUQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/types': 5.59.7 - '@typescript-eslint/visitor-keys': 5.59.7 - debug: 4.3.4 - globby: 11.1.0 - is-glob: 4.0.3 - semver: 7.5.3 - tsutils: 3.21.0(typescript@5.1.3) - typescript: 5.1.3 - transitivePeerDependencies: - - supports-color - dev: true - /@typescript-eslint/typescript-estree@5.60.0(typescript@5.1.3): - resolution: {integrity: sha512-R43thAuwarC99SnvrBmh26tc7F6sPa2B3evkXp/8q954kYL6Ro56AwASYWtEEi+4j09GbiNAHqYwNNZuNlARGQ==} + '@typescript-eslint/typescript-estree@5.62.0': + resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: typescript: '*' peerDependenciesMeta: typescript: optional: true - dependencies: - '@typescript-eslint/types': 5.60.0 - '@typescript-eslint/visitor-keys': 5.60.0 - debug: 4.3.4 - globby: 11.1.0 - is-glob: 4.0.3 - semver: 7.5.3 - tsutils: 3.21.0(typescript@5.1.3) - typescript: 5.1.3 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/utils@5.59.7(eslint@8.43.0)(typescript@5.1.3): - resolution: {integrity: sha512-yCX9WpdQKaLufz5luG4aJbOpdXf/fjwGMcLFXZVPUz3QqLirG5QcwwnIHNf8cjLjxK4qtzTO8udUtMQSAToQnQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.43.0) - '@types/json-schema': 7.0.12 - '@types/semver': 7.5.0 - '@typescript-eslint/scope-manager': 5.59.7 - '@typescript-eslint/types': 5.59.7 - '@typescript-eslint/typescript-estree': 5.59.7(typescript@5.1.3) - eslint: 8.43.0 - eslint-scope: 5.1.1 - semver: 7.5.3 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - /@typescript-eslint/utils@5.60.0(eslint@8.43.0)(typescript@5.1.3): - resolution: {integrity: sha512-ba51uMqDtfLQ5+xHtwlO84vkdjrqNzOnqrnwbMHMRY8Tqeme8C2Q8Fc7LajfGR+e3/4LoYiWXUM6BpIIbHJ4hQ==} + '@typescript-eslint/utils@5.62.0': + resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.43.0) - '@types/json-schema': 7.0.12 - '@types/semver': 7.5.0 - '@typescript-eslint/scope-manager': 5.60.0 - '@typescript-eslint/types': 5.60.0 - '@typescript-eslint/typescript-estree': 5.60.0(typescript@5.1.3) - eslint: 8.43.0 - eslint-scope: 5.1.1 - semver: 7.5.3 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - /@typescript-eslint/visitor-keys@5.59.7: - resolution: {integrity: sha512-tyN+X2jvMslUszIiYbF0ZleP+RqQsFVpGrKI6e0Eet1w8WmhsAtmzaqm8oM8WJQ1ysLwhnsK/4hYHJjOgJVfQQ==} + '@typescript-eslint/visitor-keys@5.62.0': + resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - '@typescript-eslint/types': 5.59.7 - eslint-visitor-keys: 3.4.1 - dev: true - /@typescript-eslint/visitor-keys@5.60.0: - resolution: {integrity: sha512-wm9Uz71SbCyhUKgcaPRauBdTegUyY/ZWl8gLwD/i/ybJqscrrdVSFImpvUz16BLPChIeKBK5Fa9s6KDQjsjyWw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - '@typescript-eslint/types': 5.60.0 - eslint-visitor-keys: 3.4.1 - dev: true + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - /@vitejs/plugin-basic-ssl@1.0.1(vite@4.3.9): + '@vitejs/plugin-basic-ssl@1.0.1': resolution: {integrity: sha512-pcub+YbFtFhaGRTo1832FQHQSHvMrlb43974e2eS8EKleR3p1cDdkJFPci1UhwkEf1J9Bz+wKBSzqpKp7nNj2A==} engines: {node: '>=14.6.0'} peerDependencies: vite: ^3.0.0 || ^4.0.0 - dependencies: - vite: 4.3.9(@types/node@18.0.0)(less@4.1.3)(sass@1.63.2)(terser@5.17.7) - dev: true - /@webassemblyjs/ast@1.11.6: - resolution: {integrity: sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==} - dependencies: - '@webassemblyjs/helper-numbers': 1.11.6 - '@webassemblyjs/helper-wasm-bytecode': 1.11.6 - dev: true + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} - /@webassemblyjs/floating-point-hex-parser@1.11.6: - resolution: {integrity: sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==} - dev: true + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} - /@webassemblyjs/helper-api-error@1.11.6: - resolution: {integrity: sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==} - dev: true + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} - /@webassemblyjs/helper-buffer@1.11.6: - resolution: {integrity: sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==} - dev: true + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} - /@webassemblyjs/helper-numbers@1.11.6: - resolution: {integrity: sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==} - dependencies: - '@webassemblyjs/floating-point-hex-parser': 1.11.6 - '@webassemblyjs/helper-api-error': 1.11.6 - '@xtuc/long': 4.2.2 - dev: true + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} - /@webassemblyjs/helper-wasm-bytecode@1.11.6: - resolution: {integrity: sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==} - dev: true + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} - /@webassemblyjs/helper-wasm-section@1.11.6: - resolution: {integrity: sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==} - dependencies: - '@webassemblyjs/ast': 1.11.6 - '@webassemblyjs/helper-buffer': 1.11.6 - '@webassemblyjs/helper-wasm-bytecode': 1.11.6 - '@webassemblyjs/wasm-gen': 1.11.6 - dev: true + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} - /@webassemblyjs/ieee754@1.11.6: - resolution: {integrity: sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==} - dependencies: - '@xtuc/ieee754': 1.2.0 - dev: true + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} - /@webassemblyjs/leb128@1.11.6: - resolution: {integrity: sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==} - dependencies: - '@xtuc/long': 4.2.2 - dev: true - - /@webassemblyjs/utf8@1.11.6: - resolution: {integrity: sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==} - dev: true - - /@webassemblyjs/wasm-edit@1.11.6: - resolution: {integrity: sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==} - dependencies: - '@webassemblyjs/ast': 1.11.6 - '@webassemblyjs/helper-buffer': 1.11.6 - '@webassemblyjs/helper-wasm-bytecode': 1.11.6 - '@webassemblyjs/helper-wasm-section': 1.11.6 - '@webassemblyjs/wasm-gen': 1.11.6 - '@webassemblyjs/wasm-opt': 1.11.6 - '@webassemblyjs/wasm-parser': 1.11.6 - '@webassemblyjs/wast-printer': 1.11.6 - dev: true - - /@webassemblyjs/wasm-gen@1.11.6: - resolution: {integrity: sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==} - dependencies: - '@webassemblyjs/ast': 1.11.6 - '@webassemblyjs/helper-wasm-bytecode': 1.11.6 - '@webassemblyjs/ieee754': 1.11.6 - '@webassemblyjs/leb128': 1.11.6 - '@webassemblyjs/utf8': 1.11.6 - dev: true - - /@webassemblyjs/wasm-opt@1.11.6: - resolution: {integrity: sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==} - dependencies: - '@webassemblyjs/ast': 1.11.6 - '@webassemblyjs/helper-buffer': 1.11.6 - '@webassemblyjs/wasm-gen': 1.11.6 - '@webassemblyjs/wasm-parser': 1.11.6 - dev: true - - /@webassemblyjs/wasm-parser@1.11.6: - resolution: {integrity: sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==} - dependencies: - '@webassemblyjs/ast': 1.11.6 - '@webassemblyjs/helper-api-error': 1.11.6 - '@webassemblyjs/helper-wasm-bytecode': 1.11.6 - '@webassemblyjs/ieee754': 1.11.6 - '@webassemblyjs/leb128': 1.11.6 - '@webassemblyjs/utf8': 1.11.6 - dev: true - - /@webassemblyjs/wast-printer@1.11.6: - resolution: {integrity: sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==} - dependencies: - '@webassemblyjs/ast': 1.11.6 - '@xtuc/long': 4.2.2 - dev: true + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} - /@xtuc/ieee754@1.2.0: + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + + '@wessberg/ts-evaluator@0.0.27': + resolution: {integrity: sha512-7gOpVm3yYojUp/Yn7F4ZybJRxyqfMNf0LXK5KJiawbPfL0XTsJV+0mgrEDjOIR6Bi0OYk2Cyg4tjFu1r8MCZaA==} + engines: {node: '>=10.1.0'} + deprecated: this package has been renamed to ts-evaluator. Please install ts-evaluator instead + peerDependencies: + typescript: '>=3.2.x || >= 4.x' + + '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} - dev: true - /@xtuc/long@4.2.2: + '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - dev: true - /@yarnpkg/lockfile@1.1.0: + '@yarnpkg/lockfile@1.1.0': resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==} - dev: true - /abab@2.0.6: + abab@2.0.6: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} - dev: true + deprecated: Use your platform's native atob() and btoa() methods instead - /abbrev@1.1.1: + abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} - dev: true - /accepts@1.3.8: + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} - dependencies: - mime-types: 2.1.35 - negotiator: 0.6.3 - dev: true - /acorn-import-assertions@1.9.0(acorn@8.9.0): - resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} + acorn-globals@6.0.0: + resolution: {integrity: sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==} + + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} peerDependencies: acorn: ^8 - dependencies: - acorn: 8.9.0 - dev: true - /acorn-jsx@5.3.2(acorn@8.9.0): + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - acorn: 8.9.0 - dev: true - /acorn-walk@8.2.0: - resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} + acorn-walk@7.2.0: + resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} + engines: {node: '>=0.4.0'} + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} engines: {node: '>=0.4.0'} - dev: true - /acorn@8.8.0: - resolution: {integrity: sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==} + acorn@7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} engines: {node: '>=0.4.0'} hasBin: true - dev: true - /acorn@8.9.0: - resolution: {integrity: sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==} + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} hasBin: true - dev: true - /adjust-sourcemap-loader@4.0.0: + adjust-sourcemap-loader@4.0.0: resolution: {integrity: sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==} engines: {node: '>=8.9'} - dependencies: - loader-utils: 2.0.2 - regex-parser: 2.2.11 - dev: true - /agent-base@6.0.2: + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} - dependencies: - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - dev: true - /agentkeepalive@4.2.1: - resolution: {integrity: sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==} + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} engines: {node: '>= 8.0.0'} - dependencies: - debug: 4.3.4 - depd: 1.1.2 - humanize-ms: 1.2.1 - transitivePeerDependencies: - - supports-color - dev: true - /aggregate-error@3.1.0: + aggregate-error@3.1.0: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} - dependencies: - clean-stack: 2.2.0 - indent-string: 4.0.0 - dev: true - /ajv-formats@2.1.1(ajv@8.12.0): + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: ajv: ^8.0.0 peerDependenciesMeta: ajv: optional: true - dependencies: - ajv: 8.12.0 - dev: true - /ajv-keywords@3.5.2(ajv@6.12.6): + ajv-keywords@3.5.2: resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} peerDependencies: ajv: ^6.9.1 - dependencies: - ajv: 6.12.6 - dev: true - /ajv-keywords@5.1.0(ajv@8.12.0): + ajv-keywords@5.1.0: resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} peerDependencies: ajv: ^8.8.2 - dependencies: - ajv: 8.12.0 - fast-deep-equal: 3.1.3 - dev: true - /ajv@6.12.6: + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - dev: true - /ajv@8.12.0: + ajv@8.12.0: resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} - dependencies: - fast-deep-equal: 3.1.3 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - uri-js: 4.4.1 - dev: true - /ansi-colors@4.1.3: + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} - dev: true - /ansi-escapes@4.3.2: + ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} - dependencies: - type-fest: 0.21.3 - dev: true - /ansi-html-community@0.0.8: + ansi-html-community@0.0.8: resolution: {integrity: sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==} engines: {'0': node >= 0.8.0} hasBin: true - dev: true - /ansi-regex@5.0.1: + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - /ansi-regex@6.0.1: - resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} - /ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - dependencies: - color-convert: 1.9.3 - - /ansi-styles@4.3.0: + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - dependencies: - color-convert: 2.0.1 - /ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} - /anymatch@3.1.2: - resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==} + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - dev: true - /aproba@2.0.0: - resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} - dev: true + aproba@2.1.0: + resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} - /are-we-there-yet@3.0.0: - resolution: {integrity: sha512-0GWpv50YSOcLXaN6/FAKY3vfRbllXWV2xvfA/oKJF8pzFhWXPV+yjhJXDBbjscDYowv7Yw1A3uigpzn5iEGTyw==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16} - dependencies: - delegates: 1.0.0 - readable-stream: 3.6.0 - dev: true + are-we-there-yet@3.0.1: + resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. - /arg@4.1.3: + arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} - dev: true - /argparse@1.0.10: + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - dependencies: - sprintf-js: 1.0.3 - dev: true - /argparse@2.0.1: + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - /aria-query@5.1.3: - resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} - dependencies: - deep-equal: 2.2.1 - dev: true - - /array-buffer-byte-length@1.0.0: - resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} - dependencies: - call-bind: 1.0.2 - is-array-buffer: 3.0.2 - dev: true + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} - /array-flatten@1.1.1: + array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - dev: true - /array-flatten@2.1.2: - resolution: {integrity: sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==} - dev: true - - /array-union@2.1.0: + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} - dev: true - /autoprefixer@10.4.14(postcss@8.4.24): + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + autoprefixer@10.4.14: resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: postcss: ^8.1.0 - dependencies: - browserslist: 4.21.5 - caniuse-lite: 1.0.30001507 - fraction.js: 4.2.0 - normalize-range: 0.1.2 - picocolors: 1.0.0 - postcss: 8.4.24 - postcss-value-parser: 4.2.0 - dev: true - - /available-typed-arrays@1.0.5: - resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} - engines: {node: '>= 0.4'} - dev: true - /axobject-query@3.1.1: - resolution: {integrity: sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==} - dependencies: - deep-equal: 2.2.1 - dev: true + axobject-query@4.0.0: + resolution: {integrity: sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==} - /babel-loader@9.1.2(@babel/core@7.22.5)(webpack@5.86.0): - resolution: {integrity: sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA==} + babel-loader@9.1.3: + resolution: {integrity: sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==} engines: {node: '>= 14.15.0'} peerDependencies: '@babel/core': ^7.12.0 webpack: '>=5' - dependencies: - '@babel/core': 7.22.5 - find-cache-dir: 3.3.2 - schema-utils: 4.0.0 - webpack: 5.86.0(esbuild@0.17.19) - dev: true - /babel-plugin-istanbul@6.1.1: + babel-plugin-istanbul@6.1.1: resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} engines: {node: '>=8'} - dependencies: - '@babel/helper-plugin-utils': 7.22.5 - '@istanbuljs/load-nyc-config': 1.1.0 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-instrument: 5.2.0 - test-exclude: 6.0.0 - transitivePeerDependencies: - - supports-color - dev: true - /babel-plugin-polyfill-corejs2@0.4.3(@babel/core@7.22.5): - resolution: {integrity: sha512-bM3gHc337Dta490gg+/AseNB9L4YLHxq1nGKZZSHbhXv4aTYU2MD2cjza1Ru4S6975YLTaL1K8uJf6ukJhhmtw==} + babel-plugin-polyfill-corejs2@0.4.14: + resolution: {integrity: sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/compat-data': 7.22.5 - '@babel/core': 7.22.5 - '@babel/helper-define-polyfill-provider': 0.4.0(@babel/core@7.22.5) - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: true + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - /babel-plugin-polyfill-corejs3@0.8.1(@babel/core@7.22.5): - resolution: {integrity: sha512-ikFrZITKg1xH6pLND8zT14UPgjKHiGLqex7rGEZCH2EvhsneJaJPemmpQaIZV5AL03II+lXylw3UmddDK8RU5Q==} + babel-plugin-polyfill-corejs3@0.8.7: + resolution: {integrity: sha512-KyDvZYxAzkC0Aj2dAPyDzi2Ym15e5JKZSK+maI7NAwSqofvuFglbSsxE7wUOvTg9oFVnHMzVzBKcqEb4PJgtOA==} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-define-polyfill-provider': 0.4.0(@babel/core@7.22.5) - core-js-compat: 3.31.0 - transitivePeerDependencies: - - supports-color - dev: true + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - /babel-plugin-polyfill-regenerator@0.5.0(@babel/core@7.22.5): - resolution: {integrity: sha512-hDJtKjMLVa7Z+LwnTCxoDLQj6wdc+B8dun7ayF2fYieI6OzfuvcLMB32ihJZ4UhCBwNYGl5bg/x/P9cMdnkc2g==} + babel-plugin-polyfill-regenerator@0.5.5: + resolution: {integrity: sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.5 - '@babel/helper-define-polyfill-provider': 0.4.0(@babel/core@7.22.5) - transitivePeerDependencies: - - supports-color - dev: true + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - /balanced-match@1.0.2: + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - /base64-js@1.5.1: + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - /batch@0.6.1: + baseline-browser-mapping@2.8.25: + resolution: {integrity: sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==} + hasBin: true + + batch@0.6.1: resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} - dev: true - /big.js@5.2.2: + big.js@5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} - dev: true - /binary-extensions@2.2.0: - resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - dev: true - /bl@4.1.0: + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - dependencies: - buffer: 5.7.1 - inherits: 2.0.4 - readable-stream: 3.6.0 - /body-parser@1.20.0: - resolution: {integrity: sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==} + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - dependencies: - bytes: 3.1.2 - content-type: 1.0.4 - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - on-finished: 2.4.1 - qs: 6.10.3 - raw-body: 2.5.1 - type-is: 1.6.18 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - dev: true - /bonjour-service@1.0.13: - resolution: {integrity: sha512-LWKRU/7EqDUC9CTAQtuZl5HzBALoCYwtLhffW3et7vZMwv3bWLpJf8bRYlMD5OCcDpTfnPgNCV4yo9ZIaJGMiA==} - dependencies: - array-flatten: 2.1.2 - dns-equal: 1.0.0 - fast-deep-equal: 3.1.3 - multicast-dns: 7.2.5 - dev: true + bonjour-service@1.3.0: + resolution: {integrity: sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==} - /boolbase@1.0.0: + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - dev: true - /brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - /brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} - dependencies: - balanced-match: 1.0.2 + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - /braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - dependencies: - fill-range: 7.0.1 - dev: true - /browserslist@4.21.5: - resolution: {integrity: sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==} + browser-process-hrtime@1.0.0: + resolution: {integrity: sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==} + + browserslist@4.28.0: + resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - dependencies: - caniuse-lite: 1.0.30001507 - electron-to-chromium: 1.4.440 - node-releases: 2.0.12 - update-browserslist-db: 1.0.11(browserslist@4.21.5) - dev: true - /buffer-from@1.1.2: + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - dev: true - /buffer@5.7.1: + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - - /builtins@5.0.1: - resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} - dependencies: - semver: 7.5.3 - dev: true - /bytes@3.0.0: - resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} - engines: {node: '>= 0.8'} - dev: true - - /bytes@3.1.2: + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} - dev: true - /cacache@16.1.1: - resolution: {integrity: sha512-VDKN+LHyCQXaaYZ7rA/qtkURU+/yYhviUdvqEv2LT6QPZU8jpyzEkEVAcKlKLt5dJ5BRp11ym8lo3NKLluEPLg==} + cacache@16.1.3: + resolution: {integrity: sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - dependencies: - '@npmcli/fs': 2.1.1 - '@npmcli/move-file': 2.0.0 - chownr: 2.0.0 - fs-minipass: 2.1.0 - glob: 8.1.0 - infer-owner: 1.0.4 - lru-cache: 7.13.1 - minipass: 3.3.4 - minipass-collect: 1.0.2 - minipass-flush: 1.0.5 - minipass-pipeline: 1.2.4 - mkdirp: 1.0.4 - p-map: 4.0.0 - promise-inflight: 1.0.1 - rimraf: 3.0.2 - ssri: 9.0.1 - tar: 6.1.11 - unique-filename: 1.1.1 - transitivePeerDependencies: - - bluebird - dev: true - /cacache@17.1.3: - resolution: {integrity: sha512-jAdjGxmPxZh0IipMdR7fK/4sDSrHMLUV0+GvVUsjwyGNKHsh79kW/otg+GkbXwl6Uzvy9wsvHOX4nUoWldeZMg==} + cacache@17.1.4: + resolution: {integrity: sha512-/aJwG2l3ZMJ1xNAnqbMpA40of9dj/pIH3QfiuQSqjfPJF747VR0J/bHn+/KdNnHKc6XQcWt/AfRSBft82W1d2A==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - '@npmcli/fs': 3.1.0 - fs-minipass: 3.0.2 - glob: 10.3.0 - lru-cache: 7.13.1 - minipass: 5.0.0 - minipass-collect: 1.0.2 - minipass-flush: 1.0.5 - minipass-pipeline: 1.2.4 - p-map: 4.0.0 - ssri: 10.0.4 - tar: 6.1.11 - unique-filename: 3.0.0 - dev: true - /call-bind@1.0.2: - resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} - dependencies: - function-bind: 1.1.1 - get-intrinsic: 1.2.1 - dev: true + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} - /callsites@3.1.0: + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - /camelcase@5.3.1: + camelcase@5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} - dev: true - /caniuse-lite@1.0.30001507: - resolution: {integrity: sha512-SFpUDoSLCaE5XYL2jfqe9ova/pbQHEmbheDf5r4diNwbAgR3qxM9NQtfsiSscjqoya5K7kFcHPUQ+VsUkIJR4A==} - dev: true - - /chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 + caniuse-lite@1.0.30001754: + resolution: {integrity: sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==} - /chalk@4.1.2: + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - /chardet@0.7.0: + chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - dev: true - /chokidar@3.5.3: + chokidar@3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} engines: {node: '>= 8.10.0'} - dependencies: - anymatch: 3.1.2 - braces: 3.0.2 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.2 - dev: true - /chownr@2.0.0: + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chownr@2.0.0: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} - dev: true - /chrome-trace-event@1.0.3: - resolution: {integrity: sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==} + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} - dev: true - /clean-stack@2.2.0: + clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} - dev: true - /cli-cursor@3.1.0: + cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} - dependencies: - restore-cursor: 3.1.0 - /cli-spinners@2.6.1: - resolution: {integrity: sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==} + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} - /cli-width@3.0.0: + cli-width@3.0.0: resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} engines: {node: '>= 10'} - dev: true - /clipanion@3.2.0(typanion@3.12.1): + clipanion@3.2.0: resolution: {integrity: sha512-XaPQiJQZKbyaaDbv5yR/cAt/ORfZfENkr4wGQj+Go/Uf/65ofTQBCPirgWFeJctZW24V3mxrFiEnEmqBflrJYA==} peerDependencies: typanion: '*' - dependencies: - typanion: 3.12.1 - dev: true - /cliui@8.0.1: + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - /clone-deep@4.0.1: + clone-deep@4.0.1: resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} engines: {node: '>=6'} - dependencies: - is-plain-object: 2.0.4 - kind-of: 6.0.3 - shallow-clone: 3.0.1 - dev: true - /clone@1.0.4: + clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} - /color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - dependencies: - color-name: 1.1.3 - - /color-convert@2.0.1: + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} - dependencies: - color-name: 1.1.4 - /color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - - /color-name@1.1.4: + color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - /color-support@1.1.3: + color-support@1.1.3: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} hasBin: true - dev: true - /colorette@2.0.19: - resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} - dev: true + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} - /commander@2.20.3: + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - dev: true - /commondir@1.0.1: - resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} - dev: true + common-path-prefix@3.0.0: + resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} - /compressible@2.0.18: + compressible@2.0.18: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} engines: {node: '>= 0.6'} - dependencies: - mime-db: 1.52.0 - dev: true - /compression@1.7.4: - resolution: {integrity: sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==} + compression@1.8.1: + resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} engines: {node: '>= 0.8.0'} - dependencies: - accepts: 1.3.8 - bytes: 3.0.0 - compressible: 2.0.18 - debug: 2.6.9 - on-headers: 1.0.2 - safe-buffer: 5.1.2 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - dev: true - /concat-map@0.0.1: + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - /connect-history-api-fallback@2.0.0: + connect-history-api-fallback@2.0.0: resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} engines: {node: '>=0.8'} - dev: true - /console-control-strings@1.1.0: + console-control-strings@1.1.0: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} - dev: true - /content-disposition@0.5.4: + content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} - dependencies: - safe-buffer: 5.2.1 - dev: true - /content-type@1.0.4: - resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==} + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} - dev: true - /convert-source-map@1.8.0: - resolution: {integrity: sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==} - dependencies: - safe-buffer: 5.1.2 - dev: true + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - /cookie-signature@1.0.6: + cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} - dev: true - /cookie@0.5.0: - resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} - dev: true - /copy-anything@2.0.6: + copy-anything@2.0.6: resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==} - dependencies: - is-what: 3.14.1 - dev: true - /copy-webpack-plugin@11.0.0(webpack@5.86.0): + copy-webpack-plugin@11.0.0: resolution: {integrity: sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==} engines: {node: '>= 14.15.0'} peerDependencies: webpack: ^5.1.0 - dependencies: - fast-glob: 3.2.12 - glob-parent: 6.0.2 - globby: 13.1.2 - normalize-path: 3.0.0 - schema-utils: 4.0.0 - serialize-javascript: 6.0.0 - webpack: 5.86.0(esbuild@0.17.19) - dev: true - /core-js-compat@3.31.0: - resolution: {integrity: sha512-hM7YCu1cU6Opx7MXNu0NuumM0ezNeAeRKadixyiQELWY3vT3De9S4J5ZBMraWV2vZnrE1Cirl0GtFtDtMUXzPw==} - dependencies: - browserslist: 4.21.5 - dev: true + core-js-compat@3.46.0: + resolution: {integrity: sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==} - /core-util-is@1.0.3: + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - dev: true - /cosmiconfig@8.2.0: - resolution: {integrity: sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==} + cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} engines: {node: '>=14'} - dependencies: - import-fresh: 3.3.0 - js-yaml: 4.1.0 - parse-json: 5.2.0 - path-type: 4.0.0 + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true - /create-require@1.1.1: + create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - dev: true - /critters@0.0.19: - resolution: {integrity: sha512-Fm4ZAXsG0VzWy1U30rP4qxbaWGSsqXDgSupJW1OUJGDAs0KWC+j37v7p5a2kZ9BPJvhRzWm3be+Hc9WvQOBUOw==} - dependencies: - chalk: 4.1.2 - css-select: 5.1.0 - dom-serializer: 2.0.0 - domhandler: 5.0.3 - htmlparser2: 8.0.2 - postcss: 8.4.24 - pretty-bytes: 5.6.0 - dev: true + critters@0.0.20: + resolution: {integrity: sha512-CImNRorKOl5d8TWcnAz5n5izQ6HFsvz29k327/ELy6UFcmbiZNOsinaKvzv16WZR0P6etfSWYzE47C4/56B3Uw==} - /cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - /css-loader@6.8.1(webpack@5.86.0): + css-loader@6.8.1: resolution: {integrity: sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==} engines: {node: '>= 12.13.0'} peerDependencies: webpack: ^5.0.0 - dependencies: - icss-utils: 5.1.0(postcss@8.4.24) - postcss: 8.4.24 - postcss-modules-extract-imports: 3.0.0(postcss@8.4.24) - postcss-modules-local-by-default: 4.0.3(postcss@8.4.24) - postcss-modules-scope: 3.0.0(postcss@8.4.24) - postcss-modules-values: 4.0.0(postcss@8.4.24) - postcss-value-parser: 4.2.0 - semver: 7.5.3 - webpack: 5.86.0(esbuild@0.17.19) - dev: true - /css-select@5.1.0: - resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} - dependencies: - boolbase: 1.0.0 - css-what: 6.1.0 - domhandler: 5.0.3 - domutils: 3.1.0 - nth-check: 2.1.1 - dev: true + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} - /css-what@6.1.0: - resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} - dev: true - /cssesc@3.0.0: + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true - dev: true - /debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.0.0 - dev: true + cssom@0.3.8: + resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} + + cssom@0.4.4: + resolution: {integrity: sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==} + + cssstyle@2.3.0: + resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} + engines: {node: '>=8'} + + data-urls@2.0.0: + resolution: {integrity: sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==} + engines: {node: '>=10'} - /debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: supports-color: '*' peerDependenciesMeta: supports-color: optional: true - dependencies: - ms: 2.1.3 - dev: true - optional: true - /debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' peerDependenciesMeta: supports-color: optional: true - dependencies: - ms: 2.1.2 - dev: true - - /deep-equal@2.2.1: - resolution: {integrity: sha512-lKdkdV6EOGoVn65XaOsPdH4rMxTZOnmFyuIkMjM1i5HHCbfjC97dawgTAy0deYNfuqUqW+Q5VrVaQYtUpSd6yQ==} - dependencies: - array-buffer-byte-length: 1.0.0 - call-bind: 1.0.2 - es-get-iterator: 1.1.3 - get-intrinsic: 1.2.1 - is-arguments: 1.1.1 - is-array-buffer: 3.0.2 - is-date-object: 1.0.5 - is-regex: 1.1.4 - is-shared-array-buffer: 1.0.2 - isarray: 2.0.5 - object-is: 1.1.5 - object-keys: 1.1.1 - object.assign: 4.1.4 - regexp.prototype.flags: 1.5.0 - side-channel: 1.0.4 - which-boxed-primitive: 1.0.2 - which-collection: 1.0.1 - which-typed-array: 1.1.9 - dev: true - - /deep-is@0.1.4: + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - dev: true - /default-gateway@6.0.3: + default-gateway@6.0.3: resolution: {integrity: sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==} engines: {node: '>= 10'} - dependencies: - execa: 5.1.1 - dev: true - /defaults@1.0.3: - resolution: {integrity: sha512-s82itHOnYrN0Ib8r+z7laQz3sdE+4FP3d9Q7VLO7U+KRT+CR0GsWuyHxzdAY82I7cXv0G/twrqomTJLOssO5HA==} - dependencies: - clone: 1.0.4 + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} - /define-lazy-prop@2.0.0: + define-lazy-prop@2.0.0: resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} engines: {node: '>=8'} - dev: true - /define-properties@1.2.0: - resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} - engines: {node: '>= 0.4'} - dependencies: - has-property-descriptors: 1.0.0 - object-keys: 1.1.1 - dev: true + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} - /delegates@1.0.0: + delegates@1.0.0: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} - dev: true - /depd@1.1.2: + depd@1.1.2: resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} engines: {node: '>= 0.6'} - dev: true - /depd@2.0.0: + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} - dev: true - /destroy@1.2.0: + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - dev: true - /detect-node@2.1.0: + detect-node@2.1.0: resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} - dev: true - /diff@4.0.2: + diff@4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} - dev: true - /dir-glob@3.0.1: + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} - dependencies: - path-type: 4.0.0 - dev: true - - /dns-equal@1.0.0: - resolution: {integrity: sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==} - dev: true - /dns-packet@5.4.0: - resolution: {integrity: sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==} + dns-packet@5.6.1: + resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==} engines: {node: '>=6'} - dependencies: - '@leichtgewicht/ip-codec': 2.0.4 - dev: true - /doctrine@3.0.0: + doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} - dependencies: - esutils: 2.0.3 - dev: true - /dom-serializer@2.0.0: + dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - entities: 4.5.0 - dev: true - /domelementtype@2.3.0: + domelementtype@2.3.0: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - dev: true - /domhandler@5.0.3: + domexception@2.0.1: + resolution: {integrity: sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==} + engines: {node: '>=8'} + deprecated: Use your platform's native DOMException instead + + domhandler@5.0.3: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dependencies: - domelementtype: 2.3.0 - dev: true - /domutils@3.1.0: - resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} - dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - dev: true + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} - /eastasianwidth@0.2.0: + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - /ee-first@1.1.1: + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - dev: true - /electron-to-chromium@1.4.440: - resolution: {integrity: sha512-r6dCgNpRhPwiWlxbHzZQ/d9swfPaEJGi8ekqRBwQYaR3WmA5VkqQfBWSDDjuJU1ntO+W9tHx8OHV/96Q8e0dVw==} - dev: true + electron-to-chromium@1.5.250: + resolution: {integrity: sha512-/5UMj9IiGDMOFBnN4i7/Ry5onJrAGSbOGo3s9FEKmwobGq6xw832ccET0CE3CkkMBZ8GJSlUIesZofpyurqDXw==} - /emoji-regex@8.0.0: + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - /emoji-regex@9.2.2: + emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - /emojis-list@3.0.0: + emojis-list@3.0.0: resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} engines: {node: '>= 4'} - dev: true - /encodeurl@1.0.2: + encodeurl@1.0.2: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} - dev: true - /encoding@0.1.13: + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + encoding@0.1.13: resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} - requiresBuild: true - dependencies: - iconv-lite: 0.6.3 - dev: true - optional: true - /enhanced-resolve@5.15.0: - resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} - dependencies: - graceful-fs: 4.2.11 - tapable: 2.2.1 - dev: true - /entities@4.5.0: + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - /env-paths@2.2.1: + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} - dev: true - /err-code@2.0.3: + err-code@2.0.3: resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} - dev: true - /errno@0.1.8: + errno@0.1.8: resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} hasBin: true - requiresBuild: true - dependencies: - prr: 1.0.1 - dev: true - optional: true - /error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} - dependencies: - is-arrayish: 0.2.1 + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - /es-get-iterator@1.1.3: - resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} - dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 - has-symbols: 1.0.3 - is-arguments: 1.1.1 - is-map: 2.0.2 - is-set: 2.0.2 - is-string: 1.0.7 - isarray: 2.0.5 - stop-iteration-iterator: 1.0.0 - dev: true - - /es-module-lexer@1.3.0: - resolution: {integrity: sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==} - dev: true - - /esbuild-wasm@0.17.19: - resolution: {integrity: sha512-X9UQEMJMZXwlGCfqcBmJ1jEa+KrLfd+gCBypO/TSzo5hZvbVwFqpxj1YCuX54ptTF75wxmrgorR4RL40AKtLVg==} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild-wasm@0.18.17: + resolution: {integrity: sha512-9OHGcuRzy+I8ziF9FzjfKLWAPbvi0e/metACVg9k6bK+SI4FFxeV6PcZsz8RIVaMD4YNehw+qj6UMR3+qj/EuQ==} engines: {node: '>=12'} hasBin: true - dev: true - /esbuild@0.17.19: - resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} + esbuild@0.18.17: + resolution: {integrity: sha512-1GJtYnUxsJreHYA0Y+iQz2UEykonY66HNWOb0yXYZi9/kNrORUEHVg87eQsCtqh59PEJ5YVZJO98JHznMJSWjg==} engines: {node: '>=12'} hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/android-arm': 0.17.19 - '@esbuild/android-arm64': 0.17.19 - '@esbuild/android-x64': 0.17.19 - '@esbuild/darwin-arm64': 0.17.19 - '@esbuild/darwin-x64': 0.17.19 - '@esbuild/freebsd-arm64': 0.17.19 - '@esbuild/freebsd-x64': 0.17.19 - '@esbuild/linux-arm': 0.17.19 - '@esbuild/linux-arm64': 0.17.19 - '@esbuild/linux-ia32': 0.17.19 - '@esbuild/linux-loong64': 0.17.19 - '@esbuild/linux-mips64el': 0.17.19 - '@esbuild/linux-ppc64': 0.17.19 - '@esbuild/linux-riscv64': 0.17.19 - '@esbuild/linux-s390x': 0.17.19 - '@esbuild/linux-x64': 0.17.19 - '@esbuild/netbsd-x64': 0.17.19 - '@esbuild/openbsd-x64': 0.17.19 - '@esbuild/sunos-x64': 0.17.19 - '@esbuild/win32-arm64': 0.17.19 - '@esbuild/win32-ia32': 0.17.19 - '@esbuild/win32-x64': 0.17.19 - dev: true - - /escalade@3.1.1: - resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} - /escape-html@1.0.3: + escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - dev: true - /escape-string-regexp@1.0.5: + escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} - /escape-string-regexp@4.0.0: + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - dev: true - /eslint-scope@5.1.1: + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} - dependencies: - esrecurse: 4.3.0 - estraverse: 4.3.0 - dev: true - /eslint-scope@7.2.0: - resolution: {integrity: sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==} + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - dev: true - /eslint-visitor-keys@3.4.1: - resolution: {integrity: sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==} + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - /eslint@8.43.0: - resolution: {integrity: sha512-aaCpf2JqqKesMFGgmRPessmVKjcGXqdlAYLLC3THM8t5nBRZRQ+st5WM/hoJXkdioEXLLbXgclUpM0TXo5HX5Q==} + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.43.0) - '@eslint-community/regexpp': 4.5.1 - '@eslint/eslintrc': 2.0.3 - '@eslint/js': 8.43.0 - '@humanwhocodes/config-array': 0.11.10 - '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.4 - doctrine: 3.0.0 - escape-string-regexp: 4.0.0 - eslint-scope: 7.2.0 - eslint-visitor-keys: 3.4.1 - espree: 9.5.2 - esquery: 1.5.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - find-up: 5.0.0 - glob-parent: 6.0.2 - globals: 13.20.0 - graphemer: 1.4.0 - ignore: 5.2.4 - import-fresh: 3.3.0 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-yaml: 4.1.0 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.1 - strip-ansi: 6.0.1 - strip-json-comments: 3.1.1 - text-table: 0.2.0 - transitivePeerDependencies: - - supports-color - dev: true - /espree@9.5.2: - resolution: {integrity: sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==} + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - acorn: 8.9.0 - acorn-jsx: 5.3.2(acorn@8.9.0) - eslint-visitor-keys: 3.4.1 - dev: true - /esprima@4.0.1: + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true - dev: true - /esquery@1.5.0: - resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} - dependencies: - estraverse: 5.3.0 - dev: true - /esrecurse@4.3.0: + esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} - dependencies: - estraverse: 5.3.0 - dev: true - /estraverse@4.3.0: + estraverse@4.3.0: resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} engines: {node: '>=4.0'} - dev: true - /estraverse@5.3.0: + estraverse@5.3.0: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} - dev: true - /esutils@2.0.3: + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - dev: true - /etag@1.8.1: + etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} - dev: true - /eventemitter-asyncresource@1.0.0: + eventemitter-asyncresource@1.0.0: resolution: {integrity: sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ==} - dev: true - /eventemitter3@4.0.7: + eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - dev: true - /events@3.3.0: + events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} - dev: true - /execa@5.1.1: + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} - dependencies: - cross-spawn: 7.0.3 - get-stream: 6.0.1 - human-signals: 2.1.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - dev: true - /express@4.18.1: - resolution: {integrity: sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==} + exponential-backoff@3.1.3: + resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} - dependencies: - accepts: 1.3.8 - array-flatten: 1.1.1 - body-parser: 1.20.0 - content-disposition: 0.5.4 - content-type: 1.0.4 - cookie: 0.5.0 - cookie-signature: 1.0.6 - debug: 2.6.9 - depd: 2.0.0 - encodeurl: 1.0.2 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 1.2.0 - fresh: 0.5.2 - http-errors: 2.0.0 - merge-descriptors: 1.0.1 - methods: 1.1.2 - on-finished: 2.4.1 - parseurl: 1.3.3 - path-to-regexp: 0.1.7 - proxy-addr: 2.0.7 - qs: 6.10.3 - range-parser: 1.2.1 - safe-buffer: 5.2.1 - send: 0.18.0 - serve-static: 1.15.0 - setprototypeof: 1.2.0 - statuses: 2.0.1 - type-is: 1.6.18 - utils-merge: 1.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - dev: true - /external-editor@3.1.0: + external-editor@3.1.0: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} - dependencies: - chardet: 0.7.0 - iconv-lite: 0.4.24 - tmp: 0.0.33 - dev: true - /fast-deep-equal@3.1.3: + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: true - /fast-glob@3.2.12: - resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} + fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.5 - dev: true - /fast-json-stable-stringify@2.1.0: + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - dev: true - /fast-levenshtein@2.0.6: + fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - dev: true - /fastq@1.15.0: - resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} - dependencies: - reusify: 1.0.4 - dev: true + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - /faye-websocket@0.11.4: + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + faye-websocket@0.11.4: resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} engines: {node: '>=0.8.0'} - dependencies: - websocket-driver: 0.7.4 - dev: true - /figures@3.2.0: + figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} - dependencies: - escape-string-regexp: 1.0.5 - dev: true - /file-entry-cache@6.0.1: + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} - dependencies: - flat-cache: 3.0.4 - dev: true - /fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - dependencies: - to-regex-range: 5.0.1 - dev: true - /finalhandler@1.2.0: - resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} - dependencies: - debug: 2.6.9 - encodeurl: 1.0.2 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.1 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - dev: true - /find-cache-dir@3.3.2: - resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} - engines: {node: '>=8'} - dependencies: - commondir: 1.0.1 - make-dir: 3.1.0 - pkg-dir: 4.2.0 - dev: true + find-cache-dir@4.0.0: + resolution: {integrity: sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==} + engines: {node: '>=14.16'} - /find-up@4.1.0: + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} - dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 - dev: true - /find-up@5.0.0: + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - dev: true - /flat-cache@3.0.4: - resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} + find-up@6.3.0: + resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} engines: {node: ^10.12.0 || >=12.0.0} - dependencies: - flatted: 3.2.7 - rimraf: 3.0.2 - dev: true - /flat@5.0.2: + flat@5.0.2: resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} hasBin: true - dev: false - /flatted@3.2.7: - resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} - dev: true + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - /follow-redirects@1.13.3: - resolution: {integrity: sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==} + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} engines: {node: '>=4.0'} peerDependencies: debug: '*' peerDependenciesMeta: debug: optional: true - dev: true - /for-each@0.3.3: - resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} - dependencies: - is-callable: 1.2.7 - dev: true - - /foreground-child@3.1.1: - resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - dependencies: - cross-spawn: 7.0.3 - signal-exit: 4.0.2 - /forwarded@0.2.0: + form-data@3.0.4: + resolution: {integrity: sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==} + engines: {node: '>= 6'} + + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} - dev: true - /fraction.js@4.2.0: - resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==} - dev: true + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} - /fresh@0.5.2: + fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} - dev: true - /fs-extra@11.1.1: + fs-extra@11.1.1: resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} engines: {node: '>=14.14'} - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.0 - /fs-minipass@2.1.0: + fs-minipass@2.1.0: resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} engines: {node: '>= 8'} - dependencies: - minipass: 3.3.4 - dev: true - /fs-minipass@3.0.2: - resolution: {integrity: sha512-2GAfyfoaCDRrM6jaOS3UsBts8yJ55VioXdWcOL7dK9zdAuKT71+WBA4ifnNYqVjYv+4SsPxjK0JT4yIIn4cA/g==} + fs-minipass@3.0.3: + resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - minipass: 5.0.0 - dev: true - /fs-monkey@1.0.4: - resolution: {integrity: sha512-INM/fWAxMICjttnD0DX1rBvinKskj5G1w+oy/pnm9u/tSlnBrzFonJMcalKJ30P8RRsPzKcCG7Q8l0jx5Fh9YQ==} - dev: true + fs-monkey@1.1.0: + resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==} - /fs.realpath@1.0.0: + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - /fsevents@2.3.2: + fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - requiresBuild: true - dev: true - optional: true - /function-bind@1.1.1: - resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} - dev: true + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] - /functions-have-names@1.2.3: - resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - dev: true + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - /gauge@4.0.4: + gauge@4.0.4: resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - dependencies: - aproba: 2.0.0 - color-support: 1.1.3 - console-control-strings: 1.1.0 - has-unicode: 2.0.1 - signal-exit: 3.0.7 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wide-align: 1.1.5 - dev: true + deprecated: This package is no longer supported. - /gensync@1.0.0-beta.2: + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} - dev: true - /get-caller-file@2.0.5: + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - /get-intrinsic@1.2.1: - resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} - dependencies: - function-bind: 1.1.1 - has: 1.0.3 - has-proto: 1.0.1 - has-symbols: 1.0.3 - dev: true + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} - /get-package-type@0.1.0: + get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} - dev: true - /get-stream@6.0.1: + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} - dev: true - /glob-parent@5.1.2: + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} - dependencies: - is-glob: 4.0.3 - dev: true - /glob-parent@6.0.2: + glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - dependencies: - is-glob: 4.0.3 - dev: true - /glob-to-regexp@0.4.1: + glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - dev: true - /glob@10.2.1: + glob@10.2.1: resolution: {integrity: sha512-ngom3wq2UhjdbmRE/krgkD8BQyi1KZ5l+D2dVm4+Yj+jJIBp74/ZGunL6gNGc/CYuQmvUBiavWEXIotRiv5R6A==} engines: {node: '>=16 || 14 >=14.17'} hasBin: true - dependencies: - foreground-child: 3.1.1 - fs.realpath: 1.0.0 - jackspeak: 2.2.1 - minimatch: 9.0.2 - minipass: 5.0.0 - path-scurry: 1.9.2 - dev: true - /glob@10.3.0: - resolution: {integrity: sha512-AQ1/SB9HH0yCx1jXAT4vmCbTOPe5RQ+kCurjbel5xSCGhebumUv+GJZfa1rEqor3XIViqwSEmlkZCQD43RWrBg==} - engines: {node: '>=16 || 14 >=14.17'} + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true - dependencies: - foreground-child: 3.1.1 - jackspeak: 2.2.1 - minimatch: 9.0.2 - minipass: 5.0.0 - path-scurry: 1.9.2 - /glob@7.2.3: + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 + deprecated: Glob versions prior to v9 are no longer supported - /glob@8.1.0: + glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 5.1.0 - once: 1.4.0 - dev: true - - /globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - dev: true + deprecated: Glob versions prior to v9 are no longer supported - /globals@13.20.0: - resolution: {integrity: sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==} + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} engines: {node: '>=8'} - dependencies: - type-fest: 0.20.2 - dev: true - /globby@11.1.0: + globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} - dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.2.12 - ignore: 5.2.4 - merge2: 1.4.1 - slash: 3.0.0 - dev: true - /globby@13.1.2: - resolution: {integrity: sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ==} + globby@13.2.2: + resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - dir-glob: 3.0.1 - fast-glob: 3.2.12 - ignore: 5.2.4 - merge2: 1.4.1 - slash: 4.0.0 - dev: true - /gopd@1.0.1: - resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} - dependencies: - get-intrinsic: 1.2.1 - dev: true + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} - /graceful-fs@4.2.11: + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - /grapheme-splitter@1.0.4: + grapheme-splitter@1.0.4: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} - dev: true - /graphemer@1.4.0: + graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - dev: true - - /handle-thing@2.0.1: - resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} - dev: true - /has-bigints@1.0.2: - resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} - dev: true + guess-parser@0.4.22: + resolution: {integrity: sha512-KcUWZ5ACGaBM69SbqwVIuWGoSAgD+9iJnchR9j/IarVI1jHVeXv+bUXBIMeqVMSKt3zrn0Dgf9UpcOEpPBLbSg==} + peerDependencies: + typescript: '>=3.7.5' - /has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} + handle-thing@2.0.1: + resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} - /has-flag@4.0.0: + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - /has-property-descriptors@1.0.0: - resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} - dependencies: - get-intrinsic: 1.2.1 - dev: true - - /has-proto@1.0.1: - resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} - engines: {node: '>= 0.4'} - dev: true - - /has-symbols@1.0.3: - resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} - dev: true - /has-tostringtag@1.0.0: - resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} - dependencies: - has-symbols: 1.0.3 - dev: true - /has-unicode@2.0.1: + has-unicode@2.0.1: resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} - dev: true - /has@1.0.3: - resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} - engines: {node: '>= 0.4.0'} - dependencies: - function-bind: 1.1.1 - dev: true + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} - /hdr-histogram-js@2.0.3: + hdr-histogram-js@2.0.3: resolution: {integrity: sha512-Hkn78wwzWHNCp2uarhzQ2SGFLU3JY8SBDDd3TAABK4fc30wm+MuPOrg5QVFVfkKOQd6Bfz3ukJEI+q9sXEkK1g==} - dependencies: - '@assemblyscript/loader': 0.10.1 - base64-js: 1.5.1 - pako: 1.0.11 - dev: true - /hdr-histogram-percentiles-obj@3.0.0: + hdr-histogram-percentiles-obj@3.0.0: resolution: {integrity: sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==} - dev: true - /hosted-git-info@6.1.1: - resolution: {integrity: sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==} + hosted-git-info@6.1.3: + resolution: {integrity: sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - lru-cache: 7.13.1 - dev: true - /hpack.js@2.1.6: + hpack.js@2.1.6: resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==} - dependencies: - inherits: 2.0.4 - obuf: 1.1.2 - readable-stream: 2.3.7 - wbuf: 1.7.3 - dev: true - /html-entities@2.3.3: - resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} - dev: true + html-encoding-sniffer@2.0.1: + resolution: {integrity: sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==} + engines: {node: '>=10'} + + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} - /htmlparser2@8.0.2: + htmlparser2@8.0.2: resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.1.0 - entities: 4.5.0 - dev: true - /http-cache-semantics@4.1.1: - resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} - dev: true + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} - /http-deceiver@1.2.7: + http-deceiver@1.2.7: resolution: {integrity: sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==} - dev: true - /http-errors@1.6.3: + http-errors@1.6.3: resolution: {integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==} engines: {node: '>= 0.6'} - dependencies: - depd: 1.1.2 - inherits: 2.0.3 - setprototypeof: 1.1.0 - statuses: 1.5.0 - dev: true - /http-errors@2.0.0: + http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.1 - toidentifier: 1.0.1 - dev: true - /http-parser-js@0.5.8: - resolution: {integrity: sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==} - dev: true + http-parser-js@0.5.10: + resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==} + + http-proxy-agent@4.0.1: + resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} + engines: {node: '>= 6'} - /http-proxy-agent@5.0.0: + http-proxy-agent@5.0.0: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} engines: {node: '>= 6'} - dependencies: - '@tootallnate/once': 2.0.0 - agent-base: 6.0.2 - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - dev: true - /http-proxy-middleware@2.0.6(@types/express@4.17.13): - resolution: {integrity: sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==} + http-proxy-middleware@2.0.9: + resolution: {integrity: sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==} engines: {node: '>=12.0.0'} peerDependencies: '@types/express': ^4.17.13 peerDependenciesMeta: '@types/express': optional: true - dependencies: - '@types/express': 4.17.13 - '@types/http-proxy': 1.17.9 - http-proxy: 1.18.1 - is-glob: 4.0.3 - is-plain-obj: 3.0.0 - micromatch: 4.0.5 - transitivePeerDependencies: - - debug - dev: true - /http-proxy@1.18.1: + http-proxy@1.18.1: resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} engines: {node: '>=8.0.0'} - dependencies: - eventemitter3: 4.0.7 - follow-redirects: 1.13.3 - requires-port: 1.0.0 - transitivePeerDependencies: - - debug - dev: true - /https-proxy-agent@5.0.1: + https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} - dependencies: - agent-base: 6.0.2 - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - dev: true - /human-signals@2.1.0: + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} - dev: true - /humanize-ms@1.2.1: + humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} - dependencies: - ms: 2.1.3 - dev: true - /iconv-lite@0.4.24: + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} - dependencies: - safer-buffer: 2.1.2 - dev: true - /iconv-lite@0.6.3: + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} - dependencies: - safer-buffer: 2.1.2 - dev: true - /icss-utils@5.1.0(postcss@8.4.24): + icss-utils@5.1.0: resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 - dependencies: - postcss: 8.4.24 - dev: true - /ieee754@1.2.1: + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - /ignore-walk@6.0.3: - resolution: {integrity: sha512-C7FfFoTA+bI10qfeydT8aZbvr91vAEU+2W5BZUlzPec47oNb07SsOfwYrtxuvOYdUApPP/Qlh4DtAO51Ekk2QA==} + ignore-walk@6.0.5: + resolution: {integrity: sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - minimatch: 9.0.2 - dev: true - /ignore@5.2.4: - resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - dev: true - /image-size@0.5.5: + image-size@0.5.5: resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==} engines: {node: '>=0.10.0'} hasBin: true - requiresBuild: true - dev: true - optional: true - /immutable@4.1.0: - resolution: {integrity: sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==} - dev: true + immutable@4.3.7: + resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==} - /import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - /imurmurhash@0.1.4: + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - dev: true - /indent-string@4.0.0: + indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} - dev: true - /infer-owner@1.0.4: + infer-owner@1.0.4: resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} - dev: true - /inflight@1.0.6: + inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - dependencies: - once: 1.4.0 - wrappy: 1.0.2 + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - /inherits@2.0.3: + inherits@2.0.3: resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} - dev: true - /inherits@2.0.4: + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - /ini@4.1.1: + ini@4.1.1: resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dev: true - /inquirer@8.2.4: + inquirer@8.2.4: resolution: {integrity: sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==} engines: {node: '>=12.0.0'} - dependencies: - ansi-escapes: 4.3.2 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-width: 3.0.0 - external-editor: 3.1.0 - figures: 3.2.0 - lodash: 4.17.21 - mute-stream: 0.0.8 - ora: 5.4.1 - run-async: 2.4.1 - rxjs: 7.8.1 - string-width: 4.2.3 - strip-ansi: 6.0.1 - through: 2.3.8 - wrap-ansi: 7.0.0 - dev: true - - /internal-slot@1.0.5: - resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} - engines: {node: '>= 0.4'} - dependencies: - get-intrinsic: 1.2.1 - has: 1.0.3 - side-channel: 1.0.4 - dev: true - /ip@2.0.0: - resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==} - dev: true + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} - /ipaddr.js@1.9.1: + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} - dev: true - /ipaddr.js@2.0.1: - resolution: {integrity: sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==} + ipaddr.js@2.2.0: + resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} engines: {node: '>= 10'} - dev: true - - /is-arguments@1.1.1: - resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - has-tostringtag: 1.0.0 - dev: true - - /is-array-buffer@3.0.2: - resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} - dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 - is-typed-array: 1.1.10 - dev: true - /is-arrayish@0.2.1: + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - /is-bigint@1.0.4: - resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} - dependencies: - has-bigints: 1.0.2 - dev: true - - /is-binary-path@2.1.0: + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} - dependencies: - binary-extensions: 2.2.0 - dev: true - - /is-boolean-object@1.1.2: - resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - has-tostringtag: 1.0.0 - dev: true - - /is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} - dev: true - - /is-core-module@2.12.1: - resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==} - dependencies: - has: 1.0.3 - dev: true - /is-date-object@1.0.5: - resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} - dependencies: - has-tostringtag: 1.0.0 - dev: true - /is-docker@2.2.1: + is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} hasBin: true - dev: true - /is-extglob@2.1.1: + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - dev: true - /is-fullwidth-code-point@3.0.0: + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - /is-glob@4.0.3: + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} - dependencies: - is-extglob: 2.1.1 - dev: true - /is-interactive@1.0.0: + is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} - /is-lambda@1.0.1: + is-lambda@1.0.1: resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} - dev: true - - /is-map@2.0.2: - resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} - dev: true - - /is-number-object@1.0.7: - resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} - engines: {node: '>= 0.4'} - dependencies: - has-tostringtag: 1.0.0 - dev: true - /is-number@7.0.0: + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - dev: true - /is-path-inside@3.0.3: + is-path-inside@3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} - dev: true - /is-plain-obj@3.0.0: + is-plain-obj@3.0.0: resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} engines: {node: '>=10'} - dev: true - /is-plain-object@2.0.4: + is-plain-object@2.0.4: resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} engines: {node: '>=0.10.0'} - dependencies: - isobject: 3.0.1 - dev: true - - /is-regex@1.1.4: - resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - has-tostringtag: 1.0.0 - dev: true - - /is-set@2.0.2: - resolution: {integrity: sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==} - dev: true - /is-shared-array-buffer@1.0.2: - resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} - dependencies: - call-bind: 1.0.2 - dev: true + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - /is-stream@2.0.1: + is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} - dev: true - - /is-string@1.0.7: - resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} - engines: {node: '>= 0.4'} - dependencies: - has-tostringtag: 1.0.0 - dev: true - - /is-symbol@1.0.4: - resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} - engines: {node: '>= 0.4'} - dependencies: - has-symbols: 1.0.3 - dev: true - - /is-typed-array@1.1.10: - resolution: {integrity: sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==} - engines: {node: '>= 0.4'} - dependencies: - available-typed-arrays: 1.0.5 - call-bind: 1.0.2 - for-each: 0.3.3 - gopd: 1.0.1 - has-tostringtag: 1.0.0 - dev: true - /is-unicode-supported@0.1.0: + is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} - /is-weakmap@2.0.1: - resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==} - dev: true - - /is-weakset@2.0.2: - resolution: {integrity: sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==} - dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 - dev: true - - /is-what@3.14.1: + is-what@3.14.1: resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==} - dev: true - /is-wsl@2.2.0: + is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} - dependencies: - is-docker: 2.2.1 - dev: true - /isarray@1.0.0: + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - dev: true - - /isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - dev: true - /isexe@2.0.0: + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - /isobject@3.0.1: + isobject@3.0.1: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} - dev: true - /istanbul-lib-coverage@3.2.0: - resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} - dev: true - /istanbul-lib-instrument@5.2.0: - resolution: {integrity: sha512-6Lthe1hqXHBNsqvgDzGO6l03XNeu3CrG4RqQ1KM9+l5+jNGpEJfIELx1NS3SEHmJQA8np/u+E4EPRKRiu6m19A==} + istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} engines: {node: '>=8'} - dependencies: - '@babel/core': 7.22.5 - '@babel/parser': 7.22.5 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-coverage: 3.2.0 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: true - /jackspeak@2.2.1: - resolution: {integrity: sha512-MXbxovZ/Pm42f6cDIDkl3xpwv1AGwObKwfmjs2nQePiy85tP3fatofl3FC1aBsOtP/6fq5SbtgHwWcMsLP+bDw==} + jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} engines: {node: '>=14'} - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - /jest-worker@27.5.1: + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jest-worker@27.5.1: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} - dependencies: - '@types/node': 18.0.0 - merge-stream: 2.0.0 - supports-color: 8.1.1 - dev: true - /jiti@1.18.2: - resolution: {integrity: sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==} + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true - dev: true - /js-tokens@4.0.0: + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - /js-yaml@3.14.1: + js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - dev: true - /js-yaml@4.1.0: + js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true - dependencies: - argparse: 2.0.1 - /jsesc@0.5.0: - resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} - hasBin: true - dev: true + jsdom@16.7.0: + resolution: {integrity: sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==} + engines: {node: '>=10'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true - /jsesc@2.5.2: + jsesc@2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} hasBin: true - dev: true - /json-parse-even-better-errors@2.3.1: + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - /json-parse-even-better-errors@3.0.0: - resolution: {integrity: sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==} + json-parse-even-better-errors@3.0.2: + resolution: {integrity: sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dev: true - /json-schema-traverse@0.4.1: + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - dev: true - /json-schema-traverse@1.0.0: + json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - dev: true - /json-stable-stringify-without-jsonify@1.0.1: + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - dev: true - /json5@2.2.3: + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} hasBin: true - dev: true - /jsonc-parser@3.2.0: + jsonc-parser@3.2.0: resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} - dev: true - /jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} - dependencies: - universalify: 2.0.0 - optionalDependencies: - graceful-fs: 4.2.11 + jsonc-parser@3.2.1: + resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} - /jsonparse@1.3.1: + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + jsonparse@1.3.1: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} - dev: true - /karma-source-map-support@1.4.0: + karma-source-map-support@1.4.0: resolution: {integrity: sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==} - dependencies: - source-map-support: 0.5.21 - dev: true - /kind-of@6.0.3: + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kind-of@6.0.3: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} - dev: true - /kleur@3.0.3: + kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - dev: true - /kleur@4.1.5: + kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} - dev: true - /klona@2.0.5: - resolution: {integrity: sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==} - engines: {node: '>= 8'} - dev: true - - /klona@2.0.6: + klona@2.0.6: resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} engines: {node: '>= 8'} - dev: true - /launch-editor@2.6.0: - resolution: {integrity: sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==} - dependencies: - picocolors: 1.0.0 - shell-quote: 1.8.1 - dev: true + launch-editor@2.12.0: + resolution: {integrity: sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==} - /less-loader@11.1.0(less@4.1.3)(webpack@5.86.0): + less-loader@11.1.0: resolution: {integrity: sha512-C+uDBV7kS7W5fJlUjq5mPBeBVhYpTIm5gB09APT9o3n/ILeaXVsiSFTbZpTJCJwQ/Crczfn3DmfQFwxYusWFug==} engines: {node: '>= 14.15.0'} peerDependencies: less: ^3.5.0 || ^4.0.0 webpack: ^5.0.0 - dependencies: - klona: 2.0.5 - less: 4.1.3 - webpack: 5.86.0(esbuild@0.17.19) - dev: true - /less@4.1.3: + less@4.1.3: resolution: {integrity: sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==} engines: {node: '>=6'} hasBin: true - dependencies: - copy-anything: 2.0.6 - parse-node-version: 1.0.1 - tslib: 2.5.3 - optionalDependencies: - errno: 0.1.8 - graceful-fs: 4.2.11 - image-size: 0.5.5 - make-dir: 2.1.0 - mime: 1.6.0 - needle: 3.2.0 - source-map: 0.6.1 - transitivePeerDependencies: - - supports-color - dev: true - /levn@0.4.1: + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - dev: true - /license-webpack-plugin@4.0.2(webpack@5.86.0): + license-webpack-plugin@4.0.2: resolution: {integrity: sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==} peerDependencies: webpack: '*' peerDependenciesMeta: webpack: optional: true - webpack-sources: - optional: true - dependencies: - webpack: 5.86.0(esbuild@0.17.19) - webpack-sources: 3.2.3 - dev: true - /lines-and-columns@1.2.4: + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - /loader-runner@4.3.0: - resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} + loader-runner@4.3.1: + resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} engines: {node: '>=6.11.5'} - dev: true - /loader-utils@2.0.2: - resolution: {integrity: sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==} + loader-utils@2.0.4: + resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==} engines: {node: '>=8.9.0'} - dependencies: - big.js: 5.2.2 - emojis-list: 3.0.0 - json5: 2.2.3 - dev: true - /loader-utils@3.2.1: + loader-utils@3.2.1: resolution: {integrity: sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==} engines: {node: '>= 12.13.0'} - dev: true - /locate-path@5.0.0: + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} - dependencies: - p-locate: 4.1.0 - dev: true - /locate-path@6.0.0: + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - dependencies: - p-locate: 5.0.0 - dev: true - /lodash.debounce@4.0.8: + locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} - dev: true - /lodash.kebabcase@4.1.1: + lodash.kebabcase@4.1.1: resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} - dev: false - /lodash.merge@4.6.2: + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - dev: true - /lodash@4.17.21: + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - dev: true - /log-symbols@4.1.0: + log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} - dependencies: - chalk: 4.1.2 - is-unicode-supported: 0.1.0 - /lru-cache@5.1.1: + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - dependencies: - yallist: 3.1.1 - dev: true - /lru-cache@6.0.0: + lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} - dependencies: - yallist: 4.0.0 - dev: true - /lru-cache@7.13.1: - resolution: {integrity: sha512-CHqbAq7NFlW3RSnoWXLJBxCWaZVBrfa9UEHId2M3AW8iEBurbqduNexEUCGc3SHc6iCYXNJCDi903LajSVAEPQ==} + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} - dev: true - /lru-cache@9.1.2: - resolution: {integrity: sha512-ERJq3FOzJTxBbFjZ7iDs+NiK4VI9Wz+RdrrAB8dio1oV+YvdPzUEE4QNiT2VD51DkIbCYRUUzCRkssXCHqSnKQ==} - engines: {node: 14 || >=16.14} + magic-string@0.30.1: + resolution: {integrity: sha512-mbVKXPmS0z0G4XqFDCTllmDQ6coZzn94aMlb0o/A4HEHJCKcanlDZwYJgwnkmgD3jyWhUgj9VsPrfd972yPffA==} + engines: {node: '>=12'} - /magic-string@0.30.0: - resolution: {integrity: sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==} + magic-string@0.30.8: + resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} engines: {node: '>=12'} - dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 - dev: true - /make-dir@2.1.0: + make-dir@2.1.0: resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} engines: {node: '>=6'} - requiresBuild: true - dependencies: - pify: 4.0.1 - semver: 5.7.1 - dev: true - optional: true - - /make-dir@3.1.0: - resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} - engines: {node: '>=8'} - dependencies: - semver: 6.3.0 - dev: true - /make-error@1.3.6: + make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - dev: true - /make-fetch-happen@10.2.0: - resolution: {integrity: sha512-OnEfCLofQVJ5zgKwGk55GaqosqKjaR6khQlJY3dBAA+hM25Bc5CmX5rKUfVut+rYA3uidA7zb7AvcglU87rPRg==} + make-fetch-happen@10.2.1: + resolution: {integrity: sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - dependencies: - agentkeepalive: 4.2.1 - cacache: 16.1.1 - http-cache-semantics: 4.1.1 - http-proxy-agent: 5.0.0 - https-proxy-agent: 5.0.1 - is-lambda: 1.0.1 - lru-cache: 7.13.1 - minipass: 3.3.4 - minipass-collect: 1.0.2 - minipass-fetch: 2.1.0 - minipass-flush: 1.0.5 - minipass-pipeline: 1.2.4 - negotiator: 0.6.3 - promise-retry: 2.0.1 - socks-proxy-agent: 7.0.0 - ssri: 9.0.1 - transitivePeerDependencies: - - bluebird - - supports-color - dev: true - /make-fetch-happen@11.1.1: + make-fetch-happen@11.1.1: resolution: {integrity: sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - agentkeepalive: 4.2.1 - cacache: 17.1.3 - http-cache-semantics: 4.1.1 - http-proxy-agent: 5.0.0 - https-proxy-agent: 5.0.1 - is-lambda: 1.0.1 - lru-cache: 7.13.1 - minipass: 5.0.0 - minipass-fetch: 3.0.3 - minipass-flush: 1.0.5 - minipass-pipeline: 1.2.4 - negotiator: 0.6.3 - promise-retry: 2.0.1 - socks-proxy-agent: 7.0.0 - ssri: 10.0.4 - transitivePeerDependencies: - - supports-color - dev: true - /media-typer@0.3.0: - resolution: {integrity: sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} - dev: true - /memfs@3.5.3: + memfs@3.5.3: resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} engines: {node: '>= 4.0.0'} - dependencies: - fs-monkey: 1.0.4 - dev: true - /merge-descriptors@1.0.1: - resolution: {integrity: sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=} - dev: true + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} - /merge-stream@2.0.0: + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - dev: true - /merge2@1.4.1: + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - dev: true - /methods@1.1.2: + methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} - dev: true - /micromatch@4.0.5: - resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - dependencies: - braces: 3.0.2 - picomatch: 2.3.1 - dev: true - /mime-db@1.52.0: + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} - dev: true - /mime-types@2.1.35: + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - dependencies: - mime-db: 1.52.0 - dev: true - /mime@1.6.0: + mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} hasBin: true - dev: true - /mimic-fn@2.1.0: + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} - /mini-css-extract-plugin@2.7.6(webpack@5.86.0): + mini-css-extract-plugin@2.7.6: resolution: {integrity: sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==} engines: {node: '>= 12.13.0'} peerDependencies: webpack: ^5.0.0 - dependencies: - schema-utils: 4.0.0 - webpack: 5.86.0(esbuild@0.17.19) - dev: true - /minimalistic-assert@1.0.1: + minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} - dev: true - /minimatch@3.1.2: + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - dependencies: - brace-expansion: 1.1.11 - /minimatch@5.1.0: - resolution: {integrity: sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==} + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} - dependencies: - brace-expansion: 2.0.1 - dev: true - /minimatch@9.0.2: - resolution: {integrity: sha512-PZOT9g5v2ojiTL7r1xF6plNHLtOeTpSlDI007As2NlA2aYBMfVom17yqa6QzhmDP8QOhn7LjHTg7DFCVSSa6yg==} + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} - dependencies: - brace-expansion: 2.0.1 - /minipass-collect@1.0.2: + minipass-collect@1.0.2: resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} engines: {node: '>= 8'} - dependencies: - minipass: 3.3.4 - dev: true - /minipass-fetch@2.1.0: - resolution: {integrity: sha512-H9U4UVBGXEyyWJnqYDCLp1PwD8XIkJ4akNHp1aGVI+2Ym7wQMlxDKi4IB4JbmyU+pl9pEs/cVrK6cOuvmbK4Sg==} + minipass-fetch@2.1.2: + resolution: {integrity: sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - dependencies: - minipass: 3.3.4 - minipass-sized: 1.0.3 - minizlib: 2.1.2 - optionalDependencies: - encoding: 0.1.13 - dev: true - /minipass-fetch@3.0.3: - resolution: {integrity: sha512-n5ITsTkDqYkYJZjcRWzZt9qnZKCT7nKCosJhHoj7S7zD+BP4jVbWs+odsniw5TA3E0sLomhTKOKjF86wf11PuQ==} + minipass-fetch@3.0.5: + resolution: {integrity: sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - minipass: 5.0.0 - minipass-sized: 1.0.3 - minizlib: 2.1.2 - optionalDependencies: - encoding: 0.1.13 - dev: true - /minipass-flush@1.0.5: + minipass-flush@1.0.5: resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} engines: {node: '>= 8'} - dependencies: - minipass: 3.3.4 - dev: true - /minipass-json-stream@1.0.1: - resolution: {integrity: sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==} - dependencies: - jsonparse: 1.3.1 - minipass: 3.3.4 - dev: true + minipass-json-stream@1.0.2: + resolution: {integrity: sha512-myxeeTm57lYs8pH2nxPzmEEg8DGIgW+9mv6D4JZD2pa81I/OBjeU7PtICXV6c9eRGTA5JMDsuIPUZRCyBMYNhg==} - /minipass-pipeline@1.2.4: + minipass-pipeline@1.2.4: resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} engines: {node: '>=8'} - dependencies: - minipass: 3.3.4 - dev: true - /minipass-sized@1.0.3: + minipass-sized@1.0.3: resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} engines: {node: '>=8'} - dependencies: - minipass: 3.3.4 - dev: true - /minipass@3.3.4: - resolution: {integrity: sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==} + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} engines: {node: '>=8'} - dependencies: - yallist: 4.0.0 - dev: true - /minipass@5.0.0: + minipass@5.0.0: resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} engines: {node: '>=8'} - /minizlib@2.1.2: + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@2.1.2: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} engines: {node: '>= 8'} - dependencies: - minipass: 3.3.4 - yallist: 4.0.0 - dev: true - /mkdirp@1.0.4: + mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} hasBin: true - dev: true - /mrmime@1.0.1: + mrmime@1.0.1: resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} engines: {node: '>=10'} - dev: true - /ms@2.0.0: + ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - dev: true - /ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: true - - /ms@2.1.3: + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - dev: true - /multicast-dns@7.2.5: + multicast-dns@7.2.5: resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} hasBin: true - dependencies: - dns-packet: 5.4.0 - thunky: 1.1.0 - dev: true - /mute-stream@0.0.8: + mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} - dev: true - /nanoid@3.3.6: - resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - dev: true - /natural-compare-lite@1.4.0: + natural-compare-lite@1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} - dev: true - /natural-compare@1.4.0: + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - dev: true - /needle@3.2.0: - resolution: {integrity: sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==} + needle@3.3.1: + resolution: {integrity: sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==} engines: {node: '>= 4.4.x'} hasBin: true - requiresBuild: true - dependencies: - debug: 3.2.7 - iconv-lite: 0.6.3 - sax: 1.2.4 - transitivePeerDependencies: - - supports-color - dev: true - optional: true - /negotiator@0.6.3: + negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} - dev: true - /neo-async@2.6.2: + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - dev: true - /nice-napi@1.0.2: + nice-napi@1.0.2: resolution: {integrity: sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==} os: ['!win32'] - requiresBuild: true - dependencies: - node-addon-api: 3.2.1 - node-gyp-build: 4.6.0 - dev: true - optional: true - /node-addon-api@3.2.1: + node-addon-api@3.2.1: resolution: {integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==} - requiresBuild: true - dev: true - optional: true - /node-forge@1.3.1: + node-forge@1.3.1: resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} engines: {node: '>= 6.13.0'} - dev: true - /node-gyp-build@4.6.0: - resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==} + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true - requiresBuild: true - dev: true - optional: true - /node-gyp@9.1.0: - resolution: {integrity: sha512-HkmN0ZpQJU7FLbJauJTHkHlSVAXlNGDAzH/VYFZGDOnFyn/Na3GlNJfkudmufOdS6/jNFhy88ObzL7ERz9es1g==} - engines: {node: ^12.22 || ^14.13 || >=16} + node-gyp@9.4.1: + resolution: {integrity: sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==} + engines: {node: ^12.13 || ^14.13 || >=16} hasBin: true - dependencies: - env-paths: 2.2.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - make-fetch-happen: 10.2.0 - nopt: 5.0.0 - npmlog: 6.0.2 - rimraf: 3.0.2 - semver: 7.5.3 - tar: 6.1.11 - which: 2.0.2 - transitivePeerDependencies: - - bluebird - - supports-color - dev: true - /node-releases@2.0.12: - resolution: {integrity: sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==} - dev: true + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} - /nopt@5.0.0: - resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} - engines: {node: '>=6'} + nopt@6.0.0: + resolution: {integrity: sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} hasBin: true - dependencies: - abbrev: 1.1.1 - dev: true - /normalize-package-data@5.0.0: + normalize-package-data@5.0.0: resolution: {integrity: sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - hosted-git-info: 6.1.1 - is-core-module: 2.12.1 - semver: 7.5.3 - validate-npm-package-license: 3.0.4 - dev: true - /normalize-path@3.0.0: + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - dev: true - /normalize-range@0.1.2: + normalize-range@0.1.2: resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} engines: {node: '>=0.10.0'} - dev: true - /npm-bundled@3.0.0: - resolution: {integrity: sha512-Vq0eyEQy+elFpzsKjMss9kxqb9tG3YHg4dsyWuUENuzvSUWe1TCnW/vV9FkhvBk/brEDoDiVd+M1Btosa6ImdQ==} + npm-bundled@3.0.1: + resolution: {integrity: sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - npm-normalize-package-bin: 3.0.1 - dev: true - /npm-install-checks@6.1.1: - resolution: {integrity: sha512-dH3GmQL4vsPtld59cOn8uY0iOqRmqKvV+DLGwNXV/Q7MDgD2QfOADWd/mFXcIE5LVhYYGjA3baz6W9JneqnuCw==} + npm-install-checks@6.3.0: + resolution: {integrity: sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - semver: 7.5.3 - dev: true - /npm-normalize-package-bin@3.0.1: + npm-normalize-package-bin@3.0.1: resolution: {integrity: sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dev: true - /npm-package-arg@10.1.0: + npm-package-arg@10.1.0: resolution: {integrity: sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - hosted-git-info: 6.1.1 - proc-log: 3.0.0 - semver: 7.5.3 - validate-npm-package-name: 5.0.0 - dev: true - /npm-packlist@7.0.4: + npm-packlist@7.0.4: resolution: {integrity: sha512-d6RGEuRrNS5/N84iglPivjaJPxhDbZmlbTwTDX2IbcRHG5bZCdtysYMhwiPvcF4GisXHGn7xsxv+GQ7T/02M5Q==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - ignore-walk: 6.0.3 - dev: true - /npm-pick-manifest@8.0.1: + npm-pick-manifest@8.0.1: resolution: {integrity: sha512-mRtvlBjTsJvfCCdmPtiu2bdlx8d/KXtF7yNXNWe7G0Z36qWA9Ny5zXsI2PfBZEv7SXgoxTmNaTzGSbbzDZChoA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - npm-install-checks: 6.1.1 - npm-normalize-package-bin: 3.0.1 - npm-package-arg: 10.1.0 - semver: 7.5.3 - dev: true - /npm-registry-fetch@14.0.5: + npm-registry-fetch@14.0.5: resolution: {integrity: sha512-kIDMIo4aBm6xg7jOttupWZamsZRkAqMqwqqbVXnUqstY5+tapvv6bkH/qMR76jdgV+YljEUCyWx3hRYMrJiAgA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - make-fetch-happen: 11.1.1 - minipass: 5.0.0 - minipass-fetch: 3.0.3 - minipass-json-stream: 1.0.1 - minizlib: 2.1.2 - npm-package-arg: 10.1.0 - proc-log: 3.0.0 - transitivePeerDependencies: - - supports-color - dev: true - /npm-run-path@4.0.1: + npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} - dependencies: - path-key: 3.1.1 - dev: true - /npmlog@6.0.2: + npmlog@6.0.2: resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - dependencies: - are-we-there-yet: 3.0.0 - console-control-strings: 1.1.0 - gauge: 4.0.4 - set-blocking: 2.0.0 - dev: true + deprecated: This package is no longer supported. - /nth-check@2.1.1: + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - dependencies: - boolbase: 1.0.0 - dev: true - /object-inspect@1.12.3: - resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} - dev: true - - /object-is@1.1.5: - resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - dev: true + nwsapi@2.2.22: + resolution: {integrity: sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==} - /object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} - dev: true - /object.assign@4.1.4: - resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - has-symbols: 1.0.3 - object-keys: 1.1.1 - dev: true + object-path@0.11.8: + resolution: {integrity: sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA==} + engines: {node: '>= 10.12.0'} - /obuf@1.1.2: + obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} - dev: true - /on-finished@2.4.1: + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} - dependencies: - ee-first: 1.1.1 - dev: true - /on-headers@1.0.2: - resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} + on-headers@1.1.0: + resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} engines: {node: '>= 0.8'} - dev: true - /once@1.4.0: + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - dependencies: - wrappy: 1.0.2 - /onetime@5.1.2: + onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} - dependencies: - mimic-fn: 2.1.0 - /open@8.4.2: + open@8.4.2: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} - dependencies: - define-lazy-prop: 2.0.0 - is-docker: 2.2.1 - is-wsl: 2.2.0 - dev: true - /optionator@0.9.1: - resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.3 - dev: true - /ora@5.4.1: + ora@5.4.1: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} - dependencies: - bl: 4.1.0 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-spinners: 2.6.1 - is-interactive: 1.0.0 - is-unicode-supported: 0.1.0 - log-symbols: 4.1.0 - strip-ansi: 6.0.1 - wcwidth: 1.0.1 - /os-tmpdir@1.0.2: + os-tmpdir@1.0.2: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} - dev: true - /p-limit@2.3.0: + p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} - dependencies: - p-try: 2.2.0 - dev: true - /p-limit@3.1.0: + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} - dependencies: - yocto-queue: 0.1.0 - dev: true - /p-locate@4.1.0: + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} - dependencies: - p-limit: 2.3.0 - dev: true - /p-locate@5.0.0: + p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - dependencies: - p-limit: 3.1.0 - dev: true - /p-map@4.0.0: + p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-map@4.0.0: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} - dependencies: - aggregate-error: 3.1.0 - dev: true - /p-retry@4.6.2: + p-retry@4.6.2: resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} engines: {node: '>=8'} - dependencies: - '@types/retry': 0.12.0 - retry: 0.13.1 - dev: true - /p-try@2.2.0: + p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} - dev: true - /pacote@15.2.0: + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + pacote@15.2.0: resolution: {integrity: sha512-rJVZeIwHTUta23sIZgEIM62WYwbmGbThdbnkt81ravBplQv+HjyroqnLRNH2+sLJHcGZmLRmhPwACqhfTcOmnA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} hasBin: true - dependencies: - '@npmcli/git': 4.1.0 - '@npmcli/installed-package-contents': 2.0.2 - '@npmcli/promise-spawn': 6.0.2 - '@npmcli/run-script': 6.0.2 - cacache: 17.1.3 - fs-minipass: 3.0.2 - minipass: 5.0.0 - npm-package-arg: 10.1.0 - npm-packlist: 7.0.4 - npm-pick-manifest: 8.0.1 - npm-registry-fetch: 14.0.5 - proc-log: 3.0.0 - promise-retry: 2.0.1 - read-package-json: 6.0.4 - read-package-json-fast: 3.0.2 - sigstore: 1.6.0 - ssri: 10.0.4 - tar: 6.1.11 - transitivePeerDependencies: - - bluebird - - supports-color - dev: true - /pako@1.0.11: + pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} - dev: true - /parent-module@1.0.1: + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} - dependencies: - callsites: 3.1.0 - /parse-json@5.2.0: + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} - dependencies: - '@babel/code-frame': 7.22.5 - error-ex: 1.3.2 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - /parse-node-version@1.0.1: + parse-node-version@1.0.1: resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==} engines: {node: '>= 0.10'} - dev: true - /parse5-html-rewriting-stream@7.0.0: + parse5-html-rewriting-stream@7.0.0: resolution: {integrity: sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==} - dependencies: - entities: 4.5.0 - parse5: 7.1.2 - parse5-sax-parser: 7.0.0 - dev: true - /parse5-sax-parser@7.0.0: + parse5-sax-parser@7.0.0: resolution: {integrity: sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==} - dependencies: - parse5: 7.1.2 - dev: true - /parse5@7.1.2: - resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} - dependencies: - entities: 4.5.0 + parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} - /parseurl@1.3.3: + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} - dev: true - /path-exists@4.0.0: + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - dev: true - /path-is-absolute@1.0.1: + path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} - /path-key@3.1.1: + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - /path-parse@1.0.7: + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - dev: true - /path-scurry@1.9.2: - resolution: {integrity: sha512-qSDLy2aGFPm8i4rsbHd4MNyTcrzHFsLQykrtbuGRknZZCBBVXSv2tSCDN2Cg6Rt/GFRw8GoW9y9Ecw5rIPG1sg==} - engines: {node: '>=16 || 14 >=14.17'} - dependencies: - lru-cache: 9.1.2 - minipass: 5.0.0 + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} - /path-to-regexp@0.1.7: - resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} - dev: true + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} - /path-type@4.0.0: + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - /picocolors@1.0.0: - resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - dev: true + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - /picomatch@2.3.1: + picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - dev: true - /pify@4.0.1: + picomatch@4.0.1: + resolution: {integrity: sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==} + engines: {node: '>=12'} + + pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} - dev: true - optional: true - /piscina@3.2.0: - resolution: {integrity: sha512-yn/jMdHRw+q2ZJhFhyqsmANcbF6V2QwmD84c6xRau+QpQOmtrBCoRGdvTfeuFDYXB5W2m6MfLkjkvQa9lUSmIA==} - dependencies: - eventemitter-asyncresource: 1.0.0 - hdr-histogram-js: 2.0.3 - hdr-histogram-percentiles-obj: 3.0.0 - optionalDependencies: - nice-napi: 1.0.2 - dev: true + piscina@4.0.0: + resolution: {integrity: sha512-641nAmJS4k4iqpNUqfggqUBUMmlw0ZoM5VZKdQkV2e970Inn3Tk9kroCc1wpsYLD07vCwpys5iY0d3xI/9WkTg==} - /pkg-dir@4.2.0: - resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} - engines: {node: '>=8'} - dependencies: - find-up: 4.1.0 - dev: true + pkg-dir@7.0.0: + resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==} + engines: {node: '>=14.16'} + + playwright-core@1.56.1: + resolution: {integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==} + engines: {node: '>=18'} + hasBin: true - /playwright-core@1.35.1: - resolution: {integrity: sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==} - engines: {node: '>=16'} + playwright@1.56.1: + resolution: {integrity: sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==} + engines: {node: '>=18'} hasBin: true - dev: true - /postcss-loader@7.3.2(postcss@8.4.24)(webpack@5.86.0): - resolution: {integrity: sha512-c7qDlXErX6n0VT+LUsW+nwefVtTu3ORtVvK8EXuUIDcxo+b/euYqpuHlJAvePb0Af5e8uMjR/13e0lTuYifaig==} + postcss-loader@7.3.3: + resolution: {integrity: sha512-YgO/yhtevGO/vJePCQmTxiaEwER94LABZN0ZMT4A0vsak9TpO+RvKRs7EmJ8peIlB9xfXCsS7M8LjqncsUZ5HA==} engines: {node: '>= 14.15.0'} peerDependencies: postcss: ^7.0.0 || ^8.0.1 webpack: ^5.0.0 - dependencies: - cosmiconfig: 8.2.0 - jiti: 1.18.2 - klona: 2.0.6 - postcss: 8.4.24 - semver: 7.5.3 - webpack: 5.86.0(esbuild@0.17.19) - dev: true - /postcss-modules-extract-imports@3.0.0(postcss@8.4.24): - resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} + postcss-modules-extract-imports@3.1.0: + resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 - dependencies: - postcss: 8.4.24 - dev: true - /postcss-modules-local-by-default@4.0.3(postcss@8.4.24): - resolution: {integrity: sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==} + postcss-modules-local-by-default@4.2.0: + resolution: {integrity: sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 - dependencies: - icss-utils: 5.1.0(postcss@8.4.24) - postcss: 8.4.24 - postcss-selector-parser: 6.0.10 - postcss-value-parser: 4.2.0 - dev: true - /postcss-modules-scope@3.0.0(postcss@8.4.24): - resolution: {integrity: sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==} + postcss-modules-scope@3.2.1: + resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 - dependencies: - postcss: 8.4.24 - postcss-selector-parser: 6.0.10 - dev: true - /postcss-modules-values@4.0.0(postcss@8.4.24): + postcss-modules-values@4.0.0: resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 - dependencies: - icss-utils: 5.1.0(postcss@8.4.24) - postcss: 8.4.24 - dev: true - /postcss-selector-parser@6.0.10: - resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + postcss-selector-parser@7.1.0: + resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} engines: {node: '>=4'} - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - dev: true - /postcss-value-parser@4.2.0: + postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - dev: true - /postcss@8.4.24: - resolution: {integrity: sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==} + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - dependencies: - nanoid: 3.3.6 - picocolors: 1.0.0 - source-map-js: 1.0.2 - dev: true - /prelude-ls@1.2.1: + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - dev: true - /prettier@2.8.8: + prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} hasBin: true - dev: true - /pretty-bytes@5.6.0: + pretty-bytes@5.6.0: resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} engines: {node: '>=6'} - dev: true - /proc-log@3.0.0: + proc-log@3.0.0: resolution: {integrity: sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dev: true - /process-nextick-args@2.0.1: + process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - dev: true - /promise-inflight@1.0.1: + promise-inflight@1.0.1: resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} peerDependencies: bluebird: '*' peerDependenciesMeta: bluebird: optional: true - dev: true - /promise-retry@2.0.1: + promise-retry@2.0.1: resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} engines: {node: '>=10'} - dependencies: - err-code: 2.0.3 - retry: 0.12.0 - dev: true - /prompts@2.4.2: + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} - dependencies: - kleur: 3.0.3 - sisteransi: 1.0.5 - dev: true - /proxy-addr@2.0.7: + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - dev: true - /prr@1.0.1: + prr@1.0.1: resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} - dev: true - optional: true - /punycode@2.3.0: - resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - dev: true - /qs@6.10.3: - resolution: {integrity: sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==} + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} - dependencies: - side-channel: 1.0.4 - dev: true - /queue-microtask@1.2.3: + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - dev: true - /randombytes@2.1.0: + randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - dependencies: - safe-buffer: 5.2.1 - dev: true - /range-parser@1.2.1: + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - dev: true - /raw-body@2.5.1: - resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} - dependencies: - bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - unpipe: 1.0.0 - dev: true - /read-package-json-fast@3.0.2: + read-package-json-fast@3.0.2: resolution: {integrity: sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - json-parse-even-better-errors: 3.0.0 - npm-normalize-package-bin: 3.0.1 - dev: true - /read-package-json@6.0.4: + read-package-json@6.0.4: resolution: {integrity: sha512-AEtWXYfopBj2z5N5PbkAOeNHRPUg5q+Nen7QLxV8M2zJq1ym6/lCz3fYNTCXe19puu2d06jfHhrP7v/S2PtMMw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - glob: 10.3.0 - json-parse-even-better-errors: 3.0.0 - normalize-package-data: 5.0.0 - npm-normalize-package-bin: 3.0.1 - dev: true + deprecated: This package is no longer supported. Please use @npmcli/package-json instead. - /readable-stream@2.3.7: - resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==} - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 1.0.0 - process-nextick-args: 2.0.1 - safe-buffer: 5.1.2 - string_decoder: 1.1.1 - util-deprecate: 1.0.2 - dev: true + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} - /readable-stream@3.6.0: - resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==} + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - /readdirp@3.6.0: + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} - dependencies: - picomatch: 2.3.1 - dev: true - /reflect-metadata@0.1.13: - resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==} - dev: true + reflect-metadata@0.1.14: + resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==} - /regenerate-unicode-properties@10.1.0: - resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==} + regenerate-unicode-properties@10.2.2: + resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==} engines: {node: '>=4'} - dependencies: - regenerate: 1.4.2 - dev: true - /regenerate@1.4.2: + regenerate@1.4.2: resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} - dev: true - /regenerator-runtime@0.13.11: + regenerator-runtime@0.13.11: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} - dev: true - - /regenerator-transform@0.15.1: - resolution: {integrity: sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==} - dependencies: - '@babel/runtime': 7.22.5 - dev: true - - /regex-parser@2.2.11: - resolution: {integrity: sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==} - dev: true - /regexp.prototype.flags@1.5.0: - resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - functions-have-names: 1.2.3 - dev: true + regex-parser@2.3.1: + resolution: {integrity: sha512-yXLRqatcCuKtVHsWrNg0JL3l1zGfdXeEvDa0bdu4tCDQw0RpMDZsqbkyRTUnKMR0tXF627V2oEWjBEaEdqTwtQ==} - /regexpu-core@5.3.2: - resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} + regexpu-core@6.4.0: + resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==} engines: {node: '>=4'} - dependencies: - '@babel/regjsgen': 0.8.0 - regenerate: 1.4.2 - regenerate-unicode-properties: 10.1.0 - regjsparser: 0.9.1 - unicode-match-property-ecmascript: 2.0.0 - unicode-match-property-value-ecmascript: 2.1.0 - dev: true - /regjsparser@0.9.1: - resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} + regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + + regjsparser@0.13.0: + resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==} hasBin: true - dependencies: - jsesc: 0.5.0 - dev: true - /replace-in-file@6.3.5: + replace-in-file@6.3.5: resolution: {integrity: sha512-arB9d3ENdKva2fxRnSjwBEXfK1npgyci7ZZuwysgAp7ORjHSyxz6oqIjTEv8R0Ydl4Ll7uOAZXL4vbkhGIizCg==} engines: {node: '>=10'} hasBin: true - dependencies: - chalk: 4.1.2 - glob: 7.2.3 - yargs: 17.7.2 - dev: false - /require-directory@2.1.1: + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} - /require-from-string@2.0.2: + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} - dev: true - /requires-port@1.0.0: + requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - dev: true - /resolve-from@4.0.0: + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} - /resolve-from@5.0.0: + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - dev: true - /resolve-url-loader@5.0.0: + resolve-url-loader@5.0.0: resolution: {integrity: sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==} engines: {node: '>=12'} - dependencies: - adjust-sourcemap-loader: 4.0.0 - convert-source-map: 1.8.0 - loader-utils: 2.0.2 - postcss: 8.4.24 - source-map: 0.6.1 - dev: true - /resolve@1.22.2: + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + resolve@1.22.2: resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==} hasBin: true - dependencies: - is-core-module: 2.12.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - dev: true - /restore-cursor@3.1.0: + restore-cursor@3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} - dependencies: - onetime: 5.1.2 - signal-exit: 3.0.7 - /retry@0.12.0: + retry@0.12.0: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} - dev: true - /retry@0.13.1: + retry@0.13.1: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} - dev: true - /reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - dev: true - /rimraf@3.0.2: + rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - dependencies: - glob: 7.2.3 - dev: true - /rollup@3.25.1: - resolution: {integrity: sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==} + rollup@3.29.5: + resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true - optionalDependencies: - fsevents: 2.3.2 - dev: true - /run-async@2.4.1: + run-async@2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} - dev: true - /run-parallel@1.2.0: + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - dependencies: - queue-microtask: 1.2.3 - dev: true - /rxjs@7.8.1: + rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} - dependencies: - tslib: 2.5.3 - /safe-buffer@5.1.2: + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - dev: true - /safe-buffer@5.2.1: + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - /safer-buffer@2.1.2: + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - dev: true - /sass-loader@13.3.1(sass@1.63.2)(webpack@5.86.0): - resolution: {integrity: sha512-cBTxmgyVA1nXPvIK4brjJMXOMJ2v2YrQEuHqLw3LylGb3gsR6jAvdjHMcy/+JGTmmIF9SauTrLLR7bsWDMWqgg==} + sass-loader@13.3.2: + resolution: {integrity: sha512-CQbKl57kdEv+KDLquhC+gE3pXt74LEAzm+tzywcA0/aHZuub8wTErbjAoNI57rPUWRYRNC5WUnNl8eGJNbDdwg==} engines: {node: '>= 14.15.0'} peerDependencies: fibers: '>= 3.1.0' @@ -6692,474 +3650,264 @@ packages: optional: true sass-embedded: optional: true - dependencies: - klona: 2.0.6 - neo-async: 2.6.2 - sass: 1.63.2 - webpack: 5.86.0(esbuild@0.17.19) - dev: true - /sass@1.63.2: - resolution: {integrity: sha512-u56TU0AIFqMtauKl/OJ1AeFsXqRHkgO7nCWmHaDwfxDo9GUMSqBA4NEh6GMuh1CYVM7zuROYtZrHzPc2ixK+ww==} + sass@1.64.1: + resolution: {integrity: sha512-16rRACSOFEE8VN7SCgBu1MpYCyN7urj9At898tyzdXFhC+a+yOX5dXwAR7L8/IdPJ1NB8OYoXmD55DM30B2kEQ==} engines: {node: '>=14.0.0'} hasBin: true - dependencies: - chokidar: 3.5.3 - immutable: 4.1.0 - source-map-js: 1.0.2 - dev: true - /sax@1.2.4: - resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} - dev: true - optional: true + sax@1.4.3: + resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==} + + saxes@5.0.1: + resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==} + engines: {node: '>=10'} - /schema-utils@3.3.0: + schema-utils@3.3.0: resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} engines: {node: '>= 10.13.0'} - dependencies: - '@types/json-schema': 7.0.12 - ajv: 6.12.6 - ajv-keywords: 3.5.2(ajv@6.12.6) - dev: true - /schema-utils@4.0.0: - resolution: {integrity: sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==} - engines: {node: '>= 12.13.0'} - dependencies: - '@types/json-schema': 7.0.12 - ajv: 8.12.0 - ajv-formats: 2.1.1(ajv@8.12.0) - ajv-keywords: 5.1.0(ajv@8.12.0) - dev: true + schema-utils@4.3.3: + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} + engines: {node: '>= 10.13.0'} - /select-hose@2.0.0: + select-hose@2.0.0: resolution: {integrity: sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==} - dev: true - /selfsigned@2.1.1: - resolution: {integrity: sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==} + selfsigned@2.4.1: + resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} engines: {node: '>=10'} - dependencies: - node-forge: 1.3.1 - dev: true - - /semver@5.7.1: - resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} - hasBin: true - dev: true - optional: true - /semver@6.3.0: - resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true - dev: true - /semver@7.3.8: - resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==} - engines: {node: '>=10'} + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - dependencies: - lru-cache: 6.0.0 - dev: true - /semver@7.5.1: - resolution: {integrity: sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==} + semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} engines: {node: '>=10'} hasBin: true - dependencies: - lru-cache: 6.0.0 - dev: true - /semver@7.5.3: - resolution: {integrity: sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==} + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} hasBin: true - dependencies: - lru-cache: 6.0.0 - dev: true - /send@0.18.0: - resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} - dependencies: - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - encodeurl: 1.0.2 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 0.5.2 - http-errors: 2.0.0 - mime: 1.6.0 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.1 - transitivePeerDependencies: - - supports-color - dev: true - - /serialize-javascript@6.0.0: - resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==} - dependencies: - randombytes: 2.1.0 - dev: true - /serialize-javascript@6.0.1: - resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==} - dependencies: - randombytes: 2.1.0 - dev: true + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - /serve-index@1.9.1: + serve-index@1.9.1: resolution: {integrity: sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==} engines: {node: '>= 0.8.0'} - dependencies: - accepts: 1.3.8 - batch: 0.6.1 - debug: 2.6.9 - escape-html: 1.0.3 - http-errors: 1.6.3 - mime-types: 2.1.35 - parseurl: 1.3.3 - transitivePeerDependencies: - - supports-color - dev: true - /serve-static@1.15.0: - resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} - dependencies: - encodeurl: 1.0.2 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 0.18.0 - transitivePeerDependencies: - - supports-color - dev: true - /set-blocking@2.0.0: + set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - dev: true - /setprototypeof@1.1.0: + setprototypeof@1.1.0: resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} - dev: true - /setprototypeof@1.2.0: + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - dev: true - /shallow-clone@3.0.1: + shallow-clone@3.0.1: resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} engines: {node: '>=8'} - dependencies: - kind-of: 6.0.3 - dev: true - /shebang-command@2.0.0: + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} - dependencies: - shebang-regex: 3.0.0 - /shebang-regex@3.0.0: + shebang-regex@3.0.0: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - /shell-quote@1.8.1: - resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} - dev: true + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} - /side-channel@1.0.4: - resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} - dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 - object-inspect: 1.12.3 - dev: true + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} - /signal-exit@3.0.7: + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - /signal-exit@4.0.2: - resolution: {integrity: sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - /sigstore@1.6.0: - resolution: {integrity: sha512-QODKff/qW/TXOZI6V/Clqu74xnInAS6it05mufj4/fSewexLtfEntgLZZcBtUK44CDQyUE5TUXYy1ARYzlfG9g==} + sigstore@1.9.0: + resolution: {integrity: sha512-0Zjz0oe37d08VeOtBIuB6cRriqXse2e8w+7yIy2XSXjshRKxbc2KkhXjL229jXSxEm7UbcjS76wcJDGQddVI9A==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} hasBin: true - dependencies: - '@sigstore/protobuf-specs': 0.1.0 - '@sigstore/tuf': 1.0.0 - make-fetch-happen: 11.1.1 - tuf-js: 1.1.7 - transitivePeerDependencies: - - supports-color - dev: true - /sisteransi@1.0.5: + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - dev: true - /slash@3.0.0: + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - dev: true - /slash@4.0.0: + slash@4.0.0: resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} engines: {node: '>=12'} - dev: true - /smart-buffer@4.2.0: + smart-buffer@4.2.0: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} - dev: true - /sockjs@0.3.24: + sockjs@0.3.24: resolution: {integrity: sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==} - dependencies: - faye-websocket: 0.11.4 - uuid: 8.3.2 - websocket-driver: 0.7.4 - dev: true - /socks-proxy-agent@7.0.0: + socks-proxy-agent@7.0.0: resolution: {integrity: sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==} engines: {node: '>= 10'} - dependencies: - agent-base: 6.0.2 - debug: 4.3.4 - socks: 2.7.0 - transitivePeerDependencies: - - supports-color - dev: true - /socks@2.7.0: - resolution: {integrity: sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA==} - engines: {node: '>= 10.13.0', npm: '>= 3.0.0'} - dependencies: - ip: 2.0.0 - smart-buffer: 4.2.0 - dev: true + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} - /source-map-js@1.0.2: - resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - dev: true - /source-map-loader@4.0.1(webpack@5.86.0): + source-map-loader@4.0.1: resolution: {integrity: sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA==} engines: {node: '>= 14.15.0'} peerDependencies: webpack: ^5.72.1 - dependencies: - abab: 2.0.6 - iconv-lite: 0.6.3 - source-map-js: 1.0.2 - webpack: 5.86.0(esbuild@0.17.19) - dev: true - /source-map-support@0.5.21: + source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - dev: true - /source-map@0.6.1: + source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - dev: true - /source-map@0.7.4: + source-map@0.7.4: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} engines: {node: '>= 8'} - dev: true - /spdx-correct@3.1.1: - resolution: {integrity: sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==} - dependencies: - spdx-expression-parse: 3.0.1 - spdx-license-ids: 3.0.11 - dev: true + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} - /spdx-exceptions@2.3.0: - resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==} - dev: true + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} - /spdx-expression-parse@3.0.1: + spdx-expression-parse@3.0.1: resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} - dependencies: - spdx-exceptions: 2.3.0 - spdx-license-ids: 3.0.11 - dev: true - /spdx-license-ids@3.0.11: - resolution: {integrity: sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==} - dev: true + spdx-license-ids@3.0.22: + resolution: {integrity: sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==} - /spdy-transport@3.0.0: + spdy-transport@3.0.0: resolution: {integrity: sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==} - dependencies: - debug: 4.3.4 - detect-node: 2.1.0 - hpack.js: 2.1.6 - obuf: 1.1.2 - readable-stream: 3.6.0 - wbuf: 1.7.3 - transitivePeerDependencies: - - supports-color - dev: true - /spdy@4.0.2: + spdy@4.0.2: resolution: {integrity: sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==} engines: {node: '>=6.0.0'} - dependencies: - debug: 4.3.4 - handle-thing: 2.0.1 - http-deceiver: 1.2.7 - select-hose: 2.0.0 - spdy-transport: 3.0.0 - transitivePeerDependencies: - - supports-color - dev: true - /sprintf-js@1.0.3: + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - dev: true - /ssri@10.0.4: - resolution: {integrity: sha512-12+IR2CB2C28MMAw0Ncqwj5QbTcs0nGIhgJzYWzDkb21vWmfNI83KS4f3Ci6GI98WreIfG7o9UXp3C0qbpA8nQ==} + ssri@10.0.6: + resolution: {integrity: sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - minipass: 5.0.0 - dev: true - /ssri@9.0.1: + ssri@9.0.1: resolution: {integrity: sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - dependencies: - minipass: 3.3.4 - dev: true - /statuses@1.5.0: + statuses@1.5.0: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} - dev: true - /statuses@2.0.1: + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} - dev: true - - /stop-iteration-iterator@1.0.0: - resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} - engines: {node: '>= 0.4'} - dependencies: - internal-slot: 1.0.5 - dev: true - /string-width@4.2.3: + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - /string-width@5.1.2: + string-width@5.1.2: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.0 - /string_decoder@1.1.1: + string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} - dependencies: - safe-buffer: 5.1.2 - dev: true - /string_decoder@1.3.0: + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - dependencies: - safe-buffer: 5.2.1 - /strip-ansi@6.0.1: + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - dependencies: - ansi-regex: 5.0.1 - /strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} - dependencies: - ansi-regex: 6.0.1 - /strip-final-newline@2.0.0: + strip-final-newline@2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} - dev: true - /strip-json-comments@3.1.1: + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - dev: true - - /supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} - dependencies: - has-flag: 3.0.0 - /supports-color@7.2.0: + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - dependencies: - has-flag: 4.0.0 - /supports-color@8.1.1: + supports-color@8.1.1: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} engines: {node: '>=10'} - dependencies: - has-flag: 4.0.0 - dev: true - /supports-preserve-symlinks-flag@1.0.0: + supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - dev: true - /symbol-observable@4.0.0: + symbol-observable@4.0.0: resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} engines: {node: '>=0.10'} - dev: true - /tapable@2.2.1: - resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} - dev: true - /tar@6.1.11: - resolution: {integrity: sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==} - engines: {node: '>= 10'} - dependencies: - chownr: 2.0.0 - fs-minipass: 2.1.0 - minipass: 3.3.4 - minizlib: 2.1.2 - mkdirp: 1.0.4 - yallist: 4.0.0 - dev: true + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} - /terser-webpack-plugin@5.3.9(esbuild@0.17.19)(webpack@5.86.0): - resolution: {integrity: sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==} + terser-webpack-plugin@5.3.14: + resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==} engines: {node: '>= 10.13.0'} peerDependencies: '@swc/core': '*' @@ -7173,79 +3921,56 @@ packages: optional: true uglify-js: optional: true - dependencies: - '@jridgewell/trace-mapping': 0.3.18 - esbuild: 0.17.19 - jest-worker: 27.5.1 - schema-utils: 3.3.0 - serialize-javascript: 6.0.1 - terser: 5.17.7 - webpack: 5.86.0(esbuild@0.17.19) - dev: true - /terser@5.17.7: - resolution: {integrity: sha512-/bi0Zm2C6VAexlGgLlVxA0P2lru/sdLyfCVaRMfKVo9nWxbmz7f/sD8VPybPeSUJaJcwmCJis9pBIhcVcG1QcQ==} + terser@5.19.2: + resolution: {integrity: sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==} + engines: {node: '>=10'} + hasBin: true + + terser@5.44.1: + resolution: {integrity: sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==} engines: {node: '>=10'} hasBin: true - dependencies: - '@jridgewell/source-map': 0.3.3 - acorn: 8.9.0 - commander: 2.20.3 - source-map-support: 0.5.21 - dev: true - /test-exclude@6.0.0: + test-exclude@6.0.0: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} - dependencies: - '@istanbuljs/schema': 0.1.3 - glob: 7.2.3 - minimatch: 3.1.2 - dev: true - /text-table@0.2.0: + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - dev: true - /through@2.3.8: + through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - dev: true - /thunky@1.1.0: + thunky@1.1.0: resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} - dev: true - /tmp@0.0.33: + tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} - dependencies: - os-tmpdir: 1.0.2 - dev: true - - /to-fast-properties@2.0.0: - resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} - engines: {node: '>=4'} - dev: true - /to-regex-range@5.0.1: + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - dependencies: - is-number: 7.0.0 - dev: true - /toidentifier@1.0.1: + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} - dev: true - /tree-kill@1.2.2: + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tr46@2.1.0: + resolution: {integrity: sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==} + engines: {node: '>=8'} + + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - dev: true - /ts-node@10.9.1(@types/node@18.0.0)(typescript@5.1.3): - resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true peerDependencies: '@swc/core': '>=1.2.50' @@ -7257,209 +3982,145 @@ packages: optional: true '@swc/wasm': optional: true - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.9 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.3 - '@types/node': 18.0.0 - acorn: 8.8.0 - acorn-walk: 8.2.0 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - typescript: 5.1.3 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - dev: true - /tslib@1.14.1: + tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} - dev: true - /tslib@2.5.3: - resolution: {integrity: sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==} + tslib@2.6.1: + resolution: {integrity: sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==} - /tsutils@3.21.0(typescript@5.1.3): + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsutils@3.21.0: resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' - dependencies: - tslib: 1.14.1 - typescript: 5.1.3 - dev: true - /tuf-js@1.1.7: + tuf-js@1.1.7: resolution: {integrity: sha512-i3P9Kgw3ytjELUfpuKVDNBJvk4u5bXL6gskv572mcevPbSKCV3zt3djhmlEQ65yERjIbOSncy7U4cQJaB1CBCg==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - '@tufjs/models': 1.0.4 - debug: 4.3.4 - make-fetch-happen: 11.1.1 - transitivePeerDependencies: - - supports-color - dev: true - /typanion@3.12.1: - resolution: {integrity: sha512-3SJF/czpzqq6G3lprGFLa6ps12yb1uQ1EmitNnep2fDMNh1aO/Zbq9sWY+3lem0zYb2oHJnQWyabTGUZ+L1ScQ==} - dev: true + typanion@3.14.0: + resolution: {integrity: sha512-ZW/lVMRabETuYCd9O9ZvMhAh8GslSqaUjxmK/JLPCh6l73CvLBiuXswj/+7LdnWOgYsQ130FqLzFz5aGT4I3Ug==} - /type-check@0.4.0: + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - dependencies: - prelude-ls: 1.2.1 - dev: true - /type-fest@0.20.2: + type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} - dev: true - /type-fest@0.21.3: + type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} - dev: true - /type-is@1.6.18: + type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} - dependencies: - media-typer: 0.3.0 - mime-types: 2.1.35 - dev: true - /typed-assert@1.0.9: + typed-assert@1.0.9: resolution: {integrity: sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==} - dev: true - /typescript@5.1.3: - resolution: {integrity: sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==} + typescript@5.1.6: + resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} engines: {node: '>=14.17'} hasBin: true - dev: true - /unicode-canonical-property-names-ecmascript@2.0.0: - resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + unicode-canonical-property-names-ecmascript@2.0.1: + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} engines: {node: '>=4'} - dev: true - /unicode-match-property-ecmascript@2.0.0: + unicode-match-property-ecmascript@2.0.0: resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} engines: {node: '>=4'} - dependencies: - unicode-canonical-property-names-ecmascript: 2.0.0 - unicode-property-aliases-ecmascript: 2.0.0 - dev: true - /unicode-match-property-value-ecmascript@2.1.0: - resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==} + unicode-match-property-value-ecmascript@2.2.1: + resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==} engines: {node: '>=4'} - dev: true - /unicode-property-aliases-ecmascript@2.0.0: - resolution: {integrity: sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ==} + unicode-property-aliases-ecmascript@2.2.0: + resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} engines: {node: '>=4'} - dev: true - /unique-filename@1.1.1: - resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==} - dependencies: - unique-slug: 2.0.2 - dev: true + unique-filename@2.0.1: + resolution: {integrity: sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - /unique-filename@3.0.0: + unique-filename@3.0.0: resolution: {integrity: sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - unique-slug: 4.0.0 - dev: true - /unique-slug@2.0.2: - resolution: {integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==} - dependencies: - imurmurhash: 0.1.4 - dev: true + unique-slug@3.0.0: + resolution: {integrity: sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - /unique-slug@4.0.0: + unique-slug@4.0.0: resolution: {integrity: sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - imurmurhash: 0.1.4 - dev: true - /universalify@2.0.0: - resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} - /unpipe@1.0.0: + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - dev: true - /update-browserslist-db@1.0.11(browserslist@4.21.5): - resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} + update-browserslist-db@1.1.4: + resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' - dependencies: - browserslist: 4.21.5 - escalade: 3.1.1 - picocolors: 1.0.0 - dev: true - /uri-js@4.4.1: + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - dependencies: - punycode: 2.3.0 - dev: true - /util-deprecate@1.0.2: + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - /utils-merge@1.0.1: + utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} - dev: true - /uuid@8.3.2: + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true - dev: true - /v8-compile-cache-lib@3.0.1: + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - dev: true - /validate-npm-package-license@3.0.4: + validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} - dependencies: - spdx-correct: 3.1.1 - spdx-expression-parse: 3.0.1 - dev: true - /validate-npm-package-name@5.0.0: - resolution: {integrity: sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==} + validate-npm-package-name@5.0.1: + resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - builtins: 5.0.1 - dev: true - /vary@1.1.2: + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - dev: true - /vite@4.3.9(@types/node@18.0.0)(less@4.1.3)(sass@1.63.2)(terser@5.17.7): - resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==} + vite@4.5.5: + resolution: {integrity: sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true peerDependencies: '@types/node': '>= 14' less: '*' + lightningcss: ^1.21.0 sass: '*' stylus: '*' sugarss: '*' @@ -7469,6 +4130,8 @@ packages: optional: true less: optional: true + lightningcss: + optional: true sass: optional: true stylus: @@ -7477,70 +4140,50 @@ packages: optional: true terser: optional: true - dependencies: - '@types/node': 18.0.0 - esbuild: 0.17.19 - less: 4.1.3 - postcss: 8.4.24 - rollup: 3.25.1 - sass: 1.63.2 - terser: 5.17.7 - optionalDependencies: - fsevents: 2.3.2 - dev: true - /watchpack@2.4.0: - resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} + w3c-hr-time@1.0.2: + resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} + deprecated: Use your platform's native performance.now() and performance.timeOrigin. + + w3c-xmlserializer@2.0.0: + resolution: {integrity: sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==} + engines: {node: '>=10'} + + watchpack@2.4.4: + resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} engines: {node: '>=10.13.0'} - dependencies: - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - dev: true - /wbuf@1.7.3: + wbuf@1.7.3: resolution: {integrity: sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==} - dependencies: - minimalistic-assert: 1.0.1 - dev: true - /wcwidth@1.0.1: + wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} - dependencies: - defaults: 1.0.3 - /webpack-dev-middleware@5.3.3(webpack@5.86.0): - resolution: {integrity: sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==} + webidl-conversions@5.0.0: + resolution: {integrity: sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==} + engines: {node: '>=8'} + + webidl-conversions@6.1.0: + resolution: {integrity: sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==} + engines: {node: '>=10.4'} + + webpack-dev-middleware@5.3.4: + resolution: {integrity: sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==} engines: {node: '>= 12.13.0'} peerDependencies: webpack: ^4.0.0 || ^5.0.0 - dependencies: - colorette: 2.0.19 - memfs: 3.5.3 - mime-types: 2.1.35 - range-parser: 1.2.1 - schema-utils: 4.0.0 - webpack: 5.86.0(esbuild@0.17.19) - dev: true - /webpack-dev-middleware@6.1.1(webpack@5.86.0): - resolution: {integrity: sha512-y51HrHaFeeWir0YO4f0g+9GwZawuigzcAdRNon6jErXy/SqV/+O6eaVAzDqE6t3e3NpGeR5CS+cCDaTC+V3yEQ==} + webpack-dev-middleware@6.1.2: + resolution: {integrity: sha512-Wu+EHmX326YPYUpQLKmKbTyZZJIB8/n6R09pTmB03kJmnMsVPTo9COzHZFr01txwaCAuZvfBJE4ZCHRcKs5JaQ==} engines: {node: '>= 14.15.0'} peerDependencies: webpack: ^5.0.0 peerDependenciesMeta: webpack: optional: true - dependencies: - colorette: 2.0.19 - memfs: 3.5.3 - mime-types: 2.1.35 - range-parser: 1.2.1 - schema-utils: 4.0.0 - webpack: 5.86.0(esbuild@0.17.19) - dev: true - /webpack-dev-server@4.15.0(webpack@5.86.0): - resolution: {integrity: sha512-HmNB5QeSl1KpulTBQ8UT4FPrByYyaLxpJoQ0+s7EvUrMc16m0ZS1sgb1XGqzmgCPk0c9y+aaXxn11tbLzuM7NQ==} + webpack-dev-server@4.15.1: + resolution: {integrity: sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==} engines: {node: '>= 12.13.0'} hasBin: true peerDependencies: @@ -7551,59 +4194,16 @@ packages: optional: true webpack-cli: optional: true - dependencies: - '@types/bonjour': 3.5.10 - '@types/connect-history-api-fallback': 1.3.5 - '@types/express': 4.17.13 - '@types/serve-index': 1.9.1 - '@types/serve-static': 1.13.10 - '@types/sockjs': 0.3.33 - '@types/ws': 8.5.3 - ansi-html-community: 0.0.8 - bonjour-service: 1.0.13 - chokidar: 3.5.3 - colorette: 2.0.19 - compression: 1.7.4 - connect-history-api-fallback: 2.0.0 - default-gateway: 6.0.3 - express: 4.18.1 - graceful-fs: 4.2.11 - html-entities: 2.3.3 - http-proxy-middleware: 2.0.6(@types/express@4.17.13) - ipaddr.js: 2.0.1 - launch-editor: 2.6.0 - open: 8.4.2 - p-retry: 4.6.2 - rimraf: 3.0.2 - schema-utils: 4.0.0 - selfsigned: 2.1.1 - serve-index: 1.9.1 - sockjs: 0.3.24 - spdy: 4.0.2 - webpack: 5.86.0(esbuild@0.17.19) - webpack-dev-middleware: 5.3.3(webpack@5.86.0) - ws: 8.13.0 - transitivePeerDependencies: - - bufferutil - - debug - - supports-color - - utf-8-validate - dev: true - /webpack-merge@5.9.0: + webpack-merge@5.9.0: resolution: {integrity: sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==} engines: {node: '>=10.0.0'} - dependencies: - clone-deep: 4.0.1 - wildcard: 2.0.0 - dev: true - /webpack-sources@3.2.3: - resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + webpack-sources@3.3.3: + resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} engines: {node: '>=10.13.0'} - dev: true - /webpack-subresource-integrity@5.1.0(webpack@5.86.0): + webpack-subresource-integrity@5.1.0: resolution: {integrity: sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==} engines: {node: '>= 12'} peerDependencies: @@ -7612,13 +4212,9 @@ packages: peerDependenciesMeta: html-webpack-plugin: optional: true - dependencies: - typed-assert: 1.0.9 - webpack: 5.86.0(esbuild@0.17.19) - dev: true - /webpack@5.86.0(esbuild@0.17.19): - resolution: {integrity: sha512-3BOvworZ8SO/D4GVP+GoRC3fVeg5MO4vzmq8TJJEkdmopxyazGDxN8ClqN12uzrZW9Tv8EED8v5VSb6Sqyi0pg==} + webpack@5.94.0: + resolution: {integrity: sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==} engines: {node: '>=10.13.0'} hasBin: true peerDependencies: @@ -7626,138 +4222,74 @@ packages: peerDependenciesMeta: webpack-cli: optional: true - dependencies: - '@types/eslint-scope': 3.7.4 - '@types/estree': 1.0.1 - '@webassemblyjs/ast': 1.11.6 - '@webassemblyjs/wasm-edit': 1.11.6 - '@webassemblyjs/wasm-parser': 1.11.6 - acorn: 8.9.0 - acorn-import-assertions: 1.9.0(acorn@8.9.0) - browserslist: 4.21.5 - chrome-trace-event: 1.0.3 - enhanced-resolve: 5.15.0 - es-module-lexer: 1.3.0 - eslint-scope: 5.1.1 - events: 3.3.0 - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.0 - mime-types: 2.1.35 - neo-async: 2.6.2 - schema-utils: 3.3.0 - tapable: 2.2.1 - terser-webpack-plugin: 5.3.9(esbuild@0.17.19)(webpack@5.86.0) - watchpack: 2.4.0 - webpack-sources: 3.2.3 - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - dev: true - /websocket-driver@0.7.4: + websocket-driver@0.7.4: resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} engines: {node: '>=0.8.0'} - dependencies: - http-parser-js: 0.5.8 - safe-buffer: 5.2.1 - websocket-extensions: 0.1.4 - dev: true - /websocket-extensions@0.1.4: + websocket-extensions@0.1.4: resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} engines: {node: '>=0.8.0'} - dev: true - /which-boxed-primitive@1.0.2: - resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} - dependencies: - is-bigint: 1.0.4 - is-boolean-object: 1.1.2 - is-number-object: 1.0.7 - is-string: 1.0.7 - is-symbol: 1.0.4 - dev: true + whatwg-encoding@1.0.5: + resolution: {integrity: sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==} - /which-collection@1.0.1: - resolution: {integrity: sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==} - dependencies: - is-map: 2.0.2 - is-set: 2.0.2 - is-weakmap: 2.0.1 - is-weakset: 2.0.2 - dev: true + whatwg-mimetype@2.3.0: + resolution: {integrity: sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==} - /which-pm-runs@1.1.0: + whatwg-url@8.7.0: + resolution: {integrity: sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==} + engines: {node: '>=10'} + + which-pm-runs@1.1.0: resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} engines: {node: '>=4'} - dev: true - - /which-typed-array@1.1.9: - resolution: {integrity: sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==} - engines: {node: '>= 0.4'} - dependencies: - available-typed-arrays: 1.0.5 - call-bind: 1.0.2 - for-each: 0.3.3 - gopd: 1.0.1 - has-tostringtag: 1.0.0 - is-typed-array: 1.1.10 - dev: true - /which@2.0.2: + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} hasBin: true - dependencies: - isexe: 2.0.0 - /which@3.0.1: + which@3.0.1: resolution: {integrity: sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} hasBin: true - dependencies: - isexe: 2.0.0 - dev: true - /wide-align@1.1.5: + wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} - dependencies: - string-width: 4.2.3 - dev: true - /wildcard@2.0.0: - resolution: {integrity: sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==} - dev: true + wildcard@2.0.1: + resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==} - /word-wrap@1.2.3: - resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - dev: true - /wrap-ansi@7.0.0: + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - /wrap-ansi@8.1.0: + wrap-ansi@8.1.0: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} - dependencies: - ansi-styles: 6.2.1 - string-width: 5.1.2 - strip-ansi: 7.1.0 - /wrappy@1.0.2: + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - /ws@8.13.0: - resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -7767,65 +4299,4850 @@ packages: optional: true utf-8-validate: optional: true - dev: true - /y18n@5.0.8: + xml-name-validator@3.0.0: + resolution: {integrity: sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} - /yallist@3.1.1: + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - dev: true - /yallist@4.0.0: + yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true - /yaml@2.3.1: - resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} - engines: {node: '>= 14'} - dev: true + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true - /yargs-parser@21.1.1: + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} - /yargs@17.6.2: - resolution: {integrity: sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==} - engines: {node: '>=12'} - dependencies: - cliui: 8.0.1 - escalade: 3.1.1 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - dev: true - - /yargs@17.7.2: + yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} - dependencies: - cliui: 8.0.1 - escalade: 3.1.1 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - /yn@3.1.1: + yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} - dev: true - /yocto-queue@0.1.0: + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - dev: true - /zone.js@0.13.1: - resolution: {integrity: sha512-+bIeDAFEBYuXRuU3qGQvzdPap+N1zjM4KkBAiiQuVVCrHrhjDuY6VkUhNa5+U27+9w0q3fbKiMCbpJ0XzMmSWA==} + yocto-queue@1.2.1: + resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} + engines: {node: '>=12.20'} + + zone.js@0.13.3: + resolution: {integrity: sha512-MKPbmZie6fASC/ps4dkmIhaT5eonHkEt6eAy80K42tAm0G2W+AahLJjbfi6X9NPdciOE9GRFTTM8u2IiF6O3ww==} + +snapshots: + + '@ampproject/remapping@2.2.1': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@angular-devkit/architect@0.1602.16(chokidar@3.5.3)': + dependencies: + '@angular-devkit/core': 16.2.16(chokidar@3.5.3) + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + + '@angular-devkit/architect@0.1703.17(chokidar@3.5.3)': + dependencies: + '@angular-devkit/core': 17.3.17(chokidar@3.5.3) + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + + '@angular-devkit/build-angular@16.2.16(@angular/compiler-cli@16.2.12(@angular/compiler@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(typescript@5.1.6))(@types/node@18.19.130)(typescript@5.1.6)': + dependencies: + '@ampproject/remapping': 2.2.1 + '@angular-devkit/architect': 0.1602.16(chokidar@3.5.3) + '@angular-devkit/build-webpack': 0.1602.16(chokidar@3.5.3)(webpack-dev-server@4.15.1(webpack@5.94.0))(webpack@5.94.0(esbuild@0.18.17)) + '@angular-devkit/core': 16.2.16(chokidar@3.5.3) + '@angular/compiler-cli': 16.2.12(@angular/compiler@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(typescript@5.1.6) + '@babel/core': 7.22.9 + '@babel/generator': 7.22.9 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/plugin-proposal-async-generator-functions': 7.20.7(@babel/core@7.22.9) + '@babel/plugin-transform-async-to-generator': 7.22.5(@babel/core@7.22.9) + '@babel/plugin-transform-runtime': 7.22.9(@babel/core@7.22.9) + '@babel/preset-env': 7.22.9(@babel/core@7.22.9) + '@babel/runtime': 7.22.6 + '@babel/template': 7.22.5 + '@discoveryjs/json-ext': 0.5.7 + '@ngtools/webpack': 16.2.16(@angular/compiler-cli@16.2.12(@angular/compiler@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(typescript@5.1.6))(typescript@5.1.6)(webpack@5.94.0(esbuild@0.18.17)) + '@vitejs/plugin-basic-ssl': 1.0.1(vite@4.5.5(@types/node@18.19.130)(less@4.1.3)(sass@1.64.1)(terser@5.19.2)) + ansi-colors: 4.1.3 + autoprefixer: 10.4.14(postcss@8.4.31) + babel-loader: 9.1.3(@babel/core@7.22.9)(webpack@5.94.0(esbuild@0.18.17)) + babel-plugin-istanbul: 6.1.1 + browserslist: 4.28.0 + chokidar: 3.5.3 + copy-webpack-plugin: 11.0.0(webpack@5.94.0(esbuild@0.18.17)) + critters: 0.0.20 + css-loader: 6.8.1(webpack@5.94.0(esbuild@0.18.17)) + esbuild-wasm: 0.18.17 + fast-glob: 3.3.1 + guess-parser: 0.4.22(typescript@5.1.6) + https-proxy-agent: 5.0.1 + inquirer: 8.2.4 + jsonc-parser: 3.2.0 + karma-source-map-support: 1.4.0 + less: 4.1.3 + less-loader: 11.1.0(less@4.1.3)(webpack@5.94.0(esbuild@0.18.17)) + license-webpack-plugin: 4.0.2(webpack@5.94.0(esbuild@0.18.17)) + loader-utils: 3.2.1 + magic-string: 0.30.1 + mini-css-extract-plugin: 2.7.6(webpack@5.94.0(esbuild@0.18.17)) + mrmime: 1.0.1 + open: 8.4.2 + ora: 5.4.1 + parse5-html-rewriting-stream: 7.0.0 + picomatch: 2.3.1 + piscina: 4.0.0 + postcss: 8.4.31 + postcss-loader: 7.3.3(postcss@8.4.31)(typescript@5.1.6)(webpack@5.94.0(esbuild@0.18.17)) + resolve-url-loader: 5.0.0 + rxjs: 7.8.1 + sass: 1.64.1 + sass-loader: 13.3.2(sass@1.64.1)(webpack@5.94.0(esbuild@0.18.17)) + semver: 7.5.4 + source-map-loader: 4.0.1(webpack@5.94.0(esbuild@0.18.17)) + source-map-support: 0.5.21 + terser: 5.19.2 + text-table: 0.2.0 + tree-kill: 1.2.2 + tslib: 2.6.1 + typescript: 5.1.6 + vite: 4.5.5(@types/node@18.19.130)(less@4.1.3)(sass@1.64.1)(terser@5.19.2) + webpack: 5.94.0(esbuild@0.18.17) + webpack-dev-middleware: 6.1.2(webpack@5.94.0(esbuild@0.18.17)) + webpack-dev-server: 4.15.1(webpack@5.94.0) + webpack-merge: 5.9.0 + webpack-subresource-integrity: 5.1.0(webpack@5.94.0(esbuild@0.18.17)) + optionalDependencies: + esbuild: 0.18.17 + transitivePeerDependencies: + - '@swc/core' + - '@types/node' + - bufferutil + - canvas + - debug + - fibers + - html-webpack-plugin + - lightningcss + - node-sass + - sass-embedded + - stylus + - sugarss + - supports-color + - uglify-js + - utf-8-validate + - webpack-cli + + '@angular-devkit/build-webpack@0.1602.16(chokidar@3.5.3)(webpack-dev-server@4.15.1(webpack@5.94.0))(webpack@5.94.0(esbuild@0.18.17))': + dependencies: + '@angular-devkit/architect': 0.1602.16(chokidar@3.5.3) + rxjs: 7.8.1 + webpack: 5.94.0(esbuild@0.18.17) + webpack-dev-server: 4.15.1(webpack@5.94.0) + transitivePeerDependencies: + - chokidar + + '@angular-devkit/core@16.2.16(chokidar@3.5.3)': + dependencies: + ajv: 8.12.0 + ajv-formats: 2.1.1(ajv@8.12.0) + jsonc-parser: 3.2.0 + picomatch: 2.3.1 + rxjs: 7.8.1 + source-map: 0.7.4 + optionalDependencies: + chokidar: 3.5.3 + + '@angular-devkit/core@17.3.17(chokidar@3.5.3)': + dependencies: + ajv: 8.12.0 + ajv-formats: 2.1.1(ajv@8.12.0) + jsonc-parser: 3.2.1 + picomatch: 4.0.1 + rxjs: 7.8.1 + source-map: 0.7.4 + optionalDependencies: + chokidar: 3.5.3 + + '@angular-devkit/schematics@16.2.16(chokidar@3.5.3)': + dependencies: + '@angular-devkit/core': 16.2.16(chokidar@3.5.3) + jsonc-parser: 3.2.0 + magic-string: 0.30.1 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + + '@angular-devkit/schematics@17.3.17(chokidar@3.5.3)': + dependencies: + '@angular-devkit/core': 17.3.17(chokidar@3.5.3) + jsonc-parser: 3.2.1 + magic-string: 0.30.8 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + + '@angular-eslint/bundled-angular-compiler@16.3.1': {} + + '@angular-eslint/eslint-plugin-template@16.3.1(eslint@8.57.1)(typescript@5.1.6)': + dependencies: + '@angular-eslint/bundled-angular-compiler': 16.3.1 + '@angular-eslint/utils': 16.3.1(eslint@8.57.1)(typescript@5.1.6) + '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.1)(typescript@5.1.6) + '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.1.6) + aria-query: 5.3.0 + axobject-query: 4.0.0 + eslint: 8.57.1 + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + + '@angular-eslint/eslint-plugin@16.3.1(eslint@8.57.1)(typescript@5.1.6)': + dependencies: + '@angular-eslint/utils': 16.3.1(eslint@8.57.1)(typescript@5.1.6) + '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.1.6) + eslint: 8.57.1 + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + + '@angular-eslint/template-parser@16.3.1(eslint@8.57.1)(typescript@5.1.6)': + dependencies: + '@angular-eslint/bundled-angular-compiler': 16.3.1 + eslint: 8.57.1 + eslint-scope: 7.2.2 + typescript: 5.1.6 + + '@angular-eslint/utils@16.3.1(eslint@8.57.1)(typescript@5.1.6)': + dependencies: + '@angular-eslint/bundled-angular-compiler': 16.3.1 + '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.1.6) + eslint: 8.57.1 + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + + '@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))': + dependencies: + '@angular/core': 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + tslib: 2.8.1 + + '@angular/cdk@16.2.14(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2)': + dependencies: + '@angular/common': 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2) + '@angular/core': 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + rxjs: 7.8.2 + tslib: 2.8.1 + optionalDependencies: + parse5: 7.3.0 + + '@angular/cli@16.2.16(chokidar@3.5.3)': + dependencies: + '@angular-devkit/architect': 0.1602.16(chokidar@3.5.3) + '@angular-devkit/core': 16.2.16(chokidar@3.5.3) + '@angular-devkit/schematics': 16.2.16(chokidar@3.5.3) + '@schematics/angular': 16.2.16(chokidar@3.5.3) + '@yarnpkg/lockfile': 1.1.0 + ansi-colors: 4.1.3 + ini: 4.1.1 + inquirer: 8.2.4 + jsonc-parser: 3.2.0 + npm-package-arg: 10.1.0 + npm-pick-manifest: 8.0.1 + open: 8.4.2 + ora: 5.4.1 + pacote: 15.2.0 + resolve: 1.22.2 + semver: 7.5.4 + symbol-observable: 4.0.0 + yargs: 17.7.2 + transitivePeerDependencies: + - bluebird + - chokidar + - supports-color + + '@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2)': + dependencies: + '@angular/core': 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + rxjs: 7.8.2 + tslib: 2.8.1 + + '@angular/compiler-cli@16.2.12(@angular/compiler@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(typescript@5.1.6)': + dependencies: + '@angular/compiler': 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)) + '@babel/core': 7.23.2 + '@jridgewell/sourcemap-codec': 1.5.5 + chokidar: 3.6.0 + convert-source-map: 1.9.0 + reflect-metadata: 0.1.14 + semver: 7.7.3 + tslib: 2.8.1 + typescript: 5.1.6 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + + '@angular/compiler@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))': + dependencies: + tslib: 2.8.1 + optionalDependencies: + '@angular/core': 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + + '@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)': + dependencies: + rxjs: 7.8.2 + tslib: 2.8.1 + zone.js: 0.13.3 + + '@angular/forms@16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(rxjs@7.8.2)': + dependencies: + '@angular/common': 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2) + '@angular/core': 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + '@angular/platform-browser': 16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)) + rxjs: 7.8.2 + tslib: 2.8.1 + + '@angular/platform-browser-dynamic@16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/compiler@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))': + dependencies: + '@angular/common': 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2) + '@angular/compiler': 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)) + '@angular/core': 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + '@angular/platform-browser': 16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)) + tslib: 2.8.1 + + '@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))': + dependencies: + '@angular/common': 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2) + '@angular/core': 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + tslib: 2.8.1 + optionalDependencies: + '@angular/animations': 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)) + + '@angular/router@16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(rxjs@7.8.2)': + dependencies: + '@angular/common': 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2) + '@angular/core': 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + '@angular/platform-browser': 16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)) + rxjs: 7.8.2 + tslib: 2.8.1 + + '@arcanis/slice-ansi@1.1.1': + dependencies: + grapheme-splitter: 1.0.4 + + '@assemblyscript/loader@0.10.1': {} + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.5': {} + + '@babel/core@7.22.9': + dependencies: + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.22.9 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.22.9) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/template': 7.22.5 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + convert-source-map: 1.9.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/core@7.23.2': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.23.2) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.22.9': + dependencies: + '@babel/types': 7.28.5 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 2.5.2 + + '@babel/generator@7.28.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.22.5': + dependencies: + '@babel/types': 7.28.5 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.28.5 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.5 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.0 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.28.5(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.22.9) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.28.5 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-regexp-features-plugin@7.28.5(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-annotate-as-pure': 7.27.3 + regexpu-core: 6.4.0 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.4.4(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + debug: 4.4.3 + lodash.debounce: 4.0.8 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + + '@babel/helper-define-polyfill-provider@0.5.0(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + debug: 4.4.3 + lodash.debounce: 4.0.8 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + + '@babel/helper-define-polyfill-provider@0.6.5(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + debug: 4.4.3 + lodash.debounce: 4.0.8 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + + '@babel/helper-environment-visitor@7.24.7': + dependencies: + '@babel/types': 7.28.5 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.28.5': + dependencies: + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.23.2)': + dependencies: + '@babel/core': 7.23.2 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.28.5 + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-wrap-function': 7.28.3 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-split-export-declaration@7.22.6': + dependencies: + '@babel/types': 7.28.5 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helper-wrap-function@7.28.3': + dependencies: + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-transform-optional-chaining': 7.28.5(@babel/core@7.22.9) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-async-generator-functions@7.20.7(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.22.9) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + + '@babel/plugin-proposal-unicode-property-regex@7.18.6(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.22.9) + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.22.5(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.22.9) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-block-scoping@7.28.5(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-static-block@7.28.3(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.28.4(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-globals': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.22.9) + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/template': 7.27.2 + + '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-exponentiation-operator@7.28.5(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-json-strings@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-literals@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-logical-assignment-operators@7.28.5(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-systemjs@7.28.5(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-object-rest-spread@7.28.4(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.22.9) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.22.9) + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.22.9) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-optional-chaining@7.28.5(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-regenerator@7.28.4(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-runtime@7.22.9(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.22.9) + babel-plugin-polyfill-corejs3: 0.8.7(@babel/core@7.22.9) + babel-plugin-polyfill-regenerator: 0.5.5(@babel/core@7.22.9) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-spread@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-unicode-property-regex@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-unicode-sets-regex@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/preset-env@7.22.9(@babel/core@7.22.9)': + dependencies: + '@babel/compat-data': 7.28.5 + '@babel/core': 7.22.9 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.22.9) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.22.9) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.22.9) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.22.9) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.22.9) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.22.9) + '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.22.9) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.22.9) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.22.9) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.22.9) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.22.9) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.22.9) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.22.9) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.22.9) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.22.9) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.22.9) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.22.9) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-async-generator-functions': 7.28.0(@babel/core@7.22.9) + '@babel/plugin-transform-async-to-generator': 7.22.5(@babel/core@7.22.9) + '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-block-scoping': 7.28.5(@babel/core@7.22.9) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-class-static-block': 7.28.3(@babel/core@7.22.9) + '@babel/plugin-transform-classes': 7.28.4(@babel/core@7.22.9) + '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.22.9) + '@babel/plugin-transform-dotall-regex': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-exponentiation-operator': 7.28.5(@babel/core@7.22.9) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-json-strings': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-logical-assignment-operators': 7.28.5(@babel/core@7.22.9) + '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-modules-systemjs': 7.28.5(@babel/core@7.22.9) + '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-object-rest-spread': 7.28.4(@babel/core@7.22.9) + '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-optional-chaining': 7.28.5(@babel/core@7.22.9) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.22.9) + '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-regenerator': 7.28.4(@babel/core@7.22.9) + '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-unicode-property-regex': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-unicode-sets-regex': 7.27.1(@babel/core@7.22.9) + '@babel/preset-modules': 0.1.6(@babel/core@7.22.9) + '@babel/types': 7.28.5 + babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.22.9) + babel-plugin-polyfill-corejs3: 0.8.7(@babel/core@7.22.9) + babel-plugin-polyfill-regenerator: 0.5.5(@babel/core@7.22.9) + core-js-compat: 3.46.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/preset-modules@0.1.6(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.22.9) + '@babel/plugin-transform-dotall-regex': 7.27.1(@babel/core@7.22.9) + '@babel/types': 7.28.5 + esutils: 2.0.3 + + '@babel/runtime@7.22.6': + dependencies: + regenerator-runtime: 0.13.11 + + '@babel/template@7.22.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + + '@babel/traverse@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bgotink/kdl@0.1.7': {} + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@discoveryjs/json-ext@0.5.7': {} + + '@esbuild/android-arm64@0.18.17': + optional: true + + '@esbuild/android-arm@0.18.17': + optional: true + + '@esbuild/android-x64@0.18.17': + optional: true + + '@esbuild/darwin-arm64@0.18.17': + optional: true + + '@esbuild/darwin-x64@0.18.17': + optional: true + + '@esbuild/freebsd-arm64@0.18.17': + optional: true + + '@esbuild/freebsd-x64@0.18.17': + optional: true + + '@esbuild/linux-arm64@0.18.17': + optional: true + + '@esbuild/linux-arm@0.18.17': + optional: true + + '@esbuild/linux-ia32@0.18.17': + optional: true + + '@esbuild/linux-loong64@0.18.17': + optional: true + + '@esbuild/linux-mips64el@0.18.17': + optional: true + + '@esbuild/linux-ppc64@0.18.17': + optional: true + + '@esbuild/linux-riscv64@0.18.17': + optional: true + + '@esbuild/linux-s390x@0.18.17': + optional: true + + '@esbuild/linux-x64@0.18.17': + optional: true + + '@esbuild/netbsd-x64@0.18.17': + optional: true + + '@esbuild/openbsd-x64@0.18.17': + optional: true + + '@esbuild/sunos-x64@0.18.17': + optional: true + + '@esbuild/win32-arm64@0.18.17': + optional: true + + '@esbuild/win32-ia32@0.18.17': + optional: true + + '@esbuild/win32-x64@0.18.17': + optional: true + + '@eslint-community/eslint-utils@4.9.0(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@fortawesome/angular-fontawesome@0.13.0(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@fortawesome/fontawesome-svg-core@6.7.2)': + dependencies: + '@angular/core': 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + '@fortawesome/fontawesome-svg-core': 6.7.2 + tslib: 2.8.1 + + '@fortawesome/fontawesome-common-types@6.7.2': {} + + '@fortawesome/fontawesome-svg-core@6.7.2': + dependencies: + '@fortawesome/fontawesome-common-types': 6.7.2 + + '@fortawesome/free-regular-svg-icons@6.7.2': + dependencies: + '@fortawesome/fontawesome-common-types': 6.7.2 + + '@fortawesome/free-solid-svg-icons@6.7.2': + dependencies: + '@fortawesome/fontawesome-common-types': 6.7.2 + + '@gar/promisify@1.1.3': {} + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@leichtgewicht/ip-codec@2.0.5': {} + + '@ngneat/elf-devtools@1.3.0': {} + + '@ngneat/elf-entities@4.6.0': {} + + '@ngneat/elf@2.5.1(rxjs@7.8.2)': + dependencies: + rxjs: 7.8.2 + + '@ngneat/loadoff@2.1.0': + dependencies: + tslib: 2.8.1 + + '@ngneat/reactive-forms@5.0.2(@angular/forms@16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(rxjs@7.8.2))': + dependencies: + '@angular/forms': 16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(rxjs@7.8.2) + tslib: 2.8.1 + + '@ngneat/transloco-utils@3.0.5(typescript@5.1.6)': + dependencies: + cosmiconfig: 8.3.6(typescript@5.1.6) + tslib: 2.8.1 + transitivePeerDependencies: + - typescript + + '@ngneat/transloco@4.3.0(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(fs-extra@11.1.1)(glob@10.4.5)(rxjs@7.8.2)(typescript@5.1.6)': + dependencies: + '@angular/core': 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + '@ngneat/transloco-utils': 3.0.5(typescript@5.1.6) + flat: 5.0.2 + fs-extra: 11.1.1 + glob: 10.4.5 + lodash.kebabcase: 4.1.1 + ora: 5.4.1 + replace-in-file: 6.3.5 + rxjs: 7.8.2 + tslib: 2.8.1 + transitivePeerDependencies: + - typescript + + '@ngtools/webpack@16.2.16(@angular/compiler-cli@16.2.12(@angular/compiler@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(typescript@5.1.6))(typescript@5.1.6)(webpack@5.94.0(esbuild@0.18.17))': + dependencies: + '@angular/compiler-cli': 16.2.12(@angular/compiler@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(typescript@5.1.6) + typescript: 5.1.6 + webpack: 5.94.0(esbuild@0.18.17) + + '@ngx-playwright/harness@0.10.2(@angular/cdk@16.2.14(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@playwright/test@1.56.1)': + dependencies: + '@angular/cdk': 16.2.14(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2) + '@playwright/test': 1.56.1 + + '@ngx-playwright/test@0.4.3(@angular-devkit/core@17.3.17(chokidar@3.5.3))(@angular/cdk@16.2.14(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@playwright/test@1.56.1)(chokidar@3.5.3)(typescript@5.1.6)': + dependencies: + '@angular-devkit/schematics': 17.3.17(chokidar@3.5.3) + '@angular/cdk': 16.2.14(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2) + '@ngx-playwright/harness': 0.10.2(@angular/cdk@16.2.14(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@playwright/test@1.56.1) + '@playwright/test': 1.56.1 + '@snuggery/architect': 0.9.1(chokidar@3.5.3) + '@snuggery/schematics': 0.9.1(@angular-devkit/core@17.3.17(chokidar@3.5.3))(@angular-devkit/schematics@17.3.17(chokidar@3.5.3))(chokidar@3.5.3)(typescript@5.1.6) + '@snuggery/snuggery': 0.11.1(chokidar@3.5.3) + transitivePeerDependencies: + - '@angular-devkit/core' + - chokidar + - typescript + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@npmcli/fs@2.1.2': + dependencies: + '@gar/promisify': 1.1.3 + semver: 7.5.4 + + '@npmcli/fs@3.1.1': + dependencies: + semver: 7.5.4 + + '@npmcli/git@4.1.0': + dependencies: + '@npmcli/promise-spawn': 6.0.2 + lru-cache: 7.18.3 + npm-pick-manifest: 8.0.1 + proc-log: 3.0.0 + promise-inflight: 1.0.1 + promise-retry: 2.0.1 + semver: 7.5.4 + which: 3.0.1 + transitivePeerDependencies: + - bluebird + + '@npmcli/installed-package-contents@2.1.0': + dependencies: + npm-bundled: 3.0.1 + npm-normalize-package-bin: 3.0.1 + + '@npmcli/move-file@2.0.1': + dependencies: + mkdirp: 1.0.4 + rimraf: 3.0.2 + + '@npmcli/node-gyp@3.0.0': {} + + '@npmcli/promise-spawn@6.0.2': + dependencies: + which: 3.0.1 + + '@npmcli/run-script@6.0.2': + dependencies: + '@npmcli/node-gyp': 3.0.0 + '@npmcli/promise-spawn': 6.0.2 + node-gyp: 9.4.1 + read-package-json-fast: 3.0.2 + which: 3.0.1 + transitivePeerDependencies: + - bluebird + - supports-color + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@playwright/test@1.56.1': + dependencies: + playwright: 1.56.1 + + '@schematics/angular@16.2.16(chokidar@3.5.3)': + dependencies: + '@angular-devkit/core': 16.2.16(chokidar@3.5.3) + '@angular-devkit/schematics': 16.2.16(chokidar@3.5.3) + jsonc-parser: 3.2.0 + transitivePeerDependencies: + - chokidar + + '@sigstore/bundle@1.1.0': + dependencies: + '@sigstore/protobuf-specs': 0.2.1 + + '@sigstore/protobuf-specs@0.2.1': {} + + '@sigstore/sign@1.0.0': + dependencies: + '@sigstore/bundle': 1.1.0 + '@sigstore/protobuf-specs': 0.2.1 + make-fetch-happen: 11.1.1 + transitivePeerDependencies: + - supports-color + + '@sigstore/tuf@1.0.3': + dependencies: + '@sigstore/protobuf-specs': 0.2.1 + tuf-js: 1.1.7 + transitivePeerDependencies: + - supports-color + + '@snuggery/architect@0.9.1(chokidar@3.5.3)': + dependencies: + '@angular-devkit/architect': 0.1703.17(chokidar@3.5.3) + '@angular-devkit/core': 17.3.17(chokidar@3.5.3) + '@snuggery/core': 0.7.1(chokidar@3.5.3) + fs-extra: 11.1.1 + glob: 10.2.1 + rxjs: 7.8.2 + transitivePeerDependencies: + - chokidar + + '@snuggery/core@0.7.1(chokidar@3.5.3)': + dependencies: + '@angular-devkit/core': 17.3.17(chokidar@3.5.3) + '@bgotink/kdl': 0.1.7 + jsonc-parser: 3.3.1 + micromatch: 4.0.8 + typanion: 3.14.0 + yaml: 2.8.1 + transitivePeerDependencies: + - chokidar + + '@snuggery/schematics@0.9.1(@angular-devkit/core@17.3.17(chokidar@3.5.3))(@angular-devkit/schematics@17.3.17(chokidar@3.5.3))(chokidar@3.5.3)(typescript@5.1.6)': + dependencies: + '@angular-devkit/core': 17.3.17(chokidar@3.5.3) + '@angular-devkit/schematics': 17.3.17(chokidar@3.5.3) + '@snuggery/core': 0.7.1(chokidar@3.5.3) + ignore: 5.3.2 + optionalDependencies: + typescript: 5.1.6 + transitivePeerDependencies: + - chokidar + + '@snuggery/snuggery@0.11.1(chokidar@3.5.3)': + dependencies: + '@angular-devkit/architect': 0.1703.17(chokidar@3.5.3) + '@angular-devkit/core': 17.3.17(chokidar@3.5.3) + '@angular-devkit/schematics': 17.3.17(chokidar@3.5.3) + '@arcanis/slice-ansi': 1.1.1 + '@snuggery/architect': 0.9.1(chokidar@3.5.3) + '@snuggery/core': 0.7.1(chokidar@3.5.3) + ajv: 8.17.1 + ajv-formats: 2.1.1(ajv@8.17.1) + clipanion: 3.2.0(typanion@3.14.0) + json5: 2.2.3 + kleur: 4.1.5 + prompts: 2.4.2 + rxjs: 7.8.2 + semver: 7.7.3 + strip-ansi: 6.0.1 + typanion: 3.14.0 + which-pm-runs: 1.1.0 + transitivePeerDependencies: + - chokidar + + '@tootallnate/once@1.1.2': {} + + '@tootallnate/once@2.0.0': {} + + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@tufjs/canonical-json@1.0.0': {} + + '@tufjs/models@1.0.4': + dependencies: + '@tufjs/canonical-json': 1.0.0 + minimatch: 9.0.5 + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 18.19.130 + + '@types/bonjour@3.5.13': + dependencies: + '@types/node': 18.19.130 + + '@types/connect-history-api-fallback@1.5.4': + dependencies: + '@types/express-serve-static-core': 5.1.0 + '@types/node': 18.19.130 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 18.19.130 + + '@types/estree@1.0.8': {} + + '@types/events@3.0.3': {} + + '@types/express-serve-static-core@4.19.7': + dependencies: + '@types/node': 18.19.130 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express-serve-static-core@5.1.0': + dependencies: + '@types/node': 18.19.130 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@4.17.25': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 4.19.7 + '@types/qs': 6.14.0 + '@types/serve-static': 1.15.10 + + '@types/http-errors@2.0.5': {} + + '@types/http-proxy@1.17.17': + dependencies: + '@types/node': 18.19.130 + + '@types/json-schema@7.0.15': {} + + '@types/mime@1.3.5': {} + + '@types/node-forge@1.3.14': + dependencies: + '@types/node': 18.19.130 + + '@types/node@18.19.130': + dependencies: + undici-types: 5.26.5 + + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/retry@0.12.0': {} + + '@types/semver@7.7.1': {} + + '@types/send@0.17.6': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 18.19.130 + + '@types/send@1.2.1': + dependencies: + '@types/node': 18.19.130 + + '@types/serve-index@1.9.4': + dependencies: + '@types/express': 4.17.25 + + '@types/serve-static@1.15.10': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 18.19.130 + '@types/send': 0.17.6 + + '@types/sockjs@0.3.36': + dependencies: + '@types/node': 18.19.130 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 18.19.130 + + '@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.1.6))(eslint@8.57.1)(typescript@5.1.6)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.1.6) + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.1)(typescript@5.1.6) + '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.1.6) + debug: 4.4.3 + eslint: 8.57.1 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare-lite: 1.4.0 + semver: 7.7.3 + tsutils: 3.21.0(typescript@5.1.6) + optionalDependencies: + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.1.6)': + dependencies: + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.1.6) + debug: 4.4.3 + eslint: 8.57.1 + optionalDependencies: + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@5.62.0': + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + + '@typescript-eslint/type-utils@5.62.0(eslint@8.57.1)(typescript@5.1.6)': + dependencies: + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.1.6) + '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.1.6) + debug: 4.4.3 + eslint: 8.57.1 + tsutils: 3.21.0(typescript@5.1.6) + optionalDependencies: + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@5.62.0': {} + + '@typescript-eslint/typescript-estree@5.62.0(typescript@5.1.6)': + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + debug: 4.4.3 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.7.3 + tsutils: 3.21.0(typescript@5.1.6) + optionalDependencies: + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@5.62.0(eslint@8.57.1)(typescript@5.1.6)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) + '@types/json-schema': 7.0.15 + '@types/semver': 7.7.1 + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.1.6) + eslint: 8.57.1 + eslint-scope: 5.1.1 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@5.62.0': + dependencies: + '@typescript-eslint/types': 5.62.0 + eslint-visitor-keys: 3.4.3 + + '@ungap/structured-clone@1.3.0': {} + + '@vitejs/plugin-basic-ssl@1.0.1(vite@4.5.5(@types/node@18.19.130)(less@4.1.3)(sass@1.64.1)(terser@5.19.2))': + dependencies: + vite: 4.5.5(@types/node@18.19.130)(less@4.1.3)(sass@1.64.1)(terser@5.19.2) + + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + + '@webassemblyjs/helper-api-error@1.13.2': {} + + '@webassemblyjs/helper-buffer@1.14.1': {} + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.13.2': {} + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + + '@wessberg/ts-evaluator@0.0.27(typescript@5.1.6)': + dependencies: + chalk: 4.1.2 + jsdom: 16.7.0 + object-path: 0.11.8 + tslib: 2.8.1 + typescript: 5.1.6 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - utf-8-validate + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + + '@yarnpkg/lockfile@1.1.0': {} + + abab@2.0.6: {} + + abbrev@1.1.1: {} + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + acorn-globals@6.0.0: + dependencies: + acorn: 7.4.1 + acorn-walk: 7.2.0 + + acorn-import-attributes@1.9.5(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn-walk@7.2.0: {} + + acorn-walk@8.3.4: + dependencies: + acorn: 8.15.0 + + acorn@7.4.1: {} + + acorn@8.15.0: {} + + adjust-sourcemap-loader@4.0.0: + dependencies: + loader-utils: 2.0.4 + regex-parser: 2.3.1 + + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + + ajv-formats@2.1.1(ajv@8.12.0): + optionalDependencies: + ajv: 8.12.0 + + ajv-formats@2.1.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + + ajv-keywords@3.5.2(ajv@6.12.6): + dependencies: + ajv: 6.12.6 + + ajv-keywords@5.1.0(ajv@8.17.1): + dependencies: + ajv: 8.17.1 + fast-deep-equal: 3.1.3 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.12.0: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-colors@4.1.3: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-html-community@0.0.8: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + aproba@2.1.0: {} + + are-we-there-yet@3.0.1: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + + arg@4.1.3: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + array-flatten@1.1.1: {} + + array-union@2.1.0: {} + + asynckit@0.4.0: {} + + autoprefixer@10.4.14(postcss@8.4.31): + dependencies: + browserslist: 4.28.0 + caniuse-lite: 1.0.30001754 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.4.31 + postcss-value-parser: 4.2.0 + + axobject-query@4.0.0: + dependencies: + dequal: 2.0.3 + + babel-loader@9.1.3(@babel/core@7.22.9)(webpack@5.94.0(esbuild@0.18.17)): + dependencies: + '@babel/core': 7.22.9 + find-cache-dir: 4.0.0 + schema-utils: 4.3.3 + webpack: 5.94.0(esbuild@0.18.17) + + babel-plugin-istanbul@6.1.1: + dependencies: + '@babel/helper-plugin-utils': 7.27.1 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.22.9): + dependencies: + '@babel/compat-data': 7.28.5 + '@babel/core': 7.22.9 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.22.9) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.8.7(@babel/core@7.22.9): + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-define-polyfill-provider': 0.4.4(@babel/core@7.22.9) + core-js-compat: 3.46.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.5.5(@babel/core@7.22.9): + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-define-polyfill-provider': 0.5.0(@babel/core@7.22.9) + transitivePeerDependencies: + - supports-color + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.8.25: {} + + batch@0.6.1: {} + + big.js@5.2.2: {} + + binary-extensions@2.3.0: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + body-parser@1.20.3: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + bonjour-service@1.3.0: + dependencies: + fast-deep-equal: 3.1.3 + multicast-dns: 7.2.5 + + boolbase@1.0.0: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browser-process-hrtime@1.0.0: {} + + browserslist@4.28.0: + dependencies: + baseline-browser-mapping: 2.8.25 + caniuse-lite: 1.0.30001754 + electron-to-chromium: 1.5.250 + node-releases: 2.0.27 + update-browserslist-db: 1.1.4(browserslist@4.28.0) + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bytes@3.1.2: {} + + cacache@16.1.3: + dependencies: + '@npmcli/fs': 2.1.2 + '@npmcli/move-file': 2.0.1 + chownr: 2.0.0 + fs-minipass: 2.1.0 + glob: 8.1.0 + infer-owner: 1.0.4 + lru-cache: 7.18.3 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + mkdirp: 1.0.4 + p-map: 4.0.0 + promise-inflight: 1.0.1 + rimraf: 3.0.2 + ssri: 9.0.1 + tar: 6.2.1 + unique-filename: 2.0.1 + transitivePeerDependencies: + - bluebird + + cacache@17.1.4: + dependencies: + '@npmcli/fs': 3.1.1 + fs-minipass: 3.0.3 + glob: 10.4.5 + lru-cache: 7.18.3 + minipass: 7.1.2 + minipass-collect: 1.0.2 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + p-map: 4.0.0 + ssri: 10.0.6 + tar: 6.2.1 + unique-filename: 3.0.0 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase@5.3.1: {} + + caniuse-lite@1.0.30001754: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chardet@0.7.0: {} + + chokidar@3.5.3: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chownr@2.0.0: {} + + chrome-trace-event@1.0.4: {} + + clean-stack@2.2.0: {} + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-spinners@2.9.2: {} + + cli-width@3.0.0: {} + + clipanion@3.2.0(typanion@3.14.0): + dependencies: + typanion: 3.14.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone-deep@4.0.1: + dependencies: + is-plain-object: 2.0.4 + kind-of: 6.0.3 + shallow-clone: 3.0.1 + + clone@1.0.4: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-support@1.1.3: {} + + colorette@2.0.20: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@2.20.3: {} + + common-path-prefix@3.0.0: {} + + compressible@2.0.18: + dependencies: + mime-db: 1.54.0 + + compression@1.8.1: + dependencies: + bytes: 3.1.2 + compressible: 2.0.18 + debug: 2.6.9 + negotiator: 0.6.4 + on-headers: 1.1.0 + safe-buffer: 5.2.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + concat-map@0.0.1: {} + + connect-history-api-fallback@2.0.0: {} + + console-control-strings@1.1.0: {} + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + convert-source-map@1.9.0: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.0.6: {} + + cookie@0.7.1: {} + + copy-anything@2.0.6: + dependencies: + is-what: 3.14.1 + + copy-webpack-plugin@11.0.0(webpack@5.94.0(esbuild@0.18.17)): + dependencies: + fast-glob: 3.3.1 + glob-parent: 6.0.2 + globby: 13.2.2 + normalize-path: 3.0.0 + schema-utils: 4.3.3 + serialize-javascript: 6.0.2 + webpack: 5.94.0(esbuild@0.18.17) + + core-js-compat@3.46.0: + dependencies: + browserslist: 4.28.0 + + core-util-is@1.0.3: {} + + cosmiconfig@8.3.6(typescript@5.1.6): + dependencies: + import-fresh: 3.3.1 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + optionalDependencies: + typescript: 5.1.6 + + create-require@1.1.1: {} + + critters@0.0.20: + dependencies: + chalk: 4.1.2 + css-select: 5.2.2 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + htmlparser2: 8.0.2 + postcss: 8.4.31 + pretty-bytes: 5.6.0 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-loader@6.8.1(webpack@5.94.0(esbuild@0.18.17)): + dependencies: + icss-utils: 5.1.0(postcss@8.4.31) + postcss: 8.4.31 + postcss-modules-extract-imports: 3.1.0(postcss@8.4.31) + postcss-modules-local-by-default: 4.2.0(postcss@8.4.31) + postcss-modules-scope: 3.2.1(postcss@8.4.31) + postcss-modules-values: 4.0.0(postcss@8.4.31) + postcss-value-parser: 4.2.0 + semver: 7.5.4 + webpack: 5.94.0(esbuild@0.18.17) + + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-what@6.2.2: {} + + cssesc@3.0.0: {} + + cssom@0.3.8: {} + + cssom@0.4.4: {} + + cssstyle@2.3.0: + dependencies: + cssom: 0.3.8 + + data-urls@2.0.0: + dependencies: + abab: 2.0.6 + whatwg-mimetype: 2.3.0 + whatwg-url: 8.7.0 + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + deep-is@0.1.4: {} + + default-gateway@6.0.3: + dependencies: + execa: 5.1.1 + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + define-lazy-prop@2.0.0: {} + + delayed-stream@1.0.0: {} + + delegates@1.0.0: {} + + depd@1.1.2: {} + + depd@2.0.0: {} + + dequal@2.0.3: {} + + destroy@1.2.0: {} + + detect-node@2.1.0: {} + + diff@4.0.2: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dns-packet@5.6.1: + dependencies: + '@leichtgewicht/ip-codec': 2.0.5 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domexception@2.0.1: + dependencies: + webidl-conversions: 5.0.0 + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + ee-first@1.1.1: {} + + electron-to-chromium@1.5.250: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + emojis-list@3.0.0: {} + + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + optional: true + + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + entities@4.5.0: {} + + entities@6.0.1: {} + + env-paths@2.2.1: {} + + err-code@2.0.3: {} + + errno@0.1.8: + dependencies: + prr: 1.0.1 + optional: true + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild-wasm@0.18.17: {} + + esbuild@0.18.17: + optionalDependencies: + '@esbuild/android-arm': 0.18.17 + '@esbuild/android-arm64': 0.18.17 + '@esbuild/android-x64': 0.18.17 + '@esbuild/darwin-arm64': 0.18.17 + '@esbuild/darwin-x64': 0.18.17 + '@esbuild/freebsd-arm64': 0.18.17 + '@esbuild/freebsd-x64': 0.18.17 + '@esbuild/linux-arm': 0.18.17 + '@esbuild/linux-arm64': 0.18.17 + '@esbuild/linux-ia32': 0.18.17 + '@esbuild/linux-loong64': 0.18.17 + '@esbuild/linux-mips64el': 0.18.17 + '@esbuild/linux-ppc64': 0.18.17 + '@esbuild/linux-riscv64': 0.18.17 + '@esbuild/linux-s390x': 0.18.17 + '@esbuild/linux-x64': 0.18.17 + '@esbuild/netbsd-x64': 0.18.17 + '@esbuild/openbsd-x64': 0.18.17 + '@esbuild/sunos-x64': 0.18.17 + '@esbuild/win32-arm64': 0.18.17 + '@esbuild/win32-ia32': 0.18.17 + '@esbuild/win32-x64': 0.18.17 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint@8.57.1: + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@9.6.1: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 3.4.3 + + esprima@4.0.1: {} + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + etag@1.8.1: {} + + eventemitter-asyncresource@1.0.0: {} + + eventemitter3@4.0.7: {} + + events@3.3.0: {} + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + exponential-backoff@3.1.3: {} + + express@4.21.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.1: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.1.0: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + faye-websocket@0.11.4: + dependencies: + websocket-driver: 0.7.4 + + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@1.3.1: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + find-cache-dir@4.0.0: + dependencies: + common-path-prefix: 3.0.0 + pkg-dir: 7.0.0 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + find-up@6.3.0: + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + rimraf: 3.0.2 + + flat@5.0.2: {} + + flatted@3.3.3: {} + + follow-redirects@1.15.11: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + form-data@3.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + forwarded@0.2.0: {} + + fraction.js@4.3.7: {} + + fresh@0.5.2: {} + + fs-extra@11.1.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs-minipass@3.0.3: + dependencies: + minipass: 7.1.2 + + fs-monkey@1.1.0: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gauge@4.0.4: + dependencies: + aproba: 2.1.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-package-type@0.1.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@6.0.1: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob-to-regexp@0.4.1: {} + + glob@10.2.1: + dependencies: + foreground-child: 3.3.1 + fs.realpath: 1.0.0 + jackspeak: 2.3.6 + minimatch: 9.0.5 + minipass: 5.0.0 + path-scurry: 1.11.1 + + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + glob@8.1.0: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.6 + once: 1.4.0 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + globby@13.2.2: + dependencies: + dir-glob: 3.0.1 + fast-glob: 3.3.1 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 4.0.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + grapheme-splitter@1.0.4: {} + + graphemer@1.4.0: {} + + guess-parser@0.4.22(typescript@5.1.6): + dependencies: + '@wessberg/ts-evaluator': 0.0.27(typescript@5.1.6) + typescript: 5.1.6 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - utf-8-validate + + handle-thing@2.0.1: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + has-unicode@2.0.1: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hdr-histogram-js@2.0.3: + dependencies: + '@assemblyscript/loader': 0.10.1 + base64-js: 1.5.1 + pako: 1.0.11 + + hdr-histogram-percentiles-obj@3.0.0: {} + + hosted-git-info@6.1.3: + dependencies: + lru-cache: 7.18.3 + + hpack.js@2.1.6: + dependencies: + inherits: 2.0.4 + obuf: 1.1.2 + readable-stream: 2.3.8 + wbuf: 1.7.3 + + html-encoding-sniffer@2.0.1: + dependencies: + whatwg-encoding: 1.0.5 + + html-entities@2.6.0: {} + + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + + http-cache-semantics@4.2.0: {} + + http-deceiver@1.2.7: {} + + http-errors@1.6.3: + dependencies: + depd: 1.1.2 + inherits: 2.0.3 + setprototypeof: 1.1.0 + statuses: 1.5.0 + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + http-parser-js@0.5.10: {} + + http-proxy-agent@4.0.1: + dependencies: + '@tootallnate/once': 1.1.2 + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + http-proxy-agent@5.0.0: + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + http-proxy-middleware@2.0.9(@types/express@4.17.25): + dependencies: + '@types/http-proxy': 1.17.17 + http-proxy: 1.18.1 + is-glob: 4.0.3 + is-plain-obj: 3.0.0 + micromatch: 4.0.8 + optionalDependencies: + '@types/express': 4.17.25 + transitivePeerDependencies: + - debug + + http-proxy@1.18.1: + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.15.11 + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + human-signals@2.1.0: {} + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + icss-utils@5.1.0(postcss@8.4.31): + dependencies: + postcss: 8.4.31 + + ieee754@1.2.1: {} + + ignore-walk@6.0.5: + dependencies: + minimatch: 9.0.5 + + ignore@5.3.2: {} + + image-size@0.5.5: + optional: true + + immutable@4.3.7: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + infer-owner@1.0.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.3: {} + + inherits@2.0.4: {} + + ini@4.1.1: {} + + inquirer@8.2.4: + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.2 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 7.0.0 + + ip-address@10.1.0: {} + + ipaddr.js@1.9.1: {} + + ipaddr.js@2.2.0: {} + + is-arrayish@0.2.1: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-docker@2.2.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-interactive@1.0.0: {} + + is-lambda@1.0.1: {} + + is-number@7.0.0: {} + + is-path-inside@3.0.3: {} + + is-plain-obj@3.0.0: {} + + is-plain-object@2.0.4: + dependencies: + isobject: 3.0.1 + + is-potential-custom-element-name@1.0.1: {} + + is-stream@2.0.1: {} + + is-unicode-supported@0.1.0: {} + + is-what@3.14.1: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + isarray@1.0.0: {} + + isexe@2.0.0: {} + + isobject@3.0.1: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@5.2.1: + dependencies: + '@babel/core': 7.22.9 + '@babel/parser': 7.28.5 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + jackspeak@2.3.6: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jest-worker@27.5.1: + dependencies: + '@types/node': 18.19.130 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jiti@1.21.7: {} + + js-tokens@4.0.0: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsdom@16.7.0: + dependencies: + abab: 2.0.6 + acorn: 8.15.0 + acorn-globals: 6.0.0 + cssom: 0.4.4 + cssstyle: 2.3.0 + data-urls: 2.0.0 + decimal.js: 10.6.0 + domexception: 2.0.1 + escodegen: 2.1.0 + form-data: 3.0.4 + html-encoding-sniffer: 2.0.1 + http-proxy-agent: 4.0.1 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.22 + parse5: 6.0.1 + saxes: 5.0.1 + symbol-tree: 3.2.4 + tough-cookie: 4.1.4 + w3c-hr-time: 1.0.2 + w3c-xmlserializer: 2.0.0 + webidl-conversions: 6.1.0 + whatwg-encoding: 1.0.5 + whatwg-mimetype: 2.3.0 + whatwg-url: 8.7.0 + ws: 7.5.10 + xml-name-validator: 3.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsesc@2.5.2: {} + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-parse-even-better-errors@3.0.2: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsonc-parser@3.2.0: {} + + jsonc-parser@3.2.1: {} + + jsonc-parser@3.3.1: {} + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonparse@1.3.1: {} + + karma-source-map-support@1.4.0: + dependencies: + source-map-support: 0.5.21 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kind-of@6.0.3: {} + + kleur@3.0.3: {} + + kleur@4.1.5: {} + + klona@2.0.6: {} + + launch-editor@2.12.0: + dependencies: + picocolors: 1.1.1 + shell-quote: 1.8.3 + + less-loader@11.1.0(less@4.1.3)(webpack@5.94.0(esbuild@0.18.17)): + dependencies: + klona: 2.0.6 + less: 4.1.3 + webpack: 5.94.0(esbuild@0.18.17) + + less@4.1.3: + dependencies: + copy-anything: 2.0.6 + parse-node-version: 1.0.1 + tslib: 2.8.1 + optionalDependencies: + errno: 0.1.8 + graceful-fs: 4.2.11 + image-size: 0.5.5 + make-dir: 2.1.0 + mime: 1.6.0 + needle: 3.3.1 + source-map: 0.6.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + license-webpack-plugin@4.0.2(webpack@5.94.0(esbuild@0.18.17)): + dependencies: + webpack-sources: 3.3.3 + optionalDependencies: + webpack: 5.94.0(esbuild@0.18.17) + + lines-and-columns@1.2.4: {} + + loader-runner@4.3.1: {} + + loader-utils@2.0.4: + dependencies: + big.js: 5.2.2 + emojis-list: 3.0.0 + json5: 2.2.3 + + loader-utils@3.2.1: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + locate-path@7.2.0: + dependencies: + p-locate: 6.0.0 + + lodash.debounce@4.0.8: {} + + lodash.kebabcase@4.1.1: {} + + lodash.merge@4.6.2: {} + + lodash@4.17.21: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + lru-cache@7.18.3: {} + + magic-string@0.30.1: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magic-string@0.30.8: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + make-dir@2.1.0: + dependencies: + pify: 4.0.1 + semver: 5.7.2 + optional: true + + make-error@1.3.6: {} + + make-fetch-happen@10.2.1: + dependencies: + agentkeepalive: 4.6.0 + cacache: 16.1.3 + http-cache-semantics: 4.2.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-lambda: 1.0.1 + lru-cache: 7.18.3 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-fetch: 2.1.2 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + negotiator: 0.6.4 + promise-retry: 2.0.1 + socks-proxy-agent: 7.0.0 + ssri: 9.0.1 + transitivePeerDependencies: + - bluebird + - supports-color + + make-fetch-happen@11.1.1: + dependencies: + agentkeepalive: 4.6.0 + cacache: 17.1.4 + http-cache-semantics: 4.2.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-lambda: 1.0.1 + lru-cache: 7.18.3 + minipass: 5.0.0 + minipass-fetch: 3.0.5 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + negotiator: 0.6.4 + promise-retry: 2.0.1 + socks-proxy-agent: 7.0.0 + ssri: 10.0.6 + transitivePeerDependencies: + - supports-color + + math-intrinsics@1.1.0: {} + + media-typer@0.3.0: {} + + memfs@3.5.3: + dependencies: + fs-monkey: 1.1.0 + + merge-descriptors@1.0.3: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + methods@1.1.2: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + mimic-fn@2.1.0: {} + + mini-css-extract-plugin@2.7.6(webpack@5.94.0(esbuild@0.18.17)): + dependencies: + schema-utils: 4.3.3 + webpack: 5.94.0(esbuild@0.18.17) + + minimalistic-assert@1.0.1: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.2 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minipass-collect@1.0.2: + dependencies: + minipass: 3.3.6 + + minipass-fetch@2.1.2: + dependencies: + minipass: 3.3.6 + minipass-sized: 1.0.3 + minizlib: 2.1.2 + optionalDependencies: + encoding: 0.1.13 + + minipass-fetch@3.0.5: + dependencies: + minipass: 7.1.2 + minipass-sized: 1.0.3 + minizlib: 2.1.2 + optionalDependencies: + encoding: 0.1.13 + + minipass-flush@1.0.5: + dependencies: + minipass: 3.3.6 + + minipass-json-stream@1.0.2: + dependencies: + jsonparse: 1.3.1 + minipass: 3.3.6 + + minipass-pipeline@1.2.4: + dependencies: + minipass: 3.3.6 + + minipass-sized@1.0.3: + dependencies: + minipass: 3.3.6 + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minipass@7.1.2: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + mkdirp@1.0.4: {} + + mrmime@1.0.1: {} + + ms@2.0.0: {} + + ms@2.1.3: {} + + multicast-dns@7.2.5: + dependencies: + dns-packet: 5.6.1 + thunky: 1.1.0 + + mute-stream@0.0.8: {} + + nanoid@3.3.11: {} + + natural-compare-lite@1.4.0: {} + + natural-compare@1.4.0: {} + + needle@3.3.1: + dependencies: + iconv-lite: 0.6.3 + sax: 1.4.3 + optional: true + + negotiator@0.6.3: {} + + negotiator@0.6.4: {} + + neo-async@2.6.2: {} + + nice-napi@1.0.2: + dependencies: + node-addon-api: 3.2.1 + node-gyp-build: 4.8.4 + optional: true + + node-addon-api@3.2.1: + optional: true + + node-forge@1.3.1: {} + + node-gyp-build@4.8.4: + optional: true + + node-gyp@9.4.1: + dependencies: + env-paths: 2.2.1 + exponential-backoff: 3.1.3 + glob: 7.2.3 + graceful-fs: 4.2.11 + make-fetch-happen: 10.2.1 + nopt: 6.0.0 + npmlog: 6.0.2 + rimraf: 3.0.2 + semver: 7.5.4 + tar: 6.2.1 + which: 2.0.2 + transitivePeerDependencies: + - bluebird + - supports-color + + node-releases@2.0.27: {} + + nopt@6.0.0: + dependencies: + abbrev: 1.1.1 + + normalize-package-data@5.0.0: + dependencies: + hosted-git-info: 6.1.3 + is-core-module: 2.16.1 + semver: 7.5.4 + validate-npm-package-license: 3.0.4 + + normalize-path@3.0.0: {} + + normalize-range@0.1.2: {} + + npm-bundled@3.0.1: + dependencies: + npm-normalize-package-bin: 3.0.1 + + npm-install-checks@6.3.0: + dependencies: + semver: 7.5.4 + + npm-normalize-package-bin@3.0.1: {} + + npm-package-arg@10.1.0: + dependencies: + hosted-git-info: 6.1.3 + proc-log: 3.0.0 + semver: 7.5.4 + validate-npm-package-name: 5.0.1 + + npm-packlist@7.0.4: + dependencies: + ignore-walk: 6.0.5 + + npm-pick-manifest@8.0.1: + dependencies: + npm-install-checks: 6.3.0 + npm-normalize-package-bin: 3.0.1 + npm-package-arg: 10.1.0 + semver: 7.5.4 + + npm-registry-fetch@14.0.5: + dependencies: + make-fetch-happen: 11.1.1 + minipass: 5.0.0 + minipass-fetch: 3.0.5 + minipass-json-stream: 1.0.2 + minizlib: 2.1.2 + npm-package-arg: 10.1.0 + proc-log: 3.0.0 + transitivePeerDependencies: + - supports-color + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + npmlog@6.0.2: + dependencies: + are-we-there-yet: 3.0.1 + console-control-strings: 1.1.0 + gauge: 4.0.4 + set-blocking: 2.0.0 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + nwsapi@2.2.22: {} + + object-inspect@1.13.4: {} + + object-path@0.11.8: {} + + obuf@1.1.2: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + on-headers@1.1.0: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + os-tmpdir@1.0.2: {} + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-limit@4.0.0: + dependencies: + yocto-queue: 1.2.1 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-locate@6.0.0: + dependencies: + p-limit: 4.0.0 + + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + + pacote@15.2.0: + dependencies: + '@npmcli/git': 4.1.0 + '@npmcli/installed-package-contents': 2.1.0 + '@npmcli/promise-spawn': 6.0.2 + '@npmcli/run-script': 6.0.2 + cacache: 17.1.4 + fs-minipass: 3.0.3 + minipass: 5.0.0 + npm-package-arg: 10.1.0 + npm-packlist: 7.0.4 + npm-pick-manifest: 8.0.1 + npm-registry-fetch: 14.0.5 + proc-log: 3.0.0 + promise-retry: 2.0.1 + read-package-json: 6.0.4 + read-package-json-fast: 3.0.2 + sigstore: 1.9.0 + ssri: 10.0.6 + tar: 6.2.1 + transitivePeerDependencies: + - bluebird + - supports-color + + pako@1.0.11: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-node-version@1.0.1: {} + + parse5-html-rewriting-stream@7.0.0: + dependencies: + entities: 4.5.0 + parse5: 7.3.0 + parse5-sax-parser: 7.0.0 + + parse5-sax-parser@7.0.0: + dependencies: + parse5: 7.3.0 + + parse5@6.0.1: {} + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + parseurl@1.3.3: {} + + path-exists@4.0.0: {} + + path-exists@5.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 5.0.0 + + path-to-regexp@0.1.12: {} + + path-type@4.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.1: {} + + pify@4.0.1: + optional: true + + piscina@4.0.0: + dependencies: + eventemitter-asyncresource: 1.0.0 + hdr-histogram-js: 2.0.3 + hdr-histogram-percentiles-obj: 3.0.0 + optionalDependencies: + nice-napi: 1.0.2 + + pkg-dir@7.0.0: + dependencies: + find-up: 6.3.0 + + playwright-core@1.56.1: {} + + playwright@1.56.1: + dependencies: + playwright-core: 1.56.1 + optionalDependencies: + fsevents: 2.3.2 + + postcss-loader@7.3.3(postcss@8.4.31)(typescript@5.1.6)(webpack@5.94.0(esbuild@0.18.17)): + dependencies: + cosmiconfig: 8.3.6(typescript@5.1.6) + jiti: 1.21.7 + postcss: 8.4.31 + semver: 7.5.4 + webpack: 5.94.0(esbuild@0.18.17) + transitivePeerDependencies: + - typescript + + postcss-modules-extract-imports@3.1.0(postcss@8.4.31): + dependencies: + postcss: 8.4.31 + + postcss-modules-local-by-default@4.2.0(postcss@8.4.31): + dependencies: + icss-utils: 5.1.0(postcss@8.4.31) + postcss: 8.4.31 + postcss-selector-parser: 7.1.0 + postcss-value-parser: 4.2.0 + + postcss-modules-scope@3.2.1(postcss@8.4.31): + dependencies: + postcss: 8.4.31 + postcss-selector-parser: 7.1.0 + + postcss-modules-values@4.0.0(postcss@8.4.31): + dependencies: + icss-utils: 5.1.0(postcss@8.4.31) + postcss: 8.4.31 + + postcss-selector-parser@7.1.0: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier@2.8.8: {} + + pretty-bytes@5.6.0: {} + + proc-log@3.0.0: {} + + process-nextick-args@2.0.1: {} + + promise-inflight@1.0.1: {} + + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + prr@1.0.1: + optional: true + + psl@1.15.0: + dependencies: + punycode: 2.3.1 + + punycode@2.3.1: {} + + qs@6.13.0: + dependencies: + side-channel: 1.1.0 + + querystringify@2.2.0: {} + + queue-microtask@1.2.3: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + range-parser@1.2.1: {} + + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + read-package-json-fast@3.0.2: + dependencies: + json-parse-even-better-errors: 3.0.2 + npm-normalize-package-bin: 3.0.1 + + read-package-json@6.0.4: + dependencies: + glob: 10.4.5 + json-parse-even-better-errors: 3.0.2 + normalize-package-data: 5.0.0 + npm-normalize-package-bin: 3.0.1 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + reflect-metadata@0.1.14: {} + + regenerate-unicode-properties@10.2.2: + dependencies: + regenerate: 1.4.2 + + regenerate@1.4.2: {} + + regenerator-runtime@0.13.11: {} + + regex-parser@2.3.1: {} + + regexpu-core@6.4.0: + dependencies: + regenerate: 1.4.2 + regenerate-unicode-properties: 10.2.2 + regjsgen: 0.8.0 + regjsparser: 0.13.0 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.2.1 + + regjsgen@0.8.0: {} + + regjsparser@0.13.0: + dependencies: + jsesc: 3.1.0 + + replace-in-file@6.3.5: + dependencies: + chalk: 4.1.2 + glob: 7.2.3 + yargs: 17.7.2 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + requires-port@1.0.0: {} + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve-url-loader@5.0.0: + dependencies: + adjust-sourcemap-loader: 4.0.0 + convert-source-map: 1.9.0 + loader-utils: 2.0.4 + postcss: 8.4.31 + source-map: 0.6.1 + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@1.22.2: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + retry@0.12.0: {} + + retry@0.13.1: {} + + reusify@1.1.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rollup@3.29.5: + optionalDependencies: + fsevents: 2.3.3 + + run-async@2.4.1: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rxjs@7.8.1: + dependencies: + tslib: 2.8.1 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + sass-loader@13.3.2(sass@1.64.1)(webpack@5.94.0(esbuild@0.18.17)): + dependencies: + neo-async: 2.6.2 + webpack: 5.94.0(esbuild@0.18.17) + optionalDependencies: + sass: 1.64.1 + + sass@1.64.1: + dependencies: + chokidar: 3.5.3 + immutable: 4.3.7 + source-map-js: 1.2.1 + + sax@1.4.3: + optional: true + + saxes@5.0.1: + dependencies: + xmlchars: 2.2.0 + + schema-utils@3.3.0: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + + schema-utils@4.3.3: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.17.1 + ajv-formats: 2.1.1(ajv@8.17.1) + ajv-keywords: 5.1.0(ajv@8.17.1) + + select-hose@2.0.0: {} + + selfsigned@2.4.1: + dependencies: + '@types/node-forge': 1.3.14 + node-forge: 1.3.1 + + semver@5.7.2: + optional: true + + semver@6.3.1: {} + + semver@7.5.4: + dependencies: + lru-cache: 6.0.0 + + semver@7.7.3: {} + + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + serialize-javascript@6.0.2: + dependencies: + randombytes: 2.1.0 + + serve-index@1.9.1: + dependencies: + accepts: 1.3.8 + batch: 0.6.1 + debug: 2.6.9 + escape-html: 1.0.3 + http-errors: 1.6.3 + mime-types: 2.1.35 + parseurl: 1.3.3 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + + set-blocking@2.0.0: {} + + setprototypeof@1.1.0: {} + + setprototypeof@1.2.0: {} + + shallow-clone@3.0.1: + dependencies: + kind-of: 6.0.3 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.8.3: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + sigstore@1.9.0: + dependencies: + '@sigstore/bundle': 1.1.0 + '@sigstore/protobuf-specs': 0.2.1 + '@sigstore/sign': 1.0.0 + '@sigstore/tuf': 1.0.3 + make-fetch-happen: 11.1.1 + transitivePeerDependencies: + - supports-color + + sisteransi@1.0.5: {} + + slash@3.0.0: {} + + slash@4.0.0: {} + + smart-buffer@4.2.0: {} + + sockjs@0.3.24: + dependencies: + faye-websocket: 0.11.4 + uuid: 8.3.2 + websocket-driver: 0.7.4 + + socks-proxy-agent@7.0.0: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + socks: 2.8.7 + transitivePeerDependencies: + - supports-color + + socks@2.8.7: + dependencies: + ip-address: 10.1.0 + smart-buffer: 4.2.0 + + source-map-js@1.2.1: {} + + source-map-loader@4.0.1(webpack@5.94.0(esbuild@0.18.17)): + dependencies: + abab: 2.0.6 + iconv-lite: 0.6.3 + source-map-js: 1.2.1 + webpack: 5.94.0(esbuild@0.18.17) + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.7.4: {} + + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.22 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.22 + + spdx-license-ids@3.0.22: {} + + spdy-transport@3.0.0: + dependencies: + debug: 4.4.3 + detect-node: 2.1.0 + hpack.js: 2.1.6 + obuf: 1.1.2 + readable-stream: 3.6.2 + wbuf: 1.7.3 + transitivePeerDependencies: + - supports-color + + spdy@4.0.2: + dependencies: + debug: 4.4.3 + handle-thing: 2.0.1 + http-deceiver: 1.2.7 + select-hose: 2.0.0 + spdy-transport: 3.0.0 + transitivePeerDependencies: + - supports-color + + sprintf-js@1.0.3: {} + + ssri@10.0.6: + dependencies: + minipass: 7.1.2 + + ssri@9.0.1: + dependencies: + minipass: 3.3.6 + + statuses@1.5.0: {} + + statuses@2.0.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + strip-final-newline@2.0.0: {} + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + symbol-observable@4.0.0: {} + + symbol-tree@3.2.4: {} + + tapable@2.3.0: {} + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + terser-webpack-plugin@5.3.14(esbuild@0.18.17)(webpack@5.94.0): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + serialize-javascript: 6.0.2 + terser: 5.44.1 + webpack: 5.94.0(esbuild@0.18.17) + optionalDependencies: + esbuild: 0.18.17 + + terser@5.19.2: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.15.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + terser@5.44.1: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.15.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + + text-table@0.2.0: {} + + through@2.3.8: {} + + thunky@1.1.0: {} + + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tr46@2.1.0: + dependencies: + punycode: 2.3.1 + + tree-kill@1.2.2: {} + + ts-node@10.9.2(@types/node@18.19.130)(typescript@5.1.6): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 18.19.130 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.1.6 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + tslib@1.14.1: {} + + tslib@2.6.1: {} + + tslib@2.8.1: {} + + tsutils@3.21.0(typescript@5.1.6): + dependencies: + tslib: 1.14.1 + typescript: 5.1.6 + + tuf-js@1.1.7: + dependencies: + '@tufjs/models': 1.0.4 + debug: 4.4.3 + make-fetch-happen: 11.1.1 + transitivePeerDependencies: + - supports-color + + typanion@3.14.0: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + type-fest@0.21.3: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + typed-assert@1.0.9: {} + + typescript@5.1.6: {} + + undici-types@5.26.5: {} + + unicode-canonical-property-names-ecmascript@2.0.1: {} + + unicode-match-property-ecmascript@2.0.0: + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.1 + unicode-property-aliases-ecmascript: 2.2.0 + + unicode-match-property-value-ecmascript@2.2.1: {} + + unicode-property-aliases-ecmascript@2.2.0: {} + + unique-filename@2.0.1: + dependencies: + unique-slug: 3.0.0 + + unique-filename@3.0.0: + dependencies: + unique-slug: 4.0.0 + + unique-slug@3.0.0: + dependencies: + imurmurhash: 0.1.4 + + unique-slug@4.0.0: + dependencies: + imurmurhash: 0.1.4 + + universalify@0.2.0: {} + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + update-browserslist-db@1.1.4(browserslist@4.28.0): + dependencies: + browserslist: 4.28.0 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + util-deprecate@1.0.2: {} + + utils-merge@1.0.1: {} + + uuid@8.3.2: {} + + v8-compile-cache-lib@3.0.1: {} + + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + + validate-npm-package-name@5.0.1: {} + + vary@1.1.2: {} + + vite@4.5.5(@types/node@18.19.130)(less@4.1.3)(sass@1.64.1)(terser@5.19.2): + dependencies: + esbuild: 0.18.17 + postcss: 8.4.31 + rollup: 3.29.5 + optionalDependencies: + '@types/node': 18.19.130 + fsevents: 2.3.3 + less: 4.1.3 + sass: 1.64.1 + terser: 5.19.2 + + w3c-hr-time@1.0.2: + dependencies: + browser-process-hrtime: 1.0.0 + + w3c-xmlserializer@2.0.0: + dependencies: + xml-name-validator: 3.0.0 + + watchpack@2.4.4: + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + + wbuf@1.7.3: + dependencies: + minimalistic-assert: 1.0.1 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + webidl-conversions@5.0.0: {} + + webidl-conversions@6.1.0: {} + + webpack-dev-middleware@5.3.4(webpack@5.94.0): + dependencies: + colorette: 2.0.20 + memfs: 3.5.3 + mime-types: 2.1.35 + range-parser: 1.2.1 + schema-utils: 4.3.3 + webpack: 5.94.0(esbuild@0.18.17) + + webpack-dev-middleware@6.1.2(webpack@5.94.0(esbuild@0.18.17)): + dependencies: + colorette: 2.0.20 + memfs: 3.5.3 + mime-types: 2.1.35 + range-parser: 1.2.1 + schema-utils: 4.3.3 + optionalDependencies: + webpack: 5.94.0(esbuild@0.18.17) + + webpack-dev-server@4.15.1(webpack@5.94.0): + dependencies: + '@types/bonjour': 3.5.13 + '@types/connect-history-api-fallback': 1.5.4 + '@types/express': 4.17.25 + '@types/serve-index': 1.9.4 + '@types/serve-static': 1.15.10 + '@types/sockjs': 0.3.36 + '@types/ws': 8.18.1 + ansi-html-community: 0.0.8 + bonjour-service: 1.3.0 + chokidar: 3.5.3 + colorette: 2.0.20 + compression: 1.8.1 + connect-history-api-fallback: 2.0.0 + default-gateway: 6.0.3 + express: 4.21.2 + graceful-fs: 4.2.11 + html-entities: 2.6.0 + http-proxy-middleware: 2.0.9(@types/express@4.17.25) + ipaddr.js: 2.2.0 + launch-editor: 2.12.0 + open: 8.4.2 + p-retry: 4.6.2 + rimraf: 3.0.2 + schema-utils: 4.3.3 + selfsigned: 2.4.1 + serve-index: 1.9.1 + sockjs: 0.3.24 + spdy: 4.0.2 + webpack-dev-middleware: 5.3.4(webpack@5.94.0) + ws: 8.18.3 + optionalDependencies: + webpack: 5.94.0(esbuild@0.18.17) + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + + webpack-merge@5.9.0: + dependencies: + clone-deep: 4.0.1 + wildcard: 2.0.1 + + webpack-sources@3.3.3: {} + + webpack-subresource-integrity@5.1.0(webpack@5.94.0(esbuild@0.18.17)): + dependencies: + typed-assert: 1.0.9 + webpack: 5.94.0(esbuild@0.18.17) + + webpack@5.94.0(esbuild@0.18.17): + dependencies: + '@types/estree': 1.0.8 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) + browserslist: 4.28.0 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.18.3 + es-module-lexer: 1.7.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.1 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.3.0 + tapable: 2.3.0 + terser-webpack-plugin: 5.3.14(esbuild@0.18.17)(webpack@5.94.0) + watchpack: 2.4.4 + webpack-sources: 3.3.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + + websocket-driver@0.7.4: + dependencies: + http-parser-js: 0.5.10 + safe-buffer: 5.2.1 + websocket-extensions: 0.1.4 + + websocket-extensions@0.1.4: {} + + whatwg-encoding@1.0.5: + dependencies: + iconv-lite: 0.4.24 + + whatwg-mimetype@2.3.0: {} + + whatwg-url@8.7.0: + dependencies: + lodash: 4.17.21 + tr46: 2.1.0 + webidl-conversions: 6.1.0 + + which-pm-runs@1.1.0: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + which@3.0.1: + dependencies: + isexe: 2.0.0 + + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + + wildcard@2.0.1: {} + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + + wrappy@1.0.2: {} + + ws@7.5.10: {} + + ws@8.18.3: {} + + xml-name-validator@3.0.0: {} + + xmlchars@2.2.0: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yallist@4.0.0: {} + + yaml@2.8.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yn@3.1.1: {} + + yocto-queue@0.1.0: {} + + yocto-queue@1.2.1: {} + + zone.js@0.13.3: dependencies: - tslib: 2.5.3 + tslib: 2.8.1 diff --git a/Code/skyrim_ui/src/app/app.module.ts b/Code/skyrim_ui/src/app/app.module.ts index e27772139..5632f1029 100644 --- a/Code/skyrim_ui/src/app/app.module.ts +++ b/Code/skyrim_ui/src/app/app.module.ts @@ -11,6 +11,7 @@ import { ActionButtonsComponent } from './components/action-buttons/action-butto import { ChatComponent } from './components/chat/chat.component'; import { ConnectComponent } from './components/connect/connect.component'; import { ConnectPasswordComponent } from './components/connect-password/connect-password.component'; +import { DeathScreenComponent } from './components/death-screen/death-screen.component'; import { DebugComponent } from './components/debug/debug.component'; import { DisconnectComponent } from './components/disconnect/disconnect.component'; import { DropdownOptionComponent } from './components/dropdown/dropdown-option.component'; @@ -23,14 +24,21 @@ import { NotificationPopupComponent } from './components/notification-popup/noti import { NotificationsComponent } from './components/notifications/notifications.component'; import { OrderComponent } from './components/order/order.component'; import { PartyMenuComponent } from './components/party-menu/party-menu.component'; +import { PartyOptionsComponent } from './components/party-options/party-options.component'; import { PlayerListComponent } from './components/player-list/player-list.component'; import { PlayerManagerComponent } from './components/player-manager/player-manager.component'; +import { TradePopupComponent } from './components/trade-popup/trade-popup.component'; +import { EmoteMenuComponent } from './components/emote-menu/emote-menu.component'; import { PopupComponent } from './components/popup/popup.component'; import { RootComponent } from './components/root/root.component'; import { ServerListComponent } from './components/server-list/server-list.component'; import { SettingsComponent } from './components/settings/settings.component'; +import { SyncStatusBadgeComponent } from './components/sync-status-badge/sync-status-badge.component'; import { ToggleComponent } from './components/toggle/toggle.component'; import { WindowComponent } from './components/window/window.component'; +import { PartyPinsComponent } from './components/party-pins/party-pins.component'; +import { OverlayBannerComponent } from './components/overlay-banner/overlay-banner.component'; +import { ReviveProgressComponent } from './components/revive-progress/revive-progress.component'; import { HealthDirective } from './directives/health.directive'; import { RadioDirective } from './directives/radio.directive'; import { SliderDirective } from './directives/slider.directive'; @@ -51,6 +59,7 @@ import { TranslocoRootModule } from './transloco-root.module'; ChatComponent, ConnectComponent, ConnectPasswordComponent, + DeathScreenComponent, DebugComponent, DisconnectComponent, DropdownComponent, @@ -63,15 +72,22 @@ import { TranslocoRootModule } from './transloco-root.module'; NotificationsComponent, OrderComponent, PartyMenuComponent, + PartyOptionsComponent, PlayerListComponent, PlayerManagerComponent, + TradePopupComponent, + EmoteMenuComponent, ActionButtonsComponent, PopupComponent, RootComponent, + SyncStatusBadgeComponent, ServerListComponent, SettingsComponent, ToggleComponent, WindowComponent, + PartyPinsComponent, + OverlayBannerComponent, + ReviveProgressComponent, CheckboxDirective, HealthDirective, diff --git a/Code/skyrim_ui/src/app/components/chat/chat.component.scss b/Code/skyrim_ui/src/app/components/chat/chat.component.scss index d8a07a5c9..6476a8775 100644 --- a/Code/skyrim_ui/src/app/components/chat/chat.component.scss +++ b/Code/skyrim_ui/src/app/components/chat/chat.component.scss @@ -47,6 +47,12 @@ color: #eeff00; } } + &.whisper { + .app-chat-message { + color: #c0c0c0; + font-style: italic; + } + } } } diff --git a/Code/skyrim_ui/src/app/components/chat/chat.component.ts b/Code/skyrim_ui/src/app/components/chat/chat.component.ts index 4e09f2e3f..ceecc96f8 100644 --- a/Code/skyrim_ui/src/app/components/chat/chat.component.ts +++ b/Code/skyrim_ui/src/app/components/chat/chat.component.ts @@ -38,6 +38,9 @@ function messageTypeToClassName(type: MessageTypes): string { case MessageTypes.LOCAL_CHAT: return 'local'; + case MessageTypes.WHISPER: + return 'whisper'; + default: return 'global'; } diff --git a/Code/skyrim_ui/src/app/components/connect-password/connect-password.component.html b/Code/skyrim_ui/src/app/components/connect-password/connect-password.component.html index 8540395b0..c5823d6d5 100644 --- a/Code/skyrim_ui/src/app/components/connect-password/connect-password.component.html +++ b/Code/skyrim_ui/src/app/components/connect-password/connect-password.component.html @@ -1,36 +1,54 @@ -
- - - - - - - - - - +
+ + + + + + + + + + + - - - - -
+ + + + +
diff --git a/Code/skyrim_ui/src/app/components/connect-password/connect-password.component.ts b/Code/skyrim_ui/src/app/components/connect-password/connect-password.component.ts index 7c7b40ef0..be4c73876 100644 --- a/Code/skyrim_ui/src/app/components/connect-password/connect-password.component.ts +++ b/Code/skyrim_ui/src/app/components/connect-password/connect-password.component.ts @@ -25,9 +25,10 @@ import { DestroyService } from '../../services/destroy.service'; }) export class ConnectPasswordComponent implements AfterViewInit { public address = ''; - public name = ''; + public username = ''; public port = 10578; - public password = ''; + public accountPassword = ''; + public serverPassword = ''; public savePassword = false; public hidePassword = true; @@ -80,11 +81,45 @@ export class ConnectPasswordComponent implements AfterViewInit { } }); - this.name = this.uiRepository.getConnectName(); - this.address = this.uiRepository.getConnectIp(); - this.port = this.uiRepository.getConnectPort(); - this.password = this.getStoredPasswordForAddress(this.address, this.port); - this.savePassword = this.password !== ''; + const connectIp = this.uiRepository.getConnectIp(); + if (connectIp) { + this.address = connectIp; + } + + const connectPort = this.uiRepository.getConnectPort(); + if (Number.isFinite(connectPort)) { + this.port = connectPort; + } + + const savedEntry = this.getSavedServerEntry(this.address, this.port); + const storedUsername = this.storeService.get('last_connected_username', ''); + const storedAccountPassword = this.storeService.get( + 'last_connected_password', + '', + ); + const repoUsername = this.uiRepository.getConnectUsername(); + const repoAccountPassword = this.uiRepository.getConnectAccountPassword(); + + this.username = + (repoUsername && repoUsername.length > 0 + ? repoUsername + : savedEntry?.username) || + storedUsername || + this.username; + + this.accountPassword = + (repoAccountPassword && repoAccountPassword.length > 0 + ? repoAccountPassword + : savedEntry?.accountPassword) || + storedAccountPassword || + this.accountPassword; + + this.serverPassword = savedEntry?.serverPassword ?? this.serverPassword; + + this.savePassword = + (!!this.username && this.username.length > 0) || + (!!this.accountPassword && this.accountPassword.length > 0) || + (!!this.serverPassword && this.serverPassword.length > 0); } public ngAfterViewInit(): void { @@ -105,21 +140,63 @@ export class ConnectPasswordComponent implements AfterViewInit { return; } + const username = (this.username ?? '').trim(); + + if (username.length === 0) { + this.sound.play(Sound.Fail); + const message = await firstValueFrom( + this.translocoService.selectTranslate( + 'COMPONENT.CONNECT.ERROR.INVALID_USERNAME', + ), + ); + await this.errorService.setError(message); + return; + } + this.connecting = true; this.sound.play(Sound.Ok); if (this.savePassword) { - this.storePasswordLocally(this.address, this.port, this.password); + this.saveServerEntry(this.address, this.port, { + username, + accountPassword: this.accountPassword, + serverPassword: this.serverPassword, + }); + this.storeService.set('last_connected_username', username); + this.storeService.set('last_connected_password', this.accountPassword); + } else { + this.removeSavedServerEntry(this.address, this.port); + this.storeService.remove('last_connected_username'); + this.storeService.remove('last_connected_password'); } + this.username = username; - this.client.connect(this.address, this.port, this.password); + this.client.connect( + this.address, + this.port, + username, + this.accountPassword, + this.serverPassword, + ); } public cancel(): void { - this.uiRepository.openView(View.SERVER_LIST); + const returnView = this.uiRepository.getConnectReturnView() ?? View.CONNECT; + this.uiRepository.openView(returnView); } - private getStoredPasswordForAddress(ip: string, port: number): string { + private getSavedServerEntry( + ip: string, + port: number, + ): + | { + ip: string; + port: number; + username?: string; + accountPassword?: string; + serverPassword?: string; + } + | undefined { let savedServerList = JSON.parse( this.storeService.get('savedServerList', '[]'), ); @@ -127,13 +204,27 @@ export class ConnectPasswordComponent implements AfterViewInit { saved => saved.ip === ip && saved.port === port, ); - return savedServer?.password ?? ''; + if (savedServer) { + if ( + savedServer.password && + (!savedServer.serverPassword || savedServer.serverPassword.length === 0) + ) { + savedServer.serverPassword = savedServer.password; + } + return savedServer; + } + + return undefined; } - private storePasswordLocally( + private saveServerEntry( ip: string, port: number, - password: string, + entry: { + username: string; + accountPassword: string; + serverPassword: string; + }, ): void { let savedServerList = JSON.parse( this.storeService.get('savedServerList', '[]'), @@ -142,14 +233,32 @@ export class ConnectPasswordComponent implements AfterViewInit { saved => saved.ip === ip && saved.port === port, ); - if (savedServer) { - savedServer.password = password; - } else { - savedServerList.push({ ip: ip, port: port, password: password }); + if (!savedServer) { + savedServer = { ip, port }; + savedServerList.push(savedServer); } + + savedServer.username = entry.username; + savedServer.accountPassword = entry.accountPassword; + savedServer.serverPassword = entry.serverPassword; + if (savedServer.password) { + delete savedServer.password; + } + this.storeService.set('savedServerList', JSON.stringify(savedServerList)); } + private removeSavedServerEntry(ip: string, port: number): void { + let savedServerList = JSON.parse( + this.storeService.get('savedServerList', '[]'), + ); + const filtered = savedServerList.filter( + saved => !(saved.ip === ip && saved.port === port), + ); + + this.storeService.set('savedServerList', JSON.stringify(filtered)); + } + @HostListener('window:keydown.escape', ['$event']) // @ts-ignore private activate(event: KeyboardEvent): void { diff --git a/Code/skyrim_ui/src/app/components/connect/connect.component.html b/Code/skyrim_ui/src/app/components/connect/connect.component.html index b6bdddf2c..9790c5d9d 100644 --- a/Code/skyrim_ui/src/app/components/connect/connect.component.html +++ b/Code/skyrim_ui/src/app/components/connect/connect.component.html @@ -16,7 +16,14 @@ /> + 0) || + (!!savedPassword && savedPassword.length > 0); } public ngAfterViewInit(): void { @@ -105,18 +115,42 @@ export class ConnectComponent implements OnDestroy, AfterViewInit { this.connecting = true; + const username = this.username.trim(); + + if (username.length === 0) { + this.sound.play(Sound.Fail); + const message = await firstValueFrom( + this.translocoService.selectTranslate( + 'COMPONENT.CONNECT.ERROR.INVALID_USERNAME', + ), + ); + await this.errorService.setError(message); + return; + } + + this.username = username; + this.storeService.set('last_connected_address', this.address); if (this.savePassword) { + this.storeService.set('last_connected_username', username); this.storeService.set('last_connected_password', this.password); } else { + this.storeService.remove('last_connected_username'); this.storeService.remove('last_connected_password'); } + const port = address[2] ? Number.parseInt(address[2], 10) : 10578; + const savedEntry = this.getSavedServerEntry(address[1], port); + const serverPassword = + savedEntry?.serverPassword ?? savedEntry?.password ?? ''; + this.sound.play(Sound.Ok); this.client.connect( address[1], - address[2] ? Number.parseInt(address[2]) : 10578, + port, + username, this.password, + serverPassword, ); } @@ -148,4 +182,37 @@ export class ConnectComponent implements OnDestroy, AfterViewInit { event.stopPropagation(); event.preventDefault(); } + + private getSavedServerEntry( + ip: string, + port: number, + ): + | { + ip: string; + port: number; + username?: string; + accountPassword?: string; + serverPassword?: string; + password?: string; + } + | undefined { + const savedServerList = JSON.parse( + this.storeService.get('savedServerList', '[]'), + ); + const savedServer = savedServerList.find( + (saved: any) => saved.ip === ip && saved.port === port, + ); + + if (savedServer) { + if ( + savedServer.password && + (!savedServer.serverPassword || savedServer.serverPassword.length === 0) + ) { + savedServer.serverPassword = savedServer.password; + } + return savedServer; + } + + return undefined; + } } diff --git a/Code/skyrim_ui/src/app/components/death-screen/death-screen.component.html b/Code/skyrim_ui/src/app/components/death-screen/death-screen.component.html new file mode 100644 index 000000000..62147e17b --- /dev/null +++ b/Code/skyrim_ui/src/app/components/death-screen/death-screen.component.html @@ -0,0 +1,74 @@ +
+
+
+
+
+
+
+

Spirit Released

+

YOU HAVE FALLEN

+
+
+ +
+
+ Respawn Cooldown + {{ secondsRemaining$ | async }}s + Respawn at the entrance once this reaches zero. +
+
+ Entrance Respawn + + {{ (isRespawnButtonEnabled$ | async) ? 'Ready' : 'Charging' }} + + Hold tight or return to the entrance immediately. +
+
+ + +
+
+ + {{ reviveState.healerName || 'An ally' }} is reviving you + + + {{ reviveState.remainingSeconds | number : '1.0-1' }}s remaining + +
+
+
+
+
+ Only Healing Hands revives in place. Channel time + shortens with the healer's Restoration skill. +
+
+
+ + +

+ Wait for a party member to channel Healing Hands for + at least 15 seconds (faster with higher Restoration), or respawn at + the entrance once the cooldown ends. +

+
+ + +
+
+
diff --git a/Code/skyrim_ui/src/app/components/death-screen/death-screen.component.scss b/Code/skyrim_ui/src/app/components/death-screen/death-screen.component.scss new file mode 100644 index 000000000..833cc7df3 --- /dev/null +++ b/Code/skyrim_ui/src/app/components/death-screen/death-screen.component.scss @@ -0,0 +1,203 @@ +.death-screen { + position: fixed; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; + z-index: 9999; + pointer-events: none; +} + +.death-overlay { + position: absolute; + inset: 0; + background: linear-gradient(135deg, rgba(2, 6, 14, 0.9), rgba(15, 8, 12, 0.85)); + backdrop-filter: blur(6px); +} + +.death-content { + position: relative; + z-index: 1; + width: min(540px, 92vw); + pointer-events: auto; +} + +.death-card { + background: rgba(9, 12, 20, 0.95); + border-radius: 24px; + padding: 2.5rem; + border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: 0 45px 120px rgba(0, 0, 0, 0.55); +} + +.card-header { + display: flex; + align-items: center; + gap: 1.5rem; + margin-bottom: 2rem; +} + +.skull-icon { + width: 72px; + height: 72px; + border-radius: 50%; + background: radial-gradient(circle at 30% 30%, #ffcdd2, #c62828); + position: relative; + box-shadow: 0 12px 30px rgba(198, 40, 40, 0.4); +} + +.skull-icon::before, +.skull-icon::after { + content: ''; + position: absolute; + width: 12px; + height: 16px; + border-radius: 50%; + background: #12060b; + top: 22px; +} + +.skull-icon::before { + left: 18px; +} + +.skull-icon::after { + right: 18px; + box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.4); +} + +.title-group h1 { + margin: 0; + font-size: 2.6rem; + letter-spacing: 0.4rem; + color: #fff; + text-transform: uppercase; +} + +.subtitle { + margin: 0; + text-transform: uppercase; + letter-spacing: 0.45rem; + font-size: 0.85rem; + color: rgba(255, 255, 255, 0.6); +} + +.status-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 1rem; + margin-bottom: 1.5rem; +} + +.status-tile { + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 18px; + padding: 1.25rem; + background: rgba(255, 255, 255, 0.02); + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.status-tile.ready { + border-color: rgba(67, 160, 71, 0.5); + box-shadow: 0 0 20px rgba(67, 160, 71, 0.25); +} + +.status-tile .label { + text-transform: uppercase; + font-size: 0.8rem; + letter-spacing: 0.3rem; + color: rgba(255, 255, 255, 0.5); +} + +.status-tile .value { + font-size: 1.8rem; + font-weight: 700; + color: #fff; +} + +.status-tile small { + color: rgba(255, 255, 255, 0.45); +} + +.revive-panel { + margin: 1.5rem 0; + padding: 1.5rem; + border-radius: 20px; + border: 1px solid rgba(255, 255, 255, 0.08); + background: linear-gradient(135deg, rgba(20, 15, 34, 0.9), rgba(8, 20, 34, 0.8)); +} + +.panel-header { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 1rem; +} + +.panel-header .title { + font-weight: 600; + letter-spacing: 0.2rem; + text-transform: uppercase; + color: #fff; +} + +.panel-header .eta { + font-size: 0.95rem; + color: rgba(255, 255, 255, 0.7); +} + +.progress-track { + width: 100%; + height: 12px; + margin: 1rem 0; + border-radius: 999px; + background: rgba(255, 255, 255, 0.08); + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, #7c4dff, #00bcd4); + box-shadow: 0 8px 20px rgba(0, 188, 212, 0.45); + border-radius: inherit; + transition: width 0.2s ease-out; +} + +.panel-footnote { + font-size: 0.85rem; + color: rgba(255, 255, 255, 0.65); +} + +.support-text { + margin: 1rem 0 1.5rem; + color: rgba(255, 255, 255, 0.65); + line-height: 1.5; +} + +.respawn-button { + width: 100%; + padding: 1.1rem; + border-radius: 999px; + border: none; + background: linear-gradient(90deg, #ef5350, #d32f2f); + color: #fff; + font-size: 1rem; + font-weight: 600; + letter-spacing: 0.3rem; + text-transform: uppercase; + cursor: pointer; + transition: transform 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease; +} + +.respawn-button.ready { + box-shadow: 0 14px 30px rgba(211, 47, 47, 0.45); +} + +.respawn-button:disabled { + opacity: 0.45; + cursor: not-allowed; + box-shadow: none; +} diff --git a/Code/skyrim_ui/src/app/components/death-screen/death-screen.component.ts b/Code/skyrim_ui/src/app/components/death-screen/death-screen.component.ts new file mode 100644 index 000000000..be3aa3713 --- /dev/null +++ b/Code/skyrim_ui/src/app/components/death-screen/death-screen.component.ts @@ -0,0 +1,112 @@ +import { Component, OnInit, NgZone } from '@angular/core'; +import { Observable, BehaviorSubject } from 'rxjs'; +import { ClientService } from '../../services/client.service'; + +interface ReviveUiState { + progress: number; + remainingSeconds: number; + totalSeconds: number; + healerName?: string; +} + +@Component({ + selector: 'app-death-screen', + templateUrl: './death-screen.component.html', + styleUrls: ['./death-screen.component.scss'], +}) +export class DeathScreenComponent implements OnInit { + isVisible$: Observable; + secondsRemaining$: Observable; + isRespawnButtonEnabled$: Observable; + reviveState$: Observable; + + private visibilitySubject = new BehaviorSubject(false); + private timerSubject = new BehaviorSubject(0); + private buttonEnabledSubject = new BehaviorSubject(false); + private reviveStateSubject = new BehaviorSubject( + undefined, + ); + + constructor( + private readonly clientService: ClientService, + private readonly ngZone: NgZone, + ) { + // Initialize with false/0 values + this.isVisible$ = this.visibilitySubject.asObservable(); + this.secondsRemaining$ = this.timerSubject.asObservable(); + this.isRespawnButtonEnabled$ = this.buttonEnabledSubject.asObservable(); + this.reviveState$ = this.reviveStateSubject.asObservable(); + } + + ngOnInit(): void { + // Track when death screen is shown - capture the initial timer value + this.clientService.deathScreenChange.subscribe( + (secondsRemaining: number) => { + this.ngZone.run(() => { + this.timerSubject.next(secondsRemaining); + this.visibilitySubject.next(true); + this.buttonEnabledSubject.next(false); + }); + }, + ); + + // Track timer updates + this.clientService.deathTimerChange.subscribe( + (secondsRemaining: number) => { + this.ngZone.run(() => { + this.timerSubject.next(secondsRemaining); + }); + }, + ); + + // Track respawn button enabled + this.clientService.respawnButtonEnabledChange.subscribe(() => { + this.ngZone.run(() => { + this.buttonEnabledSubject.next(true); + }); + }); + + // Track death screen hidden + this.clientService.deathScreenHiddenChange.subscribe(() => { + this.ngZone.run(() => { + this.visibilitySubject.next(false); + this.timerSubject.next(0); + this.buttonEnabledSubject.next(false); + this.reviveStateSubject.next(undefined); + }); + }); + + this.clientService.reviveVictimProgressChange.subscribe(payload => { + this.ngZone.run(() => { + if (!payload) { + this.reviveStateSubject.next(undefined); + return; + } + + const progress = + payload.totalSeconds > 0 + ? Math.min(payload.elapsedSeconds / payload.totalSeconds, 1) + : 0; + const remaining = Math.max( + payload.totalSeconds - payload.elapsedSeconds, + 0, + ); + + this.reviveStateSubject.next({ + progress, + remainingSeconds: remaining, + totalSeconds: payload.totalSeconds, + healerName: payload.label, + }); + }); + }); + } + + onRespawnClicked(): void { + try { + this.clientService.respawnButtonClicked(); + } catch (error) { + console.error('Error calling respawn button:', error); + } + } +} diff --git a/Code/skyrim_ui/src/app/components/emote-menu/emote-menu.component.html b/Code/skyrim_ui/src/app/components/emote-menu/emote-menu.component.html new file mode 100644 index 000000000..13f3b14f9 --- /dev/null +++ b/Code/skyrim_ui/src/app/components/emote-menu/emote-menu.component.html @@ -0,0 +1,82 @@ +
+
+
+ {{ 'COMPONENT.EMOTE_MENU.EYEBROW' | transloco }} +
+

{{ 'COMPONENT.EMOTE_MENU.TITLE' | transloco }}

+

+ {{ 'COMPONENT.EMOTE_MENU.SUBTITLE' | transloco }} +

+
+ +
+
+ +
+
+
+ +
+ {{ + 'COMPONENT.EMOTE_MENU.EMOTES.' + hoveredEmote.key + '.TITLE' + | transloco + }} +
+
+ {{ + 'COMPONENT.EMOTE_MENU.EMOTES.' + hoveredEmote.key + '.DESC' + | transloco + }} +
+
+ +
+ {{ 'COMPONENT.EMOTE_MENU.HINT' | transloco }} +
+
B
+
+
+ + + + + +
+ {{ 'COMPONENT.EMOTE_MENU.NO_RESULTS' | transloco }} +
+
+
+ + + + +
diff --git a/Code/skyrim_ui/src/app/components/emote-menu/emote-menu.component.scss b/Code/skyrim_ui/src/app/components/emote-menu/emote-menu.component.scss new file mode 100644 index 000000000..a33165759 --- /dev/null +++ b/Code/skyrim_ui/src/app/components/emote-menu/emote-menu.component.scss @@ -0,0 +1,250 @@ +@import 'env'; +@import 'functions/mod'; +@import 'functions/rem'; + +$-color-primary: #8ac8ff; + +:host { + display: block; +} + +.emote-menu { + display: flex; + flex-direction: column; + gap: $-size-gap; +} + +.emote-menu__header { + display: grid; + gap: rem(16, 4); + + .eyebrow { + letter-spacing: 0.1em; + font-size: rem(16, 11); + text-transform: uppercase; + color: mod($-color-primary, 0.3); + } + + h2 { + margin: 0; + font-size: rem(16, 24); + letter-spacing: 0.03em; + text-transform: uppercase; + color: mod($-color-primary, 0.1); + } + + .subtitle { + margin: 0; + color: mod($-color-background, 0.5); + } +} + +.emote-filters { + display: flex; + flex-wrap: wrap; + gap: rem(16, 8); + margin-top: rem(16, 6); +} + +.emote-filter { + padding: rem(16, 8) rem(16, 12); + border-radius: rem(16, 12); + border: 1px solid mod($-color-background, 0.4); + background: mod($-color-background, 0.2); + color: mod($-color-primary, 0.08); + letter-spacing: 0.05em; + text-transform: uppercase; + font-size: rem(16, 12); + transition: border-color 120ms ease, transform 120ms ease, + background-color 120ms ease; + + &[data-active='true'] { + border-color: mod($-color-primary, 0.1); + background: rgba(138, 200, 255, 0.12); + transform: translateY(-2px); + } +} + +.emote-wheel { + position: relative; + width: clamp(18rem, 50vw, 30rem); + aspect-ratio: 1; + margin: 0.5rem auto; + display: grid; + place-items: center; + isolation: isolate; + overflow: visible; +} + +.emote-wheel__ring { + position: absolute; + inset: 8%; + border-radius: 50%; + background: radial-gradient(circle at 50% 35%, rgba(255, 255, 255, 0.07), transparent), + conic-gradient( + from 90deg, + rgba(138, 200, 255, 0.35), + rgba(52, 82, 120, 0.4), + rgba(138, 200, 255, 0.35), + rgba(90, 170, 220, 0.42) + ); + filter: blur(10px); + opacity: 0.7; + z-index: 0; +} + +.emote-wheel__center { + position: relative; + z-index: 2; + width: 9rem; + height: 9rem; + border-radius: 50%; + background: radial-gradient(circle at 35% 30%, rgba(255, 255, 255, 0.12), rgba(0, 0, 0, 0.45)); + border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: 0 0 40px rgba(0, 0, 0, 0.45); + display: grid; + place-items: center; + text-align: center; + padding: 0.75rem; +} + +.emote-wheel__current-title { + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: mod($-color-primary, 0.05); + margin-bottom: rem(16, 4); +} + +.emote-wheel__current-desc { + font-size: rem(16, 12); + color: mod($-color-background, 0.5); + line-height: 1.3; +} + +.emote-wheel__hint { + font-size: rem(16, 11); + letter-spacing: 0.08em; + text-transform: uppercase; + color: mod($-color-background, 0.5); +} + +.emote-wheel__key { + margin-top: 0.35rem; + padding: 0.35rem 0.8rem; + border-radius: 999px; + background: rgba(255, 255, 255, 0.08); + color: $-color-primary; + border: 1px solid rgba(255, 255, 255, 0.12); + font-weight: 700; + letter-spacing: 0.05em; +} + +.emote-wheel__slice { + --i: 0; + --total: 1; + position: absolute; + width: 48%; + height: 48%; + inset: 0; + margin: auto; + border: none; + background: none; + color: white; + cursor: pointer; + transform: rotate(calc(360deg / var(--total) * var(--i))) + translateY(-48%) + rotate(calc(-360deg / var(--total) * var(--i))); + transition: transform 120ms ease, filter 120ms ease, opacity 120ms ease; + display: grid; + place-items: center; + text-align: center; + padding: 0.5rem; + z-index: 1; + opacity: 0.85; + border-radius: 12px; + + .emote-wheel__label { + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: mod($-color-background, 0.1); + background: rgba(0, 0, 0, 0.35); + padding: 0.4rem 0.55rem; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.1); + } + + .emote-wheel__desc { + margin-top: 0.35rem; + font-size: rem(16, 12); + color: mod($-color-background, 0.45); + max-width: 12ch; + line-height: 1.2; + } + + &:hover { + transform: rotate(calc(360deg / var(--total) * var(--i))) + translateY(-60%) + rotate(calc(-360deg / var(--total) * var(--i))); + filter: drop-shadow(0 0 10px rgba(138, 200, 255, 0.4)); + opacity: 1; + } +} + +.emote-wheel__slice::after { + content: ''; + position: absolute; + width: 55%; + height: 55%; + border-radius: 50%; + inset: 0; + margin: auto; + background: conic-gradient( + from calc(360deg / var(--total) * var(--i)), + rgba(255, 255, 255, 0.08), + transparent 70% + ); + opacity: 0.5; + z-index: -1; +} + +.emote-wheel__empty { + position: absolute; + inset: 0; + display: grid; + place-items: center; + color: mod($-color-background, 0.4); + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.emote-wheel__slice[data-tone='sun'] .emote-wheel__label { + background: rgba(255, 196, 99, 0.25); + color: #ffd9a1; +} + +.emote-wheel__slice[data-tone='ember'] .emote-wheel__label { + background: rgba(255, 125, 69, 0.24); + color: #ffc7aa; +} + +.emote-wheel__slice[data-tone='mystic'] .emote-wheel__label { + background: rgba(116, 140, 255, 0.26); + color: #d1dbff; +} + +.emote-wheel__slice[data-tone='bard'] .emote-wheel__label { + background: rgba(162, 88, 255, 0.24); + color: #ead6ff; +} + +.emote-wheel__slice[data-tone='ale'] .emote-wheel__label { + background: rgba(156, 117, 62, 0.28); + color: #ffe5c5; +} + +.emote-wheel__slice[data-tone='calm'] .emote-wheel__label { + background: rgba(109, 131, 118, 0.26); + color: #d6f2df; +} diff --git a/Code/skyrim_ui/src/app/components/emote-menu/emote-menu.component.ts b/Code/skyrim_ui/src/app/components/emote-menu/emote-menu.component.ts new file mode 100644 index 000000000..af1d76cd2 --- /dev/null +++ b/Code/skyrim_ui/src/app/components/emote-menu/emote-menu.component.ts @@ -0,0 +1,195 @@ +import { Component, EventEmitter, Output } from '@angular/core'; +import { ClientService } from '../../services/client.service'; +import { Sound, SoundService } from '../../services/sound.service'; +import { UiRepository } from '../../store/ui.repository'; + +interface EmoteDefinition { + key: string; + event: string; + tone: 'sun' | 'ember' | 'mystic' | 'bard' | 'calm' | 'ale'; + category: 'greeting' | 'music' | 'social' | 'relax' | 'misc'; +} + +@Component({ + selector: 'app-emote-menu', + templateUrl: './emote-menu.component.html', + styleUrls: ['./emote-menu.component.scss'], +}) +export class EmoteMenuComponent { + @Output() public done = new EventEmitter(); + + public emotes: EmoteDefinition[] = [ + { + key: 'WAVE', + event: 'IdleGetAttention', + tone: 'sun', + category: 'greeting', + }, + { + key: 'PRAY', + event: 'IdlePrayCrouchedEnter', + tone: 'mystic', + category: 'relax', + }, + { + key: 'WARM_HANDS', + event: 'IdleWarmHands', + tone: 'ember', + category: 'relax', + }, + { + key: 'SIT', + event: 'IdleSitCrossLeggedEnter', + tone: 'calm', + category: 'relax', + }, + { + key: 'LEAN', + event: 'IdleLeanTableEnter', + tone: 'calm', + category: 'relax', + }, + { + key: 'HANDS_BEHIND_BACK', + event: 'IdleHandsBehindBack', + tone: 'calm', + category: 'social', + }, + { + key: 'DRINK', + event: 'IdleTableDrinkEnter', + tone: 'ale', + category: 'social', + }, + { + key: 'EAT', + event: 'idleEatingStandingStart', + tone: 'sun', + category: 'social', + }, + { + key: 'CLAP', + event: 'IdleApplaud2', + tone: 'sun', + category: 'social', + }, + { + key: 'CHEER', + event: 'IdleCivilWarCheer', + tone: 'sun', + category: 'greeting', + }, + { + key: 'LAUGH', + event: 'IdleLaugh', + tone: 'sun', + category: 'social', + }, + { + key: 'DANCE_CICERO_1', + event: 'IdleCiceroDance1', + tone: 'bard', + category: 'music', + }, + { + key: 'DANCE_CICERO_2', + event: 'IdleCiceroDance2', + tone: 'bard', + category: 'music', + }, + { + key: 'DANCE_CICERO_3', + event: 'IdleCiceroDance3', + tone: 'bard', + category: 'music', + }, + { key: 'DRUM', event: 'IdleDrumStart', tone: 'bard', category: 'music' }, + { key: 'FLUTE', event: 'IdleFluteStart', tone: 'bard', category: 'music' }, + { key: 'LUTE', event: 'IdleLuteStart', tone: 'bard', category: 'music' }, + { + key: 'WALL_LEAN', + event: 'IdleWallLeanStart', + tone: 'calm', + category: 'relax', + }, + { + key: 'COWER', + event: 'IdleCowerEnter', + tone: 'mystic', + category: 'misc', + }, + { + key: 'BEGGAR', + event: 'IdleBeggar', + tone: 'calm', + category: 'misc', + }, + { + key: 'SWEEP', + event: 'idleLooseSweepingStart', + tone: 'ember', + category: 'misc', + }, + { + key: 'STOP', + event: 'IdleForceDefaultState', + tone: 'calm', + category: 'misc', + }, + ]; + + public activeCategory: EmoteDefinition['category'] = 'greeting'; + public hoveredEmote?: EmoteDefinition; + public filteredList: EmoteDefinition[] = []; + + public constructor( + private readonly client: ClientService, + private readonly sound: SoundService, + private readonly uiRepository: UiRepository, + ) { + const saved = this.uiRepository.getEmoteCategory(); + if ( + saved === 'greeting' || + saved === 'music' || + saved === 'social' || + saved === 'relax' || + saved === 'misc' + ) { + this.activeCategory = saved as EmoteDefinition['category']; + } else { + this.uiRepository.setEmoteCategory(this.activeCategory); + } + this.updateFiltered(); + } + + public categories(): Array { + return ['greeting', 'music', 'social', 'relax', 'misc']; + } + + private updateFiltered(): void { + this.filteredList = this.emotes.filter( + e => e.category === this.activeCategory, + ); + } + + public setCategory(category: EmoteDefinition['category']): void { + this.activeCategory = category; + this.uiRepository.setEmoteCategory(category); + this.sound.play(Sound.Focus); + this.updateFiltered(); + } + + public hover(emote?: EmoteDefinition): void { + this.hoveredEmote = emote; + } + + public play(emote: EmoteDefinition): void { + this.client.playEmote(emote.event); + this.sound.play(Sound.Focus); + this.hoveredEmote = emote; + } + + public emoteTrackBy(_index: number, emote: EmoteDefinition): string { + return emote.key; + } +} diff --git a/Code/skyrim_ui/src/app/components/group/group.component.html b/Code/skyrim_ui/src/app/components/group/group.component.html index 522ea6325..7da5efceb 100644 --- a/Code/skyrim_ui/src/app/components/group/group.component.html +++ b/Code/skyrim_ui/src/app/components/group/group.component.html @@ -3,6 +3,10 @@ *ngIf="group$ | async" class="group-container" [ngStyle]="positionStyle | async" + [ngClass]="{ + 'layout-classic': (settings.partyLayout | async) === partyLayout.CLASSIC, + 'layout-compact': (settings.partyLayout | async) === partyLayout.COMPACT + }" >
- {{ player.level }} -
-
- {{ player.name }} -
+ +
+
+ {{ player.level }} + +
+
+ + {{ player.name }} + (You) + +
+ *ngIf=" + (settings.partyShowHealth | async) && + player.health !== undefined && + player.isLoaded + " + class="group-member-health" + > +
+
diff --git a/Code/skyrim_ui/src/app/components/group/group.component.scss b/Code/skyrim_ui/src/app/components/group/group.component.scss index dc033cdf9..f690091d1 100644 --- a/Code/skyrim_ui/src/app/components/group/group.component.scss +++ b/Code/skyrim_ui/src/app/components/group/group.component.scss @@ -13,15 +13,64 @@ width: 16vw; .group-member { + display: flex; + align-items: flex-start; + gap: 0.6vw; font-size: 1.5em; color: rgb(192, 192, 192); &.not-loaded { opacity: 0.6; + + .member-avatar { + filter: grayscale(0.6); + } + } + + .member-avatar { + width: 3.2vw; + height: 3.2vw; + border-radius: 50%; + border: 2px solid rgba(50, 240, 255, 0.85); + object-fit: cover; + background: rgba(18, 18, 20, 0.9); + box-shadow: 0 0 6px rgba(0, 0, 0, 0.6); + } + + .member-details { + display: flex; + flex-direction: column; + gap: 0.3vw; + flex: 1 1 auto; + } + + .member-header { + display: flex; + align-items: center; + gap: 0.4vw; + flex-wrap: nowrap; + } + + .member-name-wrap { + display: inline-flex; + align-items: baseline; + gap: 0.35vw; + flex: 0 0 auto; + white-space: nowrap; } .member-name { + flex: 0 0 auto; + text-shadow: 1px 1px 2px #000000; + white-space: nowrap; + } + + .member-self { + flex: 0 0 auto; + font-size: 0.75em; + opacity: 0.7; text-shadow: 1px 1px 2px #000000; + white-space: nowrap; } .group-member-level { @@ -67,5 +116,23 @@ } } } + + &.layout-compact { + width: 14vw; + + .group-member { + gap: 0.4vw; + font-size: 1.25em; + } + + .member-avatar { + width: 2.6vw; + height: 2.6vw; + } + + .group-member-health { + width: 12vw; + } + } } } diff --git a/Code/skyrim_ui/src/app/components/group/group.component.ts b/Code/skyrim_ui/src/app/components/group/group.component.ts index a163674bb..b78f98b62 100644 --- a/Code/skyrim_ui/src/app/components/group/group.component.ts +++ b/Code/skyrim_ui/src/app/components/group/group.component.ts @@ -9,7 +9,12 @@ import { timer, } from 'rxjs'; import { map } from 'rxjs/operators'; -import { PartyAnchor, SettingService } from 'src/app/services/setting.service'; +import { + PartyAnchor, + PartyLayout, + PlayerNamePreference, + SettingService, +} from 'src/app/services/setting.service'; import { fadeInOutActiveAnimation } from '../../animations/fade-in-out-active.animation'; import { Group } from '../../models/group'; import { Player } from '../../models/player'; @@ -22,6 +27,8 @@ interface GroupPosition { right?: string; bottom?: string; left?: string; + transform?: string; + transformOrigin?: string; } @Component({ @@ -34,8 +41,11 @@ interface GroupPosition { export class GroupComponent implements OnInit, OnDestroy { timerSubscription: Subscription; - groupMembers$: Observable<(Player & { isOwner: boolean })[]>; + groupMembers$: Observable< + (Player & { isOwner: boolean; isLocal: boolean })[] + >; group$: Observable; + readonly defaultAvatar = 'assets/images/group/avatar-placeholder.png'; public isAutoHide = new BehaviorSubject(true); public isShown = new BehaviorSubject(true); @@ -45,6 +55,7 @@ export class GroupComponent implements OnInit, OnDestroy { left: '0%', }); public settings = this.settingService.settings; + public partyLayout = PartyLayout; constructor( private readonly destroy$: DestroyService, @@ -57,13 +68,29 @@ export class GroupComponent implements OnInit, OnDestroy { this.group$ = this.groupService.group.asObservable(); this.groupMembers$ = this.groupService.selectMembers().pipe( - combineLatestWith(this.groupService.group), - map(([members, group]) => { + combineLatestWith( + this.groupService.group, + this.settings.playerNamePreference, + ), + map(([members, group, namePreference]) => { if (!group) { return []; } + const localId = this.clientService.localPlayerId; return members - .map(member => ({ ...member, isOwner: member.id === group.owner })) + .map(member => ({ + ...member, + isOwner: member.id === group.owner, + isLocal: member.id === localId, + name: + namePreference === PlayerNamePreference.ACTOR && member.actorName + ? member.actorName + : member.name, + avatar: + member.avatar && member.avatar.length > 0 + ? member.avatar + : this.defaultAvatar, + })) .sort((a, b) => (group.owner === a.id) === (group.owner === b.id) ? 0 @@ -153,29 +180,36 @@ export class GroupComponent implements OnInit, OnDestroy { this.settings.partyAnchorOffsetX, this.settings.partyAnchorOffsetY, this.settings.partyAnchor, + this.settings.partyScale, ]) .pipe(takeUntil(this.destroy$)) - .subscribe(([x, y, anchor]) => { + .subscribe(([x, y, anchor, scale]) => { const newPosition: GroupPosition = {}; + const resolvedScale = Math.max(0.5, Math.min(2, scale)); switch (anchor) { case PartyAnchor.TOP_LEFT: newPosition.top = `${y}vh`; newPosition.left = `${x}vw`; + newPosition.transformOrigin = 'top left'; break; case PartyAnchor.TOP_RIGHT: newPosition.top = `${y}vh`; newPosition.right = `${-100 + x}vw`; + newPosition.transformOrigin = 'top right'; break; case PartyAnchor.BOTTOM_RIGHT: newPosition.bottom = `${-100 + y}vh`; newPosition.right = `${-100 + x}vw`; + newPosition.transformOrigin = 'bottom right'; break; case PartyAnchor.BOTTOM_LEFT: newPosition.bottom = `${-100 + y}vh`; newPosition.left = `${x}vw`; + newPosition.transformOrigin = 'bottom left'; break; } + newPosition.transform = `scale(${resolvedScale})`; this.positionStyle.next(newPosition); }); } diff --git a/Code/skyrim_ui/src/app/components/overlay-banner/overlay-banner.component.html b/Code/skyrim_ui/src/app/components/overlay-banner/overlay-banner.component.html new file mode 100644 index 000000000..48ad5eb40 --- /dev/null +++ b/Code/skyrim_ui/src/app/components/overlay-banner/overlay-banner.component.html @@ -0,0 +1,10 @@ +
+
{{ banner.primary }}
+
+ {{ banner.secondary }} +
+
diff --git a/Code/skyrim_ui/src/app/components/overlay-banner/overlay-banner.component.scss b/Code/skyrim_ui/src/app/components/overlay-banner/overlay-banner.component.scss new file mode 100644 index 000000000..275a226fb --- /dev/null +++ b/Code/skyrim_ui/src/app/components/overlay-banner/overlay-banner.component.scss @@ -0,0 +1,29 @@ +.overlay-banner { + position: absolute; + top: 42%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; + pointer-events: none; + color: #f8f5e1; + text-shadow: 0 0 12px rgba(0, 0, 0, 0.7); + font-size: 2.2rem; + font-weight: 600; + letter-spacing: 0.04em; + padding: 0.25rem 0.5rem; + z-index: 12; + + &__primary { + white-space: pre-line; + } + + &__secondary { + font-size: 1.4rem; + margin-top: 0.35rem; + opacity: 0.9; + } + + &--error { + color: #ff8b8b; + } +} diff --git a/Code/skyrim_ui/src/app/components/overlay-banner/overlay-banner.component.ts b/Code/skyrim_ui/src/app/components/overlay-banner/overlay-banner.component.ts new file mode 100644 index 000000000..bdb574f85 --- /dev/null +++ b/Code/skyrim_ui/src/app/components/overlay-banner/overlay-banner.component.ts @@ -0,0 +1,16 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { OverlayBannerService } from '../../services/overlay-banner.service'; + +@Component({ + selector: 'app-overlay-banner', + templateUrl: './overlay-banner.component.html', + styleUrls: ['./overlay-banner.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OverlayBannerComponent { + banner$ = this.overlayBannerService.banner$; + + constructor( + private readonly overlayBannerService: OverlayBannerService, + ) {} +} diff --git a/Code/skyrim_ui/src/app/components/party-menu/party-menu.component.html b/Code/skyrim_ui/src/app/components/party-menu/party-menu.component.html index dda94c2de..13de57ce2 100644 --- a/Code/skyrim_ui/src/app/components/party-menu/party-menu.component.html +++ b/Code/skyrim_ui/src/app/components/party-menu/party-menu.component.html @@ -1,3 +1,43 @@ +
+

+ {{ 'COMPONENT.PARTY_MENU.PROFILE_PICTURE.TITLE' | transloco }} +

+

+ {{ 'COMPONENT.PARTY_MENU.PROFILE_PICTURE.DESCRIPTION' | transloco }} +

+
+ +
+
+ + +
+

+ {{ 'COMPONENT.PARTY_MENU.PROFILE_PICTURE.SIZE_LIMIT' | transloco }} +

+

+ {{ errorKey | transloco }} +

+
+

{{ - 'COMPONENT.PARTY_MENU.ACCEPT_INVITE' | transloco: { name: player.name } + 'COMPONENT.PARTY_MENU.ACCEPT_INVITE' + | transloco : { name: player.displayName || player.name } }}

@@ -79,14 +120,29 @@ {{ player.level }} - {{ player.name }} + {{ player.displayName || player.name }} {{ player.cellName }} + + +
+
+ + + i + + {{ + 'COMPONENT.PARTY_OPTIONS.SYNC_FAST_TRAVEL_MARKERS_TOOLTIP' + | transloco + }} + + + +
+
+ + + i + + {{ + 'COMPONENT.PARTY_OPTIONS.SHOW_PARTY_MEMBER_MARKERS_TOOLTIP' + | transloco + }} + + + +
+
+ + + i + + {{ + 'COMPONENT.PARTY_OPTIONS.LOCK_PARTY_TO_LEADER_CELL_TOOLTIP' + | transloco + }} + + + +
+
+ {{ 'COMPONENT.PARTY_OPTIONS.EXPERIMENTAL' | transloco }} +
+
+ + + i + + {{ + 'COMPONENT.PARTY_OPTIONS.SYNC_DEAD_BODY_LOOT_TOOLTIP' | transloco + }} + + + +
+
+ {{ 'COMPONENT.PARTY_OPTIONS.LEADER_ONLY' | transloco }} +
+
+ + diff --git a/Code/skyrim_ui/src/app/components/party-options/party-options.component.scss b/Code/skyrim_ui/src/app/components/party-options/party-options.component.scss new file mode 100644 index 000000000..b2c657cf2 --- /dev/null +++ b/Code/skyrim_ui/src/app/components/party-options/party-options.component.scss @@ -0,0 +1,177 @@ +@import 'env'; +@import 'functions/mod'; +@import 'functions/rem'; + +$-color-party-accent: #32f0ff; +$-color-panel-bg: rgba(20, 20, 22, 0.92); +$-color-panel-border: rgba(50, 240, 255, 0.35); +$-color-panel-highlight: rgba(50, 240, 255, 0.65); + +:host { + display: block; + height: 100%; + width: 100%; +} + +.party-options { + display: flex; + flex-direction: column; + gap: rem(16, 10); + padding: rem(16, 10); + min-width: rem(16, 160); + width: 100%; + height: 100%; + justify-content: flex-start; + background: $-color-panel-bg; + border: 1px solid $-color-panel-border; + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.6); +} + +.header { + background: #2b2b2d; + border: 1px solid rgba(50, 240, 255, 0.3); + border-radius: 4px; + padding: rem(16, 6) rem(16, 8); + cursor: pointer; + text-align: left; + width: 100%; + opacity: 1; + display: flex; + align-items: center; + justify-content: space-between; + gap: rem(16, 8); + text-transform: uppercase; + letter-spacing: 0.08em; + font-weight: 700; + font-size: 0.75rem; + color: #e6eff4; + transition: border-color 0.2s ease, background 0.2s ease; +} + +.header:hover { + border-color: $-color-panel-highlight; +} + +.header-right { + display: inline-flex; + align-items: center; + gap: rem(16, 6); +} + +.leader-tag { + padding: rem(16, 4) rem(16, 8); + border-radius: 999px; + background: rgba(50, 240, 255, 0.18); + border: 1px solid rgba(50, 240, 255, 0.45); + font-size: 0.6rem; + letter-spacing: 0.12em; + color: #d9f7ff; +} + +.toggle-icon { + width: rem(16, 10); + height: rem(16, 10); + border-right: 2px solid #d4e7f2; + border-bottom: 2px solid #d4e7f2; + transform: rotate(45deg); + transition: transform 0.2s ease; + margin-left: rem(16, 4); +} + +.toggle-icon.expanded { + transform: rotate(225deg); +} + +.content { + display: flex; + flex-direction: column; + gap: rem(16, 8); +} + +.section-title { + font-size: 0.65rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: rgba(216, 221, 226, 0.7); +} + +.setting { + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + align-items: center; + column-gap: rem(16, 10); + font-size: 0.85rem; + + label { + color: #d8dde2; + display: block; + } + + input[type='checkbox'] { + accent-color: $-color-party-accent; + transform: scale(1.05); + } +} + +.setting input[type='checkbox']:disabled { + opacity: 0.45; +} + +.info-dot { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: rem(16, 16); + height: rem(16, 16); + border-radius: 999px; + border: 1px solid rgba(50, 240, 255, 0.4); + color: #bfefff; + font-size: 0.65rem; + font-weight: 700; + line-height: 1; + cursor: help; + opacity: 0.75; + transition: opacity 0.2s ease, border-color 0.2s ease; +} + +.info-dot:focus, +.info-dot:hover { + opacity: 1; + border-color: rgba(50, 240, 255, 0.7); +} + +.tooltip { + position: absolute; + left: 50%; + bottom: calc(100% + #{rem(16, 6)}); + transform: translateX(-50%) translateY(6px); + background: rgba(16, 18, 20, 0.98); + border: 1px solid rgba(110, 120, 130, 0.35); + color: #d8dde2; + padding: rem(16, 6) rem(16, 8); + border-radius: 4px; + font-size: 0.7rem; + white-space: nowrap; + width: max-content; + max-width: none; + text-transform: none; + letter-spacing: 0.01em; + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.25); + text-shadow: none; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease, transform 0.2s ease; + z-index: 10; +} + +.info-dot:hover .tooltip, +.info-dot:focus .tooltip { + opacity: 1; + transform: translateX(-50%) translateY(0); +} + +.note { + font-size: 0.7rem; + color: rgba(182, 190, 198, 0.75); +} diff --git a/Code/skyrim_ui/src/app/components/party-options/party-options.component.ts b/Code/skyrim_ui/src/app/components/party-options/party-options.component.ts new file mode 100644 index 000000000..1bc895ca2 --- /dev/null +++ b/Code/skyrim_ui/src/app/components/party-options/party-options.component.ts @@ -0,0 +1,30 @@ +import { Component } from '@angular/core'; +import { Observable } from 'rxjs'; +import { PartyOptions } from '../../models/party-options'; +import { PartyOptionsService } from '../../services/party-options.service'; + +@Component({ + selector: 'app-party-options', + templateUrl: './party-options.component.html', + styleUrls: ['./party-options.component.scss'], +}) +export class PartyOptionsComponent { + public options$: Observable; + public isLeader$: Observable; + public inParty$: Observable; + public isExpanded = true; + + public constructor(private readonly partyOptions: PartyOptionsService) { + this.options$ = this.partyOptions.options$; + this.isLeader$ = this.partyOptions.isLeader$; + this.inParty$ = this.partyOptions.inParty$; + } + + public toggleExpanded(): void { + this.isExpanded = !this.isExpanded; + } + + public updateOption(key: keyof PartyOptions, value: boolean): void { + this.partyOptions.updateOption(key, value); + } +} diff --git a/Code/skyrim_ui/src/app/components/party-pins/party-pins.component.html b/Code/skyrim_ui/src/app/components/party-pins/party-pins.component.html new file mode 100644 index 000000000..9d0d50878 --- /dev/null +++ b/Code/skyrim_ui/src/app/components/party-pins/party-pins.component.html @@ -0,0 +1,37 @@ +
+ +
+ + +
+
+ {{ pin.name }} +
+
+
diff --git a/Code/skyrim_ui/src/app/components/party-pins/party-pins.component.scss b/Code/skyrim_ui/src/app/components/party-pins/party-pins.component.scss new file mode 100644 index 000000000..0d6d32593 --- /dev/null +++ b/Code/skyrim_ui/src/app/components/party-pins/party-pins.component.scss @@ -0,0 +1,84 @@ +.party-pins-layer { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + pointer-events: none; /* never block input */ + z-index: 1000; /* ensure on top of other overlay UI */ + --pin-size: 36px; + --pin-offset: 18px; +} + +.pin { + position: absolute; + width: var(--pin-size); + height: var(--pin-size); + border-radius: 50%; + border: 2px solid rgba(50, 240, 255, 0.9); + background: rgba(18, 18, 20, 0.9); + transform: translate( + calc(var(--pin-offset) * -1), + calc(var(--pin-offset) * -1) + ); + box-shadow: 0 0 6px rgba(0, 0, 0, 0.6); + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; +} + +.pin img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.pin.oob { + border-color: rgba(180, 180, 180, 0.85); + filter: grayscale(0.6); + opacity: 0.85; +} + +.pin.no-avatar { + background: rgba(10, 10, 12, 0.7); + border-color: rgba(50, 240, 255, 0.75); +} + +.pin.no-avatar::after { + content: ''; + position: absolute; + inset: calc(var(--pin-size) * 0.22); + border-radius: 50%; + border: 1px solid rgba(50, 240, 255, 0.4); + opacity: 0.8; +} + +.pin-marker { + width: calc(var(--pin-size) * 0.28); + height: calc(var(--pin-size) * 0.28); + border-radius: 50%; + background: rgba(50, 240, 255, 0.95); + box-shadow: 0 0 8px rgba(50, 240, 255, 0.85); +} + +.pin.oob.no-avatar, +.pin.oob.no-avatar::after, +.pin.oob .pin-marker { + border-color: rgba(180, 180, 180, 0.7); + background: rgba(170, 170, 170, 0.7); + box-shadow: 0 0 6px rgba(0, 0, 0, 0.4); +} + +.label { + position: absolute; + color: #e6e6e6; + font-size: 12px; + text-shadow: 0 0 2px #000, 0 0 4px #000; + transform: translate(-50%, 0); + white-space: nowrap; +} + +.label.oob { + color: #bcbcbc; +} diff --git a/Code/skyrim_ui/src/app/components/party-pins/party-pins.component.ts b/Code/skyrim_ui/src/app/components/party-pins/party-pins.component.ts new file mode 100644 index 000000000..837352a71 --- /dev/null +++ b/Code/skyrim_ui/src/app/components/party-pins/party-pins.component.ts @@ -0,0 +1,61 @@ +import { Component, OnDestroy } from '@angular/core'; +import { Subscription, combineLatest } from 'rxjs'; +import { PartyPin } from '../../models/party-pin'; +import { ClientService } from '../../services/client.service'; +import { PlayerListService } from '../../services/player-list.service'; +import { SettingService } from '../../services/setting.service'; + +@Component({ + selector: 'app-party-pins', + templateUrl: './party-pins.component.html', + styleUrls: ['./party-pins.component.scss'], +}) +export class PartyPinsComponent implements OnDestroy { + pins: PartyPin[] = []; + readonly defaultAvatar = 'assets/images/group/avatar-placeholder.png'; + showMarkerAvatar = true; + pinSize = 36; + pinLabelOffset = 24; + private sub: Subscription; + + constructor( + private readonly client: ClientService, + private readonly playerList: PlayerListService, + private readonly settingService: SettingService, + ) { + this.sub = combineLatest([ + this.client.partyPinsChange, + this.playerList.playerList.asObservable(), + this.settingService.settings.partyPinShowAvatar, + this.settingService.settings.partyPinScale, + ]).subscribe(([pins, list, showAvatar, scale]) => { + const players = list?.players ?? []; + const playerMap = new Map(players.map(player => [player.id, player])); + this.showMarkerAvatar = showAvatar; + const resolvedScale = Math.max(0.6, Math.min(1.8, scale)); + this.pinSize = Math.round(36 * resolvedScale); + this.pinLabelOffset = Math.round(this.pinSize * 0.66); + + this.pins = pins.map(pin => { + const player = playerMap.get(pin.id); + const resolvedName = + player?.displayName ?? pin.name ?? player?.name ?? ''; + const resolvedAvatar = + pin.avatar !== undefined ? pin.avatar : player?.avatar; + + return { + ...pin, + name: resolvedName.length > 0 ? resolvedName : undefined, + avatar: + resolvedAvatar && resolvedAvatar.length > 0 + ? resolvedAvatar + : this.defaultAvatar, + }; + }); + }); + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } +} diff --git a/Code/skyrim_ui/src/app/components/player-list/player-list.component.html b/Code/skyrim_ui/src/app/components/player-list/player-list.component.html index 8bccbd5fe..180ca8ffb 100644 --- a/Code/skyrim_ui/src/app/components/player-list/player-list.component.html +++ b/Code/skyrim_ui/src/app/components/player-list/player-list.component.html @@ -21,7 +21,7 @@ {{ player.level }} - {{ player.name }} + {{ player.displayName || player.name }} {{ player.cellName }} + + + + + + diff --git a/Code/skyrim_ui/src/app/components/player-list/player-list.component.scss b/Code/skyrim_ui/src/app/components/player-list/player-list.component.scss index 00d7353b6..e8b13837f 100644 --- a/Code/skyrim_ui/src/app/components/player-list/player-list.component.scss +++ b/Code/skyrim_ui/src/app/components/player-list/player-list.component.scss @@ -45,11 +45,17 @@ padding: 15px; } + .actions-col { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; + } + .user-action-button { padding: 0.25vw; background-color: #292929; border-radius: 2px; - margin-left: 5%; } } } diff --git a/Code/skyrim_ui/src/app/components/player-list/player-list.component.ts b/Code/skyrim_ui/src/app/components/player-list/player-list.component.ts index f65bd9ae9..cfe558449 100644 --- a/Code/skyrim_ui/src/app/components/player-list/player-list.component.ts +++ b/Code/skyrim_ui/src/app/components/player-list/player-list.component.ts @@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { combineLatest, Observable, pluck, ReplaySubject, share } from 'rxjs'; import { map } from 'rxjs/operators'; import { ClientService } from 'src/app/services/client.service'; +import { TradeUiService } from 'src/app/services/trade-ui.service'; import { GroupService } from 'src/app/services/group.service'; import { PlayerListService } from 'src/app/services/player-list.service'; import { Player } from '../../models/player'; @@ -13,7 +14,14 @@ import { Player } from '../../models/player'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class PlayerListComponent { - playerList$: Observable<(Player & { isMember: boolean })[]>; + playerList$: Observable< + (Player & { + isMember: boolean; + isTrading: boolean; + tradeBusy: boolean; + pendingInvite: boolean; + })[] + >; playerListLength$: Observable; isPartyLeader$: Observable; @@ -21,19 +29,35 @@ export class PlayerListComponent { private readonly playerListService: PlayerListService, private readonly clientService: ClientService, private readonly groupService: GroupService, + private readonly tradeUiService: TradeUiService, ) { this.playerList$ = combineLatest([ this.playerListService.playerList.asObservable().pipe(pluck('players')), this.groupService.group.asObservable().pipe(pluck('members')), + this.tradeUiService.session$, + this.tradeUiService.pendingOutgoing$, ]).pipe( - map(([players, members]) => { + map(([players, members, session, pendingOutgoing]) => { if (!players) { return []; } - return players.map(player => ({ - ...player, - isMember: members.includes(player.id), - })); + const localId = this.clientService.localPlayerId; + const activePartnerId = + session && session.active ? session.partnerId : undefined; + const memberList = Array.isArray(members) ? members : []; + const outgoing = new Set( + pendingOutgoing ? Array.from(pendingOutgoing) : [], + ); + const tradeBusy = Boolean(session?.active) || outgoing.size > 0; + return players + .filter(player => player.id !== localId) + .map(player => ({ + ...player, + isMember: memberList.includes(player.id), + isTrading: activePartnerId === player.id, + tradeBusy, + pendingInvite: outgoing.has(player.id), + })); }), share({ connector: () => new ReplaySubject(1), @@ -58,4 +82,12 @@ export class PlayerListComponent { public sendPartyInvite(inviteeId: number) { this.playerListService.sendPartyInvite(inviteeId); } + + public sendTradeInvite(inviteeId: number) { + this.tradeUiService.sendInvite(inviteeId); + } + + public cancelTradeInvite(inviteeId: number) { + this.tradeUiService.cancelInvite(inviteeId); + } } diff --git a/Code/skyrim_ui/src/app/components/revive-progress/revive-progress.component.html b/Code/skyrim_ui/src/app/components/revive-progress/revive-progress.component.html new file mode 100644 index 000000000..4e851a4cf --- /dev/null +++ b/Code/skyrim_ui/src/app/components/revive-progress/revive-progress.component.html @@ -0,0 +1,18 @@ +
+
+

Channeling Healing Hands

+
+
+
+
+ {{ state.remainingSeconds | number: '1.0-1' }}s remaining + {{ (state.progress * 100) | number: '1.0-0' }}% +
+

+ Stay locked on your ally to keep the channel alive. +

+
+
diff --git a/Code/skyrim_ui/src/app/components/revive-progress/revive-progress.component.scss b/Code/skyrim_ui/src/app/components/revive-progress/revive-progress.component.scss new file mode 100644 index 000000000..f65a17250 --- /dev/null +++ b/Code/skyrim_ui/src/app/components/revive-progress/revive-progress.component.scss @@ -0,0 +1,63 @@ +:host { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 5000; +} + +.revive-overlay { + position: absolute; + top: 28%; + left: 50%; + transform: translateX(-50%); + width: 100%; + display: flex; + justify-content: center; +} + +.revive-box { + min-width: 320px; + background: rgba(3, 14, 22, 0.85); + border-radius: 18px; + border: 1px solid rgba(0, 188, 212, 0.35); + padding: 1.2rem 1.5rem; + box-shadow: 0 20px 50px rgba(0, 188, 212, 0.2); + text-align: center; +} + +.title { + margin: 0 0 0.75rem; + text-transform: uppercase; + letter-spacing: 0.3rem; + font-size: 0.85rem; + color: #b2ebf2; +} + +.progress-track { + width: 100%; + height: 10px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.08); + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, #00bcd4, #26c6da); + box-shadow: 0 0 25px rgba(38, 198, 218, 0.5); + transition: width 0.2s ease-out; +} + +.meta { + display: flex; + justify-content: space-between; + margin-top: 0.75rem; + font-size: 0.85rem; + color: rgba(255, 255, 255, 0.75); +} + +.footnote { + margin: 0.5rem 0 0; + font-size: 0.75rem; + color: rgba(255, 255, 255, 0.55); +} diff --git a/Code/skyrim_ui/src/app/components/revive-progress/revive-progress.component.ts b/Code/skyrim_ui/src/app/components/revive-progress/revive-progress.component.ts new file mode 100644 index 000000000..ed40cdc4b --- /dev/null +++ b/Code/skyrim_ui/src/app/components/revive-progress/revive-progress.component.ts @@ -0,0 +1,53 @@ +import { Component, OnInit, NgZone } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { ClientService } from '../../services/client.service'; + +interface HealerReviveState { + progress: number; + remainingSeconds: number; + totalSeconds: number; +} + +@Component({ + selector: 'app-revive-progress', + templateUrl: './revive-progress.component.html', + styleUrls: ['./revive-progress.component.scss'], +}) +export class ReviveProgressComponent implements OnInit { + state$: Observable; + private stateSubject = new BehaviorSubject( + undefined, + ); + + constructor( + private readonly clientService: ClientService, + private readonly ngZone: NgZone, + ) { + this.state$ = this.stateSubject.asObservable(); + } + + ngOnInit(): void { + this.clientService.reviveHealerProgressChange.subscribe((payload) => { + this.ngZone.run(() => { + if (!payload) { + this.stateSubject.next(undefined); + return; + } + + const progress = + payload.totalSeconds > 0 + ? Math.min(payload.elapsedSeconds / payload.totalSeconds, 1) + : 0; + + this.stateSubject.next({ + progress, + remainingSeconds: Math.max( + payload.totalSeconds - payload.elapsedSeconds, + 0, + ), + totalSeconds: payload.totalSeconds, + }); + }); + }); + } +} diff --git a/Code/skyrim_ui/src/app/components/root/root.component.html b/Code/skyrim_ui/src/app/components/root/root.component.html index e660fa4c6..edfb76a0e 100644 --- a/Code/skyrim_ui/src/app/components/root/root.component.html +++ b/Code/skyrim_ui/src/app/components/root/root.component.html @@ -1,4 +1,8 @@ + + + + @@ -8,22 +12,16 @@ +
- + + + + + +
@@ -116,6 +140,12 @@ > + + + + + + diff --git a/Code/skyrim_ui/src/app/components/root/root.component.scss b/Code/skyrim_ui/src/app/components/root/root.component.scss index 698735f2c..42b648922 100644 --- a/Code/skyrim_ui/src/app/components/root/root.component.scss +++ b/Code/skyrim_ui/src/app/components/root/root.component.scss @@ -4,6 +4,8 @@ @import 'functions/mod'; @import 'functions/rem'; +$-color-primary: #8ac8ff; + :host { display: block; @@ -85,6 +87,12 @@ min-width: 40vw; } + > app-popup[data-popup='emotes'] > app-window { + max-width: 70vw; + min-width: 48vw; + overflow: visible; + } + > app-popup[data-popup='playerManager'] > app-window { max-width: 30vw; min-width: 25vw; @@ -99,7 +107,9 @@ left: $-size-gap; display: flex; flex-direction: column; - width: 32vw; + align-items: flex-start; + width: max-content; + min-width: 32vw; bottom: $-size-gap; } @@ -111,17 +121,26 @@ .app-root-menu { display: flex; height: 4rem; - margin: 0 auto; + width: max-content; + min-width: 32vw; > button { flex: 1 1 auto; padding-top: 0; padding-bottom: 0; + white-space: nowrap; } } +.app-root-chat-row { + display: flex; + align-items: flex-end; + gap: $-size-gap; +} + .app-root-chat { - width: 100%; + width: 32vw; + flex: 0 0 32vw; height: 30vh; display: flex; flex-direction: column; @@ -130,9 +149,48 @@ margin: 0.1rem; flex: 1 1 auto; } + + .emote-quick-button { + align-self: flex-end; + margin: rem(16, 12) rem(16, 8) rem(16, 4) 0; + display: inline-flex; + align-items: center; + gap: rem(16, 8); + padding: rem(16, 10) rem(16, 14); + border-radius: rem(16, 14); + border: 1px solid mod($-color-background, 0.4); + background: linear-gradient( + 120deg, + mod($-color-background, 0.1), + mod($-color-background, 0.25) + ); + color: mod($-color-primary, 0.05); + box-shadow: 0 rem(16, 6) rem(16, 14) rgba(0, 0, 0, 0.35); + + .hotkey-tag { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 rem(16, 8); + height: rem(16, 22); + border-radius: 999px; + background: mod($-color-background, 0.35); + border: 1px solid mod($-color-background, 0.3); + font-weight: 700; + letter-spacing: 0.04em; + } + } +} + +.app-root-party-options { + width: rem(16, 220); + max-height: 30vh; + flex: 0 0 rem(16, 220); + display: flex; + align-self: flex-start; } -// Note: due to how `-webkit-background-clip` renders clipped text (that's needed for +// Note: due to how `-webkit-background-clip` renders clipped text (that's needed for // the cooldown effect), we need to apply a few workarounds to make it look just right. I'm sorry! .cooldown-supporting-text-normal { @@ -147,8 +205,8 @@ .text-cooldown-effect { --cooldown-duration: 10s; background: linear-gradient( - to left, - mod($-color-background, 0.3) 50%, + to left, + mod($-color-background, 0.3) 50%, mod($-color-background, 1) 50% ); background-clip: text; diff --git a/Code/skyrim_ui/src/app/components/root/root.component.ts b/Code/skyrim_ui/src/app/components/root/root.component.ts index 79393f797..1f48fe442 100644 --- a/Code/skyrim_ui/src/app/components/root/root.component.ts +++ b/Code/skyrim_ui/src/app/components/root/root.component.ts @@ -7,6 +7,7 @@ import { fadeInOutActiveAnimation } from '../../animations/fade-in-out-active.an import { View } from '../../models/view.enum'; import { ClientService } from '../../services/client.service'; import { DestroyService } from '../../services/destroy.service'; +import { PartyOptionsService } from '../../services/party-options.service'; import { SettingService, fontSizeToPixels, @@ -19,7 +20,7 @@ import { controlsAnimation } from './controls.animation'; import { notificationsAnimation } from './notifications.animation'; import { map } from 'rxjs/operators'; -const REVEAL_EFFECT_DURATION_MS = 10000 // todo: pass value from C++? +const REVEAL_EFFECT_DURATION_MS = 10000; // todo: pass value from C++? @Component({ selector: 'app-root', @@ -43,7 +44,9 @@ export class RootComponent implements OnInit { menuOpen$ = this.client.openingMenuChange.asObservable(); inGame$ = this.client.inGameStateChange.asObservable(); active$ = this.client.activationStateChange.asObservable(); - connectionInProgress$ = this.client.isConnectionInProgressChange.asObservable(); + connectionInProgress$ = + this.client.isConnectionInProgressChange.asObservable(); + partyOptionsVisible$ = this.partyOptions.inParty$; revealingInProgress$ = false; @ViewChild('chat') private chatComp!: ChatComponent; @@ -56,6 +59,7 @@ export class RootComponent implements OnInit { private readonly uiRepository: UiRepository, private readonly translocoService: TranslocoService, private readonly settingService: SettingService, + private readonly partyOptions: PartyOptionsService, public readonly overlay: Overlay, // used for mockup ) { this.translocoService.setActiveLang( @@ -67,6 +71,7 @@ export class RootComponent implements OnInit { this.onInGameStateSubscription(); this.onActivationStateSubscription(); this.onFontSizeSubscription(); + this.onConnectionStateSubscription(); } public onInGameStateSubscription() { @@ -107,6 +112,23 @@ export class RootComponent implements OnInit { }); } + public onConnectionStateSubscription() { + this.client.connectionStateChange + .pipe(takeUntil(this.destroy$)) + .subscribe(state => { + if (!state) { + const currentView = this.uiRepository.getView(); + if ( + currentView === null || + currentView === View.DISCONNECT || + currentView === View.RECONNECT + ) { + this.setView(View.CONNECT); + } + } + }); + } + public setView(view: View | null) { this.uiRepository.openView(view); @@ -125,12 +147,17 @@ export class RootComponent implements OnInit { this.client.reconnect(); } + public openEmoteWheel(): void { + this.setView(this.RootView.EMOTES); + } + public revealPlayers(): void { - if (this.revealingInProgress$) - return; + if (this.revealingInProgress$) return; this.revealingInProgress$ = true; - setTimeout(() => { this.revealingInProgress$ = false }, REVEAL_EFFECT_DURATION_MS); + setTimeout(() => { + this.revealingInProgress$ = false; + }, REVEAL_EFFECT_DURATION_MS); this.sound.play(Sound.Focus); this.client.revealPlayers(); diff --git a/Code/skyrim_ui/src/app/components/server-list/server-list.component.ts b/Code/skyrim_ui/src/app/components/server-list/server-list.component.ts index 9952c197a..38171553f 100644 --- a/Code/skyrim_ui/src/app/components/server-list/server-list.component.ts +++ b/Code/skyrim_ui/src/app/components/server-list/server-list.component.ts @@ -115,7 +115,7 @@ export class ServerListComponent { const shortClientVersion = this.getClientVersion(clientVersion); return { ...server, - hasPassword: server.pass, + hasPassword: !!server.pass, isFavorite: !!favorites[`${server.ip}:${server.port}`], isFull: server.player_count >= server.max_player_count, shortVersion, @@ -199,9 +199,13 @@ export class ServerListComponent { map(fontSize => fontSizeToPixels[fontSize] * 2), ); - this.hideVersionMismatchedServers.next(this.uiRepository.getHideVersionMismatchedServers()); + this.hideVersionMismatchedServers.next( + this.uiRepository.getHideVersionMismatchedServers(), + ); this.hideFullServers.next(this.uiRepository.getHideFullServers()); - this.hidePasswordProtectedServers.next(this.uiRepository.getHidePasswordProtectedServers()); + this.hidePasswordProtectedServers.next( + this.uiRepository.getHidePasswordProtectedServers(), + ); } public cancel(): void { @@ -213,12 +217,12 @@ export class ServerListComponent { } public onHideVersionMismatchedServers(state: boolean) { - this.hideVersionMismatchedServers.next(state) + this.hideVersionMismatchedServers.next(state); this.uiRepository.setHideVersionMismatchedServers(state); } public onHideFullServers(state: boolean) { - this.hideFullServers.next(state) + this.hideFullServers.next(state); this.uiRepository.setHideFullServers(state); } @@ -310,7 +314,12 @@ export class ServerListComponent { // } public connect(server: Server) { - this.clientService.connect(server.ip, server.port ? server.port : 10578); + this.clientService.connect( + server.ip, + server.port ? server.port : 10578, + this.storeService.get('last_connected_username', ''), + this.storeService.get('last_connected_password', ''), + ); this.soundService.play(Sound.Ok); this.close(); } @@ -320,6 +329,9 @@ export class ServerListComponent { server.ip, server.port, server.name, + View.SERVER_LIST, + this.storeService.get('last_connected_username', ''), + this.storeService.get('last_connected_password', ''), ); } diff --git a/Code/skyrim_ui/src/app/components/settings/settings.component.html b/Code/skyrim_ui/src/app/components/settings/settings.component.html index 0fa74bdc7..d37550737 100644 --- a/Code/skyrim_ui/src/app/components/settings/settings.component.html +++ b/Code/skyrim_ui/src/app/components/settings/settings.component.html @@ -4,7 +4,9 @@

{{ 'COMPONENT.SETTINGS.VERSION.HEADER' | transloco }}

{{ 'COMPONENT.SETTINGS.VERSION.INFO' | transloco }} -
{{ clientVersion$ | async }}
+
+ {{ clientVersion$ | async }} +
{{ 'COMPONENT.SETTINGS.VERSION.OUTDATED' | transloco }} @@ -71,6 +73,36 @@

{{ 'COMPONENT.SETTINGS.UI' | transloco }}

>
+
+ + + + +
+
+ + + + +
@@ -113,6 +145,102 @@

{{ 'COMPONENT.SETTINGS.PARTY' | transloco }}

>
+
+ + +
+
+ + + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
version.split('-')[0])); + this.clientVersion$ = this.client.versionSet.pipe( + map(version => version.split('-')[0]), + ); } ngOnInit(): void { @@ -82,9 +127,7 @@ export class SettingsComponent { } private getVersionTagList(): Promise { - return lastValueFrom( - this.http - .get(`${ environment.githubUrl }`)); + return lastValueFrom(this.http.get(`${environment.githubUrl}`)); } async isGameVersionOutdated(): Promise { diff --git a/Code/skyrim_ui/src/app/components/sync-status-badge/sync-status-badge.component.html b/Code/skyrim_ui/src/app/components/sync-status-badge/sync-status-badge.component.html new file mode 100644 index 000000000..cd4a1b865 --- /dev/null +++ b/Code/skyrim_ui/src/app/components/sync-status-badge/sync-status-badge.component.html @@ -0,0 +1,35 @@ + +
+
+
+
+
+ {{ status.title || 'Quest Isolation' }} +
+ +
+
+ {{ status.detail || 'Sync paused' }} +
+
+ {{ status.moreInfo }} +
+
+
+
diff --git a/Code/skyrim_ui/src/app/components/sync-status-badge/sync-status-badge.component.scss b/Code/skyrim_ui/src/app/components/sync-status-badge/sync-status-badge.component.scss new file mode 100644 index 000000000..b3ca8adbb --- /dev/null +++ b/Code/skyrim_ui/src/app/components/sync-status-badge/sync-status-badge.component.scss @@ -0,0 +1,111 @@ +@import 'env'; + +@import 'functions/mod'; +@import 'functions/rem'; + +$-badge-blue: #49b6ff; +$-badge-blue-soft: rgba(73, 182, 255, 0.35); +$-badge-dark: rgba(14, 16, 22, 0.66); + +.sync-status-badge { + position: fixed; + top: rem(16, 18); + right: rem(16, 18); + z-index: 9999; + + display: inline-flex; + align-items: flex-start; + gap: rem(16, 10); + padding: rem(16, 10) rem(16, 14); + + border-radius: rem(16, 14); + border: 1px solid rgba(73, 182, 255, 0.35); + background: linear-gradient(135deg, rgba(21, 26, 39, 0.72), $-badge-dark); + box-shadow: + 0 rem(16, 8) rem(16, 22) rgba(0, 0, 0, 0.55), + 0 0 rem(16, 18) rgba(73, 182, 255, 0.22); + + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); +} + +.sync-status-badge-indicator { + width: rem(16, 10); + height: rem(16, 10); + border-radius: 999px; + margin-top: rem(16, 3); + background: radial-gradient(circle at 30% 30%, #e8f7ff 0%, $-badge-blue 55%, #1c67ff 100%); + box-shadow: 0 0 rem(16, 14) $-badge-blue-soft; +} + +.sync-status-badge-text { + display: flex; + flex-direction: column; + line-height: 1.2; + letter-spacing: 0.01em; + max-width: min(#{rem(16, 360)}, calc(100vw - #{rem(16, 64)})); +} + +.sync-status-badge-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: rem(16, 12); +} + +.sync-status-badge-title { + font-weight: 800; + text-transform: uppercase; + font-size: rem(16, 13); + color: rgba(230, 245, 255, 0.95); + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.75); +} + +.sync-status-badge-detail { + margin-top: rem(16, 2); + font-weight: 600; + font-size: rem(16, 11); + color: rgba(190, 220, 255, 0.85); + word-break: break-word; +} + +.sync-status-badge-more-button { + width: rem(16, 20); + height: rem(16, 20); + border-radius: 999px; + border: 1px solid rgba(73, 182, 255, 0.45); + background: radial-gradient(circle at 30% 30%, rgba(38, 54, 84, 0.85), rgba(15, 20, 32, 0.6)); + color: rgba(220, 240, 255, 0.9); + font-size: rem(16, 14); + font-weight: 800; + line-height: 1; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8); + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + transition: transform 0.2s ease; +} + +.sync-status-badge-more-button:hover { + border-color: rgba(73, 182, 255, 0.75); + color: rgba(240, 250, 255, 0.98); +} + +.sync-status-badge-more-button.is-open { + transform: rotate(180deg); +} + +.sync-status-badge-more { + margin-top: rem(16, 8); + padding: rem(16, 8) rem(16, 10); + border-radius: rem(16, 10); + border: 1px solid rgba(73, 182, 255, 0.25); + background: rgba(10, 14, 22, 0.55); + font-size: rem(16, 11); + font-weight: 600; + color: rgba(180, 210, 245, 0.9); + line-height: 1.3; + white-space: pre-line; + word-break: break-word; +} diff --git a/Code/skyrim_ui/src/app/components/sync-status-badge/sync-status-badge.component.ts b/Code/skyrim_ui/src/app/components/sync-status-badge/sync-status-badge.component.ts new file mode 100644 index 000000000..9579b0f8d --- /dev/null +++ b/Code/skyrim_ui/src/app/components/sync-status-badge/sync-status-badge.component.ts @@ -0,0 +1,33 @@ +import { Component, OnDestroy } from '@angular/core'; +import { Subject, takeUntil } from 'rxjs'; +import { ClientService } from '../../services/client.service'; + +@Component({ + selector: 'app-sync-status-badge', + templateUrl: './sync-status-badge.component.html', + styleUrls: ['./sync-status-badge.component.scss'], +}) +export class SyncStatusBadgeComponent implements OnDestroy { + public readonly status$ = this.client.syncStatusChange.asObservable(); + public showMoreInfo = false; + private readonly destroy$ = new Subject(); + private lastInfo = ''; + + public constructor(private readonly client: ClientService) { + this.status$.pipe(takeUntil(this.destroy$)).subscribe(status => { + if (!status.isolated || status.moreInfo !== this.lastInfo) { + this.showMoreInfo = false; + } + this.lastInfo = status.moreInfo; + }); + } + + public toggleMoreInfo() { + this.showMoreInfo = !this.showMoreInfo; + } + + public ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/Code/skyrim_ui/src/app/components/trade-popup/trade-popup.component.html b/Code/skyrim_ui/src/app/components/trade-popup/trade-popup.component.html new file mode 100644 index 000000000..f2739259e --- /dev/null +++ b/Code/skyrim_ui/src/app/components/trade-popup/trade-popup.component.html @@ -0,0 +1,214 @@ + + +
+
+ + {{ + 'COMPONENT.TRADE_MENU.TRADING_WITH' + | transloco : { name: session.partnerName } + }} + +
+
+
+ {{ + 'COMPONENT.TRADE_MENU.COUNTDOWN' + | transloco : { seconds: formatCountdown(session.countdownMs) } + }} +
+
+
+
+
+
+ +
+ + +
+
+

+ {{ 'COMPONENT.TRADE_MENU.INVENTORY_TITLE' | transloco }} +

+
+ {{ 'COMPONENT.TRADE_MENU.NO_INVENTORY' | transloco }} +
+ + + + + + + + + + + + + + + + + +
+ {{ 'COMPONENT.TRADE_MENU.ITEM_COLUMNS.NAME' | transloco }} + + {{ + 'COMPONENT.TRADE_MENU.ITEM_COLUMNS.AVAILABLE' | transloco + }} + + {{ 'COMPONENT.TRADE_MENU.ITEM_COLUMNS.OFFERED' | transloco }} +
+
{{ item.name }}
+
    +
  • {{ detail }}
  • +
+
+ {{ 'COMPONENT.TRADE_MENU.QUEST_ITEM_WARNING' | transloco }} +
+
{{ item.available }} +
+ + + +
+
+ + + +
+
+ +
+
+ +
+
+

+ {{ 'COMPONENT.TRADE_MENU.OFFER_TITLE' | transloco }} +

+
+ {{ 'COMPONENT.TRADE_MENU.EMPTY_OFFER' | transloco }} +
+
    +
  • +
    + {{ item.name }} + ×{{ item.count }} +
    +
      +
    • {{ detail }}
    • +
    +
  • +
+
+
+

+ {{ 'COMPONENT.TRADE_MENU.PARTNER_OFFER_TITLE' | transloco }} +

+
+ {{ 'COMPONENT.TRADE_MENU.EMPTY_PARTNER_OFFER' | transloco }} +
+
    +
  • +
    + {{ item.name }} + ×{{ item.count }} +
    +
      +
    • {{ detail }}
    • +
    +
  • +
+
+
+
+
+
+ + +
+ {{ 'COMPONENT.TRADE_MENU.NO_ACTIVE_TRADE' | transloco }} +
+
+
diff --git a/Code/skyrim_ui/src/app/components/trade-popup/trade-popup.component.scss b/Code/skyrim_ui/src/app/components/trade-popup/trade-popup.component.scss new file mode 100644 index 000000000..9139a5ec9 --- /dev/null +++ b/Code/skyrim_ui/src/app/components/trade-popup/trade-popup.component.scss @@ -0,0 +1,306 @@ +.trade-popup { + width: 70vw; + max-width: 1100px; + min-height: 520px; + background: linear-gradient(180deg, rgba(12, 12, 12, 0.95), #050505); + color: #f5deb3; + font-family: 'Trajan Pro', serif; +} + +.trade-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 1rem 0.25rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + + .title { + font-size: 1.2rem; + letter-spacing: 0.08em; + text-transform: uppercase; + } + + .countdown { + display: flex; + flex-direction: column; + gap: 0.35rem; + min-width: 180px; + + .label { + font-size: 0.9rem; + text-align: right; + } + + .bar { + height: 6px; + border: 1px solid rgba(255, 255, 255, 0.3); + background: rgba(0, 0, 0, 0.5); + + .fill { + height: 100%; + background: linear-gradient(90deg, #fde08d, #f5b642); + transition: width 0.2s linear; + } + } + } +} + +.trade-body { + display: flex; + gap: 1.5rem; + padding: 1rem; + min-height: 420px; +} + +.status-pane { + flex: 0 0 220px; + display: flex; + flex-direction: column; + gap: 1.5rem; + background: rgba(0, 0, 0, 0.35); + padding: 1rem; + border: 1px solid rgba(255, 255, 255, 0.08); + align-self: flex-start; + + .status { + display: flex; + flex-direction: column; + gap: 0.35rem; + font-size: 0.9rem; + + span { + &[data-ready='true'] { + color: #9fd175; + } + + &[data-ready='false'] { + color: #f2b56d; + } + } + } + + .actions { + display: flex; + flex-direction: column; + gap: 0.5rem; + + button { + width: 100%; + padding: 0.55rem; + text-transform: uppercase; + letter-spacing: 0.08em; + + &.secondary { + background: rgba(0, 0, 0, 0.4); + border: 1px solid rgba(255, 255, 255, 0.2); + } + + &.ghost { + opacity: 0.8; + } + } + } +} + +.trade-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.inventory-section { + background: rgba(0, 0, 0, 0.25); + padding: 1rem; + border: 1px solid rgba(255, 255, 255, 0.08); + overflow: auto; + max-height: 320px; + + .section-title { + margin-bottom: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.1em; + font-size: 0.95rem; + } + + table { + width: 100%; + border-collapse: collapse; + + thead th { + text-align: left; + font-size: 0.85rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + } + + tbody tr { + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + } + + td { + padding: 0.55rem 0.35rem; + vertical-align: top; + } + + td.numeric { + text-align: right; + font-variant-numeric: tabular-nums; + } + + tr[data-quest='true'] { + opacity: 0.65; + } + + tr[data-gold='true'] { + background: rgba(251, 215, 103, 0.08); + } + } + + .item-name { + font-size: 0.95rem; + } + + .item-details { + list-style: none; + margin: 0.25rem 0 0; + padding: 0; + font-size: 0.8rem; + color: rgba(245, 222, 179, 0.8); + + li + li { + margin-top: 0.15rem; + } + } + + .quest-warning { + margin-top: 0.3rem; + font-size: 0.75rem; + color: #ffb347; + } + + .offer-editor { + display: inline-flex; + align-items: center; + gap: 0.3rem; + + input { + width: 4.2rem; + text-align: center; + background: rgba(0, 0, 0, 0.4); + border: 1px solid rgba(255, 255, 255, 0.15); + color: inherit; + padding: 0.25rem 0.35rem; + } + + button { + width: 1.6rem; + height: 1.6rem; + padding: 0; + } + } + + .quick-actions { + margin-top: 0.4rem; + display: flex; + gap: 0.3rem; + + button { + padding: 0.25rem 0.6rem; + font-size: 0.75rem; + } + } + + .row-actions { + text-align: right; + + button { + padding: 0.3rem 0.75rem; + } + } +} + +.offers-section { + display: flex; + gap: 1rem; + + .offer-column { + flex: 1; + background: rgba(0, 0, 0, 0.18); + padding: 0.9rem; + border: 1px solid rgba(255, 255, 255, 0.08); + + &.partner { + border-color: rgba(255, 255, 255, 0.12); + } + + .section-title { + text-transform: uppercase; + letter-spacing: 0.1em; + font-size: 0.9rem; + margin-bottom: 0.6rem; + } + + .empty { + font-size: 0.85rem; + opacity: 0.75; + } + + ul { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.4rem; + + li { + display: flex; + flex-direction: column; + gap: 0.35rem; + font-size: 0.85rem; + + .line { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 0.5rem; + + .count { + font-variant-numeric: tabular-nums; + } + } + + .item-details { + list-style: none; + padding: 0; + margin: 0; + font-size: 0.75rem; + color: rgba(245, 222, 179, 0.75); + display: flex; + flex-direction: column; + gap: 0.2rem; + } + } + } + } +} + +.no-trade { + padding: 2rem; + text-align: center; + opacity: 0.75; +} + +.empty { + opacity: 0.75; +} + +button.secondary { + background: rgba(0, 0, 0, 0.35); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +button.secondary.ghost { + border-style: dashed; +} diff --git a/Code/skyrim_ui/src/app/components/trade-popup/trade-popup.component.ts b/Code/skyrim_ui/src/app/components/trade-popup/trade-popup.component.ts new file mode 100644 index 000000000..695e11cb7 --- /dev/null +++ b/Code/skyrim_ui/src/app/components/trade-popup/trade-popup.component.ts @@ -0,0 +1,65 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { Observable } from 'rxjs'; +import { + TradeInventoryItemView, + TradeOfferItemView, + TradeSessionView, + TradeUiService, +} from '../../services/trade-ui.service'; + +@Component({ + selector: 'app-trade-popup', + templateUrl: './trade-popup.component.html', + styleUrls: ['./trade-popup.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TradePopupComponent { + public readonly session$: Observable = + this.tradeUiService.session$; + + constructor(public readonly tradeUiService: TradeUiService) {} + + public close(): void { + this.tradeUiService.closePopup(); + } + + public cancelTrade(): void { + this.tradeUiService.cancelTrade(); + } + + public toggleReady(session: TradeSessionView): void { + this.tradeUiService.setReady(!session.selfReady); + } + + public updateOfferFromInput( + item: TradeInventoryItemView, + value: string | number, + ): void { + this.tradeUiService.updateOfferFromInput(item.index, value); + } + + public addToOffer(item: TradeInventoryItemView, delta: number): void { + this.tradeUiService.addToOffer(item.index, delta); + } + + public offerAll(item: TradeInventoryItemView): void { + this.tradeUiService.offerAll(item.index); + } + + public clearOffer(item: TradeInventoryItemView): void { + this.tradeUiService.clearOffer(item.index); + } + + public trackByInventory(_: number, item: TradeInventoryItemView): number { + return item.index; + } + + public trackByOffer(_: number, item: TradeOfferItemView): string { + return item.key ?? `${item.modId}:${item.baseId}:${item.index ?? -1}`; + } + + public formatCountdown(ms: number): string { + if (ms <= 0) return ''; + return (ms / 1000).toFixed(1); + } +} diff --git a/Code/skyrim_ui/src/app/mock/skyrimtogether.mock.ts b/Code/skyrim_ui/src/app/mock/skyrimtogether.mock.ts index 50f2d4815..c15ab0441 100644 --- a/Code/skyrim_ui/src/app/mock/skyrimtogether.mock.ts +++ b/Code/skyrim_ui/src/app/mock/skyrimtogether.mock.ts @@ -28,19 +28,38 @@ export class SkyrimtogetherMock extends EventEmitter implements SkyrimTogether { private playerName = 'Local Player'; private showEvents = true; private localPlayerId: number; + private nametagMode = 0; + private playerNamePreference = 'username'; + private partyOptions = { + syncFastTravelMarkers: true, + showPartyMemberMarkers: true, + syncDeadBodyLoot: false, + lockPartyToLeaderCell: false, + }; public readonly players$ = playerStore.pipe(selectAllEntities()); + private pendingTeleportRequests = new Set(); - connect(host: string, port: number, password: string): void { + connect( + host: string, + port: number, + username: string, + password: string, + serverPassword = '', + ): void { if (!this.connected) { let error: ErrorEvents | boolean; + const effectivePassword = serverPassword || password; + if (username && username.length > 0) { + this.playerName = username; + } switch (host) { case 't-port': case 't-host': error = true; break; case 't-password': - if (password !== 'test') { - error = { error: 'wrong_password' }; + if (effectivePassword !== 'test') { + error = { error: 'wrong_server_password' }; } break; case 't-version': @@ -106,11 +125,21 @@ export class SkyrimtogetherMock extends EventEmitter implements SkyrimTogether { } revealPlayers(): void { - this.sendMessage(MessageTypes.SYSTEM_MESSAGE, "Revealing players..."); + this.sendMessage(MessageTypes.SYSTEM_MESSAGE, 'Revealing players...'); + } + + playEmote(eventName: string): void { + this.sendMessage( + MessageTypes.SYSTEM_MESSAGE, + `Playing emote "${eventName}"`, + ); } setTime(hours: number, minutes: number): void { - this.sendMessage(MessageTypes.SYSTEM_MESSAGE, `Setting time to "${hours}:${minutes}"!`); + this.sendMessage( + MessageTypes.SYSTEM_MESSAGE, + `Setting time to "${hours}:${minutes}"!`, + ); } sendMessage(type: MessageTypes, message: string): void { @@ -124,28 +153,91 @@ export class SkyrimtogetherMock extends EventEmitter implements SkyrimTogether { } teleportToPlayer(playerId: number): void { - if (this.connected) { - const player = playerStore.query(getEntity(playerId)); - if (player) { - console.log( - `%cTELEPORT`, - 'background: #F09688; color: #fff; padding: 3px; font-size: 9px;', - 'Teleport to player', - JSON.stringify(player.name), - `(${player.id})`, - 'in', - JSON.stringify(player.cellName), - ); - } + if (!this.connected) { + return; + } + + const player = playerStore.query(getEntity(playerId)); + const name = player ? player.name : `Player ${playerId}`; + + this.sendMessage( + MessageTypes.SYSTEM_MESSAGE, + `Teleport request sent to "${name}".`, + ); + } + + respondTeleportRequest(requesterId: number, accepted: boolean): void { + if (!this.connected) { + return; + } + + this.pendingTeleportRequests.delete(requesterId); + const player = playerStore.query(getEntity(requesterId)); + const name = player ? player.name : `Player ${requesterId}`; + this.sendMessage( + MessageTypes.SYSTEM_MESSAGE, + accepted + ? `Accepted teleport request from player ${requesterId}.` + : `Declined teleport request from player ${requesterId}.`, + ); + + if (accepted) { + this.emit('teleportCountdown', requesterId, name, 5, false, ''); + setTimeout(() => { + this.emit('teleportCountdown', requesterId, name, 0, true, ''); + }, 1000); + } + } + + setMockTeleportRequest(requesterId: number, requesterName: string): void { + if (!this.connected) { + return; } + + this.pendingTeleportRequests.add(requesterId); + this.emit('teleportRequest', requesterId, requesterName); + } + + setMockTeleportCountdown( + targetPlayerId: number, + targetName: string, + secondsRemaining: number, + cancelled = false, + reason = '', + ): void { + if (!this.connected) { + return; + } + + this.emit( + 'teleportCountdown', + targetPlayerId, + targetName, + secondsRemaining, + cancelled, + reason, + ); } launchParty(): void { if (this.connected) { this.emit('partyCreated'); + this.emit('partyOptions', { ...this.partyOptions }); } } + showBanner(message: string, durationMs?: number): void { + this.emit('showBanner', message, durationMs); + } + + openEmoteMenu(openedFromInactive?: boolean): void { + this.emit('openEmoteMenu', openedFromInactive); + } + + toggleEmoteMenu(): void { + this.emit('toggleEmoteMenu'); + } + createPartyInvite(playerId: number): void { playerStore.update(updateEntities(playerId, { invited: true })); } @@ -162,6 +254,7 @@ export class SkyrimtogetherMock extends EventEmitter implements SkyrimTogether { ], inviterId, ); + this.emit('partyOptions', { ...this.partyOptions }); } kickPartyMember(playerId: number): void { @@ -195,6 +288,85 @@ export class SkyrimtogetherMock extends EventEmitter implements SkyrimTogether { ], playerId, ); + this.emit('partyOptions', { ...this.partyOptions }); + } + + sendTradeInvite(playerId: number): void { + if (!this.connected) { + return; + } + if (this.showEvents) { + console.log('[mock] sendTradeInvite', playerId); + } + } + + respondTradeInvite(playerId: number, accept: boolean): void { + if (!this.connected) { + return; + } + if (this.showEvents) { + console.log('[mock] respondTradeInvite', playerId, accept); + } + } + + cancelTrade(): void { + if (!this.connected) { + return; + } + if (this.showEvents) { + console.log('[mock] cancelTrade'); + } + } + + setTradeReady(ready: boolean): void { + if (!this.connected) { + return; + } + if (this.showEvents) { + console.log('[mock] setTradeReady', ready); + } + } + + updateTradeOffer(entries: SkyrimTogetherTypes.TradeOfferEntry[]): void { + if (!this.connected) { + return; + } + if (this.showEvents) { + console.log('[mock] updateTradeOffer', entries); + } + } + + setNameTagMode(mode: number): void { + this.nametagMode = mode; + if (this.showEvents) { + console.log('[mock] setNameTagMode', mode); + } + } + + setPlayerNamePreference(preference: string): void { + this.playerNamePreference = preference; + if (this.showEvents) { + console.log('[mock] setPlayerNamePreference', preference); + } + } + + setPartyOptions(options: SkyrimTogetherTypes.PartyOptionsPayload): void { + this.partyOptions = { + syncFastTravelMarkers: !!options?.syncFastTravelMarkers, + showPartyMemberMarkers: !!options?.showPartyMemberMarkers, + syncDeadBodyLoot: + typeof options?.syncDeadBodyLoot === 'boolean' + ? options.syncDeadBodyLoot + : false, + lockPartyToLeaderCell: + typeof options?.lockPartyToLeaderCell === 'boolean' + ? options.lockPartyToLeaderCell + : false, + }; + this.emit('partyOptions', { ...this.partyOptions }); + if (this.showEvents) { + console.log('[mock] setPartyOptions', this.partyOptions); + } } initMock() { @@ -217,6 +389,7 @@ export class SkyrimtogetherMock extends EventEmitter implements SkyrimTogether { this.emit('enterGame'); this.emit('setVersion', this.version); this.emit('setName', this.playerName); + this.emit('setSyncStatus', false, '', '', ''); fromEvent(window, 'keydown').subscribe((event: KeyboardEvent) => { if (event.ctrlKey && event.location === 2) { diff --git a/Code/skyrim_ui/src/app/models/nametag-mode.enum.ts b/Code/skyrim_ui/src/app/models/nametag-mode.enum.ts new file mode 100644 index 000000000..3489624ec --- /dev/null +++ b/Code/skyrim_ui/src/app/models/nametag-mode.enum.ts @@ -0,0 +1,6 @@ +export enum NametagMode { + Detailed = 0, + Basic = 1, + Hidden = 2, + Normal = 3, +} diff --git a/Code/skyrim_ui/src/app/models/party-options.ts b/Code/skyrim_ui/src/app/models/party-options.ts new file mode 100644 index 000000000..97d65f3db --- /dev/null +++ b/Code/skyrim_ui/src/app/models/party-options.ts @@ -0,0 +1,13 @@ +export interface PartyOptions { + syncFastTravelMarkers: boolean; + showPartyMemberMarkers: boolean; + syncDeadBodyLoot: boolean; + lockPartyToLeaderCell: boolean; +} + +export const DEFAULT_PARTY_OPTIONS: PartyOptions = { + syncFastTravelMarkers: false, + showPartyMemberMarkers: true, + syncDeadBodyLoot: false, + lockPartyToLeaderCell: false, +}; diff --git a/Code/skyrim_ui/src/app/models/party-pin.ts b/Code/skyrim_ui/src/app/models/party-pin.ts new file mode 100644 index 000000000..86072533b --- /dev/null +++ b/Code/skyrim_ui/src/app/models/party-pin.ts @@ -0,0 +1,8 @@ +export interface PartyPin { + x: number; // screen-space X in pixels + y: number; // screen-space Y in pixels + id: number; // player id + oob?: boolean; // true if placed as an edge indicator (different worldspace/interior) + name?: string; + avatar?: string; +} diff --git a/Code/skyrim_ui/src/app/models/player.ts b/Code/skyrim_ui/src/app/models/player.ts index 98e551a6a..bbc997df5 100644 --- a/Code/skyrim_ui/src/app/models/player.ts +++ b/Code/skyrim_ui/src/app/models/player.ts @@ -1,7 +1,7 @@ export interface Friend { id: number; name: string; - avatar: string; + avatar?: string; online: boolean; } @@ -13,7 +13,13 @@ export class Player implements Friend { /** Username. */ name: string; - avatar: string; + /** Character name, if available. */ + actorName?: string; + + /** Preferred display name based on settings. */ + displayName?: string; + + avatar?: string; /** Current health. */ health?: number; @@ -33,6 +39,9 @@ export class Player implements Friend { /** invitation sent. */ hasBeenInvited: boolean; + /** Pending teleport request awaiting response. */ + hasTeleportRequest: boolean; + /** CellName */ cellName: string; @@ -45,6 +54,7 @@ export class Player implements Friend { options: { id?: number; name?: string; + actorName?: string; avatar?: string; online?: boolean; connected?: boolean; @@ -55,11 +65,17 @@ export class Player implements Friend { cellName?: string; isLoaded?: boolean; isInLocalParty?: boolean; + hasTeleportRequest?: boolean; } = {}, ) { this.id = options.id || 0; this.name = options.name || ''; - this.avatar = options.avatar || ''; + if (options.actorName) { + this.actorName = options.actorName; + } + if (options.avatar) { + this.avatar = options.avatar; + } this.hasInvitedLocalPlayer = options.hasInvitedLocalPlayer || false; this.cellName = options.cellName || 'vide'; @@ -103,5 +119,6 @@ export class Player implements Friend { } this.isInLocalParty = options.isInLocalParty || false; + this.hasTeleportRequest = options.hasTeleportRequest || false; } } diff --git a/Code/skyrim_ui/src/app/models/view.enum.ts b/Code/skyrim_ui/src/app/models/view.enum.ts index d9d0e0465..d18c864c2 100644 --- a/Code/skyrim_ui/src/app/models/view.enum.ts +++ b/Code/skyrim_ui/src/app/models/view.enum.ts @@ -6,4 +6,6 @@ export enum View { SERVER_LIST, SETTINGS, PLAYER_MANAGER, + TRADE, + EMOTES, } diff --git a/Code/skyrim_ui/src/app/services/chat.service.ts b/Code/skyrim_ui/src/app/services/chat.service.ts index 18fbc414d..ca83efeef 100644 --- a/Code/skyrim_ui/src/app/services/chat.service.ts +++ b/Code/skyrim_ui/src/app/services/chat.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { ReplaySubject } from 'rxjs'; import { environment } from 'src/environments/environment'; -import { Command, CommandHandler } from './chat/commands'; +import { CommandHandler } from './chat/commands'; export enum MessageTypes { SYSTEM_MESSAGE = 0, @@ -9,6 +9,7 @@ export enum MessageTypes { PLAYER_DIALOGUE = 2, PARTY_CHAT = 3, LOCAL_CHAT = 4, + WHISPER = 5, } export interface ChatMessage { @@ -23,6 +24,7 @@ export interface ChatMessage { }) export class ChatService { public messageList = new ReplaySubject(); + public serverCommands: SkyrimTogetherTypes.CommandListEntry[] = []; /** * Send chat message to the Server. Does not add anything to the local mesesage list. @@ -40,8 +42,9 @@ export class ChatService { } if (content.startsWith(this.CommandHandler.COMMAND_PREFIX)) { - this.CommandHandler.tryExecute(content); - return; + if (this.CommandHandler.tryExecute(content)) { + return; + } } skyrimtogether.sendMessage(type, content); @@ -78,27 +81,23 @@ export class ChatService { private CommandHandler: CommandHandler; - private LocalChat: Command = { - name: 'local', - executor: async args => { - const content = args.join(' '); - this.sendMessage(MessageTypes.LOCAL_CHAT, content); - }, - }; - - private PartyChat: Command = { - name: 'party', - executor: async args => { - const content = args.join(' '); - this.sendMessage(MessageTypes.PARTY_CHAT, content); - }, - }; + private onCommandListReceived(commandsJson: string): void { + try { + const parsed = JSON.parse( + commandsJson, + ) as SkyrimTogetherTypes.CommandListEntry[]; + if (Array.isArray(parsed)) { + this.serverCommands = parsed; + } + } catch { + this.serverCommands = []; + } + } constructor() { skyrimtogether.on('message', this.onMessageRecieved.bind(this)); + skyrimtogether.on('commandList', this.onCommandListReceived.bind(this)); this.CommandHandler = new CommandHandler(this); - this.CommandHandler.register(this.LocalChat); - this.CommandHandler.register(this.PartyChat); } } diff --git a/Code/skyrim_ui/src/app/services/chat/commands.ts b/Code/skyrim_ui/src/app/services/chat/commands.ts index f2101dafa..b7610b474 100644 --- a/Code/skyrim_ui/src/app/services/chat/commands.ts +++ b/Code/skyrim_ui/src/app/services/chat/commands.ts @@ -6,49 +6,9 @@ export interface Command { } export class CommandHandler { - private Help: Command = { - name: 'help', - executor: async () => { - const cmds = [...this.commands.keys()].join(', '); - this.chatService.pushSystemMessage( - 'SERVICE.COMMANDS.AVAILABLE_COMMANDS', - { 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 constructor(private readonly chatService: ChatService) {} public readonly COMMAND_PREFIX = '/'; @@ -58,16 +18,14 @@ export class CommandHandler { } } - public async tryExecute(input: string) { + public tryExecute(input: string): boolean { const inputWithoutPrefix = input.slice(this.COMMAND_PREFIX.length); const [commandName, ...args] = inputWithoutPrefix.split(' '); const command = this.commands.get(commandName); if (command) { command.executor(args); - } else { - this.chatService.pushSystemMessage('SERVICE.COMMANDS.COMMAND_NOT_FOUND', { - cmd: commandName, - }); + return true; } + return false; } } diff --git a/Code/skyrim_ui/src/app/services/client.service.ts b/Code/skyrim_ui/src/app/services/client.service.ts index 2dc68f759..55beee6ba 100644 --- a/Code/skyrim_ui/src/app/services/client.service.ts +++ b/Code/skyrim_ui/src/app/services/client.service.ts @@ -4,10 +4,65 @@ import { AsyncSubject, BehaviorSubject, ReplaySubject, Subject } from 'rxjs'; import { environment } from '../../environments/environment'; import { Debug } from '../models/debug'; import { PartyInfo } from '../models/party-info'; +import { DEFAULT_PARTY_OPTIONS, PartyOptions } from '../models/party-options'; import { Player } from '../models/player'; +import { View } from '../models/view.enum'; import { ChatService } from './chat.service'; import { ErrorEvents, ErrorService } from './error.service'; import { LoadingService } from './loading.service'; +import { PartyPin } from '../models/party-pin'; +import { UiRepository } from '../store/ui.repository'; +import { OverlayBannerService } from './overlay-banner.service'; + +export interface TradeItemPayload { + modId: number; + baseId: number; + count: number; + isQuestItem: boolean; + name: string; + inventoryIndex?: number; + offeredCount?: number; + isGold?: boolean; + details?: string[]; + raw?: any; +} + +export interface TradeStatePayload { + active: boolean; + partnerId: number; + initiatedBySelf: boolean; + selfReady: boolean; + partnerReady: boolean; + selfItems: TradeItemPayload[]; + partnerItems: TradeItemPayload[]; + inventory: TradeItemPayload[]; + countdownMs: number; + countdownTotalMs: number; +} + +export interface TradeInvitePayload { + inviterId: number; + expiryTick: number; +} + +export interface TradeCancellationPayload { + partnerId: number; + reason: number; + wasInitiator: boolean; +} + +export interface ReviveProgressPayload { + elapsedSeconds: number; + totalSeconds: number; + label?: string; +} + +export interface SyncStatusPayload { + isolated: boolean; + title: string; + detail: string; + moreInfo: string; +} /** Client game service. */ @Injectable({ @@ -31,6 +86,14 @@ export class ClientService implements OnDestroy { public isConnectionInProgressChange = new BehaviorSubject(false); + /** Quest isolation / sync gating status. */ + public syncStatusChange = new BehaviorSubject({ + isolated: false, + title: '', + detail: '', + moreInfo: '', + }); + /** Player name change. */ public nameChange = new BehaviorSubject(environment.game ? '' : 'test'); @@ -49,12 +112,40 @@ export class ClientService implements OnDestroy { /** Connect party info change. */ public partyInfoChange = new Subject(); + /** Party options change. */ + public partyOptionsChange = new BehaviorSubject( + DEFAULT_PARTY_OPTIONS, + ); + /** Connect party info change. */ public partyLeftChange = new Subject(); + /** Party pins for world map overlay. */ + public partyPinsChange = new BehaviorSubject([]); + /** Connect party invite received. */ public partyInviteReceivedChange = new Subject(); + /** Trade invite received. */ + public tradeInviteChange = new Subject(); + + /** Trade invite expired. */ + public tradeInviteExpiredChange = new Subject(); + + /** Active trade state change. */ + public tradeStateChange = new BehaviorSubject( + undefined, + ); + + /** Trade cancellation notifications. */ + public tradeCancelledChange = new Subject(); + + /** Trade completion notifications. */ + public tradeCompletedChange = new Subject(); + + /** Player avatar update. */ + public avatarChange = new Subject(); + /** Disconnect player to server change. */ public playerDisconnectedChange = new Subject(); @@ -67,6 +158,9 @@ export class ClientService implements OnDestroy { /** Player cell change. */ public cellChange = new Subject(); + /** Player actor name change. */ + public actorNameChange = new Subject(); + /** Player isLoaded change. */ public isLoadedChange = new Subject(); @@ -78,6 +172,18 @@ export class ClientService implements OnDestroy { /** Used purely for debugging. */ public debugChange = new Subject(); + /** Teleport request received. */ + public teleportRequestChange = new Subject<{ + requesterId: number; + requesterName: string; + }>(); + + /** Teleport request handled. */ + public teleportRequestHandledChange = new Subject<{ + requesterId: number; + accepted: boolean; + }>(); + // The below emitters are used in the mocking service /** Used for when a party leader changed. */ @@ -95,13 +201,42 @@ export class ClientService implements OnDestroy { /** Used for when a party leader changed. */ public partyLeaderChange = new Subject(); + /** Death screen shown with countdown. */ + public deathScreenChange = new Subject(); + + /** Death screen timer update. */ + public deathTimerChange = new Subject(); + + /** Respawn button enabled. */ + public respawnButtonEnabledChange = new Subject(); + + /** Death screen hidden. */ + public deathScreenHiddenChange = new Subject(); + + /** Revive progress updates for the downed player. */ + public reviveVictimProgressChange = new BehaviorSubject< + ReviveProgressPayload | undefined + >(undefined); + + /** Revive progress updates for the healer. */ + public reviveHealerProgressChange = new BehaviorSubject< + ReviveProgressPayload | undefined + >(undefined); + public localPlayerId = undefined; + public localPlayerIdChange = new BehaviorSubject( + undefined, + ); + + private _host = ''; + + private _port = 0; - private _host: string; + private _username = ''; - private _port: number; + private _password = ''; - private _password: string; + private _serverPassword = ''; private _remainingReconnectionAttempt = environment.nbReconnectionAttempts; @@ -114,6 +249,8 @@ export class ClientService implements OnDestroy { private readonly loadingService: LoadingService, private readonly translocoService: TranslocoService, private readonly chatService: ChatService, + private readonly uiRepository: UiRepository, + private readonly overlayBannerService: OverlayBannerService, ) { skyrimtogether.on('init', this.onInit.bind(this)); skyrimtogether.on('activate', this.onActivate.bind(this)); @@ -132,25 +269,75 @@ export class ClientService implements OnDestroy { 'playerDisconnected', this.onPlayerDisconnected.bind(this), ); + skyrimtogether.on('playerAvatarUpdated', this.onPlayerAvatar.bind(this)); skyrimtogether.on('setHealth', this.onSetHealth.bind(this)); skyrimtogether.on('setLevel', this.onSetLevel.bind(this)); skyrimtogether.on('setCell', this.onSetCell.bind(this)); + skyrimtogether.on('setActorName', this.onSetActorName.bind(this)); skyrimtogether.on('setPlayer3dLoaded', this.onSetPlayer3dLoaded.bind(this)); skyrimtogether.on( 'setPlayer3dUnloaded', this.onSetPlayer3dUnloaded.bind(this), ); skyrimtogether.on('setLocalPlayerId', this.onSetLocalPlayerId.bind(this)); + (skyrimtogether as any).on( + 'setSyncStatus', + this.onSetSyncStatus.bind(this), + ); skyrimtogether.on('protocolMismatch', this.onProtocolMismatch.bind(this)); skyrimtogether.on('triggerError', this.onTriggerError.bind(this)); skyrimtogether.on('dummyData', this.onDummyData.bind(this)); skyrimtogether.on('partyInfo', this.onPartyInfo.bind(this)); + skyrimtogether.on('partyOptions', this.onPartyOptions.bind(this)); + skyrimtogether.on('teleportRequest', this.onTeleportRequest.bind(this)); + skyrimtogether.on('teleportCountdown', this.onTeleportCountdown.bind(this)); skyrimtogether.on('partyCreated', this.onPartyCreated.bind(this)); skyrimtogether.on('partyLeft', this.onPartyLeft.bind(this)); skyrimtogether.on( 'partyInviteReceived', this.onPartyInviteReceived.bind(this), ); + skyrimtogether.on( + 'tradeInviteReceived', + this.onTradeInviteReceived.bind(this), + ); + skyrimtogether.on( + 'tradeInviteExpired', + this.onTradeInviteExpired.bind(this), + ); + skyrimtogether.on('tradeStateUpdated', this.onTradeStateUpdated.bind(this)); + skyrimtogether.on('tradeCancelled', this.onTradeCancelled.bind(this)); + skyrimtogether.on('tradeCompleted', this.onTradeCompleted.bind(this)); + (skyrimtogether as any).on('setPartyPins', this.onSetPartyPins.bind(this)); + skyrimtogether.on('showDeathScreen', this.onShowDeathScreen.bind(this)); + skyrimtogether.on('updateDeathTimer', this.onUpdateDeathTimer.bind(this)); + skyrimtogether.on( + 'enableRespawnButton', + this.onEnableRespawnButton.bind(this), + ); + skyrimtogether.on('hideDeathScreen', this.onHideDeathScreen.bind(this)); + skyrimtogether.on( + 'updateReviveVictimProgress', + this.onUpdateReviveVictimProgress.bind(this), + ); + skyrimtogether.on( + 'stopReviveVictimProgress', + this.onStopReviveVictimProgress.bind(this), + ); + skyrimtogether.on( + 'updateReviveHealerProgress', + this.onUpdateReviveHealerProgress.bind(this), + ); + skyrimtogether.on( + 'stopReviveHealerProgress', + this.onStopReviveHealerProgress.bind(this), + ); + skyrimtogether.on('showBanner', this.onShowBanner.bind(this)); + skyrimtogether.on('openEmoteMenu', this.onOpenEmoteMenu.bind(this)); + (skyrimtogether as any).on( + 'toggleEmoteMenu', + this.onToggleEmoteMenu.bind(this), + ); } /** @@ -171,19 +358,42 @@ export class ClientService implements OnDestroy { skyrimtogether.off('debugData'); skyrimtogether.off('playerConnected'); skyrimtogether.off('playerDisconnected'); + skyrimtogether.off('playerAvatarUpdated'); skyrimtogether.off('setHealth'); skyrimtogether.off('setLevel'); skyrimtogether.off('setCell'); + skyrimtogether.off('setActorName'); skyrimtogether.off('setPlayer3dLoaded'); skyrimtogether.off('setPlayer3dUnloaded'); skyrimtogether.off('setLocalPlayerId'); + skyrimtogether.off('setSyncStatus'); skyrimtogether.off('protocolMismatch'); skyrimtogether.off('triggerError'); skyrimtogether.off('dummyData'); skyrimtogether.off('partyInfo'); + skyrimtogether.off('partyOptions'); + skyrimtogether.off('teleportRequest'); + skyrimtogether.off('teleportCountdown'); skyrimtogether.off('partyCreated'); skyrimtogether.off('partyLeft'); skyrimtogether.off('partyInviteReceived'); + skyrimtogether.off('tradeInviteReceived'); + skyrimtogether.off('tradeInviteExpired'); + skyrimtogether.off('tradeStateUpdated'); + skyrimtogether.off('tradeCancelled'); + skyrimtogether.off('tradeCompleted'); + (skyrimtogether as any).off('setPartyPins'); + skyrimtogether.off('showBanner'); + skyrimtogether.off('openEmoteMenu'); + (skyrimtogether as any).off('toggleEmoteMenu'); + skyrimtogether.off('showDeathScreen'); + skyrimtogether.off('updateDeathTimer'); + skyrimtogether.off('enableRespawnButton'); + skyrimtogether.off('hideDeathScreen'); + skyrimtogether.off('updateReviveVictimProgress'); + skyrimtogether.off('stopReviveVictimProgress'); + skyrimtogether.off('updateReviveHealerProgress'); + skyrimtogether.off('stopReviveHealerProgress'); } /** @@ -191,14 +401,34 @@ export class ClientService implements OnDestroy { * * @param host IP address or hostname. * @param port Port. - * @param password Password or admin password + * @param username Account username used for server-side authentication. + * @param password Account password used for server-side authentication. + * @param serverPassword Optional legacy server password/admin token. */ - public connect(host: string, port: number, password = ''): void { - skyrimtogether.connect(host, port, password); + public connect( + host: string, + port: number, + username: string, + password: string, + serverPassword = '', + ): void { + if (serverPassword && serverPassword.length > 0) { + skyrimtogether.connect(host, port, username, password, serverPassword); + } else { + skyrimtogether.connect(host, port, username, password); + } this.isConnectionInProgressChange.next(true); this._host = host; this._port = port; + this._username = username; this._password = password; + this._serverPassword = serverPassword; + + if (username && username.length > 0) { + this.zone.run(() => { + this.nameChange.next(username); + }); + } } /** @@ -216,6 +446,16 @@ export class ClientService implements OnDestroy { skyrimtogether.revealPlayers(); } + /** + * Trigger an emote animation locally (and sync it to other players). + */ + public playEmote(eventName: string): void { + if (!eventName) { + return; + } + skyrimtogether.playEmote(eventName); + } + /** * Launch a party. */ @@ -258,6 +498,36 @@ export class ClientService implements OnDestroy { skyrimtogether.changePartyLeader(playerId); } + /** Send a trade invite to another player. */ + public sendTradeInvite(playerId: number): void { + skyrimtogether.sendTradeInvite(playerId); + } + + /** Respond to an incoming trade invite. */ + public respondTradeInvite(playerId: number, accept: boolean): void { + skyrimtogether.respondTradeInvite(playerId, accept); + } + + /** Cancel the current trade session or outstanding invite. */ + public cancelTrade(): void { + skyrimtogether.cancelTrade(); + } + + /** Toggle readiness state in the current trade session. */ + public setTradeReady(ready: boolean): void { + skyrimtogether.setTradeReady(ready); + } + + /** Update the offered items for the current trade session. */ + public updateTradeOffer(entries: { index: number; count: number }[]): void { + skyrimtogether.updateTradeOffer(entries); + } + + /** Upload or clear the local profile picture. */ + public setProfilePicture(imageData: string): void { + skyrimtogether.setProfilePicture(imageData); + } + /** * Deactivate UI and release control. */ @@ -273,10 +543,32 @@ export class ClientService implements OnDestroy { this._remainingReconnectionAttempt = 0; } - public teleportToPlayer(playerId: number): void { + /** + * Request teleportation to another player. + * + * @param playerId Target player identifier. + */ + public requestTeleport(playerId: number): void { skyrimtogether.teleportToPlayer(playerId); } + /** + * Respond to an incoming teleport request. + * + * @param requesterId Requesting player's identifier. + * @param accepted Whether the request is accepted. + */ + public respondTeleportRequest(requesterId: number, accepted: boolean): void { + skyrimtogether.respondTeleportRequest(requesterId, accepted); + this.zone.run(() => { + this.teleportRequestHandledChange.next({ requesterId, accepted }); + }); + } + + public teleportToPlayer(playerId: number): void { + this.requestTeleport(playerId); + } + /** * Called when the UI is first initialized. */ @@ -353,13 +645,26 @@ export class ClientService implements OnDestroy { private onDisconnect(isError: boolean): void { void this.zone.run(async () => { this.localPlayerId = undefined; + this.localPlayerIdChange.next(undefined); this.connectionStateChange.next(false); this.isConnectionInProgressChange.next(false); + this.syncStatusChange.next({ + isolated: false, + title: '', + detail: '', + moreInfo: '', + }); if (isError && this._remainingReconnectionAttempt > 0) { this._remainingReconnectionAttempt--; this.chatService.pushSystemMessage('SERVICE.CLIENT.CONNECTION_LOST'); - this.connect(this._host, this._port, this._password); + this.connect( + this._host, + this._port, + this._username ?? '', + this._password ?? '', + this._serverPassword ?? '', + ); } else { this.chatService.pushSystemMessage('SERVICE.CLIENT.DISCONNECTED'); } @@ -373,7 +678,9 @@ export class ClientService implements OnDestroy { */ private onSetName(name: string): void { this.zone.run(() => { - this.nameChange.next(name); + const effectiveName = + this._username && this._username.length > 0 ? this._username : name; + this.nameChange.next(effectiveName); }); } @@ -430,6 +737,7 @@ export class ClientService implements OnDestroy { username: string, level: number, cellName: string, + avatar: string, ) { if (environment.game) { console.log( @@ -446,6 +754,7 @@ export class ClientService implements OnDestroy { connected: true, level: level, cellName: cellName, + avatar: avatar, }), ); }); @@ -470,6 +779,24 @@ export class ClientService implements OnDestroy { }); } + private onPlayerAvatar(playerId: number, avatar: string) { + if (environment.game) { + console.log( + `%conPlayerAvatar`, + 'background: #009688; color: #fff; padding: 3px; font-size: 9px;', + ...Array.from(arguments).map(v => JSON.stringify(v)), + ); + } + this.zone.run(() => { + this.avatarChange.next( + new Player({ + id: playerId, + avatar: avatar, + }), + ); + }); + } + private onSetHealth(playerId: number, health: number) { this.zone.run(() => { this.healthChange.next(new Player({ id: playerId, health: health })); @@ -502,6 +829,21 @@ export class ClientService implements OnDestroy { }); } + private onSetActorName(playerId: number, actorName: string) { + if (environment.game) { + console.log( + `%conSetActorName`, + 'background: #009688; color: #fff; padding: 3px; font-size: 9px;', + ...Array.from(arguments).map(v => JSON.stringify(v)), + ); + } + this.zone.run(() => { + this.actorNameChange.next( + new Player({ id: playerId, actorName: actorName }), + ); + }); + } + private onSetPlayer3dLoaded(playerId: number, health: number) { if (environment.game) { console.log( @@ -540,6 +882,23 @@ export class ClientService implements OnDestroy { } this.zone.run(() => { this.localPlayerId = playerId; + this.localPlayerIdChange.next(playerId); + }); + } + + private onSetSyncStatus( + isolated: boolean, + title: string, + detail: string, + moreInfo?: string, + ) { + this.zone.run(() => { + this.syncStatusChange.next({ + isolated, + title, + detail, + moreInfo: moreInfo || '', + }); }); } @@ -553,10 +912,71 @@ export class ClientService implements OnDestroy { this.zone.run(() => { const error = JSON.parse(rawError) as ErrorEvents; this.triggerError.next(error); + if (error.error === 'wrong_server_password' && this._host) { + this._serverPassword = ''; + const currentView = this.uiRepository.getView(); + const defaultReturnView = + currentView === View.SERVER_LIST ? View.SERVER_LIST : View.CONNECT; + const storedReturnView = this.uiRepository.getConnectReturnView(); + const returnView = + currentView === View.CONNECT + ? defaultReturnView + : storedReturnView ?? defaultReturnView; + const storedName = this.uiRepository.getConnectName(); + const connectName = + storedName && storedName.length > 0 ? storedName : this._host; + this.uiRepository.openConnectWithPasswordView( + this._host, + this._port, + connectName, + returnView, + this._username, + this._password, + ); + } else if (error.error === 'wrong_account_password') { + this.uiRepository.openView(View.CONNECT); + } else if (error.error === 'duplicate_user') { + this.uiRepository.openView(View.CONNECT); + } void this.errorService.setError(error); }); } + private emoteOpenedFromInactive = false; + + private onOpenEmoteMenu(openedFromInactive?: boolean) { + this.emoteOpenedFromInactive = !!openedFromInactive; + this.zone.run(() => { + this.activationStateChange.next(true); + this.uiRepository.openView(View.EMOTES); + }); + } + + private onToggleEmoteMenu() { + this.zone.run(() => { + const currentView = this.uiRepository.getView(); + if (currentView === View.EMOTES) { + this.uiRepository.closeView(); + if (this.emoteOpenedFromInactive) { + this.emoteOpenedFromInactive = false; + this.deactivate(); + } + } + }); + } + + private onShowBanner(message: string, durationMs?: number) { + this.zone.run(() => { + this.overlayBannerService.show( + { + primary: message, + tone: 'info', + }, + durationMs && durationMs > 0 ? durationMs : undefined, + ); + }); + } + private onDummyData(data: Array) { this.zone.run(() => { this.debugChange.next(); @@ -587,6 +1007,33 @@ export class ClientService implements OnDestroy { }); } + public onPartyOptions(options: PartyOptions) { + if (environment.game) { + console.log( + `%conPartyOptions`, + 'background: #009688; color: #fff; padding: 3px; font-size: 9px;', + ...Array.from(arguments).map(v => JSON.stringify(v)), + ); + } + if (!options) { + return; + } + this.zone.run(() => { + this.partyOptionsChange.next({ + syncFastTravelMarkers: !!options.syncFastTravelMarkers, + showPartyMemberMarkers: !!options.showPartyMemberMarkers, + syncDeadBodyLoot: + typeof options.syncDeadBodyLoot === 'boolean' + ? options.syncDeadBodyLoot + : DEFAULT_PARTY_OPTIONS.syncDeadBodyLoot, + lockPartyToLeaderCell: + typeof options.lockPartyToLeaderCell === 'boolean' + ? options.lockPartyToLeaderCell + : DEFAULT_PARTY_OPTIONS.lockPartyToLeaderCell, + }); + }); + } + private onPartyCreated() { if (environment.game) { console.log( @@ -616,9 +1063,102 @@ export class ClientService implements OnDestroy { } this.zone.run(() => { this.partyLeftChange.next(); + this.partyOptionsChange.next(DEFAULT_PARTY_OPTIONS); + }); + } + + private onTeleportRequest(requesterId: number, requesterName: string): void { + this.zone.run(() => { + this.teleportRequestChange.next({ requesterId, requesterName }); + }); + } + + private onTeleportCountdown( + targetPlayerId: number, + targetName: string, + secondsRemaining: number, + cancelled: boolean, + reason: string, + ): void { + if (environment.game) { + console.log( + `%conTeleportCountdown`, + 'background: #3f51b5; color: #fff; padding: 3px; font-size: 9px;', + targetPlayerId, + targetName, + secondsRemaining, + cancelled, + reason, + ); + } + + this.zone.run(() => { + if (cancelled) { + if (reason && reason.length > 0) { + this.overlayBannerService.show( + { + primary: reason, + tone: 'error', + }, + 4000, + ); + } else { + this.overlayBannerService.hide(); + } + return; + } + + const safeSeconds = Math.max(0, secondsRemaining); + const primary = this.translocoService.translate( + 'SERVICE.OVERLAY_BANNER.TELEPORT_COUNTDOWN_TITLE', + { name: targetName }, + ); + const secondary = this.translocoService.translate( + 'SERVICE.OVERLAY_BANNER.TELEPORT_COUNTDOWN_SUBTITLE', + { seconds: safeSeconds }, + ); + this.overlayBannerService.show({ + primary, + secondary, + tone: 'info', + }); }); } + private onSetPartyPins(json: string) { + try { + const pins = JSON.parse(json) as Array<{ + x: unknown; + y: unknown; + id: unknown; + oob?: unknown; + name?: unknown; + avatar?: unknown; + }>; + const normalized = pins.map(pin => { + const asNumber = (value: unknown) => { + if (typeof value === 'number') { + return value; + } + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : 0; + }; + + return { + x: asNumber(pin.x), + y: asNumber(pin.y), + id: Math.trunc(asNumber(pin.id)), + oob: Boolean(pin.oob), + name: typeof pin.name === 'string' ? pin.name : undefined, + avatar: typeof pin.avatar === 'string' ? pin.avatar : undefined, + }; + }); + this.zone.run(() => this.partyPinsChange.next(normalized)); + } catch (e) { + // ignore invalid payloads + } + } + private onPartyInviteReceived(inviterId: number) { if (environment.game) { console.log( @@ -631,4 +1171,304 @@ export class ClientService implements OnDestroy { this.partyInviteReceivedChange.next(inviterId); }); } + + private onTradeInviteReceived(inviterId: number, expiryTick: number) { + if (environment.game) { + console.log( + `%conTradeInviteReceived`, + 'background: #FFC107; color: #000; padding: 3px; font-size: 9px;', + inviterId, + expiryTick, + ); + } + this.zone.run(() => { + this.tradeInviteChange.next({ inviterId, expiryTick }); + }); + } + + private onTradeInviteExpired(inviterId: number) { + if (environment.game) { + console.log( + `%conTradeInviteExpired`, + 'background: #FFC107; color: #000; padding: 3px; font-size: 9px;', + inviterId, + ); + } + this.zone.run(() => { + this.tradeInviteExpiredChange.next(inviterId); + }); + } + + private onTradeStateUpdated( + active: boolean, + partnerId: number, + initiatedBySelf: boolean, + selfReady: boolean, + partnerReady: boolean, + selfItems: unknown[], + partnerItems: unknown[], + inventory: unknown[], + countdownMs: number, + countdownTotalMs: number, + ) { + if (environment.game) { + console.log( + `%conTradeStateUpdated`, + 'background: #FFC107; color: #000; padding: 3px; font-size: 9px;', + active, + partnerId, + initiatedBySelf, + selfReady, + partnerReady, + selfItems, + partnerItems, + inventory, + countdownMs, + countdownTotalMs, + ); + } + + const normalized: TradeStatePayload = { + active: Boolean(active), + partnerId: Number.isFinite(partnerId) ? partnerId : 0, + initiatedBySelf: Boolean(initiatedBySelf), + selfReady: Boolean(selfReady), + partnerReady: Boolean(partnerReady), + selfItems: Array.isArray(selfItems) + ? selfItems.map(item => this.normalizeTradeItem(item)) + : [], + partnerItems: Array.isArray(partnerItems) + ? partnerItems.map(item => this.normalizeTradeItem(item)) + : [], + inventory: Array.isArray(inventory) + ? inventory.map(item => this.normalizeTradeItem(item)) + : [], + countdownMs: Number.isFinite(countdownMs) ? Number(countdownMs) : 0, + countdownTotalMs: Number.isFinite(countdownTotalMs) + ? Number(countdownTotalMs) + : 0, + }; + + this.zone.run(() => { + this.tradeStateChange.next(normalized); + }); + } + + private onTradeCancelled( + partnerId: number, + reason: number, + wasInitiator: boolean, + ) { + if (environment.game) { + console.log( + `%conTradeCancelled`, + 'background: #FFC107; color: #000; padding: 3px; font-size: 9px;', + partnerId, + reason, + wasInitiator, + ); + } + this.zone.run(() => { + this.tradeCancelledChange.next({ partnerId, reason, wasInitiator }); + }); + } + + private onTradeCompleted(partnerId: number) { + if (environment.game) { + console.log( + `%conTradeCompleted`, + 'background: #FFC107; color: #000; padding: 3px; font-size: 9px;', + partnerId, + ); + } + this.zone.run(() => { + this.tradeCompletedChange.next(partnerId); + }); + } + + private normalizeTradeItem(payload: any): TradeItemPayload { + const modId = Number.isFinite(payload?.modId) ? Number(payload.modId) : 0; + const baseId = Number.isFinite(payload?.baseId) + ? Number(payload.baseId) + : 0; + const count = Math.max( + 0, + Number.isFinite(payload?.count) ? Number(payload.count) : 0, + ); + const isQuestItem = Boolean(payload?.isQuestItem); + const fallbackName = `0x${modId.toString(16).padStart(8, '0')}:0x${baseId + .toString(16) + .padStart(8, '0')}`; + const name = + typeof payload?.name === 'string' && payload.name.length > 0 + ? payload.name + : fallbackName; + + const item: TradeItemPayload = { + modId, + baseId, + count, + isQuestItem, + name, + }; + + if (payload?.inventoryIndex !== undefined) { + item.inventoryIndex = Number(payload.inventoryIndex); + } + if (payload?.offeredCount !== undefined) { + item.offeredCount = Math.max( + 0, + Number.isFinite(payload.offeredCount) + ? Number(payload.offeredCount) + : 0, + ); + } + if (payload?.isGold !== undefined) { + item.isGold = Boolean(payload.isGold); + } + if (Array.isArray(payload?.details)) { + item.details = payload.details.map((entry: any) => String(entry)); + } + if (item.isGold === undefined) { + item.isGold = modId === 0 && baseId === 0x0000000f; + } + item.raw = payload; + + return item; + } + + /** + * Called when the death screen is shown. + */ + private onShowDeathScreen(secondsRemaining: number): void { + if (environment.game) { + console.log( + `%conShowDeathScreen`, + 'background: #f44336; color: #fff; padding: 3px; font-size: 9px;', + secondsRemaining, + ); + } + this.zone.run(() => { + this.deathScreenChange.next(secondsRemaining); + }); + } + + /** + * Called when the death screen timer updates. + */ + private onUpdateDeathTimer(secondsRemaining: number): void { + if (environment.game) { + console.log( + `%conUpdateDeathTimer`, + 'background: #f44336; color: #fff; padding: 3px; font-size: 9px;', + secondsRemaining, + ); + } + this.zone.run(() => { + this.deathTimerChange.next(secondsRemaining); + }); + } + + /** + * Called when the respawn button is enabled. + */ + private onEnableRespawnButton(): void { + if (environment.game) { + console.log( + `%conEnableRespawnButton`, + 'background: #4caf50; color: #fff; padding: 3px; font-size: 9px;', + ); + } + this.zone.run(() => { + this.respawnButtonEnabledChange.next(); + }); + } + + /** + * Called when the death screen is hidden. + */ + private onHideDeathScreen(): void { + if (environment.game) { + console.log( + `%conHideDeathScreen`, + 'background: #2196f3; color: #fff; padding: 3px; font-size: 9px;', + ); + } + this.zone.run(() => { + this.deathScreenHiddenChange.next(); + }); + } + + private onUpdateReviveVictimProgress( + elapsedSeconds: number, + totalSeconds: number, + healerName: string, + ): void { + if (environment.game) { + console.log( + `%conUpdateReviveVictimProgress`, + 'background: #9c27b0; color: #fff; padding: 3px; font-size: 9px;', + elapsedSeconds, + totalSeconds, + ); + } + this.zone.run(() => { + this.reviveVictimProgressChange.next({ + elapsedSeconds, + totalSeconds, + label: healerName, + }); + }); + } + + private onStopReviveVictimProgress(): void { + if (environment.game) { + console.log( + `%conStopReviveVictimProgress`, + 'background: #9c27b0; color: #fff; padding: 3px; font-size: 9px;', + ); + } + this.zone.run(() => { + this.reviveVictimProgressChange.next(undefined); + }); + } + + private onUpdateReviveHealerProgress( + elapsedSeconds: number, + totalSeconds: number, + ): void { + if (environment.game) { + console.log( + `%conUpdateReviveHealerProgress`, + 'background: #00acc1; color: #fff; padding: 3px; font-size: 9px;', + elapsedSeconds, + totalSeconds, + ); + } + this.zone.run(() => { + this.reviveHealerProgressChange.next({ + elapsedSeconds, + totalSeconds, + }); + }); + } + + private onStopReviveHealerProgress(): void { + if (environment.game) { + console.log( + `%conStopReviveHealerProgress`, + 'background: #00acc1; color: #fff; padding: 3px; font-size: 9px;', + ); + } + this.zone.run(() => { + this.reviveHealerProgressChange.next(undefined); + }); + } + + /** + * Called when the player clicks the respawn button. + */ + public respawnButtonClicked(): void { + skyrimtogether.respawnButtonClicked(); + } } diff --git a/Code/skyrim_ui/src/app/services/error.service.ts b/Code/skyrim_ui/src/app/services/error.service.ts index 422b8f955..6491cbda3 100644 --- a/Code/skyrim_ui/src/app/services/error.service.ts +++ b/Code/skyrim_ui/src/app/services/error.service.ts @@ -8,7 +8,9 @@ export interface ErrorEvent { | 'wrong_version' | 'mods_mismatch' | 'client_mods_disallowed' - | 'wrong_password' + | 'wrong_account_password' + | 'wrong_server_password' + | 'duplicate_user' | 'server_full' | 'no_reason' | 'bad_uGridsToLoad' @@ -93,6 +95,9 @@ export class ErrorService { case 'client_mods_disallowed': data = { mods: error.data.mods.join(', ') }; break; + case 'duplicate_user': + data = {}; + break; } message = await firstValueFrom( this.translocoService.selectTranslate( diff --git a/Code/skyrim_ui/src/app/services/group.service.ts b/Code/skyrim_ui/src/app/services/group.service.ts index 706a19f93..8e0850369 100644 --- a/Code/skyrim_ui/src/app/services/group.service.ts +++ b/Code/skyrim_ui/src/app/services/group.service.ts @@ -18,6 +18,7 @@ import { ErrorService } from './error.service'; import { LoadingService } from './loading.service'; import { PlayerListService } from './player-list.service'; import { Sound, SoundService } from './sound.service'; +import { SettingService } from './setting.service'; @Injectable({ providedIn: 'root', @@ -45,6 +46,7 @@ export class GroupService implements OnDestroy { private readonly playerListService: PlayerListService, private readonly loadingService: LoadingService, private readonly translocoService: TranslocoService, + private readonly settingService: SettingService, ) { this.onDebug(); this.onConnectionStateChanged(); @@ -186,7 +188,7 @@ export class GroupService implements OnDestroy { if (p) { p.level = player.level; this.chatService.pushSystemMessage('SERVICE.GROUP.LEVEL_UP', { - name: p.name, + name: this.settingService.resolvePlayerName(p, p.name), level: player.level, }); } @@ -404,7 +406,7 @@ export class GroupService implements OnDestroy { if (group) { let player = this.playerListService.getPlayerById(group.owner); if (player) { - return player.name; + return this.settingService.resolvePlayerName(player, player.name); } } return ''; diff --git a/Code/skyrim_ui/src/app/services/overlay-banner.service.ts b/Code/skyrim_ui/src/app/services/overlay-banner.service.ts new file mode 100644 index 000000000..4a2af0024 --- /dev/null +++ b/Code/skyrim_ui/src/app/services/overlay-banner.service.ts @@ -0,0 +1,48 @@ +import { Injectable, OnDestroy } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +export type OverlayBannerTone = 'info' | 'error'; + +export interface OverlayBanner { + primary: string; + secondary?: string; + tone?: OverlayBannerTone; +} + +@Injectable({ + providedIn: 'root', +}) +export class OverlayBannerService implements OnDestroy { + private readonly bannerSubject = new BehaviorSubject( + null, + ); + public readonly banner$ = this.bannerSubject.asObservable(); + + private hideTimeout: number | undefined; + + ngOnDestroy(): void { + this.clearTimeout(); + } + + show(banner: OverlayBanner, durationMs?: number): void { + this.clearTimeout(); + this.bannerSubject.next({ tone: 'info', ...banner }); + if (durationMs && durationMs > 0) { + this.hideTimeout = window.setTimeout(() => { + this.hide(); + }, durationMs); + } + } + + hide(): void { + this.clearTimeout(); + this.bannerSubject.next(null); + } + + private clearTimeout(): void { + if (this.hideTimeout !== undefined) { + window.clearTimeout(this.hideTimeout); + this.hideTimeout = undefined; + } + } +} diff --git a/Code/skyrim_ui/src/app/services/party-options.service.ts b/Code/skyrim_ui/src/app/services/party-options.service.ts new file mode 100644 index 000000000..dde741262 --- /dev/null +++ b/Code/skyrim_ui/src/app/services/party-options.service.ts @@ -0,0 +1,132 @@ +import { Injectable, OnDestroy } from '@angular/core'; +import { BehaviorSubject, Subscription } from 'rxjs'; +import { environment } from '../../environments/environment'; +import { PartyInfo } from '../models/party-info'; +import { DEFAULT_PARTY_OPTIONS, PartyOptions } from '../models/party-options'; +import { ClientService } from './client.service'; +import { StoreService } from './store.service'; + +@Injectable({ + providedIn: 'root', +}) +export class PartyOptionsService implements OnDestroy { + public options$ = new BehaviorSubject(DEFAULT_PARTY_OPTIONS); + public isLeader$ = new BehaviorSubject(false); + public inParty$ = new BehaviorSubject(false); + + private storedOptions = DEFAULT_PARTY_OPTIONS; + private partyInfoSubscription: Subscription; + private partyLeftSubscription: Subscription; + private partyOptionsSubscription: Subscription; + + constructor( + private readonly clientService: ClientService, + private readonly storeService: StoreService, + ) { + this.storedOptions = this.loadStoredOptions(); + this.options$.next(this.storedOptions); + + this.partyOptionsSubscription = + this.clientService.partyOptionsChange.subscribe(options => { + this.options$.next(options); + }); + + this.partyInfoSubscription = this.clientService.partyInfoChange.subscribe( + partyInfo => this.handlePartyInfo(partyInfo), + ); + + this.partyLeftSubscription = this.clientService.partyLeftChange.subscribe( + () => this.handlePartyLeft(), + ); + } + + public ngOnDestroy(): void { + this.partyInfoSubscription.unsubscribe(); + this.partyLeftSubscription.unsubscribe(); + this.partyOptionsSubscription.unsubscribe(); + } + + public updateOption(key: keyof PartyOptions, value: boolean): void { + const nextOptions = { ...this.options$.getValue(), [key]: value }; + this.options$.next(nextOptions); + this.storeOptions(nextOptions); + + if (this.isLeader$.getValue()) { + this.pushPartyOptions(nextOptions); + } + } + + private handlePartyInfo(partyInfo: PartyInfo): void { + this.inParty$.next(true); + + const localId = this.clientService.localPlayerId; + const isLeader = localId !== undefined && localId === partyInfo.leaderId; + const wasLeader = this.isLeader$.getValue(); + + this.isLeader$.next(isLeader); + + if (isLeader && !wasLeader) { + const stored = this.loadStoredOptions(); + this.options$.next(stored); + this.pushPartyOptions(stored); + } + } + + private handlePartyLeft(): void { + this.inParty$.next(false); + this.isLeader$.next(false); + this.options$.next(this.storedOptions); + } + + private loadStoredOptions(): PartyOptions { + const raw = this.storeService.get('party_options', ''); + if (!raw) { + return { ...DEFAULT_PARTY_OPTIONS }; + } + + try { + const parsed = JSON.parse(raw); + return { + syncFastTravelMarkers: + typeof parsed.syncFastTravelMarkers === 'boolean' + ? parsed.syncFastTravelMarkers + : DEFAULT_PARTY_OPTIONS.syncFastTravelMarkers, + showPartyMemberMarkers: + typeof parsed.showPartyMemberMarkers === 'boolean' + ? parsed.showPartyMemberMarkers + : DEFAULT_PARTY_OPTIONS.showPartyMemberMarkers, + syncDeadBodyLoot: + typeof parsed.syncDeadBodyLoot === 'boolean' + ? parsed.syncDeadBodyLoot + : DEFAULT_PARTY_OPTIONS.syncDeadBodyLoot, + lockPartyToLeaderCell: + typeof parsed.lockPartyToLeaderCell === 'boolean' + ? parsed.lockPartyToLeaderCell + : DEFAULT_PARTY_OPTIONS.lockPartyToLeaderCell, + }; + } catch { + return { ...DEFAULT_PARTY_OPTIONS }; + } + } + + private storeOptions(options: PartyOptions): void { + this.storedOptions = { ...options }; + this.storeService.set('party_options', JSON.stringify(options)); + } + + private pushPartyOptions(options: PartyOptions): void { + if (!environment.game) { + return; + } + + const api = (globalThis as any).skyrimtogether; + if (api && typeof api.setPartyOptions === 'function') { + api.setPartyOptions({ + syncFastTravelMarkers: options.syncFastTravelMarkers, + showPartyMemberMarkers: options.showPartyMemberMarkers, + syncDeadBodyLoot: options.syncDeadBodyLoot, + lockPartyToLeaderCell: options.lockPartyToLeaderCell, + }); + } + } +} diff --git a/Code/skyrim_ui/src/app/services/player-list.service.ts b/Code/skyrim_ui/src/app/services/player-list.service.ts index cc81ac68e..720ee9d12 100644 --- a/Code/skyrim_ui/src/app/services/player-list.service.ts +++ b/Code/skyrim_ui/src/app/services/player-list.service.ts @@ -8,12 +8,14 @@ import { View } from '../models/view.enum'; import { UiRepository } from '../store/ui.repository'; import { ClientService } from './client.service'; import { PopupNotificationService } from './popup-notification.service'; +import { SettingService } from './setting.service'; @Injectable({ providedIn: 'root', }) export class PlayerListService implements OnDestroy { public playerList = new BehaviorSubject(undefined); + private readonly defaultAvatar = 'assets/images/group/avatar-placeholder.png'; private debugSubscription: Subscription; private connectionSubscription: Subscription; @@ -22,6 +24,13 @@ export class PlayerListService implements OnDestroy { private memberKickedSubscription: Subscription; private cellSubscription: Subscription; private partyInviteReceivedSubscription: Subscription; + private teleportRequestSubscription: Subscription; + private teleportRequestHandledSubscription: Subscription; + private avatarSubscription: Subscription; + private actorNameSubscription: Subscription; + private nameSubscription: Subscription; + private localPlayerSubscription: Subscription; + private namePreferenceSubscription: Subscription; private isConnect = false; @@ -30,6 +39,7 @@ export class PlayerListService implements OnDestroy { private readonly popupNotificationService: PopupNotificationService, private readonly uiRepository: UiRepository, private readonly translocoService: TranslocoService, + private readonly settingService: SettingService, ) { this.onDebug(); this.onConnectionStateChanged(); @@ -38,6 +48,12 @@ export class PlayerListService implements OnDestroy { this.onMemberKicked(); this.onCellChange(); this.onPartyInviteReceived(); + this.onTeleportRequest(); + this.onTeleportRequestHandled(); + this.onAvatarChange(); + this.onActorNameChange(); + this.onLocalPlayerMetadata(); + this.onNamePreferenceChange(); } ngOnDestroy() { @@ -47,6 +63,13 @@ export class PlayerListService implements OnDestroy { this.playerDisconnectedSubscription.unsubscribe(); this.cellSubscription.unsubscribe(); this.partyInviteReceivedSubscription.unsubscribe(); + this.teleportRequestSubscription.unsubscribe(); + this.teleportRequestHandledSubscription.unsubscribe(); + this.avatarSubscription.unsubscribe(); + this.actorNameSubscription.unsubscribe(); + this.nameSubscription.unsubscribe(); + this.localPlayerSubscription.unsubscribe(); + this.namePreferenceSubscription.unsubscribe(); } private onDebug() { @@ -65,6 +88,9 @@ export class PlayerListService implements OnDestroy { this.playerList.next(undefined); this.updatePlayerList(); + if (connect) { + this.ensureLocalPlayerEntry(); + } }); } @@ -74,8 +100,33 @@ export class PlayerListService implements OnDestroy { const playerList = this.getPlayerList(); if (playerList) { - playerList.players.push(player); - + const existing = playerList.players.find( + entry => entry.id === player.id, + ); + if (existing) { + existing.name = player.name; + existing.level = player.level; + existing.cellName = player.cellName; + existing.connected = player.connected; + existing.online = player.online; + this.updateDisplayName(existing); + existing.avatar = + player.avatar && player.avatar.length > 0 + ? player.avatar + : existing.avatar || this.defaultAvatar; + } else { + playerList.players.push( + new Player({ + ...player, + avatar: + player.avatar && player.avatar.length > 0 + ? player.avatar + : this.defaultAvatar, + }), + ); + const created = playerList.players[playerList.players.length - 1]; + this.updateDisplayName(created); + } this.playerList.next(playerList); } }); @@ -133,9 +184,10 @@ export class PlayerListService implements OnDestroy { if (playerList) { const invitingPlayer = this.getPlayerById(inviterId); invitingPlayer.hasInvitedLocalPlayer = true; + this.updateDisplayName(invitingPlayer); this.playerList.next(playerList); this.popupNotificationService.addPartyInvite( - invitingPlayer.name, + invitingPlayer.displayName || invitingPlayer.name, () => this.acceptPartyInvite(inviterId), ); } @@ -143,6 +195,191 @@ export class PlayerListService implements OnDestroy { ); } + private onTeleportRequest() { + this.teleportRequestSubscription = + this.clientService.teleportRequestChange.subscribe( + ({ requesterId, requesterName }) => { + const playerList = this.getPlayerList(); + const player = playerList + ? this.getPlayerById(requesterId) + : undefined; + const displayName = this.resolveDisplayName(player, requesterName); + + if (player && playerList) { + player.hasTeleportRequest = true; + this.playerList.next(playerList); + } + + this.popupNotificationService.addTeleportRequest( + displayName, + () => { + if (player && playerList) { + player.hasTeleportRequest = false; + this.playerList.next(playerList); + } + this.clientService.respondTeleportRequest(requesterId, true); + }, + () => { + if (player && playerList) { + player.hasTeleportRequest = false; + this.playerList.next(playerList); + } + this.clientService.respondTeleportRequest(requesterId, false); + }, + ); + }, + ); + } + + private onTeleportRequestHandled() { + this.teleportRequestHandledSubscription = + this.clientService.teleportRequestHandledChange.subscribe( + ({ requesterId }) => { + const playerList = this.playerList.getValue(); + if (!playerList) { + return; + } + + const player = playerList.players.find( + existing => existing.id === requesterId, + ); + if (player) { + player.hasTeleportRequest = false; + this.playerList.next(playerList); + } + }, + ); + } + + private onAvatarChange() { + this.avatarSubscription = this.clientService.avatarChange.subscribe( + (player: Player) => { + const playerList = this.playerList.getValue(); + if (!playerList) { + return; + } + + let existing = playerList.players.find(entry => entry.id === player.id); + + if (!existing && player.id === this.clientService.localPlayerId) { + this.ensureLocalPlayerEntry(); + existing = playerList.players.find(entry => entry.id === player.id); + } + + if (!existing) { + return; + } + + existing.avatar = player.avatar; + this.playerList.next(playerList); + }, + ); + } + + private onActorNameChange() { + this.actorNameSubscription = this.clientService.actorNameChange.subscribe( + (player: Player) => { + const playerList = this.playerList.getValue(); + if (!playerList) { + return; + } + + const existing = playerList.players.find( + entry => entry.id === player.id, + ); + + if (!existing) { + return; + } + + existing.actorName = player.actorName; + this.updateDisplayName(existing); + this.playerList.next(playerList); + }, + ); + } + + private onLocalPlayerMetadata() { + this.nameSubscription = this.clientService.nameChange.subscribe(() => { + this.ensureLocalPlayerEntry(); + }); + + this.localPlayerSubscription = + this.clientService.localPlayerIdChange.subscribe(() => { + this.ensureLocalPlayerEntry(); + }); + } + + private ensureLocalPlayerEntry() { + const localId = this.clientService.localPlayerId; + if (localId === undefined || localId === null) { + return; + } + + const playerList = this.playerList.getValue() ?? this.getPlayerList(); + if (!playerList) { + return; + } + + const displayName = this.clientService.nameChange.getValue(); + let existing = playerList.players.find(player => player.id === localId); + + if (!existing) { + existing = new Player({ + id: localId, + name: displayName && displayName.length > 0 ? displayName : 'You', + connected: true, + online: true, + cellName: '', + isLoaded: true, + avatar: this.defaultAvatar, + }); + this.updateDisplayName(existing); + playerList.players.push(existing); + } else { + if (displayName && displayName.length > 0) { + existing.name = displayName; + } + if (!existing.avatar) { + existing.avatar = this.defaultAvatar; + } + this.updateDisplayName(existing); + } + + existing.connected = true; + existing.online = true; + existing.isLoaded = true; + + this.playerList.next(playerList); + } + + private onNamePreferenceChange() { + this.namePreferenceSubscription = + this.settingService.settings.playerNamePreference.subscribe(() => { + const playerList = this.getPlayerList(); + if (!playerList) { + return; + } + for (const player of playerList.players) { + this.updateDisplayName(player); + } + this.playerList.next(playerList); + }); + } + + private updateDisplayName(player: Player) { + player.displayName = this.settingService.resolvePlayerName(player); + } + + private resolveDisplayName(player?: Player, fallback?: string): string { + if (player) { + this.updateDisplayName(player); + return player.displayName || player.name || fallback || ''; + } + + return fallback || ''; + } + public getLocalPlayer(): Player { let localPlayerId = this.clientService.localPlayerId; return this.getPlayerById(localPlayerId); diff --git a/Code/skyrim_ui/src/app/services/popup-notification.service.ts b/Code/skyrim_ui/src/app/services/popup-notification.service.ts index f55320c42..58701d8c9 100644 --- a/Code/skyrim_ui/src/app/services/popup-notification.service.ts +++ b/Code/skyrim_ui/src/app/services/popup-notification.service.ts @@ -1,5 +1,9 @@ import { Injectable } from '@angular/core'; -import { faHandshakeSimple } from '@fortawesome/free-solid-svg-icons'; +import { + faHandshakeSimple, + faLocationArrow, + faScaleBalanced, +} from '@fortawesome/free-solid-svg-icons'; import { Subject } from 'rxjs'; import { PopupNotification } from '../models/popup-notification'; import { Sound, SoundService } from './sound.service'; @@ -35,6 +39,85 @@ export class PopupNotificationService { }); } + public addTradeInvite( + from: string, + acceptCallback: () => void, + declineCallback: () => void, + ) { + this.addMessage({ + messageKey: 'SERVICE.PLAYER_LIST.TRADE_INVITE', + messageParams: { from }, + icon: faScaleBalanced, + duration: 30000, + actions: [ + { + nameKey: 'COMPONENT.NOTIFICATIONS.ACCEPT', + callback: acceptCallback, + }, + { + nameKey: 'COMPONENT.NOTIFICATIONS.DECLINE', + callback: declineCallback, + }, + ], + }); + } + + public addTradeInviteSent(target: string, cancelCallback: () => void): void { + this.addMessage({ + messageKey: 'SERVICE.TRADE.INVITE_SENT', + messageParams: { target }, + icon: faScaleBalanced, + duration: 10000, + actions: [ + { + nameKey: 'COMPONENT.NOTIFICATIONS.CANCEL', + callback: cancelCallback, + }, + ], + }); + } + + public addTradeCancelled(partner: string, reason: string) { + this.addMessage({ + messageKey: 'SERVICE.TRADE.CANCELLED', + messageParams: { partner, reason }, + icon: faScaleBalanced, + duration: 8000, + }); + } + + public addTradeCompleted(partner: string) { + this.addMessage({ + messageKey: 'SERVICE.TRADE.COMPLETED', + messageParams: { partner }, + icon: faScaleBalanced, + duration: 8000, + }); + } + + public addTeleportRequest( + from: string, + acceptCallback: () => void, + declineCallback: () => void, + ) { + this.addMessage({ + messageKey: 'SERVICE.PLAYER_LIST.TELEPORT_REQUEST', + messageParams: { from }, + icon: faLocationArrow, + duration: 30000, + actions: [ + { + nameKey: 'COMPONENT.NOTIFICATIONS.ACCEPT', + callback: acceptCallback, + }, + { + nameKey: 'COMPONENT.NOTIFICATIONS.DECLINE', + callback: declineCallback, + }, + ], + }); + } + public clearMessages() { this.messagesCleared.next(); } diff --git a/Code/skyrim_ui/src/app/services/setting.service.ts b/Code/skyrim_ui/src/app/services/setting.service.ts index 9c933ff3a..b94d9ffac 100644 --- a/Code/skyrim_ui/src/app/services/setting.service.ts +++ b/Code/skyrim_ui/src/app/services/setting.service.ts @@ -1,6 +1,8 @@ import { Injectable } from '@angular/core'; import { TranslocoService } from '@ngneat/transloco'; import { BehaviorSubject } from 'rxjs'; +import { environment } from '../../environments/environment'; +import { NametagMode } from '../models/nametag-mode.enum'; import { StoreService } from './store.service'; export enum FontSize { @@ -18,6 +20,16 @@ export enum PartyAnchor { BOTTOM_LEFT, } +export enum PartyLayout { + CLASSIC = 'classic', + COMPACT = 'compact', +} + +export enum PlayerNamePreference { + USERNAME = 'username', + ACTOR = 'actor', +} + export const autoHideTimerLengths = [1, 3, 5]; export const fontSizeToPixels: Record = { @@ -100,7 +112,16 @@ export class SettingService { private readonly partyAnchorValues = Object.values( PartyAnchor, ) as PartyAnchor[]; + private readonly partyLayoutValues = Object.values(PartyLayout); + private readonly playerNamePreferenceValues = + Object.values(PlayerNamePreference); private readonly autoHideTimeValues = autoHideTimerLengths; + private readonly nametagModeValues: NametagMode[] = [ + NametagMode.Detailed, + NametagMode.Basic, + NametagMode.Hidden, + NametagMode.Normal, + ]; public settings = { volume: new SliderSetting(this.storeService, 'audio_volume', 0.5), @@ -117,6 +138,18 @@ export class SettingService { this.fontSizeValues, FontSize.M, ), + nametagMode: new SelectSetting( + this.storeService, + 'nametag_mode', + this.nametagModeValues, + NametagMode.Normal, + ), + playerNamePreference: new SelectSetting( + this.storeService, + 'player_name_preference', + this.playerNamePreferenceValues, + PlayerNamePreference.USERNAME, + ), isPartyShown: new ToggleSetting(this.storeService, 'party_isShown', true), autoHideParty: new ToggleSetting( this.storeService, @@ -145,6 +178,39 @@ export class SettingService { 'party_anchor_offset_y', 3, ), + partyScale: new SliderSetting(this.storeService, 'party_scale', 1), + partyLayout: new SelectSetting( + this.storeService, + 'party_layout', + this.partyLayoutValues, + PartyLayout.CLASSIC, + ), + partyShowAvatar: new ToggleSetting( + this.storeService, + 'party_show_avatar', + true, + ), + partyShowName: new ToggleSetting( + this.storeService, + 'party_show_name', + true, + ), + partyShowLevel: new ToggleSetting( + this.storeService, + 'party_show_level', + true, + ), + partyShowHealth: new ToggleSetting( + this.storeService, + 'party_show_health', + true, + ), + partyPinShowAvatar: new ToggleSetting( + this.storeService, + 'party_pin_show_avatar', + true, + ), + partyPinScale: new SliderSetting(this.storeService, 'party_pin_scale', 1), isDebugShown: new ToggleSetting(this.storeService, 'debug_isShown', false), }; @@ -155,5 +221,55 @@ export class SettingService { this.settings.language.subscribe(lang => translocoService.setActiveLang(lang), ); + this.settings.nametagMode.subscribe(mode => this.pushNametagMode(mode)); + this.settings.playerNamePreference.subscribe(pref => + this.pushPlayerNamePreference(pref), + ); + } + + private pushNametagMode(mode: NametagMode): void { + if (!environment.game) { + return; + } + + const api = (globalThis as any).skyrimtogether; + if (api && typeof api.setNameTagMode === 'function') { + api.setNameTagMode(mode); + } + } + + private pushPlayerNamePreference(pref: PlayerNamePreference): void { + if (!environment.game) { + return; + } + + const api = (globalThis as any).skyrimtogether; + if (api && typeof api.setPlayerNamePreference === 'function') { + api.setPlayerNamePreference(pref); + } + } + + public resolvePlayerName( + player?: { name?: string; actorName?: string }, + fallback = '', + ): string { + const preference = this.settings.playerNamePreference.getValue(); + if ( + preference === PlayerNamePreference.ACTOR && + player?.actorName && + player.actorName.length > 0 + ) { + return player.actorName; + } + + if (player?.name && player.name.length > 0) { + return player.name; + } + + if (player?.actorName && player.actorName.length > 0) { + return player.actorName; + } + + return fallback; } } diff --git a/Code/skyrim_ui/src/app/services/trade-ui.service.ts b/Code/skyrim_ui/src/app/services/trade-ui.service.ts new file mode 100644 index 000000000..74821032d --- /dev/null +++ b/Code/skyrim_ui/src/app/services/trade-ui.service.ts @@ -0,0 +1,648 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { TranslocoService } from '@ngneat/transloco'; + +import { + ClientService, + TradeCancellationPayload, + TradeInvitePayload, + TradeItemPayload, + TradeStatePayload, +} from './client.service'; +import { PlayerListService } from './player-list.service'; +import { PopupNotificationService } from './popup-notification.service'; +import { PlayerList } from '../models/player-list'; +import { Player } from '../models/player'; +import { UiRepository } from '../store/ui.repository'; +import { View } from '../models/view.enum'; +import { SettingService } from './setting.service'; + +export interface TradeInventoryItemView { + index: number; + name: string; + modId: number; + baseId: number; + available: number; + offered: number; + isQuestItem: boolean; + isGold: boolean; + details: string[]; + key: string; + source: TradeItemPayload; +} + +export interface TradeOfferItemView { + index?: number; + name: string; + modId: number; + baseId: number; + count: number; + isQuestItem: boolean; + details: string[]; + key: string; +} + +export interface TradeSessionView { + active: boolean; + partnerId: number; + partnerName: string; + initiatedBySelf: boolean; + selfReady: boolean; + partnerReady: boolean; + selfOffer: TradeOfferItemView[]; + partnerOffer: TradeOfferItemView[]; + inventory: TradeInventoryItemView[]; + countdownMs: number; + countdownTotalMs: number; + countdownProgress: number; +} + +@Injectable({ + providedIn: 'root', +}) +export class TradeUiService { + private readonly sessionSubject = new BehaviorSubject< + TradeSessionView | undefined + >(undefined); + + public readonly session$ = this.sessionSubject.asObservable(); + private readonly pendingOutgoing = new Set(); + private readonly pendingOutgoingSubject = new BehaviorSubject< + ReadonlySet + >(new Set()); + public readonly pendingOutgoing$ = this.pendingOutgoingSubject.asObservable(); + + private latestState?: TradeStatePayload; + private readonly playerMap = new Map(); + private previousView: View | null = null; + private readonly fallbackCountdownMs = 4000; + + private readonly cancellationReasonKeys: Record = { + 0: 'SERVICE.TRADE.CANCELLED_REASONS.DECLINED', + 1: 'SERVICE.TRADE.CANCELLED_REASONS.CANCELLED', + 2: 'SERVICE.TRADE.CANCELLED_REASONS.PARTNER_BUSY', + 3: 'SERVICE.TRADE.CANCELLED_REASONS.SELF_BUSY', + 4: 'SERVICE.TRADE.CANCELLED_REASONS.PLAYER_LEFT', + 5: 'SERVICE.TRADE.CANCELLED_REASONS.TIMEOUT', + 6: 'SERVICE.TRADE.CANCELLED_REASONS.FAILED_VALIDATION', + }; + + constructor( + private readonly clientService: ClientService, + private readonly playerListService: PlayerListService, + private readonly popupNotificationService: PopupNotificationService, + private readonly transloco: TranslocoService, + private readonly uiRepository: UiRepository, + private readonly settingService: SettingService, + ) { + this.clientService.tradeInviteChange.subscribe(invite => + this.handleTradeInvite(invite), + ); + this.clientService.tradeInviteExpiredChange.subscribe(inviterId => + this.handleTradeInviteExpired(inviterId), + ); + this.clientService.tradeStateChange.subscribe(state => + this.handleTradeState(state), + ); + this.clientService.tradeCancelledChange.subscribe(payload => + this.handleTradeCancelled(payload), + ); + this.clientService.tradeCompletedChange.subscribe(partnerId => + this.handleTradeCompleted(partnerId), + ); + + this.playerListService.playerList.subscribe(list => + this.refreshPlayerMap(list), + ); + } + + public sendInvite(playerId: number): void { + this.clientService.sendTradeInvite(playerId); + this.pendingOutgoing.add(playerId); + this.emitPendingOutgoing(); + const partnerName = this.resolvePlayerName(playerId); + this.popupNotificationService.addTradeInviteSent(partnerName, () => + this.cancelInvite(playerId), + ); + } + + public acceptInvite(playerId: number): void { + this.clientService.respondTradeInvite(playerId, true); + } + + public declineInvite(playerId: number): void { + this.clientService.respondTradeInvite(playerId, false); + } + + public cancelInvite(playerId: number): void { + this.removePendingInvite(playerId); + this.clientService.cancelTrade(); + } + + public cancelTrade(): void { + this.clientService.cancelTrade(); + this.clearPendingInvites(); + this.closePopup(); + } + + public setReady(ready: boolean): void { + this.clientService.setTradeReady(ready); + } + + public updateOfferFromInput(index: number, value: string | number): void { + const parsed = + typeof value === 'number' + ? value + : Number.isFinite(Number(value)) + ? Number(value) + : NaN; + if (!Number.isFinite(parsed)) { + return; + } + this.setOfferCount(index, parsed); + } + + public addToOffer(index: number, delta: number): void { + const item = this.sessionSubject + .getValue() + ?.inventory.find(entry => entry.index === index); + if (!item) return; + this.setOfferCount(index, item.offered + delta); + } + + public offerAll(index: number): void { + const item = this.sessionSubject + .getValue() + ?.inventory.find(entry => entry.index === index); + if (!item) return; + this.setOfferCount(index, item.available); + } + + public clearOffer(index: number): void { + this.setOfferCount(index, 0); + } + + public closePopup(): void { + if (this.uiRepository.getView() === View.TRADE) { + if (this.previousView !== null && this.previousView !== View.TRADE) { + this.uiRepository.openView(this.previousView); + } else { + this.uiRepository.openView(null); + } + } + this.previousView = null; + } + + public getDisplayName(playerId: number): string { + return this.resolvePlayerName(playerId); + } + + private setOfferCount(index: number, value: number): void { + if (!this.latestState || !this.latestState.active) { + return; + } + + const inventoryEntry = this.latestState.inventory.find( + item => item.inventoryIndex === index, + ); + if (!inventoryEntry) { + return; + } + + const bounded = Math.max( + 0, + Math.min(Math.floor(value), inventoryEntry.count), + ); + + if (bounded === Math.max(0, inventoryEntry.offeredCount ?? 0)) { + return; + } + + const selections = this.latestState.inventory + .filter(item => item.inventoryIndex !== undefined) + .map(item => ({ + index: item.inventoryIndex as number, + count: + item.inventoryIndex === index + ? bounded + : Math.max(0, item.offeredCount ?? 0), + })) + .filter(entry => entry.count > 0); + + inventoryEntry.offeredCount = bounded; + if (this.latestState) { + this.latestState.selfReady = false; + this.latestState.partnerReady = false; + } + this.refreshSessionView(); + + this.clientService.updateTradeOffer(selections); + } + + private handleTradeInvite(invite: TradeInvitePayload): void { + const partnerName = this.resolvePlayerName(invite.inviterId); + this.popupNotificationService.addTradeInvite( + partnerName, + () => this.acceptInvite(invite.inviterId), + () => this.declineInvite(invite.inviterId), + ); + } + + private handleTradeInviteExpired(_inviterId: number): void { + // Nothing to do beyond removing notification hooks; invites are handled through popups. + } + + private handleTradeState(state?: TradeStatePayload): void { + if (state && Array.isArray(state.inventory)) { + state.inventory.forEach((entry, idx) => { + (entry as any).inventoryIndex = idx; + }); + } + + this.latestState = state; + + if (!state || !state.active) { + this.sessionSubject.next(undefined); + this.closePopup(); + return; + } + + this.removePendingInvite(state.partnerId); + this.openPopupIfNeeded(); + this.sessionSubject.next(this.buildSessionView(state)); + } + + private handleTradeCancelled(payload: TradeCancellationPayload): void { + this.removePendingInvite(payload.partnerId); + const partnerName = this.resolvePlayerName(payload.partnerId); + const reasonKey = + this.cancellationReasonKeys[payload.reason] ?? + 'SERVICE.TRADE.CANCELLED_REASONS.UNKNOWN'; + const reason = this.transloco.translate(reasonKey); + + this.popupNotificationService.addTradeCancelled(partnerName, reason); + this.latestState = undefined; + this.sessionSubject.next(undefined); + this.closePopup(); + } + + private handleTradeCompleted(partnerId: number): void { + this.removePendingInvite(partnerId); + const partnerName = this.resolvePlayerName(partnerId); + this.popupNotificationService.addTradeCompleted(partnerName); + this.latestState = undefined; + this.sessionSubject.next(undefined); + this.closePopup(); + } + + private refreshPlayerMap(list?: PlayerList): void { + this.playerMap.clear(); + if (list?.players) { + list.players.forEach(player => this.playerMap.set(player.id, player)); + } + + if (this.latestState?.active) { + this.sessionSubject.next(this.buildSessionView(this.latestState)); + } + } + + private buildSessionView(state: TradeStatePayload): TradeSessionView { + const partnerName = this.resolvePlayerName(state.partnerId); + const countdownActive = (state.countdownMs ?? 0) > 0; + const countdownMs = Math.max(0, state.countdownMs ?? 0); + const totalMs = countdownActive + ? state.countdownTotalMs && state.countdownTotalMs > 0 + ? state.countdownTotalMs + : this.fallbackCountdownMs + : 0; + const countdownProgress = + countdownActive && totalMs > 0 + ? Math.max(0, Math.min(1, 1 - countdownMs / totalMs)) + : 0; + + const rawInventory = (state.inventory ?? []).filter( + item => item.inventoryIndex !== undefined, + ); + + const inventory: TradeInventoryItemView[] = rawInventory + .map((item, idx) => this.createInventoryItem(item, idx)) + .sort((a, b) => a.name.localeCompare(b.name)); + + const inventoryBuckets = new Map(); + for (const item of inventory) { + const bucket = inventoryBuckets.get(item.key) ?? []; + bucket.push(item); + inventoryBuckets.set(item.key, bucket); + item.offered = 0; + } + + const selfOffer: TradeOfferItemView[] = []; + for (const payload of state.selfItems ?? []) { + const key = this.buildEntryKey(payload); + const bucket = inventoryBuckets.get(key); + let inventoryItem: TradeInventoryItemView | undefined; + if (bucket && bucket.length > 0) { + inventoryItem = bucket.shift(); + if (bucket.length === 0) { + inventoryBuckets.delete(key); + } else { + inventoryBuckets.set(key, bucket); + } + } + + const offer = this.createOfferItem(payload); + offer.count = Math.max(0, payload.count); + if (inventoryItem) { + offer.index = inventoryItem.index; + inventoryItem.offered = Math.min(inventoryItem.available, offer.count); + } + selfOffer.push(offer); + } + + const partnerOffer: TradeOfferItemView[] = (state.partnerItems ?? []).map( + payload => { + const offer = this.createOfferItem(payload); + offer.count = Math.max(0, payload.count); + return offer; + }, + ); + + selfOffer.sort((a, b) => a.name.localeCompare(b.name)); + partnerOffer.sort((a, b) => a.name.localeCompare(b.name)); + + return { + active: state.active, + partnerId: state.partnerId, + partnerName, + initiatedBySelf: state.initiatedBySelf, + selfReady: state.selfReady, + partnerReady: state.partnerReady, + selfOffer, + partnerOffer, + inventory, + countdownMs, + countdownTotalMs: totalMs, + countdownProgress, + }; + } + + private openPopupIfNeeded(): void { + if (this.uiRepository.getView() !== View.TRADE) { + this.previousView = this.uiRepository.getView(); + this.uiRepository.openView(View.TRADE); + } + } + + private refreshSessionView(): void { + if (this.latestState && this.latestState.active) + this.sessionSubject.next(this.buildSessionView(this.latestState)); + } + + private createInventoryItem( + payload: TradeItemPayload, + index: number, + ): TradeInventoryItemView { + const resolvedIndex = + (payload as any).inventoryIndex !== undefined + ? Number((payload as any).inventoryIndex) + : index; + const name = this.resolveItemName(payload); + const isGold = this.isGold(payload); + const details = this.buildItemDetails(payload); + return { + index: resolvedIndex, + name, + modId: payload.modId, + baseId: payload.baseId, + available: Math.max(0, payload.count), + offered: Math.max(0, payload.offeredCount ?? 0), + isQuestItem: Boolean(payload.isQuestItem), + isGold, + details, + key: this.buildEntryKey(payload), + source: payload, + }; + } + + private emitPendingOutgoing(): void { + this.pendingOutgoingSubject.next(new Set(this.pendingOutgoing)); + } + + private removePendingInvite(playerId: number | undefined): void { + if (playerId === undefined) { + return; + } + if (this.pendingOutgoing.delete(playerId)) { + this.emitPendingOutgoing(); + } + } + + private clearPendingInvites(): void { + if (this.pendingOutgoing.size === 0) { + return; + } + this.pendingOutgoing.clear(); + this.emitPendingOutgoing(); + } + + private createOfferItem(payload: TradeItemPayload): TradeOfferItemView { + return { + name: this.resolveItemName(payload), + modId: payload.modId, + baseId: payload.baseId, + count: 0, + isQuestItem: Boolean(payload.isQuestItem), + details: this.buildItemDetails(payload), + key: this.buildEntryKey(payload), + }; + } + + private resolvePlayerName(playerId: number): string { + if (!playerId) { + return this.transloco.translate('SERVICE.TRADE.UNKNOWN_PLAYER', { + id: playerId, + }); + } + + const player = this.playerMap.get(playerId); + if (player) { + return ( + this.settingService.resolvePlayerName(player, player.name) || + player.name + ); + } + + return this.transloco.translate('SERVICE.TRADE.UNKNOWN_PLAYER', { + id: playerId, + }); + } + + private resolveItemName(payload: TradeItemPayload): string { + const raw: any = payload as any; + const customName = raw?.raw?.CustomName ?? raw?.CustomName; + if (typeof customName === 'string' && customName.length > 0) { + return customName; + } + + if (typeof payload.name === 'string' && payload.name.length > 0) { + return payload.name; + } + + return this.getFallbackItemName(payload); + } + + private buildItemDetails(payload: TradeItemPayload): string[] { + const details: string[] = []; + const raw: any = (payload as any).raw ?? payload; + + if (Array.isArray(payload.details)) { + for (const entry of payload.details) { + details.push(String(entry)); + } + } + + if (payload.isQuestItem) { + details.push( + this.transloco.translate('COMPONENT.TRADE_MENU.DETAILS.QUEST_ITEM'), + ); + } + + const customName = raw?.CustomName; + if (typeof customName === 'string' && customName.length > 0) { + details.push( + this.transloco.translate('COMPONENT.TRADE_MENU.DETAILS.CUSTOM_NAME'), + ); + } + + const hasEnchant = this.hasEnchantData(raw); + if (hasEnchant) { + details.push( + this.transloco.translate('COMPONENT.TRADE_MENU.DETAILS.ENCHANTED'), + ); + } + + if (raw?.ExtraEnchantRemoveUnequip) { + details.push( + this.transloco.translate('COMPONENT.TRADE_MENU.DETAILS.ENCHANT_REMOVE'), + ); + } + + if (typeof raw?.ExtraCharge === 'number' && raw.ExtraCharge > 0) { + details.push( + this.transloco.translate('COMPONENT.TRADE_MENU.DETAILS.CHARGE', { + value: Math.round(raw.ExtraCharge), + }), + ); + } + + if (typeof raw?.ExtraPoisonCount === 'number' && raw.ExtraPoisonCount > 0) { + details.push( + this.transloco.translate('COMPONENT.TRADE_MENU.DETAILS.POISON', { + count: raw.ExtraPoisonCount, + }), + ); + } + + if (typeof raw?.ExtraSoulLevel === 'number' && raw.ExtraSoulLevel > 0) { + details.push( + this.transloco.translate('COMPONENT.TRADE_MENU.DETAILS.SOUL_LEVEL', { + level: raw.ExtraSoulLevel, + }), + ); + } + + if ( + typeof raw?.ExtraHealth === 'number' && + Math.abs(raw.ExtraHealth) > 0.01 + ) { + details.push( + this.transloco.translate('COMPONENT.TRADE_MENU.DETAILS.EXTRA_HEALTH', { + value: Math.round(raw.ExtraHealth), + }), + ); + } + + return Array.from(new Set(details)); + } + + private buildEntryKey(payload: TradeItemPayload): string { + const raw: any = (payload as any).raw ?? payload; + const enchantId = raw?.ExtraEnchantId ?? {}; + const poisonId = raw?.ExtraPoisonId ?? {}; + const toFinite = (value: unknown): number => { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : 0; + }; + const enchantSignature = + this.hasEnchantData(raw) && raw?.EnchantData + ? JSON.stringify(raw.EnchantData) + : ''; + const parts = [ + payload.modId, + payload.baseId, + toFinite(raw?.ExtraCharge ?? 0).toFixed(3), + toFinite(raw?.ExtraHealth ?? 0).toFixed(3), + toFinite(raw?.ExtraSoulLevel ?? 0), + toFinite(raw?.ExtraPoisonCount ?? 0), + toFinite(enchantId.ModId ?? enchantId.ModID ?? 0), + toFinite(enchantId.BaseId ?? enchantId.BaseID ?? 0), + toFinite(poisonId.ModId ?? poisonId.ModID ?? 0), + toFinite(poisonId.BaseId ?? poisonId.BaseID ?? 0), + enchantSignature, + raw?.CustomName ?? '', + ]; + return parts.join('|'); + } + + private isGold(payload: TradeItemPayload): boolean { + return payload.modId === 0 && payload.baseId === 0x0000000f; + } + + private getFallbackItemName(payload: TradeItemPayload): string { + return `0x${payload.modId.toString(16).padStart(8, '0')}:0x${payload.baseId + .toString(16) + .padStart(8, '0')}`; + } + + private hasEnchantData(raw: any): boolean { + if (!raw) { + return false; + } + + const toFiniteNumber = (value: unknown): number => { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : 0; + }; + + const enchantId = raw.ExtraEnchantId ?? raw.extraEnchantId ?? {}; + const hasId = + toFiniteNumber(enchantId.ModId ?? enchantId.ModID ?? 0) !== 0 || + toFiniteNumber(enchantId.BaseId ?? enchantId.BaseID ?? 0) !== 0; + + const effects = raw.EnchantData?.Effects ?? raw.enchantData?.effects; + const hasEffects = + Array.isArray(effects) && + effects.some(effect => { + if (!effect) { + return false; + } + const magnitude = toFiniteNumber( + effect.Magnitude ?? effect.magnitude ?? 0, + ); + const duration = toFiniteNumber( + effect.Duration ?? effect.duration ?? 0, + ); + const area = toFiniteNumber(effect.Area ?? effect.area ?? 0); + const effectId = effect.EffectId ?? effect.effectId ?? {}; + const effectIdPresent = + toFiniteNumber(effectId.ModId ?? effectId.ModID ?? 0) !== 0 || + toFiniteNumber(effectId.BaseId ?? effectId.BaseID ?? 0) !== 0; + + return ( + effectIdPresent || magnitude !== 0 || duration !== 0 || area !== 0 + ); + }); + + return hasId || hasEffects; + } +} diff --git a/Code/skyrim_ui/src/app/store/ui.repository.ts b/Code/skyrim_ui/src/app/store/ui.repository.ts index d72036efe..38508fb01 100644 --- a/Code/skyrim_ui/src/app/store/ui.repository.ts +++ b/Code/skyrim_ui/src/app/store/ui.repository.ts @@ -9,9 +9,13 @@ interface UiProps { connectIp: string; connectPort: number; connectName: string; + connectUsername: string; + connectAccountPassword: string; + connectReturnView: View | null; hideVersionMismatchedServers: boolean; hideFullServers: boolean; hidePasswordProtectedServers: boolean; + emoteCategory: string; } const uiStore = createStore( @@ -22,9 +26,13 @@ const uiStore = createStore( connectIp: null, connectPort: null, connectName: null, + connectUsername: null, + connectAccountPassword: null, + connectReturnView: null, hideVersionMismatchedServers: true, hideFullServers: true, - hidePasswordProtectedServers: true + hidePasswordProtectedServers: true, + emoteCategory: 'greeting', }), ); @@ -36,12 +44,22 @@ export class UiRepository { public readonly playerManagerTab$ = uiStore.pipe( select(state => state.playerManagerTab), ); - public readonly connectIp$ = uiStore.pipe(select((state) => state.connectIp)); - public readonly connectPort$ = uiStore.pipe(select((state) => state.connectPort)); - public readonly connectName$ = uiStore.pipe(select((state) => state.connectName)); - public readonly hideVersionMismatchedServers$ = uiStore.pipe(select((state) => state.hideVersionMismatchedServers)); - public readonly hideFullServers$ = uiStore.pipe(select((state) => state.hideFullServers)); - public readonly hidePasswordProtectedServers$ = uiStore.pipe(select((state) => state.hidePasswordProtectedServers)); + public readonly connectIp$ = uiStore.pipe(select(state => state.connectIp)); + public readonly connectPort$ = uiStore.pipe( + select(state => state.connectPort), + ); + public readonly connectName$ = uiStore.pipe( + select(state => state.connectName), + ); + public readonly hideVersionMismatchedServers$ = uiStore.pipe( + select(state => state.hideVersionMismatchedServers), + ); + public readonly hideFullServers$ = uiStore.pipe( + select(state => state.hideFullServers), + ); + public readonly hidePasswordProtectedServers$ = uiStore.pipe( + select(state => state.hidePasswordProtectedServers), + ); openView(view: UiProps['view']) { uiStore.update(state => ({ @@ -77,6 +95,18 @@ export class UiRepository { return uiStore.getValue().connectName; } + getConnectUsername() { + return uiStore.getValue().connectUsername; + } + + getConnectAccountPassword() { + return uiStore.getValue().connectAccountPassword; + } + + getConnectReturnView() { + return uiStore.getValue().connectReturnView; + } + getHideVersionMismatchedServers() { return uiStore.getValue().hideVersionMismatchedServers; } @@ -89,25 +119,36 @@ export class UiRepository { return uiStore.getValue().hidePasswordProtectedServers; } + getEmoteCategory(): string { + return uiStore.getValue().emoteCategory ?? 'greeting'; + } + setHideVersionMismatchedServers(hideVersionMismatchedServers: boolean) { uiStore.update(state => ({ ...state, hideVersionMismatchedServers, - })) + })); } setHideFullServers(hideFullServers: boolean) { uiStore.update(state => ({ ...state, hideFullServers, - })) + })); } setHidePasswordProtectedServers(hidePasswordProtectedServers: boolean) { uiStore.update(state => ({ ...state, hidePasswordProtectedServers, - })) + })); + } + + setEmoteCategory(category: string) { + uiStore.update(state => ({ + ...state, + emoteCategory: category ?? 'greeting', + })); } openPlayerManagerTab(playerManagerTab: UiProps['playerManagerTab']) { @@ -117,13 +158,25 @@ export class UiRepository { })); } - openConnectWithPasswordView(connectIp: string, connectPort: number, connectName: string) { - uiStore.update((state) => ({ + openConnectWithPasswordView( + connectIp: string, + connectPort: number, + connectName: string, + returnView: View, + connectUsername?: string, + connectAccountPassword?: string, + ) { + const port = connectPort ?? 10578; + uiStore.update(state => ({ ...state, view: View.CONNECT_PASSWORD, connectIp, - connectPort, + connectPort: port, connectName, - })) + connectUsername: connectUsername ?? state.connectUsername ?? '', + connectAccountPassword: + connectAccountPassword ?? state.connectAccountPassword ?? '', + connectReturnView: returnView ?? state.connectReturnView ?? View.CONNECT, + })); } } diff --git a/Code/skyrim_ui/src/assets/i18n/cs.json b/Code/skyrim_ui/src/assets/i18n/cs.json index 3ed3a172d..93d8296b4 100644 --- a/Code/skyrim_ui/src/assets/i18n/cs.json +++ b/Code/skyrim_ui/src/assets/i18n/cs.json @@ -62,6 +62,20 @@ }, "LEAVE_PARTY": "Opustit skupinu" }, + "PARTY_OPTIONS": { + "TITLE": "Party options", + "LEADER": "Leader", + "SYNC_FAST_TRAVEL_MARKERS": "Sync fast travel markers", + "SHOW_PARTY_MEMBER_MARKERS": "Party member markers", + "SYNC_FAST_TRAVEL_MARKERS_TOOLTIP": "Shares discovered fast travel markers with your party.", + "SHOW_PARTY_MEMBER_MARKERS_TOOLTIP": "Shows party member markers on your world map.", + "LOCK_PARTY_TO_LEADER_CELL": "Lock party to leader cell", + "LOCK_PARTY_TO_LEADER_CELL_TOOLTIP": "Prevents non-leaders from changing cells. Party members follow the leader when they move.", + "EXPERIMENTAL": "Experimental (not recommended)", + "SYNC_DEAD_BODY_LOOT": "Sync dead body loot", + "SYNC_DEAD_BODY_LOOT_TOOLTIP": "When disabled, party members can loot corpses independently. Experimental and not recommended.", + "LEADER_ONLY": "Only the party leader can change these." + }, "PARTY_LIST": { "NO_OTHER_PLAYERS": "Nikdo tu není", "TABLE_HEADERS": { @@ -124,6 +138,23 @@ }, "PARTY_ANCHOR_OFFSET_X": "Horizontální odsazení", "PARTY_ANCHOR_OFFSET_Y": "Vertikální odsazení", + "PARTY_SCALE": "Party Scale", + "PARTY_LAYOUT": "Layout", + "PARTY_LAYOUTS": { + "CLASSIC": "Classic", + "COMPACT": "Compact" + }, + "PARTY_SHOW_AVATAR": "Show Avatars", + "PARTY_SHOW_NAME": "Show Names", + "PARTY_NAME_SOURCE": "Name Source", + "PARTY_NAME_SOURCES": { + "USERNAME": "Username", + "ACTOR": "Actor name (if available)" + }, + "PARTY_SHOW_LEVEL": "Show Level", + "PARTY_SHOW_HEALTH": "Show Health Bar", + "PARTY_PIN_SHOW_AVATAR": "Show Marker Avatars", + "PARTY_PIN_SCALE": "Marker Size", "LENGTH": "{{length}}s", "UI": "UI", "LANGUAGE": "Jazyk", @@ -137,6 +168,18 @@ "L": "Velké", "XL": "Velmi Velké" }, + "NAMETAG_MODE": "Nametag Style", + "NAMETAG_MODES": { + "NORMAL": "Classic (avatar + level stacked)", + "DETAILED": "Compact (avatar + level)", + "BASIC": "Name only", + "HIDDEN": "Hidden" + }, + "PLAYER_NAME_PREFERENCE": "Player Name Preference", + "PLAYER_NAME_PREFERENCES": { + "USERNAME": "Username", + "ACTOR": "Actor name (if available)" + }, "BACK": "Zpět" } }, diff --git a/Code/skyrim_ui/src/assets/i18n/de.json b/Code/skyrim_ui/src/assets/i18n/de.json index 66d951ab9..ecf6924a5 100644 --- a/Code/skyrim_ui/src/assets/i18n/de.json +++ b/Code/skyrim_ui/src/assets/i18n/de.json @@ -62,6 +62,20 @@ }, "LEAVE_PARTY": "Gruppe verlassen" }, + "PARTY_OPTIONS": { + "TITLE": "Party options", + "LEADER": "Leader", + "SYNC_FAST_TRAVEL_MARKERS": "Sync fast travel markers", + "SHOW_PARTY_MEMBER_MARKERS": "Party member markers", + "SYNC_FAST_TRAVEL_MARKERS_TOOLTIP": "Shares discovered fast travel markers with your party.", + "SHOW_PARTY_MEMBER_MARKERS_TOOLTIP": "Shows party member markers on your world map.", + "LOCK_PARTY_TO_LEADER_CELL": "Lock party to leader cell", + "LOCK_PARTY_TO_LEADER_CELL_TOOLTIP": "Prevents non-leaders from changing cells. Party members follow the leader when they move.", + "EXPERIMENTAL": "Experimental (not recommended)", + "SYNC_DEAD_BODY_LOOT": "Sync dead body loot", + "SYNC_DEAD_BODY_LOOT_TOOLTIP": "When disabled, party members can loot corpses independently. Experimental and not recommended.", + "LEADER_ONLY": "Only the party leader can change these." + }, "PARTY_LIST": { "NO_OTHER_PLAYERS": "Hm... keiner da. Mist.", "TABLE_HEADERS": { @@ -125,6 +139,23 @@ }, "PARTY_ANCHOR_OFFSET_X": "Horizontaler Versatz", "PARTY_ANCHOR_OFFSET_Y": "Vertikaler Versatz", + "PARTY_SCALE": "Party Scale", + "PARTY_LAYOUT": "Layout", + "PARTY_LAYOUTS": { + "CLASSIC": "Classic", + "COMPACT": "Compact" + }, + "PARTY_SHOW_AVATAR": "Show Avatars", + "PARTY_SHOW_NAME": "Show Names", + "PARTY_NAME_SOURCE": "Name Source", + "PARTY_NAME_SOURCES": { + "USERNAME": "Username", + "ACTOR": "Actor name (if available)" + }, + "PARTY_SHOW_LEVEL": "Show Level", + "PARTY_SHOW_HEALTH": "Show Health Bar", + "PARTY_PIN_SHOW_AVATAR": "Show Marker Avatars", + "PARTY_PIN_SCALE": "Marker Size", "LENGTH": "{{length}}s", "UI": "Oberfläche", "LANGUAGE": "Sprache", @@ -138,6 +169,18 @@ "L": "Groß", "XL": "Extra groß" }, + "NAMETAG_MODE": "Nametag Style", + "NAMETAG_MODES": { + "NORMAL": "Classic (avatar + level stacked)", + "DETAILED": "Compact (avatar + level)", + "BASIC": "Name only", + "HIDDEN": "Hidden" + }, + "PLAYER_NAME_PREFERENCE": "Player Name Preference", + "PLAYER_NAME_PREFERENCES": { + "USERNAME": "Username", + "ACTOR": "Actor name (if available)" + }, "BACK": "Zurück" } }, diff --git a/Code/skyrim_ui/src/assets/i18n/en.json b/Code/skyrim_ui/src/assets/i18n/en.json index aa105935e..f6719c389 100644 --- a/Code/skyrim_ui/src/assets/i18n/en.json +++ b/Code/skyrim_ui/src/assets/i18n/en.json @@ -13,6 +13,9 @@ "HELGEN": "Do NOT connect to any server if you are still in the Helgen intro sequence. Make sure you just escaped Helgen first." }, "ADDRESS": "Address", + "USERNAME": "Username", + "ACCOUNT_PASSWORD": "Account password", + "SERVER_PASSWORD": "Server password", "PASSWORD": "Password", "SAVE_PASSWORD": "Save Password?", "HIDE_PASSWORD": "Hide Password?", @@ -22,7 +25,8 @@ "ERROR": { "CONNECTION": "Could not connect to the specified server. Please make sure you've entered the correct address and that you're not experiencing network issues", "VERSION_MISMATCH": "You cannot connect to the server because it does not have the same version of Skyrim Together as you.", - "INVALID_ADDRESS": "The address is of invalid format." + "INVALID_ADDRESS": "The address is of invalid format.", + "INVALID_USERNAME": "Please provide a username to continue." } }, "DISCONNECT": { @@ -36,7 +40,8 @@ "NOTIFICATIONS": { "MESSAGE_FROM": "Message from", "ACCEPT": "Accept", - "DECLINE": "Decline" + "DECLINE": "Decline", + "CANCEL": "Cancel" }, "PARTY_MENU": { "ACCEPT_INVITE": "Accept invite from {{name}}", @@ -59,10 +64,36 @@ }, "ACTIONS": { "KICK": "Kick", - "TELEPORT": "Teleport", + "ASK_TELEPORT": "Ask to teleport", + "ACCEPT_TELEPORT": "Accept teleport", + "DENY_TELEPORT": "Deny teleport", "MAKE_LEADER": "Make leader" }, - "LEAVE_PARTY": "Leave party" + "LEAVE_PARTY": "Leave party", + "PROFILE_PICTURE": { + "TITLE": "Profile picture", + "DESCRIPTION": "Shown to your party and on the world map.", + "UPLOAD": "Upload image", + "REMOVE": "Remove image", + "SIZE_LIMIT": "PNG or JPG, auto-resized to 128x128 (max 256 KB).", + "ERROR_SIZE": "Image is too large (max 256 KB).", + "ERROR_TYPE": "Unsupported file type. Please use PNG or JPG.", + "ERROR_PROCESS": "Couldn't process that image. Please try another PNG or JPG." + } + }, + "PARTY_OPTIONS": { + "TITLE": "Party options", + "LEADER": "Leader", + "SYNC_FAST_TRAVEL_MARKERS": "Sync fast travel markers", + "SHOW_PARTY_MEMBER_MARKERS": "Party member markers", + "SYNC_FAST_TRAVEL_MARKERS_TOOLTIP": "Shares discovered fast travel markers with your party.", + "SHOW_PARTY_MEMBER_MARKERS_TOOLTIP": "Shows party member markers on your world map.", + "LOCK_PARTY_TO_LEADER_CELL": "Lock party to leader cell", + "LOCK_PARTY_TO_LEADER_CELL_TOOLTIP": "Prevents non-leaders from changing cells. Party members follow the leader when they move.", + "EXPERIMENTAL": "Experimental (not recommended)", + "SYNC_DEAD_BODY_LOOT": "Sync dead body loot", + "SYNC_DEAD_BODY_LOOT_TOOLTIP": "When disabled, party members can loot corpses independently. Experimental and not recommended.", + "LEADER_ONLY": "Only the party leader can change these." }, "PARTY_LIST": { "NO_OTHER_PLAYERS": "Nobody here but us chickens", @@ -71,7 +102,54 @@ "NAME": "Name", "LOCATION": "Location" }, - "INVITE": "Invite" + "INVITE": "Invite", + "TRADE": "Trade", + "CANCEL_TRADE_INVITE": "Cancel trade invite" + }, + "TRADE_MENU": { + "INVITES_TITLE": "Trade invitations", + "NO_INVITES": "No pending trade invitations.", + "INVITE_ACTIONS": { + "ACCEPT": "Accept", + "DECLINE": "Decline" + }, + "TRADING_WITH": "Trading with {{name}}", + "COUNTDOWN": "Finalizing in {{seconds}}s", + "SELF_READY": "You are ready", + "SELF_NOT_READY": "You are not ready", + "PARTNER_READY": "{{name}} is ready", + "PARTNER_NOT_READY": "{{name}} is not ready", + "ACTIONS": { + "READY": "Ready up", + "UNREADY": "Cancel ready", + "CANCEL_TRADE": "Cancel trade", + "CLEAR": "Clear", + "OFFER_ALL": "Offer all", + "CLOSE": "Close window" + }, + "INVENTORY_TITLE": "Your inventory", + "NO_INVENTORY": "No tradable items available.", + "ITEM_COLUMNS": { + "NAME": "Item", + "AVAILABLE": "Available", + "OFFERED": "Offered" + }, + "QUEST_ITEM_WARNING": "Quest items cannot be traded.", + "OFFER_TITLE": "Your offer", + "EMPTY_OFFER": "You are not offering any items.", + "PARTNER_OFFER_TITLE": "Partner offer", + "EMPTY_PARTNER_OFFER": "The partner is not offering any items.", + "NO_ACTIVE_TRADE": "No active trade session.", + "DETAILS": { + "QUEST_ITEM": "Quest item", + "ENCHANTED": "Enchanted", + "ENCHANT_REMOVE": "Removes enchant on unequip", + "CHARGE": "Charge: {{value}}", + "POISON": "Poisoned ({{count}})", + "SOUL_LEVEL": "Soul level: {{level}}", + "EXTRA_HEALTH": "Refined (+{{value}})", + "CUSTOM_NAME": "Custom item" + } }, "PLAYER_MANAGER": { "PLAYER_LIST": "Player List", @@ -84,9 +162,115 @@ "REVEAL_PLAYERS": "Reveal Players", "RECONNECT": "Reconnect", "PLAYER_MANAGER": "Player Manager", + "EMOTES": "Emotes", "SETTINGS": "Settings", "CONNECTION_IN_PROGRESS": "Connection in progress..." }, + "EMOTE_MENU": { + "TITLE": "Emote Wheel", + "EYEBROW": "Express yourself", + "SUBTITLE": "Pick a pose or flourish; the animation plays for everyone nearby.", + "HINT": "Tap B to reopen quickly", + "NO_RESULTS": "No emotes in this category", + "FILTERS": { + "GREETING": "Greetings", + "MUSIC": "Music", + "SOCIAL": "Social", + "RELAX": "Relax", + "MISC": "Utility" + }, + "CLOSE": "Close menu", + "EMOTES": { + "WAVE": { + "TITLE": "Wave", + "DESC": "Friendly greeting with a quick arm sweep." + }, + "PRAY": { + "TITLE": "Kneel & Pray", + "DESC": "Drop to one knee and offer a silent prayer." + }, + "WARM_HANDS": { + "TITLE": "Warm Hands", + "DESC": "Rub your hands together like you are gathered around a fire." + }, + "HANDS_BEHIND_BACK": { + "TITLE": "Hands Behind Back", + "DESC": "Stand at ease." + }, + "SIT": { + "TITLE": "Sit", + "DESC": "Sit cross-legged and rest for a moment." + }, + "LEAN": { + "TITLE": "Lean", + "DESC": "Lean against nearby cover and take a breather." + }, + "DRINK": { + "TITLE": "Drink", + "DESC": "Take a hearty sip from your mug." + }, + "EAT": { + "TITLE": "Snack", + "DESC": "Take a bite from your provisions." + }, + "CLAP": { + "TITLE": "Clap", + "DESC": "Clap enthusiastically." + }, + "CHEER": { + "TITLE": "Cheer", + "DESC": "Pump both arms in celebration." + }, + "DANCE_CICERO_1": { + "TITLE": "Dance (Wild Jig)", + "DESC": "Spin and kick like a jester." + }, + "DANCE_CICERO_2": { + "TITLE": "Dance (Clap & Step)", + "DESC": "Clap in rhythm and shuffle your feet." + }, + "DANCE_CICERO_3": { + "TITLE": "Dance (Twirl)", + "DESC": "Twirl dramatically like Cicero." + }, + "DRUM": { + "TITLE": "Drum Solo", + "DESC": "Bust out a deep, percussive rhythm." + }, + "LAUGH": { + "TITLE": "Laugh", + "DESC": "Laugh out loud at the scene." + }, + "WALL_LEAN": { + "TITLE": "Wall Lean", + "DESC": "Lean back and take a rest." + }, + "COWER": { + "TITLE": "Cower", + "DESC": "Curl up defensively." + }, + "BEGGAR": { + "TITLE": "Beg", + "DESC": "Sit and hold out your hands." + }, + "SWEEP": { + "TITLE": "Sweep", + "DESC": "Grab a broom and tidy up the floor." + }, + "FLUTE": { + "TITLE": "Flute", + "DESC": "Play a light tune to break the silence." + }, + "LUTE": { + "TITLE": "Lute", + "DESC": "Strum a bardic chord progression." + }, + "STOP": { + "TITLE": "Reset", + "DESC": "Cancel the current emote and return to idle." + } + } + }, "SERVER_LIST": { "SEARCH": "Search...", "REFRESH": "Refresh", @@ -127,6 +311,23 @@ }, "PARTY_ANCHOR_OFFSET_X": "Horizontal Offset", "PARTY_ANCHOR_OFFSET_Y": "Vertical Offset", + "PARTY_SCALE": "Party Scale", + "PARTY_LAYOUT": "Layout", + "PARTY_LAYOUTS": { + "CLASSIC": "Classic", + "COMPACT": "Compact" + }, + "PARTY_SHOW_AVATAR": "Show Avatars", + "PARTY_SHOW_NAME": "Show Names", + "PARTY_NAME_SOURCE": "Name Source", + "PARTY_NAME_SOURCES": { + "USERNAME": "Username", + "ACTOR": "Actor name (if available)" + }, + "PARTY_SHOW_LEVEL": "Show Level", + "PARTY_SHOW_HEALTH": "Show Health Bar", + "PARTY_PIN_SHOW_AVATAR": "Show Marker Avatars", + "PARTY_PIN_SCALE": "Marker Size", "LENGTH": "{{length}}s", "UI": "User Interface", "LANGUAGE": "Language", @@ -140,6 +341,18 @@ "L": "Large", "XL": "Extra Large" }, + "NAMETAG_MODE": "Nametag Style", + "NAMETAG_MODES": { + "NORMAL": "Classic (avatar + level stacked)", + "DETAILED": "Compact (avatar + level)", + "BASIC": "Name only", + "HIDDEN": "Hidden" + }, + "PLAYER_NAME_PREFERENCE": "Player Name Preference", + "PLAYER_NAME_PREFERENCES": { + "USERNAME": "Username", + "ACTOR": "Actor name (if available)" + }, "BACK": "Back" } }, @@ -168,6 +381,9 @@ "MODS_MISMATCH_INSTALL": "Please install the following mods to join:\n{{mods}}", "CLIENT_MODS_DISALLOWED": "This server disallows {{mods}}. Please remove them to join.", "WRONG_PASSWORD": "The password you entered is incorrect.", + "WRONG_ACCOUNT_PASSWORD": "The account password you entered is incorrect.", + "WRONG_SERVER_PASSWORD": "The server password you entered is incorrect.", + "DUPLICATE_USER": "You are already connected to this server.", "NO_REASON": "The server refused connection without reason.", "SERVER_FULL": "This server is full.", "BAD_UGRIDSTOLOAD": "It seems that your Skyrim.ini file has a non-default value for 'uGridsToLoad'. THIS IS A VERY BAD IDEA, and will most likely break the mod. Please set this setting back to its default (5), or remove the Skyrim.ini file and launch Skyrim vanilla once to generate a new file. The settings 'uExterior Cell Buffer' and 'uInterior Cell Buffer' should also be left at the default setting. If you do not know how to do this, please refer to our wiki or ask on our Discord server/Reddit sub for help.", @@ -175,7 +391,29 @@ } }, "PLAYER_LIST": { - "PARTY_INVITE": "{{from}} invited you to join a party" + "PARTY_INVITE": "{{from}} invited you to join a party", + "TELEPORT_REQUEST": "{{from}} wants to teleport to you.", + "TRADE_INVITE": "{{from}} wants to trade with you." + }, + "TRADE": { + "UNKNOWN_PLAYER": "Player {{id}}", + "CANCELLED": "{{partner}} cancelled the trade ({{reason}}).", + "COMPLETED": "Trade completed with {{partner}}.", + "INVITE_SENT": "Trade invite sent to {{target}}.", + "CANCELLED_REASONS": { + "DECLINED": "Invite declined", + "CANCELLED": "Trade cancelled", + "PARTNER_BUSY": "Partner is busy", + "SELF_BUSY": "You are busy", + "PLAYER_LEFT": "Player left the session", + "TIMEOUT": "Invitation expired", + "FAILED_VALIDATION": "Trade failed validation", + "UNKNOWN": "Unknown reason" + } + }, + "OVERLAY_BANNER": { + "TELEPORT_COUNTDOWN_TITLE": "Teleporting to {{name}}", + "TELEPORT_COUNTDOWN_SUBTITLE": "Teleporting in {{seconds}}s" } } } diff --git a/Code/skyrim_ui/src/assets/i18n/es.json b/Code/skyrim_ui/src/assets/i18n/es.json index 0fef3a1d3..cf1f73261 100644 --- a/Code/skyrim_ui/src/assets/i18n/es.json +++ b/Code/skyrim_ui/src/assets/i18n/es.json @@ -63,6 +63,20 @@ }, "LEAVE_PARTY": "Abandonar grupo" }, + "PARTY_OPTIONS": { + "TITLE": "Party options", + "LEADER": "Leader", + "SYNC_FAST_TRAVEL_MARKERS": "Sync fast travel markers", + "SHOW_PARTY_MEMBER_MARKERS": "Party member markers", + "SYNC_FAST_TRAVEL_MARKERS_TOOLTIP": "Shares discovered fast travel markers with your party.", + "SHOW_PARTY_MEMBER_MARKERS_TOOLTIP": "Shows party member markers on your world map.", + "LOCK_PARTY_TO_LEADER_CELL": "Lock party to leader cell", + "LOCK_PARTY_TO_LEADER_CELL_TOOLTIP": "Prevents non-leaders from changing cells. Party members follow the leader when they move.", + "EXPERIMENTAL": "Experimental (not recommended)", + "SYNC_DEAD_BODY_LOOT": "Sync dead body loot", + "SYNC_DEAD_BODY_LOOT_TOOLTIP": "When disabled, party members can loot corpses independently. Experimental and not recommended.", + "LEADER_ONLY": "Only the party leader can change these." + }, "PARTY_LIST": { "NO_OTHER_PLAYERS": "¡Aquí no hay nadie más que nosotros, polluelo!", "TABLE_HEADERS": { @@ -120,6 +134,23 @@ }, "PARTY_ANCHOR_OFFSET_X": "Desplazamiento horizontal", "PARTY_ANCHOR_OFFSET_Y": "Desplazamiento vertical", + "PARTY_SCALE": "Party Scale", + "PARTY_LAYOUT": "Layout", + "PARTY_LAYOUTS": { + "CLASSIC": "Classic", + "COMPACT": "Compact" + }, + "PARTY_SHOW_AVATAR": "Show Avatars", + "PARTY_SHOW_NAME": "Show Names", + "PARTY_NAME_SOURCE": "Name Source", + "PARTY_NAME_SOURCES": { + "USERNAME": "Username", + "ACTOR": "Actor name (if available)" + }, + "PARTY_SHOW_LEVEL": "Show Level", + "PARTY_SHOW_HEALTH": "Show Health Bar", + "PARTY_PIN_SHOW_AVATAR": "Show Marker Avatars", + "PARTY_PIN_SCALE": "Marker Size", "LENGTH": "{{length}}s", "UI": "INTERFAZ", "LANGUAGE": "Idioma", @@ -133,6 +164,18 @@ "L": "Grande", "XL": "Extra grande" }, + "NAMETAG_MODE": "Nametag Style", + "NAMETAG_MODES": { + "NORMAL": "Classic (avatar + level stacked)", + "DETAILED": "Compact (avatar + level)", + "BASIC": "Name only", + "HIDDEN": "Hidden" + }, + "PLAYER_NAME_PREFERENCE": "Player Name Preference", + "PLAYER_NAME_PREFERENCES": { + "USERNAME": "Username", + "ACTOR": "Actor name (if available)" + }, "BACK": "Atrás" } }, diff --git a/Code/skyrim_ui/src/assets/i18n/fr.json b/Code/skyrim_ui/src/assets/i18n/fr.json index ea4efd353..afeef2127 100644 --- a/Code/skyrim_ui/src/assets/i18n/fr.json +++ b/Code/skyrim_ui/src/assets/i18n/fr.json @@ -13,6 +13,9 @@ "HELGEN": "Ne vous connectez PAS à un serveur si vous êtes toujours dans la séquence d'introduction de Helgen. Assurez-vous d'abord d'avoir fui Helgen." }, "ADDRESS": "Addresse", + "USERNAME": "Nom d'utilisateur", + "ACCOUNT_PASSWORD": "Mot de passe du compte", + "SERVER_PASSWORD": "Mot de passe du serveur", "PASSWORD": "Mot de passe", "SAVE_PASSWORD": "Enregistrer le mot de passe ?", "CONNECT": "Connexion", @@ -21,7 +24,8 @@ "ERROR": { "CONNECTION": "Impossible de se connecter au serveur spécifié. Veuillez vous assurer que vous avez correctement entré l'adresse et que vous ne rencontrez pas de problèmes de réseau.", "VERSION_MISMATCH": "Vous ne pouvez pas vous connecter au serveur car il ne dispose pas de la même version de Skyrim Together que vous.", - "INVALID_ADDRESS": "Le format de l'adresse est invalide. [Adresse:Port]" + "INVALID_ADDRESS": "Le format de l'adresse est invalide. [Adresse:Port]", + "INVALID_USERNAME": "Veuillez saisir un nom d'utilisateur." } }, "DISCONNECT": { @@ -58,11 +62,27 @@ }, "ACTIONS": { "KICK": "Exclure", - "TELEPORT": "Téléporter", + "ASK_TELEPORT": "Demander une téléportation", + "ACCEPT_TELEPORT": "Accepter la téléportation", + "DENY_TELEPORT": "Refuser la téléportation", "MAKE_LEADER": "En faire le chef de groupe" }, "LEAVE_PARTY": "Quitter le groupe" }, + "PARTY_OPTIONS": { + "TITLE": "Party options", + "LEADER": "Leader", + "SYNC_FAST_TRAVEL_MARKERS": "Sync fast travel markers", + "SHOW_PARTY_MEMBER_MARKERS": "Party member markers", + "SYNC_FAST_TRAVEL_MARKERS_TOOLTIP": "Shares discovered fast travel markers with your party.", + "SHOW_PARTY_MEMBER_MARKERS_TOOLTIP": "Shows party member markers on your world map.", + "LOCK_PARTY_TO_LEADER_CELL": "Lock party to leader cell", + "LOCK_PARTY_TO_LEADER_CELL_TOOLTIP": "Prevents non-leaders from changing cells. Party members follow the leader when they move.", + "EXPERIMENTAL": "Experimental (not recommended)", + "SYNC_DEAD_BODY_LOOT": "Sync dead body loot", + "SYNC_DEAD_BODY_LOOT_TOOLTIP": "When disabled, party members can loot corpses independently. Experimental and not recommended.", + "LEADER_ONLY": "Only the party leader can change these." + }, "PARTY_LIST": { "NO_OTHER_PLAYERS": "Il n'y a personne ici à part nous poulets ", "TABLE_HEADERS": { @@ -120,6 +140,23 @@ }, "PARTY_ANCHOR_OFFSET_X": "Décalage horizontal", "PARTY_ANCHOR_OFFSET_Y": "Décalage vertical", + "PARTY_SCALE": "Party Scale", + "PARTY_LAYOUT": "Layout", + "PARTY_LAYOUTS": { + "CLASSIC": "Classic", + "COMPACT": "Compact" + }, + "PARTY_SHOW_AVATAR": "Show Avatars", + "PARTY_SHOW_NAME": "Show Names", + "PARTY_NAME_SOURCE": "Name Source", + "PARTY_NAME_SOURCES": { + "USERNAME": "Username", + "ACTOR": "Actor name (if available)" + }, + "PARTY_SHOW_LEVEL": "Show Level", + "PARTY_SHOW_HEALTH": "Show Health Bar", + "PARTY_PIN_SHOW_AVATAR": "Show Marker Avatars", + "PARTY_PIN_SCALE": "Marker Size", "LENGTH": "{{length}}s", "UI": "INTERFACE", "LANGUAGE": "Langues", @@ -133,6 +170,18 @@ "L": "Grande", "XL": "Très grande" }, + "NAMETAG_MODE": "Mode des étiquettes", + "NAMETAG_MODES": { + "NORMAL": "Classique (avatar + niveau empilé)", + "DETAILED": "Compact (avatar + niveau)", + "BASIC": "Nom uniquement", + "HIDDEN": "Masqué" + }, + "PLAYER_NAME_PREFERENCE": "Player Name Preference", + "PLAYER_NAME_PREFERENCES": { + "USERNAME": "Username", + "ACTOR": "Actor name (if available)" + }, "BACK": "Retour" } }, @@ -160,12 +209,19 @@ "MODS_MISMATCH_INSTALL": "Veuillez installer les mods suivants pour rejoindre ce serveur :\n{{mods}}", "CLIENT_MODS_DISALLOWED": "Ce serveur n'autorise pas : {{mods}}. Veuillez les retirer pour rejoindre le serveur.", "WRONG_PASSWORD": "Le mot de passe que vous avez saisi est incorrect.", + "WRONG_ACCOUNT_PASSWORD": "Le mot de passe du compte est incorrect.", + "WRONG_SERVER_PASSWORD": "Le mot de passe du serveur est incorrect.", "NO_REASON": "Le serveur a refusé la connexion sans raison.", "SERVER_FULL": "Le serveur est plein." } }, "PLAYER_LIST": { - "PARTY_INVITE": "{{from}} vous a invité à jouer." + "PARTY_INVITE": "{{from}} vous a invité à jouer.", + "TELEPORT_REQUEST": "{{from}} souhaite se téléporter jusqu'à vous." + }, + "OVERLAY_BANNER": { + "TELEPORT_COUNTDOWN_TITLE": "Téléportation vers {{name}}", + "TELEPORT_COUNTDOWN_SUBTITLE": "Téléportation dans {{seconds}} s" } } } diff --git a/Code/skyrim_ui/src/assets/i18n/ja.json b/Code/skyrim_ui/src/assets/i18n/ja.json index 8727c1040..a0fc90d84 100644 --- a/Code/skyrim_ui/src/assets/i18n/ja.json +++ b/Code/skyrim_ui/src/assets/i18n/ja.json @@ -62,6 +62,20 @@ }, "LEAVE_PARTY": "パーティーを抜ける" }, + "PARTY_OPTIONS": { + "TITLE": "Party options", + "LEADER": "Leader", + "SYNC_FAST_TRAVEL_MARKERS": "Sync fast travel markers", + "SHOW_PARTY_MEMBER_MARKERS": "Party member markers", + "SYNC_FAST_TRAVEL_MARKERS_TOOLTIP": "Shares discovered fast travel markers with your party.", + "SHOW_PARTY_MEMBER_MARKERS_TOOLTIP": "Shows party member markers on your world map.", + "LOCK_PARTY_TO_LEADER_CELL": "Lock party to leader cell", + "LOCK_PARTY_TO_LEADER_CELL_TOOLTIP": "Prevents non-leaders from changing cells. Party members follow the leader when they move.", + "EXPERIMENTAL": "Experimental (not recommended)", + "SYNC_DEAD_BODY_LOOT": "Sync dead body loot", + "SYNC_DEAD_BODY_LOOT_TOOLTIP": "When disabled, party members can loot corpses independently. Experimental and not recommended.", + "LEADER_ONLY": "Only the party leader can change these." + }, "PARTY_LIST": { "NO_OTHER_PLAYERS": "ニワトリしかいないね。", "TABLE_HEADERS": { @@ -124,6 +138,23 @@ }, "PARTY_ANCHOR_OFFSET_X": "水平オフセット", "PARTY_ANCHOR_OFFSET_Y": "垂直オフセット", + "PARTY_SCALE": "Party Scale", + "PARTY_LAYOUT": "Layout", + "PARTY_LAYOUTS": { + "CLASSIC": "Classic", + "COMPACT": "Compact" + }, + "PARTY_SHOW_AVATAR": "Show Avatars", + "PARTY_SHOW_NAME": "Show Names", + "PARTY_NAME_SOURCE": "Name Source", + "PARTY_NAME_SOURCES": { + "USERNAME": "Username", + "ACTOR": "Actor name (if available)" + }, + "PARTY_SHOW_LEVEL": "Show Level", + "PARTY_SHOW_HEALTH": "Show Health Bar", + "PARTY_PIN_SHOW_AVATAR": "Show Marker Avatars", + "PARTY_PIN_SCALE": "Marker Size", "LENGTH": "{{length}}", "UI": "ユーザーインターフェース", "LANGUAGE": "言語", @@ -137,6 +168,11 @@ "L": "大", "XL": "最大" }, + "PLAYER_NAME_PREFERENCE": "Player Name Preference", + "PLAYER_NAME_PREFERENCES": { + "USERNAME": "Username", + "ACTOR": "Actor name (if available)" + }, "BACK": "戻る" } }, @@ -174,4 +210,4 @@ "PARTY_INVITE": "{{from}} がパーティーに招待しました。" } } -} \ No newline at end of file +} diff --git a/Code/skyrim_ui/src/assets/i18n/ko.json b/Code/skyrim_ui/src/assets/i18n/ko.json index e1fd35c62..d63680e17 100644 --- a/Code/skyrim_ui/src/assets/i18n/ko.json +++ b/Code/skyrim_ui/src/assets/i18n/ko.json @@ -62,6 +62,20 @@ }, "LEAVE_PARTY": "파티에서 탈퇴" }, + "PARTY_OPTIONS": { + "TITLE": "Party options", + "LEADER": "Leader", + "SYNC_FAST_TRAVEL_MARKERS": "Sync fast travel markers", + "SHOW_PARTY_MEMBER_MARKERS": "Party member markers", + "SYNC_FAST_TRAVEL_MARKERS_TOOLTIP": "Shares discovered fast travel markers with your party.", + "SHOW_PARTY_MEMBER_MARKERS_TOOLTIP": "Shows party member markers on your world map.", + "LOCK_PARTY_TO_LEADER_CELL": "Lock party to leader cell", + "LOCK_PARTY_TO_LEADER_CELL_TOOLTIP": "Prevents non-leaders from changing cells. Party members follow the leader when they move.", + "EXPERIMENTAL": "Experimental (not recommended)", + "SYNC_DEAD_BODY_LOOT": "Sync dead body loot", + "SYNC_DEAD_BODY_LOOT_TOOLTIP": "When disabled, party members can loot corpses independently. Experimental and not recommended.", + "LEADER_ONLY": "Only the party leader can change these." + }, "PARTY_LIST": { "NO_OTHER_PLAYERS": "여기엔 우리 닭들 외에는 아무도 없어", "TABLE_HEADERS": { @@ -124,6 +138,23 @@ }, "PARTY_ANCHOR_OFFSET_X": "수평 오프셋", "PARTY_ANCHOR_OFFSET_Y": "수직 오프셋", + "PARTY_SCALE": "Party Scale", + "PARTY_LAYOUT": "Layout", + "PARTY_LAYOUTS": { + "CLASSIC": "Classic", + "COMPACT": "Compact" + }, + "PARTY_SHOW_AVATAR": "Show Avatars", + "PARTY_SHOW_NAME": "Show Names", + "PARTY_NAME_SOURCE": "Name Source", + "PARTY_NAME_SOURCES": { + "USERNAME": "Username", + "ACTOR": "Actor name (if available)" + }, + "PARTY_SHOW_LEVEL": "Show Level", + "PARTY_SHOW_HEALTH": "Show Health Bar", + "PARTY_PIN_SHOW_AVATAR": "Show Marker Avatars", + "PARTY_PIN_SCALE": "Marker Size", "LENGTH": "{{length}}초", "UI": "사용자 인터페이스", "LANGUAGE": "언어", @@ -137,6 +168,11 @@ "L": "크게", "XL": "매우 크게" }, + "PLAYER_NAME_PREFERENCE": "Player Name Preference", + "PLAYER_NAME_PREFERENCES": { + "USERNAME": "Username", + "ACTOR": "Actor name (if available)" + }, "BACK": "뒤로" } }, diff --git a/Code/skyrim_ui/src/assets/i18n/nl.json b/Code/skyrim_ui/src/assets/i18n/nl.json index 5d9435b2a..733e2aac0 100644 --- a/Code/skyrim_ui/src/assets/i18n/nl.json +++ b/Code/skyrim_ui/src/assets/i18n/nl.json @@ -64,6 +64,20 @@ }, "LEAVE_PARTY": "Verlaat groep" }, + "PARTY_OPTIONS": { + "TITLE": "Party options", + "LEADER": "Leader", + "SYNC_FAST_TRAVEL_MARKERS": "Sync fast travel markers", + "SHOW_PARTY_MEMBER_MARKERS": "Party member markers", + "SYNC_FAST_TRAVEL_MARKERS_TOOLTIP": "Shares discovered fast travel markers with your party.", + "SHOW_PARTY_MEMBER_MARKERS_TOOLTIP": "Shows party member markers on your world map.", + "LOCK_PARTY_TO_LEADER_CELL": "Lock party to leader cell", + "LOCK_PARTY_TO_LEADER_CELL_TOOLTIP": "Prevents non-leaders from changing cells. Party members follow the leader when they move.", + "EXPERIMENTAL": "Experimental (not recommended)", + "SYNC_DEAD_BODY_LOOT": "Sync dead body loot", + "SYNC_DEAD_BODY_LOOT_TOOLTIP": "When disabled, party members can loot corpses independently. Experimental and not recommended.", + "LEADER_ONLY": "Only the party leader can change these." + }, "PARTY_LIST": { "NO_OTHER_PLAYERS": "Niemand aanwezig in de groep.", "TABLE_HEADERS": { @@ -118,9 +132,31 @@ }, "PARTY_ANCHOR_OFFSET_X": "Horizontale verschuiving", "PARTY_ANCHOR_OFFSET_Y": "Verticale verschuiving", + "PARTY_SCALE": "Party Scale", + "PARTY_LAYOUT": "Layout", + "PARTY_LAYOUTS": { + "CLASSIC": "Classic", + "COMPACT": "Compact" + }, + "PARTY_SHOW_AVATAR": "Show Avatars", + "PARTY_SHOW_NAME": "Show Names", + "PARTY_NAME_SOURCE": "Name Source", + "PARTY_NAME_SOURCES": { + "USERNAME": "Username", + "ACTOR": "Actor name (if available)" + }, + "PARTY_SHOW_LEVEL": "Show Level", + "PARTY_SHOW_HEALTH": "Show Health Bar", + "PARTY_PIN_SHOW_AVATAR": "Show Marker Avatars", + "PARTY_PIN_SCALE": "Marker Size", "LENGTH": "{{length}}s", "DEBUG": "Debug", "SHOW_DEBUG_INFO": "Debug informatie weergeven", + "PLAYER_NAME_PREFERENCE": "Player Name Preference", + "PLAYER_NAME_PREFERENCES": { + "USERNAME": "Username", + "ACTOR": "Actor name (if available)" + }, "BACK": "Terug" } }, diff --git a/Code/skyrim_ui/src/assets/i18n/no.json b/Code/skyrim_ui/src/assets/i18n/no.json index 5d4eb7c1c..6389338e0 100644 --- a/Code/skyrim_ui/src/assets/i18n/no.json +++ b/Code/skyrim_ui/src/assets/i18n/no.json @@ -62,6 +62,20 @@ }, "LEAVE_PARTY": "Leave party" }, + "PARTY_OPTIONS": { + "TITLE": "Party options", + "LEADER": "Leader", + "SYNC_FAST_TRAVEL_MARKERS": "Sync fast travel markers", + "SHOW_PARTY_MEMBER_MARKERS": "Party member markers", + "SYNC_FAST_TRAVEL_MARKERS_TOOLTIP": "Shares discovered fast travel markers with your party.", + "SHOW_PARTY_MEMBER_MARKERS_TOOLTIP": "Shows party member markers on your world map.", + "LOCK_PARTY_TO_LEADER_CELL": "Lock party to leader cell", + "LOCK_PARTY_TO_LEADER_CELL_TOOLTIP": "Prevents non-leaders from changing cells. Party members follow the leader when they move.", + "EXPERIMENTAL": "Experimental (not recommended)", + "SYNC_DEAD_BODY_LOOT": "Sync dead body loot", + "SYNC_DEAD_BODY_LOOT_TOOLTIP": "When disabled, party members can loot corpses independently. Experimental and not recommended.", + "LEADER_ONLY": "Only the party leader can change these." + }, "PARTY_LIST": { "NO_OTHER_PLAYERS": "Nobody here but us chickens", "TABLE_HEADERS": { @@ -124,6 +138,23 @@ }, "PARTY_ANCHOR_OFFSET_X": "Horizontalt Offset", "PARTY_ANCHOR_OFFSET_Y": "Vertikal Offset", + "PARTY_SCALE": "Party Scale", + "PARTY_LAYOUT": "Layout", + "PARTY_LAYOUTS": { + "CLASSIC": "Classic", + "COMPACT": "Compact" + }, + "PARTY_SHOW_AVATAR": "Show Avatars", + "PARTY_SHOW_NAME": "Show Names", + "PARTY_NAME_SOURCE": "Name Source", + "PARTY_NAME_SOURCES": { + "USERNAME": "Username", + "ACTOR": "Actor name (if available)" + }, + "PARTY_SHOW_LEVEL": "Show Level", + "PARTY_SHOW_HEALTH": "Show Health Bar", + "PARTY_PIN_SHOW_AVATAR": "Show Marker Avatars", + "PARTY_PIN_SCALE": "Marker Size", "LENGTH": "{{length}}s", "UI": "Brukergrensesnitt", "LANGUAGE": "Språk", @@ -137,6 +168,11 @@ "L": "Stor", "XL": "Extra stor" }, + "PLAYER_NAME_PREFERENCE": "Player Name Preference", + "PLAYER_NAME_PREFERENCES": { + "USERNAME": "Username", + "ACTOR": "Actor name (if available)" + }, "BACK": "Tilbake" } }, diff --git a/Code/skyrim_ui/src/assets/i18n/pl.json b/Code/skyrim_ui/src/assets/i18n/pl.json index 500b29349..113c9fe02 100644 --- a/Code/skyrim_ui/src/assets/i18n/pl.json +++ b/Code/skyrim_ui/src/assets/i18n/pl.json @@ -63,6 +63,20 @@ }, "LEAVE_PARTY": "Odejdź z grupy" }, + "PARTY_OPTIONS": { + "TITLE": "Party options", + "LEADER": "Leader", + "SYNC_FAST_TRAVEL_MARKERS": "Sync fast travel markers", + "SHOW_PARTY_MEMBER_MARKERS": "Party member markers", + "SYNC_FAST_TRAVEL_MARKERS_TOOLTIP": "Shares discovered fast travel markers with your party.", + "SHOW_PARTY_MEMBER_MARKERS_TOOLTIP": "Shows party member markers on your world map.", + "LOCK_PARTY_TO_LEADER_CELL": "Lock party to leader cell", + "LOCK_PARTY_TO_LEADER_CELL_TOOLTIP": "Prevents non-leaders from changing cells. Party members follow the leader when they move.", + "EXPERIMENTAL": "Experimental (not recommended)", + "SYNC_DEAD_BODY_LOOT": "Sync dead body loot", + "SYNC_DEAD_BODY_LOOT_TOOLTIP": "When disabled, party members can loot corpses independently. Experimental and not recommended.", + "LEADER_ONLY": "Only the party leader can change these." + }, "PARTY_LIST": { "NO_OTHER_PLAYERS": "Nikogo tutaj nie ma poza nami Kurczakami!", "TABLE_HEADERS": { @@ -125,6 +139,23 @@ }, "PARTY_ANCHOR_OFFSET_X": "Przesunięcie w poziomie", "PARTY_ANCHOR_OFFSET_Y": "Przesunięcie pionowe", + "PARTY_SCALE": "Party Scale", + "PARTY_LAYOUT": "Layout", + "PARTY_LAYOUTS": { + "CLASSIC": "Classic", + "COMPACT": "Compact" + }, + "PARTY_SHOW_AVATAR": "Show Avatars", + "PARTY_SHOW_NAME": "Show Names", + "PARTY_NAME_SOURCE": "Name Source", + "PARTY_NAME_SOURCES": { + "USERNAME": "Username", + "ACTOR": "Actor name (if available)" + }, + "PARTY_SHOW_LEVEL": "Show Level", + "PARTY_SHOW_HEALTH": "Show Health Bar", + "PARTY_PIN_SHOW_AVATAR": "Show Marker Avatars", + "PARTY_PIN_SCALE": "Marker Size", "LENGTH": "{{length}}s", "UI": "UI", "LANGUAGE": "Język", @@ -138,6 +169,11 @@ "L": "Duża", "XL": "Bardzo Duża" }, + "PLAYER_NAME_PREFERENCE": "Player Name Preference", + "PLAYER_NAME_PREFERENCES": { + "USERNAME": "Username", + "ACTOR": "Actor name (if available)" + }, "BACK": "Wróć" } }, diff --git a/Code/skyrim_ui/src/assets/i18n/ru.json b/Code/skyrim_ui/src/assets/i18n/ru.json index ed4c872e7..e8d20bd5e 100644 --- a/Code/skyrim_ui/src/assets/i18n/ru.json +++ b/Code/skyrim_ui/src/assets/i18n/ru.json @@ -64,6 +64,20 @@ }, "LEAVE_PARTY": "Покинуть группу" }, + "PARTY_OPTIONS": { + "TITLE": "Party options", + "LEADER": "Leader", + "SYNC_FAST_TRAVEL_MARKERS": "Sync fast travel markers", + "SHOW_PARTY_MEMBER_MARKERS": "Party member markers", + "SYNC_FAST_TRAVEL_MARKERS_TOOLTIP": "Shares discovered fast travel markers with your party.", + "SHOW_PARTY_MEMBER_MARKERS_TOOLTIP": "Shows party member markers on your world map.", + "LOCK_PARTY_TO_LEADER_CELL": "Lock party to leader cell", + "LOCK_PARTY_TO_LEADER_CELL_TOOLTIP": "Prevents non-leaders from changing cells. Party members follow the leader when they move.", + "EXPERIMENTAL": "Experimental (not recommended)", + "SYNC_DEAD_BODY_LOOT": "Sync dead body loot", + "SYNC_DEAD_BODY_LOOT_TOOLTIP": "When disabled, party members can loot corpses independently. Experimental and not recommended.", + "LEADER_ONLY": "Only the party leader can change these." + }, "PARTY_LIST": { "NO_OTHER_PLAYERS": "Тут нет никого, кроме нас, курей", "TABLE_HEADERS": { @@ -127,6 +141,23 @@ }, "PARTY_ANCHOR_OFFSET_X": "Смещение по горизонтали", "PARTY_ANCHOR_OFFSET_Y": "Смещение по вертикали", + "PARTY_SCALE": "Party Scale", + "PARTY_LAYOUT": "Layout", + "PARTY_LAYOUTS": { + "CLASSIC": "Classic", + "COMPACT": "Compact" + }, + "PARTY_SHOW_AVATAR": "Show Avatars", + "PARTY_SHOW_NAME": "Show Names", + "PARTY_NAME_SOURCE": "Name Source", + "PARTY_NAME_SOURCES": { + "USERNAME": "Username", + "ACTOR": "Actor name (if available)" + }, + "PARTY_SHOW_LEVEL": "Show Level", + "PARTY_SHOW_HEALTH": "Show Health Bar", + "PARTY_PIN_SHOW_AVATAR": "Show Marker Avatars", + "PARTY_PIN_SCALE": "Marker Size", "LENGTH": "{{length}} сек.", "UI": "Интерфейс", "LANGUAGE": "Язык", @@ -140,6 +171,11 @@ "L": "Большой", "XL": "Огромный" }, + "PLAYER_NAME_PREFERENCE": "Player Name Preference", + "PLAYER_NAME_PREFERENCES": { + "USERNAME": "Username", + "ACTOR": "Actor name (if available)" + }, "BACK": "Назад" } }, diff --git a/Code/skyrim_ui/src/assets/i18n/tr.json b/Code/skyrim_ui/src/assets/i18n/tr.json index 546a04883..7dcaa0cb7 100644 --- a/Code/skyrim_ui/src/assets/i18n/tr.json +++ b/Code/skyrim_ui/src/assets/i18n/tr.json @@ -62,7 +62,31 @@ "TELEPORT": "Işınla", "MAKE_LEADER": "Lider yap" }, - "LEAVE_PARTY": "Partiden çık" + "LEAVE_PARTY": "Partiden çık", + "PROFILE_PICTURE": { + "TITLE": "Profil resmi", + "DESCRIPTION": "Partinizde ve dünya haritasında gösterilir.", + "UPLOAD": "Resim yükle", + "REMOVE": "Resmi kaldır", + "SIZE_LIMIT": "PNG veya JPG, otomatik olarak 128x128 boyutuna ölçeklenir (en fazla 256 KB).", + "ERROR_SIZE": "Resim çok büyük (en fazla 256 KB).", + "ERROR_TYPE": "Desteklenmeyen dosya türü. Lütfen PNG veya JPG kullanın.", + "ERROR_PROCESS": "Resim işlenemedi. Lütfen başka bir PNG veya JPG deneyin." + } + }, + "PARTY_OPTIONS": { + "TITLE": "Party options", + "LEADER": "Leader", + "SYNC_FAST_TRAVEL_MARKERS": "Sync fast travel markers", + "SHOW_PARTY_MEMBER_MARKERS": "Party member markers", + "SYNC_FAST_TRAVEL_MARKERS_TOOLTIP": "Shares discovered fast travel markers with your party.", + "SHOW_PARTY_MEMBER_MARKERS_TOOLTIP": "Shows party member markers on your world map.", + "LOCK_PARTY_TO_LEADER_CELL": "Lock party to leader cell", + "LOCK_PARTY_TO_LEADER_CELL_TOOLTIP": "Prevents non-leaders from changing cells. Party members follow the leader when they move.", + "EXPERIMENTAL": "Experimental (not recommended)", + "SYNC_DEAD_BODY_LOOT": "Sync dead body loot", + "SYNC_DEAD_BODY_LOOT_TOOLTIP": "When disabled, party members can loot corpses independently. Experimental and not recommended.", + "LEADER_ONLY": "Only the party leader can change these." }, "PARTY_LIST": { "NO_OTHER_PLAYERS": "Kimsecikler yok.", @@ -127,6 +151,23 @@ }, "PARTY_ANCHOR_OFFSET_X": "Yatay Denge", "PARTY_ANCHOR_OFFSET_Y": "Dikey Denge", + "PARTY_SCALE": "Party Scale", + "PARTY_LAYOUT": "Layout", + "PARTY_LAYOUTS": { + "CLASSIC": "Classic", + "COMPACT": "Compact" + }, + "PARTY_SHOW_AVATAR": "Show Avatars", + "PARTY_SHOW_NAME": "Show Names", + "PARTY_NAME_SOURCE": "Name Source", + "PARTY_NAME_SOURCES": { + "USERNAME": "Username", + "ACTOR": "Actor name (if available)" + }, + "PARTY_SHOW_LEVEL": "Show Level", + "PARTY_SHOW_HEALTH": "Show Health Bar", + "PARTY_PIN_SHOW_AVATAR": "Show Marker Avatars", + "PARTY_PIN_SCALE": "Marker Size", "LENGTH": "{{length}}s", "UI": "Arayüz", "LANGUAGE": "Dil", @@ -140,6 +181,11 @@ "L": "Büyük", "XL": "Ekstra Büyük" }, + "PLAYER_NAME_PREFERENCE": "Player Name Preference", + "PLAYER_NAME_PREFERENCES": { + "USERNAME": "Username", + "ACTOR": "Actor name (if available)" + }, "BACK": "Geri" } }, @@ -168,6 +214,9 @@ "MODS_MISMATCH_INSTALL": "Katılmak için lütfen bu modları indirin:\n{{mods}}", "CLIENT_MODS_DISALLOWED": "Bu sunucu {{mods}} modlarını kabul etmiyor. Katılmak için lütfen siliniz.", "WRONG_PASSWORD": "Girdiğiniz parola yanlış.", + "WRONG_ACCOUNT_PASSWORD": "Girdiğiniz hesap parolası yanlış.", + "WRONG_SERVER_PASSWORD": "Girdiğiniz sunucu parolası yanlış.", + "DUPLICATE_USER": "Bu sunucuya zaten bağlısınız.", "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.", @@ -178,4 +227,4 @@ "PARTY_INVITE": "{{from}} parti daveti attı!" } } -} \ No newline at end of file +} diff --git a/Code/skyrim_ui/src/assets/i18n/zh-CN.json b/Code/skyrim_ui/src/assets/i18n/zh-CN.json index 89176f1e9..1b7dbfa94 100644 --- a/Code/skyrim_ui/src/assets/i18n/zh-CN.json +++ b/Code/skyrim_ui/src/assets/i18n/zh-CN.json @@ -63,6 +63,20 @@ }, "LEAVE_PARTY": "离开团队" }, + "PARTY_OPTIONS": { + "TITLE": "Party options", + "LEADER": "Leader", + "SYNC_FAST_TRAVEL_MARKERS": "Sync fast travel markers", + "SHOW_PARTY_MEMBER_MARKERS": "Party member markers", + "SYNC_FAST_TRAVEL_MARKERS_TOOLTIP": "Shares discovered fast travel markers with your party.", + "SHOW_PARTY_MEMBER_MARKERS_TOOLTIP": "Shows party member markers on your world map.", + "LOCK_PARTY_TO_LEADER_CELL": "Lock party to leader cell", + "LOCK_PARTY_TO_LEADER_CELL_TOOLTIP": "Prevents non-leaders from changing cells. Party members follow the leader when they move.", + "EXPERIMENTAL": "Experimental (not recommended)", + "SYNC_DEAD_BODY_LOOT": "Sync dead body loot", + "SYNC_DEAD_BODY_LOOT_TOOLTIP": "When disabled, party members can loot corpses independently. Experimental and not recommended.", + "LEADER_ONLY": "Only the party leader can change these." + }, "PARTY_LIST": { "NO_OTHER_PLAYERS": "没有其他的玩家。", "TABLE_HEADERS": { @@ -119,6 +133,23 @@ }, "PARTY_ANCHOR_OFFSET_X": "水平偏移", "PARTY_ANCHOR_OFFSET_Y": "垂直偏移", + "PARTY_SCALE": "Party Scale", + "PARTY_LAYOUT": "Layout", + "PARTY_LAYOUTS": { + "CLASSIC": "Classic", + "COMPACT": "Compact" + }, + "PARTY_SHOW_AVATAR": "Show Avatars", + "PARTY_SHOW_NAME": "Show Names", + "PARTY_NAME_SOURCE": "Name Source", + "PARTY_NAME_SOURCES": { + "USERNAME": "Username", + "ACTOR": "Actor name (if available)" + }, + "PARTY_SHOW_LEVEL": "Show Level", + "PARTY_SHOW_HEALTH": "Show Health Bar", + "PARTY_PIN_SHOW_AVATAR": "Show Marker Avatars", + "PARTY_PIN_SCALE": "Marker Size", "LENGTH": "{{length}}秒", "UI": "UI", "LANGUAGE": "语言", @@ -132,6 +163,11 @@ "L": "大", "XL": "特大" }, + "PLAYER_NAME_PREFERENCE": "Player Name Preference", + "PLAYER_NAME_PREFERENCES": { + "USERNAME": "Username", + "ACTOR": "Actor name (if available)" + }, "BACK": "返回" } }, diff --git a/Code/skyrim_ui/src/assets/images/group/avatar-placeholder.png b/Code/skyrim_ui/src/assets/images/group/avatar-placeholder.png new file mode 100644 index 000000000..09892098a Binary files /dev/null and b/Code/skyrim_ui/src/assets/images/group/avatar-placeholder.png differ diff --git a/Code/skyrim_ui/src/typings.d.ts b/Code/skyrim_ui/src/typings.d.ts index 0104f5d0c..69afba1fc 100644 --- a/Code/skyrim_ui/src/typings.d.ts +++ b/Code/skyrim_ui/src/typings.d.ts @@ -31,6 +31,13 @@ declare namespace SkyrimTogetherTypes { sender: string, ) => void; + type CommandListEntry = { + name: string; + description: string; + }; + + type CommandListCallback = (commandsJson: string) => void; + /** Connection callback */ type ConnectCallback = () => void; @@ -62,8 +69,11 @@ declare namespace SkyrimTogetherTypes { username: string, level: number, cellName: string, + avatar: string, ) => void; + type PlayerAvatarUpdatedCallback = (playerId: number, avatar: string) => void; + type PlayerDisconnectedCallback = ( playerId: number, username: string, @@ -75,12 +85,22 @@ declare namespace SkyrimTogetherTypes { type SetCellCallback = (playerId: number, cellName: string) => void; + type SetActorNameCallback = (playerId: number, actorName: string) => void; + type SetPlayer3dLoadedCallback = (playerId: number, health: number) => void; type SetPlayer3dUnloadedCallback = (playerId: number) => void; type SetLocalPlayerIdCallback = (playerId: number) => void; + /** Quest isolation / sync gating status callback */ + type SetSyncStatusCallback = ( + isolated: boolean, + title: string, + detail: string, + moreInfo?: string, + ) => void; + type ProtocolMismatch = () => void; type TriggerError = () => void; @@ -89,11 +109,116 @@ declare namespace SkyrimTogetherTypes { type PartyInfoCallback = (playerIds: Array, leaderId: number) => void; + type PartyOptionsPayload = { + syncFastTravelMarkers: boolean; + showPartyMemberMarkers: boolean; + syncDeadBodyLoot: boolean; + lockPartyToLeaderCell: boolean; + }; + + type PartyOptionsCallback = (options: PartyOptionsPayload) => void; + type PartyCreatedCallback = () => void; type PartyLeftCallback = (inviterId: number) => void; type PartyInviteReceivedCallback = (inviterId: number) => void; + + /** World-map party pins payload as JSON string */ + type SetPartyPinsCallback = (json: string) => void; + + /** Death screen shown with countdown timer */ + type ShowDeathScreenCallback = (secondsRemaining: number) => void; + + /** Death screen timer update */ + type UpdateDeathTimerCallback = (secondsRemaining: number) => void; + + /** Respawn button enabled */ + type EnableRespawnButtonCallback = () => void; + + /** Death screen hidden */ + type HideDeathScreenCallback = () => void; + + /** Revive progress for the downed player */ + type UpdateReviveVictimProgressCallback = ( + elapsedSeconds: number, + totalSeconds: number, + healerName: string, + ) => void; + + /** Downed revive progress stopped/reset */ + type StopReviveVictimProgressCallback = () => void; + + /** Revive progress for the healer channeling */ + type UpdateReviveHealerProgressCallback = ( + elapsedSeconds: number, + totalSeconds: number, + ) => void; + + /** Healer revive overlay hidden */ + type StopReviveHealerProgressCallback = () => void; + + /** Teleport request received */ + type TeleportRequestCallback = ( + requesterId: number, + requesterName: string, + ) => void; + + /** Teleport countdown update */ + type TeleportCountdownCallback = ( + targetPlayerId: number, + targetName: string, + secondsRemaining: number, + cancelled: boolean, + reason: string, + ) => void; + + /** Banner notification payload */ + type BannerCallback = (message: string, durationMs?: number) => void; + + /** Emote menu open request */ + type OpenEmoteMenuCallback = (openedFromInactive?: boolean) => void; + /** Emote menu toggle request */ + type ToggleEmoteMenuCallback = () => void; + + type TradeInviteCallback = (inviterId: number, expiryTick: number) => void; + type TradeInviteExpiredCallback = (inviterId: number) => void; + + interface TradeItemPayload { + modId: number; + baseId: number; + count: number; + isQuestItem: boolean; + name: string; + inventoryIndex?: number; + offeredCount?: number; + } + + interface TradeInventoryPayload extends TradeItemPayload { + inventoryIndex: number; + offeredCount: number; + } + + type TradeStateUpdatedCallback = ( + active: boolean, + partnerId: number, + initiatedBySelf: boolean, + selfReady: boolean, + partnerReady: boolean, + selfItems: TradeItemPayload[], + partnerItems: TradeItemPayload[], + inventory: TradeInventoryPayload[], + ) => void; + + type TradeCancelledCallback = ( + partnerId: number, + reason: number, + wasInitiator: boolean, + ) => void; + + type TradeCompletedCallback = (partnerId: number) => void; + + type TradeOfferEntry = { index: number; count: number }; } /** Global Skyrim: Together object. */ @@ -128,6 +253,12 @@ interface SkyrimTogether { /** Add listener to when a player message is received. */ on(event: 'message', callback: SkyrimTogetherTypes.MessageCallback): void; + /** Add listener to when the server command list is updated. */ + on( + event: 'commandList', + callback: SkyrimTogetherTypes.CommandListCallback, + ): void; + /** Add listener to when the player connects to a server. */ on(event: 'connect', callback: SkyrimTogetherTypes.ConnectCallback): void; @@ -172,6 +303,11 @@ interface SkyrimTogether { callback: SkyrimTogetherTypes.PlayerDisconnectedCallback, ): void; + on( + event: 'playerAvatarUpdated', + callback: SkyrimTogetherTypes.PlayerAvatarUpdatedCallback, + ): void; + on(event: 'setHealth', callback: SkyrimTogetherTypes.SetHealthCallback): void; /** Add listener to when one player change level in server. */ @@ -180,6 +316,12 @@ interface SkyrimTogether { /** Add listener to when one player change cell in server. */ on(event: 'setCell', callback: SkyrimTogetherTypes.SetCellCallback): void; + /** Add listener to when one player actor name is updated. */ + on( + event: 'setActorName', + callback: SkyrimTogetherTypes.SetActorNameCallback, + ): void; + /** Add listener to when a player is loaded or unloaded in 3D. */ on( event: 'setPlayer3dLoaded', @@ -196,6 +338,11 @@ interface SkyrimTogether { callback: SkyrimTogetherTypes.SetLocalPlayerIdCallback, ): void; + on( + event: 'setSyncStatus', + callback: SkyrimTogetherTypes.SetSyncStatusCallback, + ): void; + on( event: 'protocolMismatch', callback: SkyrimTogetherTypes.ProtocolMismatch, @@ -207,6 +354,11 @@ interface SkyrimTogether { on(event: 'partyInfo', callback: SkyrimTogetherTypes.PartyInfoCallback): void; + on( + event: 'partyOptions', + callback: SkyrimTogetherTypes.PartyOptionsCallback, + ): void; + on( event: 'partyCreated', callback: SkyrimTogetherTypes.PartyCreatedCallback, @@ -218,6 +370,94 @@ interface SkyrimTogether { event: 'partyInviteReceived', callback: SkyrimTogetherTypes.PartyInviteReceivedCallback, ): void; + on( + event: 'tradeInviteReceived', + callback: SkyrimTogetherTypes.TradeInviteCallback, + ): void; + on( + event: 'tradeInviteExpired', + callback: SkyrimTogetherTypes.TradeInviteExpiredCallback, + ): void; + on( + event: 'tradeStateUpdated', + callback: SkyrimTogetherTypes.TradeStateUpdatedCallback, + ): void; + on( + event: 'tradeCancelled', + callback: SkyrimTogetherTypes.TradeCancelledCallback, + ): void; + on( + event: 'tradeCompleted', + callback: SkyrimTogetherTypes.TradeCompletedCallback, + ): void; + + on( + event: 'teleportRequest', + callback: SkyrimTogetherTypes.TeleportRequestCallback, + ): void; + + on( + event: 'teleportCountdown', + callback: SkyrimTogetherTypes.TeleportCountdownCallback, + ): void; + + /** Add listener to open the emote menu from native input. */ + on( + event: 'openEmoteMenu', + callback: SkyrimTogetherTypes.OpenEmoteMenuCallback, + ): void; + /** Add listener to toggle the emote menu from native input. */ + on( + event: 'toggleEmoteMenu', + callback: SkyrimTogetherTypes.ToggleEmoteMenuCallback, + ): void; + + /** Add listener to transient overlay banners. */ + on(event: 'showBanner', callback: SkyrimTogetherTypes.BannerCallback): void; + + /** Add listener to when the death screen is shown. */ + on( + event: 'showDeathScreen', + callback: SkyrimTogetherTypes.ShowDeathScreenCallback, + ): void; + + /** Add listener to when the death screen timer updates. */ + on( + event: 'updateDeathTimer', + callback: SkyrimTogetherTypes.UpdateDeathTimerCallback, + ): void; + + /** Add listener to when the respawn button is enabled. */ + on( + event: 'enableRespawnButton', + callback: SkyrimTogetherTypes.EnableRespawnButtonCallback, + ): void; + + /** Add listener to when the death screen is hidden. */ + on( + event: 'hideDeathScreen', + callback: SkyrimTogetherTypes.HideDeathScreenCallback, + ): void; + + on( + event: 'updateReviveVictimProgress', + callback: SkyrimTogetherTypes.UpdateReviveVictimProgressCallback, + ): void; + + on( + event: 'stopReviveVictimProgress', + callback: SkyrimTogetherTypes.StopReviveVictimProgressCallback, + ): void; + + on( + event: 'updateReviveHealerProgress', + callback: SkyrimTogetherTypes.UpdateReviveHealerProgressCallback, + ): void; + + on( + event: 'stopReviveHealerProgress', + callback: SkyrimTogetherTypes.StopReviveHealerProgressCallback, + ): void; /** Remove listener from when the application is first initialized. */ off(event: 'init', callback?: SkyrimTogetherTypes.InitCallback): void; @@ -287,6 +527,11 @@ interface SkyrimTogether { callback?: SkyrimTogetherTypes.PlayerDisconnectedCallback, ): void; + off( + event: 'playerAvatarUpdated', + callback?: SkyrimTogetherTypes.PlayerAvatarUpdatedCallback, + ): void; + off( event: 'userDataSet', callback?: SkyrimTogetherTypes.UserDataSetCallback, @@ -301,6 +546,11 @@ interface SkyrimTogether { off(event: 'setCell', callback?: SkyrimTogetherTypes.SetCellCallback): void; + off( + event: 'setActorName', + callback?: SkyrimTogetherTypes.SetActorNameCallback, + ): void; + /** Add listener to when a player is loaded or unloaded in 3D. */ off( event: 'setPlayer3dLoaded', @@ -317,6 +567,11 @@ interface SkyrimTogether { callback?: SkyrimTogetherTypes.SetLocalPlayerIdCallback, ): void; + off( + event: 'setSyncStatus', + callback?: SkyrimTogetherTypes.SetSyncStatusCallback, + ): void; + off( event: 'protocolMismatch', callback?: SkyrimTogetherTypes.ProtocolMismatch, @@ -334,6 +589,11 @@ interface SkyrimTogether { callback?: SkyrimTogetherTypes.PartyInfoCallback, ): void; + off( + event: 'partyOptions', + callback?: SkyrimTogetherTypes.PartyOptionsCallback, + ): void; + off( event: 'partyCreated', callback?: SkyrimTogetherTypes.PartyCreatedCallback, @@ -348,15 +608,111 @@ interface SkyrimTogether { event: 'partyInviteReceived', callback?: SkyrimTogetherTypes.PartyInviteReceivedCallback, ): void; + off( + event: 'tradeInviteReceived', + callback?: SkyrimTogetherTypes.TradeInviteCallback, + ): void; + off( + event: 'tradeInviteExpired', + callback?: SkyrimTogetherTypes.TradeInviteExpiredCallback, + ): void; + off( + event: 'tradeStateUpdated', + callback?: SkyrimTogetherTypes.TradeStateUpdatedCallback, + ): void; + off( + event: 'tradeCancelled', + callback?: SkyrimTogetherTypes.TradeCancelledCallback, + ): void; + off( + event: 'tradeCompleted', + callback?: SkyrimTogetherTypes.TradeCompletedCallback, + ): void; + + off( + event: 'teleportRequest', + callback?: SkyrimTogetherTypes.TeleportRequestCallback, + ): void; + + off( + event: 'teleportCountdown', + callback?: SkyrimTogetherTypes.TeleportCountdownCallback, + ): void; + + /** Remove listener from emote menu open requests. */ + off( + event: 'openEmoteMenu', + callback?: SkyrimTogetherTypes.OpenEmoteMenuCallback, + ): void; + /** Remove listener from emote menu toggle requests. */ + off( + event: 'toggleEmoteMenu', + callback?: SkyrimTogetherTypes.ToggleEmoteMenuCallback, + ): void; + + /** Remove listener from overlay banners. */ + off(event: 'showBanner', callback?: SkyrimTogetherTypes.BannerCallback): void; + + /** Remove listener from when the death screen is shown. */ + off( + event: 'showDeathScreen', + callback?: SkyrimTogetherTypes.ShowDeathScreenCallback, + ): void; + + /** Remove listener from when the death screen timer updates. */ + off( + event: 'updateDeathTimer', + callback?: SkyrimTogetherTypes.UpdateDeathTimerCallback, + ): void; + + /** Remove listener from when the respawn button is enabled. */ + off( + event: 'enableRespawnButton', + callback?: SkyrimTogetherTypes.EnableRespawnButtonCallback, + ): void; + + /** Remove listener from when the death screen is hidden. */ + off( + event: 'hideDeathScreen', + callback?: SkyrimTogetherTypes.HideDeathScreenCallback, + ): void; + + off( + event: 'updateReviveVictimProgress', + callback?: SkyrimTogetherTypes.UpdateReviveVictimProgressCallback, + ): void; + + off( + event: 'stopReviveVictimProgress', + callback?: SkyrimTogetherTypes.StopReviveVictimProgressCallback, + ): void; + + off( + event: 'updateReviveHealerProgress', + callback?: SkyrimTogetherTypes.UpdateReviveHealerProgressCallback, + ): void; + + off( + event: 'stopReviveHealerProgress', + callback?: SkyrimTogetherTypes.StopReviveHealerProgressCallback, + ): void; /** * Connect to server at given address and port. * * @param host IP address or hostname. * @param port Port. - * @param password Server password. + * @param username Account username. + * @param password Account password. + * @param serverPassword Optional legacy server password. */ - connect(host: string, port: number, password: string): void; + connect( + host: string, + port: number, + username: string, + password: string, + serverPassword?: string, + ): void; /** * Disconnect from server or cancel connection. @@ -368,6 +724,13 @@ interface SkyrimTogether { */ revealPlayers(): void; + /** + * Trigger a pre-defined emote animation on the local player. + * + * @param eventName Animation graph event to fire. + */ + playEmote(eventName: string): void; + /** * Send message to server. */ @@ -384,12 +747,20 @@ interface SkyrimTogether { deactivate(): void; /** - * Teleport to given player + * Request teleportation to a given player. * - * @param playerId Id of the player to which the requester should be teleported to + * @param playerId Id of the player to whom the request should be sent. */ teleportToPlayer(playerId: number): void; + /** + * Respond to an incoming teleport request. + * + * @param requesterId Id of the player that issued the request. + * @param accepted Whether the request is accepted. + */ + respondTeleportRequest(requesterId: number, accepted: boolean): void; + /** * Reconnect the client. */ @@ -432,4 +803,69 @@ interface SkyrimTogether { * @param playerId Id of the new leader. */ changePartyLeader(playerId: number): void; + + /** + * Send a trade invite to another player. + * + * @param playerId Id of the player to trade with. + */ + sendTradeInvite(playerId: number): void; + + /** + * Respond to a trade invite. + * + * @param playerId Id of the inviter. + * @param accept Whether to accept the invitation. + */ + respondTradeInvite(playerId: number, accept: boolean): void; + + /** + * Cancel the current trade session or pending invite. + */ + cancelTrade(): void; + + /** + * Update the local ready state for the current trade session. + * + * @param ready Whether the player is ready to finalize the trade. + */ + setTradeReady(ready: boolean): void; + + /** + * Update the items offered in the current trade session. + * + * @param entries Selection of inventory indices and counts to offer. + */ + updateTradeOffer(entries: SkyrimTogetherTypes.TradeOfferEntry[]): void; + + /** + * Upload or clear the local profile picture shown to party members. + * + * @param imageData Data URL (e.g. "data:image/png;base64,...") or empty string to clear. + */ + setProfilePicture(imageData: string): void; + + /** + * Select how player name tags are rendered in the world. + * + * @param mode Numeric representation of the desired nametag display mode. + */ + setNameTagMode(mode: number): void; + + /** + * Select whether to prefer account usernames or in-game actor names. + * + * @param preference Either "username" or "actor". + */ + setPlayerNamePreference(preference: string): void; + + /** + * Update party leader options for the current party. + */ + setPartyOptions(options: SkyrimTogetherTypes.PartyOptionsPayload): void; + + /** + * Called when the player clicks the respawn button on the death screen. + */ + respawnButtonClicked(): void; } diff --git a/Code/tests/encoding.cpp b/Code/tests/encoding.cpp index ca09deb6b..32f6f6a9d 100644 --- a/Code/tests/encoding.cpp +++ b/Code/tests/encoding.cpp @@ -15,6 +15,10 @@ #include #include +#include +#include +#include +#include #include #include @@ -346,6 +350,8 @@ TEST_CASE("Packets", "[encoding.packets]") sendMessage.UserMods.ModList.push_back({"Hi", 14}); sendMessage.UserMods.ModList.push_back({"Test", 8}); sendMessage.UserMods.ModList.push_back({"Toast", 49}); + sendMessage.Username = "TestUser"; + sendMessage.Password = Credential::HashPassword("SuperSecret"); Buffer::Writer writer(&buff); sendMessage.Serialize(writer); @@ -384,6 +390,72 @@ TEST_CASE("Packets", "[encoding.packets]") REQUIRE(sendMessage == recvMessage); } + SECTION("TeleportResponse") + { + Buffer buff(100); + + TeleportResponse sendMessage, recvMessage; + sendMessage.RequesterId = 77; + sendMessage.Accepted = true; + + Buffer::Writer writer(&buff); + sendMessage.Serialize(writer); + + Buffer::Reader reader(&buff); + + uint64_t opcode; + reader.ReadBits(opcode, 8); + + recvMessage.DeserializeRaw(reader); + + REQUIRE(sendMessage == recvMessage); + } + + SECTION("NotifyTeleportRequest") + { + Buffer buff(100); + + NotifyTeleportRequest sendMessage, recvMessage; + sendMessage.RequesterId = 91; + sendMessage.RequesterName = "RequesterName"; + + Buffer::Writer writer(&buff); + sendMessage.Serialize(writer); + + Buffer::Reader reader(&buff); + + uint64_t opcode; + reader.ReadBits(opcode, 8); + + recvMessage.DeserializeRaw(reader); + + REQUIRE(sendMessage == recvMessage); + } + + SECTION("NotifyTeleportCountdown") + { + Buffer buff(128); + + NotifyTeleportCountdown sendMessage, recvMessage; + sendMessage.TargetPlayerId = 77; + sendMessage.TargetName = "Target"; + sendMessage.DurationSeconds = 5; + sendMessage.Cancelled = true; + sendMessage.Reason = "Movement detected"; + + Buffer::Writer writer(&buff); + sendMessage.Serialize(writer); + + Buffer::Reader reader(&buff); + + uint64_t opcode; + reader.ReadBits(opcode, 8); + + recvMessage.DeserializeRaw(reader); + + REQUIRE(sendMessage == recvMessage); + } + SECTION("CancelAssignmentRequest") { Buffer buff(1000); @@ -486,6 +558,7 @@ TEST_CASE("Packets", "[encoding.packets]") REQUIRE(recvMessage.Updates[1].UpdatedMovement == sendMessage.Updates[1].UpdatedMovement); } + } TEST_CASE("StringCache", "[encoding.string_cache]") diff --git a/Code/tests/xmake.lua b/Code/tests/xmake.lua index 1ce224fa8..5fb8617d6 100644 --- a/Code/tests/xmake.lua +++ b/Code/tests/xmake.lua @@ -3,10 +3,10 @@ target("TPTests") set_kind("binary") set_group("Tests") add_includedirs( - ".", "../encoding") + ".", "../encoding", "../common") add_headerfiles("**.h") add_files("*.cpp") - add_deps("SkyrimEncoding") + add_deps("SkyrimEncoding", "CommonLib") add_packages( "tiltedcore", "hopscotch-map", diff --git a/Code/tp_process/ProcessHandler.cpp b/Code/tp_process/ProcessHandler.cpp index f0d4ffcb0..3af549854 100644 --- a/Code/tp_process/ProcessHandler.cpp +++ b/Code/tp_process/ProcessHandler.cpp @@ -25,8 +25,23 @@ void ProcessHandler::OnContextCreated(CefRefPtr browser, CefRefPtrSetValue("acceptPartyInvite", CefV8Value::CreateFunction("acceptPartyInvite", m_pOverlayHandler), V8_PROPERTY_ATTRIBUTE_NONE); m_pCoreObject->SetValue("kickPartyMember", CefV8Value::CreateFunction("kickPartyMember", m_pOverlayHandler), V8_PROPERTY_ATTRIBUTE_NONE); m_pCoreObject->SetValue("changePartyLeader", CefV8Value::CreateFunction("changePartyLeader", m_pOverlayHandler), V8_PROPERTY_ATTRIBUTE_NONE); + m_pCoreObject->SetValue("setProfilePicture", CefV8Value::CreateFunction("setProfilePicture", m_pOverlayHandler), V8_PROPERTY_ATTRIBUTE_NONE); + m_pCoreObject->SetValue("setNameTagMode", CefV8Value::CreateFunction("setNameTagMode", m_pOverlayHandler), V8_PROPERTY_ATTRIBUTE_NONE); + m_pCoreObject->SetValue("setPlayerNamePreference", CefV8Value::CreateFunction("setPlayerNamePreference", m_pOverlayHandler), V8_PROPERTY_ATTRIBUTE_NONE); + m_pCoreObject->SetValue("setPartyOptions", CefV8Value::CreateFunction("setPartyOptions", m_pOverlayHandler), V8_PROPERTY_ATTRIBUTE_NONE); m_pCoreObject->SetValue("teleportToPlayer", CefV8Value::CreateFunction("teleportToPlayer", m_pOverlayHandler), V8_PROPERTY_ATTRIBUTE_NONE); + m_pCoreObject->SetValue("respondTeleportRequest", CefV8Value::CreateFunction("respondTeleportRequest", m_pOverlayHandler), V8_PROPERTY_ATTRIBUTE_NONE); + m_pCoreObject->SetValue("sendTradeInvite", CefV8Value::CreateFunction("sendTradeInvite", m_pOverlayHandler), V8_PROPERTY_ATTRIBUTE_NONE); + m_pCoreObject->SetValue("respondTradeInvite", CefV8Value::CreateFunction("respondTradeInvite", m_pOverlayHandler), V8_PROPERTY_ATTRIBUTE_NONE); + m_pCoreObject->SetValue("cancelTrade", CefV8Value::CreateFunction("cancelTrade", m_pOverlayHandler), V8_PROPERTY_ATTRIBUTE_NONE); + m_pCoreObject->SetValue("setTradeReady", CefV8Value::CreateFunction("setTradeReady", m_pOverlayHandler), V8_PROPERTY_ATTRIBUTE_NONE); + m_pCoreObject->SetValue("updateTradeOffer", CefV8Value::CreateFunction("updateTradeOffer", m_pOverlayHandler), V8_PROPERTY_ATTRIBUTE_NONE); + m_pCoreObject->SetValue("playEmote", CefV8Value::CreateFunction("playEmote", m_pOverlayHandler), V8_PROPERTY_ATTRIBUTE_NONE); + m_pCoreObject->SetValue("openEmoteMenu", CefV8Value::CreateFunction("openEmoteMenu", m_pOverlayHandler), V8_PROPERTY_ATTRIBUTE_NONE); m_pCoreObject->SetValue("toggleDebugUI", CefV8Value::CreateFunction("toggleDebugUI", m_pOverlayHandler), V8_PROPERTY_ATTRIBUTE_NONE); + m_pCoreObject->SetValue("respawnButtonClicked", CefV8Value::CreateFunction("respawnButtonClicked", m_pOverlayHandler), V8_PROPERTY_ATTRIBUTE_NONE); + m_pCoreObject->SetValue("showBanner", CefV8Value::CreateFunction("showBanner", m_pOverlayHandler), V8_PROPERTY_ATTRIBUTE_NONE); + m_pCoreObject->SetValue("toggleEmoteMenu", CefV8Value::CreateFunction("toggleEmoteMenu", m_pOverlayHandler), V8_PROPERTY_ATTRIBUTE_NONE); } void ProcessHandler::OnContextReleased(CefRefPtr browser, CefRefPtr frame, CefRefPtr context) diff --git a/Code/xmake.lua b/Code/xmake.lua index 941d311da..403f220d0 100644 --- a/Code/xmake.lua +++ b/Code/xmake.lua @@ -8,7 +8,6 @@ end includes("common") includes("components") includes("base") -includes("admin_protocol") includes("server_runner") includes("server") includes("encoding") diff --git a/GameFiles/Skyrim/scripts/source/TO_PartyTracker.psc b/GameFiles/Skyrim/scripts/source/TO_PartyTracker.psc new file mode 100644 index 000000000..61f06852a --- /dev/null +++ b/GameFiles/Skyrim/scripts/source/TO_PartyTracker.psc @@ -0,0 +1,49 @@ +Scriptname TO_PartyTracker extends Quest +{Party quest used to show objective markers on party members.} + +Int Property ObjectiveBase Auto ; First objective index +Int Property AliasCount Auto ; Number of party alias slots in this quest + +Function SetEnabled(Bool enabled) + if enabled + Start() + SetActive(True) + else + SetActive(False) + Stop() + EndIf +EndFunction + +Function ClearAll() + int i = 0 + while i < AliasCount + Alias a = GetAlias(i) + ReferenceAlias ra = a as ReferenceAlias + if ra + ra.Clear() + endif + SetObjectiveDisplayed(ObjectiveBase + i, False, True) + i += 1 + EndWhile +EndFunction + +Function SetPartyAlias(Int index, ObjectReference ref) + if index < 0 || index >= AliasCount + return + endif + + Alias a = GetAlias(index) + ReferenceAlias ra = a as ReferenceAlias + if !ra + return + endif + + if ref + ra.ForceRefTo(ref) + SetObjectiveDisplayed(ObjectiveBase + index, True, False) + else + ra.Clear() + SetObjectiveDisplayed(ObjectiveBase + index, False, True) + endif +EndFunction + diff --git a/Images/death ui.png b/Images/death ui.png new file mode 100644 index 000000000..179dd9e44 Binary files /dev/null and b/Images/death ui.png differ diff --git a/Images/nametags.jpg b/Images/nametags.jpg new file mode 100644 index 000000000..5e96ec2eb Binary files /dev/null and b/Images/nametags.jpg differ diff --git a/Images/party markers.jpg b/Images/party markers.jpg new file mode 100644 index 000000000..0c09e5e3f Binary files /dev/null and b/Images/party markers.jpg differ diff --git a/Images/trading ui.jpg b/Images/trading ui.jpg new file mode 100644 index 000000000..f7294cb16 Binary files /dev/null and b/Images/trading ui.jpg differ diff --git a/Isolation/mq.json b/Isolation/mq.json new file mode 100644 index 000000000..066dd773a --- /dev/null +++ b/Isolation/mq.json @@ -0,0 +1,101 @@ +{ + "docs": [ + "Drop quest gating files in this Isolation folder. Each JSON file can contain any number of rule objects.", + "Only the fields below are parsed: quest.idName, blacklist[].stageMin, blacklist[].stageMax.", + "All other fields (name, notes, docs) are ignored by the parser but can be used for documentation.", + "Stage ranges are inclusive; hitting any matching range forces the client into GhostOnly mode." + ], + "rules": [ + { + "name": "Unbound", + "quest": { + "idName": "MQ101" + }, + "blacklist": [ + { + "stageMin": 0, + "stageMax": 900, + "notes": "Helgen intro: cart ride, execution, keep selection, forced movement and controls." + } + ] + }, + { + "name": "Diplomatic Immunity", + "quest": { + "idName": "MQ201" + }, + "blacklist": [ + { + "stageMin": 100, + "stageMax": 230, + "notes": "Embassy party, equipment confiscation, smuggling logic, torture chamber recovery, inventory restoration." + } + ] + }, + { + "name": "Diplomatic Immunity - Recover Equipment Misc", + "quest": { + "idName": "MQ201RecoverGearMISC" + }, + "blacklist": [ + { + "stageMin": 0, + "stageMax": 100, + "notes": "Standalone misc quest handling all equipment recovery logic." + } + ] + }, + { + "name": "Alduins Bane", + "quest": { + "idName": "MQ206" + }, + "blacklist": [ + { + "stageMin": 10, + "stageMax": 100, + "notes": "Time-Wound vision and Dragonrend acquisition cutscene with forced camera and player disable." + } + ] + }, + { + "name": "Season Unending", + "quest": { + "idName": "MQ302" + }, + "blacklist": [ + { + "stageMin": 10, + "stageMax": 300, + "notes": "Peace council scene, forced seating, dialogue-driven civil war state resolution." + } + ] + }, + { + "name": "The Fallen", + "quest": { + "idName": "MQ301" + }, + "blacklist": [ + { + "stageMin": 30, + "stageMax": 220, + "notes": "Odahviing trap scene and dragon riding transition to Skuldafn." + } + ] + }, + { + "name": "Sovngarde", + "quest": { + "idName": "MQ304" + }, + "blacklist": [ + { + "stageMin": 0, + "stageMax": 150, + "notes": "Portal entry, mist soul-snare logic, hero gating, Hall of Valor scene scripting." + } + ] + } + ] +} diff --git a/Libraries/TiltedUI b/Libraries/TiltedUI index 93489d5db..637488efa 160000 --- a/Libraries/TiltedUI +++ b/Libraries/TiltedUI @@ -1 +1 @@ -Subproject commit 93489d5db7e441bf9c126a97c88f2f39aae959b0 +Subproject commit 637488efaeb3a54a869ecf9ad51a17858e52070b diff --git a/README.md b/README.md index 707806163..cb48bfcc0 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,90 @@ -# Tilted Online -![Build status](https://github.com/tiltedphoques/TiltedEvolution/workflows/Build%20windows/badge.svg?branch=master) [![Build linux](https://github.com/tiltedphoques/TiltedEvolution/actions/workflows/linux.yml/badge.svg)](https://github.com/tiltedphoques/TiltedEvolution/actions/workflows/linux.yml) [![Discord](https://img.shields.io/discord/247835175860305931.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/skyrimtogether) +# Tilted Online (Custom Fork) +![Build status](https://github.com/jacobwasbeast/TiltedEvolution/workflows/Build%20windows/badge.svg?branch=master) [![Build linux](https://github.com/jacobwasbeast/TiltedEvolution/actions/workflows/linux.yml/badge.svg)](https://github.com/jacobwasbeast/TiltedEvolution/actions/workflows/linux.yml) -Size Limit logo by Anton Lovchikov -Tilted Online is a framework created to enable multiplayer in Bethesda games, currently supporting **Skyrim Special Edition**. +**Tilted Online** is a framework created to enable multiplayer in Bethesda games, currently supporting **Skyrim Special Edition**. +To play the original Tilted Online, go to the [nexus page](https://www.nexusmods.com/skyrimspecialedition/mods/69993) or the [github](https://github.com/tiltedphoques/TiltedEvolution) +> **Fork Notice:** This repository is a heavily modified fork maintained by **Jacobwasbeast**. It introduces entirely new systems (Trading, Auth, Database Sync) not present in the original codebase. This fork is intended for custom server implementation and is **not** intended to be merged back into the upstream TiltedPhoques repository. + +## Exclusive Features & Overhauls + +This fork fundamentally changes several core systems to support a more persistent and cooperative MMO-lite experience. + +### Economy & Item Persistence + + * **Player-to-Player Trading:** A brand new, secure trading interface allowing players to exchange items directly. + * **Server-Sided Item Drops:** + * *Original Behavior:* Item drops were synced loosely by clients within a specific cell. + * *Fork Behavior:* Item drops are now **Database-Backed and Server-Authoritative**, ensuring items remain persistent and synced regardless of client cell states. + +### Combat & Revive System + + * **Party & In-Place Resurrection:** + * Implemented a downed state mechanics. + * Players can be revived in-place by **Party Members**, encouraging cooperative play. + * **Visual Indicators:** Dead/Downed party members now **glow**, making it easier to locate teammates in chaotic fights for a revive. + +### Navigation & Social + + * **TPA (Teleport Ask) System:** Replaced the immediate teleport command with a request-based system (Request -\> Accept/Deny), preventing sudden crashes and uninvited teleportation. + * **Advanced Map Markers:** + * Added **Party Icons** to the map for easier tracking. + * **Identity System:** + * **Custom Login:** Replaced the standard connection flow with a **Username & Password** system (securely hashed). + * **Profile Pictures:** Added support for user avatars/profile pictures in nametags and UI. + +### Stability & Crash Fixes + + * **Crash Fixes:** Major stability improvements regarding inventory manipulation, quest updates, and cell loading. + * **Sync Improvements:** Fixed equipment desync upon respawning. + * **Security:** Console commands are disabled while connected to the server to prevent client-side abuse. + +----- ## Getting started -To play Tilted Online, go to the [nexus page](https://www.nexusmods.com/skyrimspecialedition/mods/69993). -For general information, go to the [Tilted Online Wiki](https://wiki.tiltedphoques.com/tilted-online/). +To use this specific version, you must build from the source provided in this repository. + +For general information regarding the underlying framework, visit the [Tilted Online Wiki](https://wiki.tiltedphoques.com/tilted-online/). -Check out the [build guide](https://wiki.tiltedphoques.com/tilted-online/technical-documentation/build-guide) for setup and development info on the project. When writing code, check the CODE_GUIDELINES.md and make sure to run clang-format! +Check out the [build guide](https://wiki.tiltedphoques.com/tilted-online/technical-documentation/build-guide) for setup and development info on the project. ## Reporting bugs -If you would like to report a bug please report them in the "Issues" tab on this page. Detailed and reproducible bug reports are of great importance for the development of the project. + +If you encounter bugs specific to these custom features (Trading, TPA, Login, Reviving), please report them in the **Issues** tab of **this repository**, not the original Tilted Online repository. ## Contributing -Have some experience in C++, and want to help advance the project faster? Contribute! -- Check the issues, it's a good place to start when you don't know what to do. -- Fork the repository and create pull requests to this repository. -- Create pull requests to the "dev" branch, not to master or prerel. -- Try to keep your code clean, following the code guidelines. + +If you wish to contribute to this custom fork: + + - Check the issues tab for current tasks. + - Fork this repository and create pull requests here. + - Ensure you follow the existing code guidelines. ## Main project source tree -* [**client/**](./Code/client): Sources for the SkyrimSE and FO4 clients. -* [**immersive_launcher/**](./Code/immersive_launcher): Game starter/updater. -* [**common/**](./Code/common): Common code shared between plugin and server. -* [**encoding/**](./Code/encoding): Net-message definitions. -* [**server/**](./Code/server): GameServer implementation. -* [**skyrim_ui/**](./Code/skyrim_ui): Source code for the ui, written in typescript. -* [**tests/**](./Code/tests): Tests for the encoding and serialization code. -* [**tp_process/**](./Code/tp_process): Worker for CEF (Chromium Embedded Framework) overlay. + * [**client/**](https://www.google.com/search?q=./Code/client): Sources for the SkyrimSE and FO4 clients (Modified). + * [**immersive\_launcher/**](https://www.google.com/search?q=./Code/immersive_launcher): Game starter/updater. + * [**common/**](https://www.google.com/search?q=./Code/common): Common code shared between plugin and server. + * [**encoding/**](https://www.google.com/search?q=./Code/encoding): Net-message definitions. + * [**server/**](https://www.google.com/search?q=./Code/server): GameServer implementation (Heavily Modified with DB support). + * [**skyrim\_ui/**](https://www.google.com/search?q=./Code/skyrim_ui): Source code for the UI (Trading, Login, Nametags). + * [**tests/**](https://www.google.com/search?q=./Code/tests): Tests for the encoding and serialization code. + * [**tp\_process/**](https://www.google.com/search?q=./Code/tp_process): Worker for CEF (Chromium Embedded Framework) overlay. + +## Some images + +* **Name Tags** + + +* **Death UI** + + +* **Trading UI** + + +* **Party Markers** + ## License [![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html) diff --git a/build_output.log b/build_output.log new file mode 100644 index 000000000..a0b102729 --- /dev/null +++ b/build_output.log @@ -0,0 +1,85 @@ +checking for platform ... linux +checking for architecture ... x86_64 + => install sentry-native 0.7.1 .. failed + +ninja: Entering directory `/home/jacobwasbeast/.xmake/cache/packages/2511/s/sentry-native/0.7.1/source/build_1ee624b5' +[1/233] Building CXX object crashpad_build/third_party/mini_chromium/CMakeFiles/mini_chromium.dir/mini_chromium/base/debug/alias.cc.o +[2/233] Building CXX object crashpad_build/third_party/mini_chromium/CMakeFiles/mini_chromium.dir/mini_chromium/base/memory/page_size_posix.cc.o +[3/233] Building CXX object crashpad_build/third_party/mini_chromium/CMakeFiles/mini_chromium.dir/mini_chromium/base/process/memory.cc.o +[4/233] Building CXX object crashpad_build/compat/CMakeFiles/crashpad_compat.dir/linux/sys/mman_memfd_create.cc.o +In file included from /home/jacobwasbeast/.xmake/cache/packages/2511/s/sentry-native/0.7.1/source/external/crashpad/compat/linux/sys/mman_memfd_create.cc:21: +/home/jacobwasbeast/.xmake/cache/packages/2511/s/sentry-native/0.7.1/source/external/crashpad/util/misc/no_cfi_icall.h:55:75: warning: ‘cfi-icall’ attribute directive ignored [-Wattributes] + 55 | DISABLE_CFI_ICALL static R Invoke(Function&& function, RunArgs&&... args) { + | ^ +/home/jacobwasbeast/.xmake/cache/packages/2511/s/sentry-native/0.7.1/source/external/crashpad/util/misc/no_cfi_icall.h:63:75: warning: ‘cfi-icall’ attribute directive ignored [-Wattributes] + 63 | DISABLE_CFI_ICALL static R Invoke(Function&& function, RunArgs&&... args) { + | ^ +/home/jacobwasbeast/.xmake/cache/packages/2511/s/sentry-native/0.7.1/source/external/crashpad/util/misc/no_cfi_icall.h:87:75: warning: ‘cfi-icall’ attribute directive ignored [-Wattributes] + 87 | DISABLE_CFI_ICALL static R Invoke(Function&& function, RunArgs&&... args) { + | ^ +/home/jacobwasbeast/.xmake/cache/packages/2511/s/sentry-native/0.7.1/source/external/crashpad/util/misc/no_cfi_icall.h:95:75: warning: ‘cfi-icall’ attribute directive ignored [-Wattributes] + 95 | DISABLE_CFI_ICALL static R Invoke(Function&& function, RunArgs&&... args) { +if you want to get more verbose errors, please see: + -> /home/jacobwasbeast/.xmake/cache/packages/2511/s/sentry-native/0.7.1/installdir.failed/logs/install.txt + => install abseil 20250127.1 .. failed + +ninja: Entering directory `/tmp/.xmake1000/251110/_9373ba58e0e1d332a88a035a7a08ccf2.dir' +[1/250] Building CXX object absl/strings/CMakeFiles/strings_internal.dir/internal/utf8.cc.o +[2/250] Building CXX object absl/debugging/CMakeFiles/utf8_for_code_point.dir/internal/utf8_for_code_point.cc.o +[3/250] Building CXX object absl/base/CMakeFiles/base.dir/internal/unscaledcycleclock.cc.o +[4/250] Linking CXX static library absl/debugging/libabsl_utf8_for_code_point.a +[5/250] Building CXX object absl/base/CMakeFiles/spinlock_wait.dir/internal/spinlock_wait.cc.o +[6/250] Building CXX object absl/base/CMakeFiles/base.dir/internal/cycleclock.cc.o +[7/250] Linking CXX static library absl/base/libabsl_spinlock_wait.a +[8/250] Building CXX object absl/types/CMakeFiles/bad_optional_access.dir/bad_optional_access.cc.o +[9/250] Building CXX object absl/strings/CMakeFiles/strings_internal.dir/internal/ostringstream.cc.o +[10/250] Building CXX object absl/base/CMakeFiles/log_severity.dir/log_severity.cc.o +[11/250] Building CXX object absl/base/CMakeFiles/raw_logging_internal.dir/internal/raw_logging.cc.o +[12/250] Building CXX object absl/base/CMakeFiles/throw_delegate.dir/internal/throw_delegate.cc.o +[13/250] Building CXX object absl/crc/CMakeFiles/crc_cpu_detect.dir/internal/cpu_detect.cc.o +[14/250] Linking CXX static library absl/base/libabsl_log_severity.a +[15/250] Building CXX object absl/strings/CMakeFiles/string_view.dir/string_view.cc.o +[16/250] Linking CXX static library absl/base/libabsl_raw_logging_internal.a +if you want to get more verbose errors, please see: + -> /home/jacobwasbeast/.xmake/cache/packages/2511/a/abseil/20250127.1/installdir.failed/logs/install.txt + => install snappy 1.1.10 .. failed + +-- The C compiler identification is GNU 15.2.1 +-- The CXX compiler identification is GNU 15.2.1 +-- Detecting C compiler ABI info +-- Detecting C compiler ABI info - done +-- Check for working C compiler: /usr/bin/cc - skipped +-- Detecting C compile features +-- Detecting C compile features - done +-- Detecting CXX compiler ABI info +-- Detecting CXX compiler ABI info - done +-- Check for working CXX compiler: /usr/bin/c++ - skipped +-- Detecting CXX compile features +-- Detecting CXX compile features - done +-- Looking for sys/mman.h +-- Looking for sys/mman.h - found +-- Looking for sys/resource.h +-- Looking for sys/resource.h - found +-- Looking for sys/time.h +if you want to get more verbose errors, please see: + -> /home/jacobwasbeast/.xmake/cache/packages/2511/s/snappy/1.1.10/installdir.failed/logs/install.txt + => install tiltedcore v0.2.7 .. failed + +[ 76%]: cache compiling.release Code/tests/src/core.cpp +[ 76%]: cache compiling.release Code/tests/src/main.cpp +[ 76%]: cache compiling.release Code/core/src/Allocator.cpp +[ 76%]: cache compiling.release Code/core/src/Buffer.cpp +[ 76%]: cache compiling.release Code/core/src/BoundedAllocator.cpp +[ 76%]: cache compiling.release Code/core/src/Filesystem.cpp +[ 76%]: cache compiling.release Code/core/src/Hash.cpp +[ 76%]: cache compiling.release Code/core/src/MimallocAllocator.cpp +[ 76%]: cache compiling.release Code/core/src/Math.cpp +[ 76%]: cache compiling.release Code/core/src/ScratchAllocator.cpp +[ 76%]: cache compiling.release Code/core/src/Serialization.cpp +[ 76%]: cache compiling.release Code/core/src/StandardAllocator.cpp +[ 76%]: cache compiling.release Code/core/src/TaskQueue.cpp +[ 76%]: cache compiling.release Code/core/src/ViewBuffer.cpp +if you want to get more verbose errors, please see: + -> /home/jacobwasbeast/.xmake/cache/packages/2511/t/tiltedcore/v0.2.7/installdir.failed/logs/install.txt +error: install failed! +warning: {buildir = } has been deprecated, please use {builddir = } in cmake.install diff --git a/xmake.lua b/xmake.lua index 7108f6b73..95cbe1fc5 100644 --- a/xmake.lua +++ b/xmake.lua @@ -12,6 +12,8 @@ if is_plat("windows") then add_cxflags("/bigobj") add_syslinks("kernel32") set_arch("x64") + -- Force static runtime to match CEF library + set_runtimes(is_mode("debug") and "MTd" or "MT") end if is_plat("linux") then @@ -30,24 +32,27 @@ if has_config("unitybuild") then add_rules("c++.unity_build", {batchsize = 12}) end --- direct dependencies version pinning +-- direct dependencies version pinning add_requires( - "entt v3.10.0", - "recastnavigation v1.6.0", - "tiltedcore v0.2.7", - "cryptopp 8.9.0", - "spdlog v1.13.0", + "entt v3.10.0", + "recastnavigation v1.6.0", + "tiltedcore v0.2.7", + "cryptopp 8.9.0", + "spdlog v1.13.0", "cpp-httplib 0.14.0", - "gtest v1.14.0", - "mem 1.0.0", - "glm 0.9.9+8", - "sentry-native 0.7.1", - "zlib v1.3.1" + "gtest v1.14.0", + "mem 1.0.0", + "glm 0.9.9+8", + "sentry-native 0.7.1", + "zlib v1.3.1", + "fmt" ) if is_plat("windows") then add_requires( - "discord 3.2.1", - "imgui v1.89.7" + "discord 3.2.1", + "imgui v1.89.7", + "magnum", + "magnum-integration" ) end @@ -62,12 +67,13 @@ end add_requireconfs("cpp-httplib", {configs = {ssl = true}}) add_requireconfs("sentry-native", { configs = { backend = "crashpad" } }) ---[[ +add_requireconfs("imgui", { version = "v1.89.7", override = true }) +add_requireconfs("*.imgui", { version = "v1.89.7", override = true }) +add_requireconfs("magnum-integration.imgui", { version = "v1.89.7", override = true }) add_requireconfs("magnum", { configs = { sdl2 = true }}) add_requireconfs("magnum-integration", { configs = { imgui = true }}) add_requireconfs("magnum-integration.magnum", { configs = { sdl2 = true }}) add_requireconfs("magnum-integration.imgui", { override = true }) ---]] before_build(function (target) import("modules.version") @@ -78,9 +84,9 @@ before_build(function (target) #define IS_MASTER %d #define IS_BRANCH_BETA %d #define IS_BRANCH_PREREL %d - ]], - bool_to_number[branch == "master"], - bool_to_number[branch == "bluedove"], + ]], + bool_to_number[branch == "master"], + bool_to_number[branch == "bluedove"], bool_to_number[branch == "prerel"]) -- fix always-compiles problem by updating the file only if content has changed. @@ -121,7 +127,7 @@ task("upload-symbols") config.load() local sentrybin = path.join(os.projectdir(), "build", "sentry-cli.exe") - if not os.exists(sentrybin) then + if not os.exists(sentrybin) then http.download("https://github.com/getsentry/sentry-cli/releases/download/2.0.2/sentry-cli-Windows-x86_64.exe", sentrybin) end