diff --git a/.github/workflows/windows-playable-build.yml b/.github/workflows/windows-playable-build.yml index 2749cd965..313175354 100644 --- a/.github/workflows/windows-playable-build.yml +++ b/.github/workflows/windows-playable-build.yml @@ -20,7 +20,7 @@ jobs: upload-build-for-next-job: true package-and-upload: - runs-on: windows-latest + runs-on: windows-2022 needs: build-windows steps: - name: Download artifact from the previous job diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index f2070226e..3c9b01311 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -25,7 +25,7 @@ jobs: build: strategy: matrix: - os: [windows-latest] + os: [windows-2022] arch: [x64] runs-on: ${{ matrix.os }} diff --git a/.gitignore b/.gitignore index 9161be0ee..1c1a7de2c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,15 @@ vs2019 vs2022 vsxmake2019 vsxmake2022 +vsxmake* .xmake vs2019 +# Local working files (not shared) +CLAUDE.md +.claude/ +docs/superpowers/ + build Build Build/*.log diff --git a/Code/client/Events/ScriptAnimationEvent.h b/Code/client/Events/ScriptAnimationEvent.h index 514a42226..08823205a 100644 --- a/Code/client/Events/ScriptAnimationEvent.h +++ b/Code/client/Events/ScriptAnimationEvent.h @@ -12,6 +12,8 @@ struct ScriptAnimationEvent { } + // Local form id of the animating reference; ObjectService::OnScriptAnimationEvent + // translates it to a server id before it goes on the wire. uint32_t FormID; String Animation; String EventName; diff --git a/Code/client/Events/WaveCommandEvent.h b/Code/client/Events/WaveCommandEvent.h new file mode 100644 index 000000000..2d0ab81fe --- /dev/null +++ b/Code/client/Events/WaveCommandEvent.h @@ -0,0 +1,8 @@ +#pragma once + +/** + * @brief Dispatched when the local player uses the /wave chat command. + */ +struct WaveCommandEvent +{ +}; diff --git a/Code/client/Games/Misc/BSScript.cpp b/Code/client/Games/Misc/BSScript.cpp index 4eef585e3..01d7efd1d 100644 --- a/Code/client/Games/Misc/BSScript.cpp +++ b/Code/client/Games/Misc/BSScript.cpp @@ -9,6 +9,9 @@ #include #include #include +#include +#include +#include TP_THIS_FUNCTION(TRegisterPapyrusFunction, void, BSScript::IVirtualMachine, NativeFunction*); TP_THIS_FUNCTION(TBindEverythingToScript, void, BSScript::IVirtualMachine*); @@ -20,6 +23,23 @@ TBindEverythingToScript* RealBindEverythingToScript = nullptr; TSignaturesMatch* RealSignaturesMatch = nullptr; TCompareVariables* RealCompareVariables = nullptr; +// Papyrus global native: Debug.SendAnimationEvent(ObjectReference arRef, string asEventName). +// ABI matches the repo's PapyrusFunction<> convention: (VM*, stackId, this/tag, args...). +using TDebugSendAnimationEvent = void(BSScript::IVirtualMachine*, uint32_t, void*, TESObjectREFR*, BSFixedString*); +static TDebugSendAnimationEvent* RealDebugSendAnimationEvent = nullptr; + +static void HookDebugSendAnimationEvent(BSScript::IVirtualMachine* apVm, uint32_t aStackId, void* apTag, TESObjectREFR* apRef, BSFixedString* apEventName) +{ + const char* pcEventName = apEventName ? apEventName->AsAscii() : nullptr; + if (apRef && pcEventName && pcEventName[0] != '\0') + { + spdlog::debug("Debug.SendAnimationEvent captured: ref {:X}, event {}", apRef->formID, pcEventName); + World::Get().GetRunner().Trigger(ScriptAnimationEvent(apRef->formID, String{}, pcEventName)); + } + + RealDebugSendAnimationEvent(apVm, aStackId, apTag, apRef, apEventName); +} + void TP_MAKE_THISCALL(HookRegisterPapyrusFunction, BSScript::IVirtualMachine, NativeFunction* apFunction) { auto& runner = World::Get().GetRunner(); @@ -28,6 +48,13 @@ void TP_MAKE_THISCALL(HookRegisterPapyrusFunction, BSScript::IVirtualMachine, Na runner.Trigger(std::move(event)); + if (!RealDebugSendAnimationEvent && apFunction->functionAddress && !strcmp(apFunction->typeName.AsAscii(), "Debug") && !strcmp(apFunction->functionName.AsAscii(), "SendAnimationEvent")) + { + RealDebugSendAnimationEvent = reinterpret_cast(apFunction->functionAddress); + TP_HOOK_IMMEDIATE(&RealDebugSendAnimationEvent, HookDebugSendAnimationEvent); + spdlog::info("Hooked Debug.SendAnimationEvent at {}", apFunction->functionAddress); + } + TiltedPhoques::ThisCall(RealRegisterPapyrusFunction, apThis, apFunction); } diff --git a/Code/client/Games/Skyrim/ObScript.cpp b/Code/client/Games/Skyrim/ObScript.cpp new file mode 100644 index 000000000..25a4b2b03 --- /dev/null +++ b/Code/client/Games/Skyrim/ObScript.cpp @@ -0,0 +1,76 @@ +#include + +#include +#include + +#include + +#include + +// Bound from CommonLibSSE-NG SCRIPT_FUNCTION::LocateScriptCommand, which scans +// kScriptCommandsEnd (0x02E0) entries from the first script command. +static constexpr uint32_t kScriptCommandCount = 0x2E0; + +using TParseParameters = bool(const ObScriptParam* apParamInfo, ObScriptData* apScriptData, uint32_t& arOpcodeOffsetPtr, TESObjectREFR* apThisObj, TESObjectREFR* apContainingObj, Script* apScriptObj, ScriptLocals* apLocals, ...); + +static ObScriptCommand::TExecute* RealSendAnimationEventExecute = nullptr; +static ObScriptCommand* s_pSaeCommand = nullptr; // kept for a future unhook/restore path + +static bool HookSendAnimationEventExecute(const ObScriptParam* apParamInfo, ObScriptData* apScriptData, TESObjectREFR* apThisObj, TESObjectREFR* apContainingObj, Script* apScriptObj, ScriptLocals* apLocals, double& arResult, uint32_t& arOpcodeOffsetPtr) +{ + POINTER_SKYRIMSE(TParseParameters, s_parseParameters, 21910); + + // Parse a copy so the original execute below still sees the unconsumed offset. + uint32_t opcodeOffset = arOpcodeOffsetPtr; + char eventNameBuffer[512]{}; + + const bool cParsed = s_parseParameters.Get()(apParamInfo, apScriptData, opcodeOffset, apThisObj, apContainingObj, apScriptObj, apLocals, eventNameBuffer); + + const bool cResult = RealSendAnimationEventExecute(apParamInfo, apScriptData, apThisObj, apContainingObj, apScriptObj, apLocals, arResult, arOpcodeOffsetPtr); + + if (cResult && cParsed && apThisObj && eventNameBuffer[0] != '\0') + { + spdlog::debug("Console sae captured: ref {:X}, event {}", apThisObj->formID, eventNameBuffer); + World::Get().GetRunner().Trigger(ScriptAnimationEvent(apThisObj->formID, String{}, eventNameBuffer)); + } + + return cResult; +} + +static TiltedPhoques::Initializer s_obScriptHooks( + []() + { + // First script command, AE address-library id 361120 (CommonLibSSE-NG include/RE/Offsets.h:481). + // The sae command is a SCRIPT function ("SendAnimEvent"), not a console command — + // confirmed via the in-game help listing. + POINTER_SKYRIMSE(ObScriptCommand, s_firstScriptCommand, 361120); + + ObScriptCommand* pCommands = s_firstScriptCommand.Get(); + if (!pCommands) + { + spdlog::error("ObScript: script command table not found, sae sync disabled"); + return; + } + + for (uint32_t i = 0; i < kScriptCommandCount; ++i) + { + ObScriptCommand& command = pCommands[i]; + + if (command.pFunctionName && _stricmp(command.pFunctionName, "SendAnimEvent") == 0) + { + s_pSaeCommand = &command; + RealSendAnimationEventExecute = command.pExecuteFunction; + command.pExecuteFunction = HookSendAnimationEventExecute; + + spdlog::info("ObScript: hooked script command SendAnimEvent (sae)"); + return; + } + } + + spdlog::error("ObScript: SendAnimEvent script command not found, sae sync disabled"); + for (uint32_t i = 0; i < kScriptCommandCount; ++i) + { + const ObScriptCommand& command = pCommands[i]; + spdlog::debug("ObScript table [{}]: {} ({})", i, command.pFunctionName ? command.pFunctionName : "", command.pShortName ? command.pShortName : ""); + } + }); diff --git a/Code/client/Games/Skyrim/ObScript.h b/Code/client/Games/Skyrim/ObScript.h new file mode 100644 index 000000000..cdfcf9716 --- /dev/null +++ b/Code/client/Games/Skyrim/ObScript.h @@ -0,0 +1,53 @@ +#pragma once + +struct Script; +struct ScriptLocals; +struct TESObjectREFR; + +// Minimal layout of the engine's ObScript (console/script) command table entry. +// Cross-checked against CommonLibSSE-NG RE::SCRIPT_FUNCTION (include/RE/C/CommandTable.h). +struct ObScriptParam +{ + const char* pParamName; // 00 + uint32_t paramType; // 08 + bool optional; // 0C + uint8_t pad0D; // 0D + uint16_t pad0E; // 0E +}; + +static_assert(sizeof(ObScriptParam) == 0x10); + +struct ObScriptData +{ + uint16_t opcode; // 00 + uint16_t chunkSize; // 02 + uint16_t numParams; // 04 +}; + +static_assert(sizeof(ObScriptData) == 0x6); + +struct ObScriptCommand +{ + using TExecute = bool(const ObScriptParam* apParamInfo, ObScriptData* apScriptData, TESObjectREFR* apThisObj, TESObjectREFR* apContainingObj, Script* apScriptObj, ScriptLocals* apLocals, double& arResult, uint32_t& arOpcodeOffsetPtr); + + const char* pFunctionName; // 00 + const char* pShortName; // 08 + uint32_t output; // 10 + uint32_t pad14; // 14 + const char* pHelpString; // 18 + bool referenceFunction; // 20 + uint8_t pad21; // 21 + uint16_t numParams; // 22 + uint32_t pad24; // 24 + ObScriptParam* pParams; // 28 + TExecute* pExecuteFunction; // 30 + void* pCompileFunction; // 38 + void* pConditionFunction; // 40 + bool editorFilter; // 48 + bool invalidatesCellList; // 49 + uint16_t pad4A; // 4A + uint32_t pad4C; // 4C +}; + +static_assert(offsetof(ObScriptCommand, pExecuteFunction) == 0x30); +static_assert(sizeof(ObScriptCommand) == 0x50); diff --git a/Code/client/Services/Generic/ObjectService.cpp b/Code/client/Services/Generic/ObjectService.cpp index b78b68588..21cfa89ce 100644 --- a/Code/client/Services/Generic/ObjectService.cpp +++ b/Code/client/Services/Generic/ObjectService.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,7 @@ ObjectService::ObjectService(World& aWorld, entt::dispatcher& aDispatcher, Trans m_assignObjectConnection = aDispatcher.sink().connect<&ObjectService::OnAssignObjectsResponse>(this); m_scriptAnimationConnection = aDispatcher.sink().connect<&ObjectService::OnScriptAnimationEvent>(this); m_scriptAnimationNotifyConnection = aDispatcher.sink().connect<&ObjectService::OnNotifyScriptAnimation>(this); + m_waveCommandConnection = aDispatcher.sink().connect<&ObjectService::OnWaveCommand>(this); EventDispatcherManager::Get()->activateEvent.RegisterSink(this); } @@ -378,34 +380,78 @@ void ObjectService::OnLockChangeNotify(const NotifyLockChange& acMessage) noexce pObject->LockChange(); } +// ScriptAnimationEvent carries a LOCAL form id; the wire format +// (ScriptAnimationRequest/NotifyScriptAnimation.FormID) carries a SERVER id. +// The translation happens here; refs without a server id are local-only. void ObjectService::OnScriptAnimationEvent(const ScriptAnimationEvent& acEvent) noexcept { + if (!m_transport.IsOnline()) + return; + + auto view = m_world.view(); + const auto it = std::find_if(view.begin(), view.end(), [view, id = acEvent.FormID](auto entity) { return view.get(entity).Id == id; }); + + if (it == view.end()) + { + spdlog::debug("{}: no synced entity for form id {:X}, event {}", __FUNCTION__, acEvent.FormID, acEvent.EventName.c_str()); + return; + } + + std::optional serverIdRes = Utils::GetServerId(*it); + if (!serverIdRes.has_value()) + return; + ScriptAnimationRequest request{}; - request.FormID = acEvent.FormID; + request.FormID = serverIdRes.value(); request.Animation = acEvent.Animation; request.EventName = acEvent.EventName; m_transport.Send(request); } -void ObjectService::OnNotifyScriptAnimation(const NotifyScriptAnimation& acMessage) noexcept +void ObjectService::OnWaveCommand(const WaveCommandEvent& acEvent) noexcept { - if (acMessage.FormID == 0) + if (!m_transport.IsOnline()) return; - auto* pForm = TESForm::GetById(acMessage.FormID); - auto* pObject = Cast(pForm); + auto view = m_world.view(); + const auto it = std::find_if(view.begin(), view.end(), [view](auto entity) { return view.get(entity).Id == 0x14; }); - if (!pObject) + if (it == view.end()) + { + spdlog::debug("{}: local player entity not found", __FUNCTION__); + return; + } + + std::optional serverIdRes = Utils::GetServerId(*it); + if (!serverIdRes.has_value()) { - spdlog::error("Failed to fetch notify script animation object, form id: {:X}", acMessage.FormID); + spdlog::debug("{}: local player has no server id yet", __FUNCTION__); return; } + ScriptAnimationRequest request{}; + request.FormID = serverIdRes.value(); + request.EventName = "IdleWave"; + + m_transport.Send(request); + + // The relay no longer echoes to the sender; play the wave locally. + BSFixedString eventName("IdleWave"); + PlayerCharacter::Get()->SendAnimationEvent(&eventName); +} + +void ObjectService::OnNotifyScriptAnimation(const NotifyScriptAnimation& acMessage) noexcept +{ + // FormID carries a server id; resolve it to whatever local form mirrors that entity + TESObjectREFR* pObject = Utils::GetByServerId(acMessage.FormID); + if (!pObject) + return; + BSFixedString eventName(acMessage.EventName.c_str()); if (acMessage.Animation == String{}) { - pObject->PlayAnimation(&eventName); + pObject->SendAnimationEvent(&eventName); } else { diff --git a/Code/client/Services/Generic/OverlayClient.cpp b/Code/client/Services/Generic/OverlayClient.cpp index 6e6b983f5..97d41430e 100644 --- a/Code/client/Services/Generic/OverlayClient.cpp +++ b/Code/client/Services/Generic/OverlayClient.cpp @@ -10,6 +10,7 @@ #include #include +#include #include @@ -51,6 +52,8 @@ bool OverlayClient::OnProcessMessageReceived(CefRefPtr browser, CefR ProcessChatMessage(eventArgs); else if (eventName == "setTime") ProcessSetTimeCommand(eventArgs); + else if (eventName == "wave") + ProcessWaveCommand(); else if (eventName == "launchParty") World::Get().GetPartyService().CreateParty(); else if (eventName == "leaveParty") @@ -137,6 +140,11 @@ void OverlayClient::ProcessSetTimeCommand(CefRefPtr aEventArgs) World::Get().GetDispatcher().trigger(SetTimeCommandEvent(hours, minutes, senderId)); } +void OverlayClient::ProcessWaveCommand() +{ + World::Get().GetRunner().Trigger(WaveCommandEvent()); +} + void OverlayClient::ProcessTeleportMessage(CefRefPtr aEventArgs) { TeleportRequest request{}; diff --git a/Code/client/Services/ObjectService.h b/Code/client/Services/ObjectService.h index 9adf8f39b..d2aff1a0b 100644 --- a/Code/client/Services/ObjectService.h +++ b/Code/client/Services/ObjectService.h @@ -13,6 +13,7 @@ struct LockChangeEvent; struct NotifyLockChange; struct CellChangeEvent; struct ScriptAnimationEvent; +struct WaveCommandEvent; struct AssignObjectsResponse; struct NotifyScriptAnimation; @@ -33,6 +34,7 @@ class ObjectService final : public BSTEventSink void OnLockChange(const LockChangeEvent&) noexcept; void OnLockChangeNotify(const NotifyLockChange&) noexcept; void OnScriptAnimationEvent(const ScriptAnimationEvent&) noexcept; + void OnWaveCommand(const WaveCommandEvent&) noexcept; void OnNotifyScriptAnimation(const NotifyScriptAnimation&) noexcept; BSTEventResult OnEvent(const TESActivateEvent*, const EventDispatcher*) override; @@ -51,4 +53,5 @@ class ObjectService final : public BSTEventSink entt::scoped_connection m_assignObjectConnection; entt::scoped_connection m_scriptAnimationConnection; entt::scoped_connection m_scriptAnimationNotifyConnection; + entt::scoped_connection m_waveCommandConnection; }; diff --git a/Code/client/Services/OverlayClient.h b/Code/client/Services/OverlayClient.h index b495b0e5f..7e9fde89e 100644 --- a/Code/client/Services/OverlayClient.h +++ b/Code/client/Services/OverlayClient.h @@ -27,6 +27,7 @@ struct OverlayClient : TiltedPhoques::OverlayClient void ProcessRevealPlayersMessage(); void ProcessChatMessage(CefRefPtr aEventArgs); void ProcessSetTimeCommand(CefRefPtr aEventArgs); + void ProcessWaveCommand(); void ProcessTeleportMessage(CefRefPtr aEventArgs); void ProcessToggleDebugUI(); void SetUIVisible(bool aVisible) noexcept; diff --git a/Code/immersive_launcher/loader/MemoryLayout.cpp b/Code/immersive_launcher/loader/MemoryLayout.cpp index d1e31e0ce..3f7a7b713 100644 --- a/Code/immersive_launcher/loader/MemoryLayout.cpp +++ b/Code/immersive_launcher/loader/MemoryLayout.cpp @@ -32,7 +32,7 @@ namespace { extern "C" const IMAGE_DOS_HEADER __ImageBase; -const uint8_t* const pBasePtr = reinterpret_cast(&__ImageBase); +constinit const uint8_t* pBasePtr = reinterpret_cast(&__ImageBase); const uint8_t* kpImageEnd{pBasePtr + ((PIMAGE_NT_HEADERS)(pBasePtr + __ImageBase.e_lfanew))->OptionalHeader.SizeOfImage}; bool InRange(const uint8_t* apObj, const uint8_t* apLo, const uint8_t* apHi) diff --git a/Code/server/Game/Player.h b/Code/server/Game/Player.h index 0ae7af3bb..ed80ad004 100644 --- a/Code/server/Game/Player.h +++ b/Code/server/Game/Player.h @@ -16,6 +16,7 @@ struct Player [[nodiscard]] ConnectionId_t GetConnectionId() const noexcept { return m_connectionId; } [[nodiscard]] std::optional GetCharacter() const noexcept { return m_character; } [[nodiscard]] PartyComponent& GetParty() noexcept { return m_party; } + [[nodiscard]] const PartyComponent& GetParty() const noexcept { return m_party; } [[nodiscard]] const String& GetUsername() const noexcept { return m_username; } [[nodiscard]] const String& GetEndPoint() const noexcept { return m_endpoint; } [[nodiscard]] const uint64_t GetDiscordId() const noexcept { return m_discordId; } diff --git a/Code/server/Scripting/Player_Bindings.cpp b/Code/server/Scripting/Player_Bindings.cpp index 999fea81d..90ea953ca 100644 --- a/Code/server/Scripting/Player_Bindings.cpp +++ b/Code/server/Scripting/Player_Bindings.cpp @@ -13,7 +13,7 @@ void BindPlayer(sol::state_view aState) playerType["GetId"] = &Player::GetId; playerType["GetConnectionId"] = &Player::GetConnectionId; playerType["GetCharacter"] = &Player::GetCharacter; - playerType["GetParty"] = &Player::GetParty; + playerType["GetParty"] = [](Player& aSelf) -> PartyComponent& { return aSelf.GetParty(); }; playerType["GetUsername"] = &Player::GetUsername; playerType["GetEndPoint"] = &Player::GetEndPoint; playerType["GetDiscordId"] = &Player::GetDiscordId; diff --git a/Code/server/Services/CommandService.cpp b/Code/server/Services/CommandService.cpp index 7bb66ce86..527e780a5 100644 --- a/Code/server/Services/CommandService.cpp +++ b/Code/server/Services/CommandService.cpp @@ -9,6 +9,13 @@ #include #include +#include + +namespace +{ +Console::Setting bAnnounceServer{"LiveServices:bAnnounceServer", "Whether to list the server on the public server list", false}; +} + CommandService::CommandService(World& aWorld, entt::dispatcher& aDispatcher) noexcept : m_world(aWorld) { @@ -22,7 +29,7 @@ void CommandService::OnSetTimeCommand(const PacketEvent& const auto cPlayerId = static_cast(acMessage.Packet.PlayerId); - // Only set time if player is an admin + // Admin override: always allowed for (const auto session : GameServer::Get()->GetAdminSessions()) { if (PlayerManager::Get()->GetByConnectionId(session)->GetId() == cPlayerId) @@ -39,6 +46,21 @@ void CommandService::OnSetTimeCommand(const PacketEvent& } } + // Party leader allowed on private servers only + const auto* pPartyService = &m_world.GetPartyService(); + if (pPartyService->IsPlayerLeader(acMessage.pPlayer) && !bAnnounceServer) + { + const auto cHours = static_cast(acMessage.Packet.Hours); + const auto cMinutes = static_cast(acMessage.Packet.Minutes); + + m_world.GetCalendarService().SetTime(cHours, cMinutes, m_world.GetCalendarService().GetTimeScale()); + + response.Result = NotifySetTimeResult::SetTimeResult::kSuccess; + acMessage.pPlayer->Send(response); + + return; + } + response.Result = NotifySetTimeResult::SetTimeResult::kNoPermission; acMessage.pPlayer->Send(response); } diff --git a/Code/server/Services/ObjectService.cpp b/Code/server/Services/ObjectService.cpp index 2bd7d154f..028c198c7 100644 --- a/Code/server/Services/ObjectService.cpp +++ b/Code/server/Services/ObjectService.cpp @@ -178,6 +178,9 @@ void ObjectService::OnScriptAnimationRequest(const PacketEventSend(message); } } diff --git a/Code/server/Services/PartyService.cpp b/Code/server/Services/PartyService.cpp index deabdd974..7dce6c51a 100644 --- a/Code/server/Services/PartyService.cpp +++ b/Code/server/Services/PartyService.cpp @@ -53,13 +53,13 @@ bool PartyService::IsPlayerInParty(Player* const apPlayer) const noexcept return apPlayer->GetParty().JoinedPartyId.has_value(); } -bool PartyService::IsPlayerLeader(Player* const apPlayer) noexcept +bool PartyService::IsPlayerLeader(const Player* const apPlayer) const noexcept { - auto& inviterPartyComponent = apPlayer->GetParty(); + const auto& inviterPartyComponent = apPlayer->GetParty(); if (inviterPartyComponent.JoinedPartyId) { - Party& party = m_parties[*inviterPartyComponent.JoinedPartyId]; - return party.LeaderPlayerId == apPlayer->GetId(); + if (const auto* const pParty = GetById(*inviterPartyComponent.JoinedPartyId)) + return pParty->LeaderPlayerId == apPlayer->GetId(); } return false; diff --git a/Code/server/Services/PartyService.h b/Code/server/Services/PartyService.h index fbb0f7f7d..e1803ca19 100644 --- a/Code/server/Services/PartyService.h +++ b/Code/server/Services/PartyService.h @@ -33,7 +33,7 @@ struct PartyService const Party* GetById(uint32_t aId) const noexcept; bool IsPlayerInParty(Player* const apPlayer) const noexcept; - bool IsPlayerLeader(Player* const apPlayer) noexcept; + bool IsPlayerLeader(const Player* const apPlayer) const noexcept; Party* GetPlayerParty(Player* const apPlayer) noexcept; protected: 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..712f2c59a 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 @@ -12,9 +12,6 @@ {{ 'COMPONENT.PARTY_LIST.TABLE_HEADERS.LEVEL' | transloco }} {{ 'COMPONENT.PARTY_LIST.TABLE_HEADERS.NAME' | transloco }} - - {{ 'COMPONENT.PARTY_LIST.TABLE_HEADERS.LOCATION' | transloco }} - @@ -22,7 +19,6 @@ {{ player.level }} {{ player.name }} - {{ player.cellName }}