diff --git a/Code/client/CrashHandler.cpp b/Code/client/CrashHandler.cpp index dadaa4adf..dfe707e3f 100644 --- a/Code/client/CrashHandler.cpp +++ b/Code/client/CrashHandler.cpp @@ -22,7 +22,9 @@ std::string SerializeTimePoint(const time_point& time, const std::string& format LONG WINAPI VectoredExceptionHandler(PEXCEPTION_POINTERS pExceptionInfo) { - if (pExceptionInfo->ExceptionRecord->ExceptionCode == 0xC0000005) + static int alreadycrashed = 0; + + if (pExceptionInfo->ExceptionRecord->ExceptionCode == 0xC0000005 && alreadycrashed++ == 0) { spdlog::error("Crash occurred!"); MINIDUMP_EXCEPTION_INFORMATION M; diff --git a/Code/client/Games/Primitives.h b/Code/client/Games/Primitives.h index 91a71021a..d5ed65064 100644 --- a/Code/client/Games/Primitives.h +++ b/Code/client/Games/Primitives.h @@ -101,6 +101,17 @@ template struct GameList Entry entry; inline bool Empty() const noexcept { return entry.data == nullptr; } + inline size_t Size() const noexcept + { + if (entry.data == nullptr) + return 0ULL; + + size_t size = 0; + for (const Entry* current = &entry; current; current = current->next) + size++; + + return size; + } // Range for loop compatibility struct Iterator diff --git a/Code/client/Games/Skyrim/Actor.cpp b/Code/client/Games/Skyrim/Actor.cpp index a199b4507..ad6207a01 100644 --- a/Code/client/Games/Skyrim/Actor.cpp +++ b/Code/client/Games/Skyrim/Actor.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -44,6 +45,8 @@ #include #include #include +#include +#include #ifdef SAVE_STUFF @@ -191,6 +194,48 @@ TESForm* Actor::GetEquippedAmmo() const noexcept return nullptr; } +bool Actor::IsWearingBodyPiece() const noexcept +{ + return GetContainerChanges()->GetArmor(32) != nullptr; +} + +bool Actor::ShouldWearBodyPiece() const noexcept +{ + TESNPC* pBase = Cast(baseForm); + if (!pBase) + return false; + + BGSOutfit* pDefaultOutfit = pBase->outfits[0]; + if (!pDefaultOutfit) + return false; + + for (auto* pItem : pDefaultOutfit->outfitItems) + { + TESObjectARMO* pArmor = nullptr; + + if (pItem->formType == FormType::Armor) + pArmor = Cast(pItem); + else if (pItem->formType == FormType::LeveledItem) + { + TESLevItem* pLevItem = Cast(pItem); + if (!pLevItem || !pLevItem->pLeveledListA || !pLevItem->pLeveledListA->pForm) + continue; + + pArmor = Cast(pLevItem->pLeveledListA->pForm); + } + else + continue; + + if (!pArmor) + continue; + + if (pArmor->IsBodyPiece()) + return true; + } + + return false; +} + // Get owner of a summon or raised corpse Actor* Actor::GetCommandingActor() const noexcept { @@ -207,7 +252,7 @@ Actor* Actor::GetCommandingActor() const noexcept // Get owner of a summon or raised corpse void Actor::SetCommandingActor(BSPointerHandle aCommandingActor) noexcept { - if (currentProcess && currentProcess->middleProcess && currentProcess->middleProcess) + if (currentProcess && currentProcess->middleProcess) { currentProcess->middleProcess->commandingActor = aCommandingActor; flags2 |= ActorFlags::IS_COMMANDED_ACTOR; diff --git a/Code/client/Games/Skyrim/Actor.h b/Code/client/Games/Skyrim/Actor.h index a69dff910..24cfd4438 100644 --- a/Code/client/Games/Skyrim/Actor.h +++ b/Code/client/Games/Skyrim/Actor.h @@ -205,6 +205,8 @@ struct Actor : TESObjectREFR [[nodiscard]] Actor* GetCombatTarget() const noexcept; [[nodiscard]] bool HasPerk(uint32_t aPerkFormId) const noexcept; [[nodiscard]] uint8_t GetPerkRank(uint32_t aPerkFormId) const noexcept; + [[nodiscard]] bool IsWearingBodyPiece() const noexcept; + [[nodiscard]] bool ShouldWearBodyPiece() const noexcept; // Setters void SetSpeed(float aSpeed) noexcept; diff --git a/Code/client/Games/Skyrim/EquipManager.cpp b/Code/client/Games/Skyrim/EquipManager.cpp index e9da5d200..f1b53f16f 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()) + if (pExtension->IsLocal() && !apItem->IsConsumable() && !apData->bQueueEquip) { 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()) + if (pExtension->IsLocal() && !ScopedUnequipOverride::IsOverriden() && !apData->bQueueEquip) { 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()) + if (pExtension->IsLocal() && !apData->bQueueEquip) { 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()) + if (pExtension->IsLocal() && !ScopedUnequipOverride::IsOverriden() && !apData->bQueueEquip) { EquipmentChangeEvent evt{}; evt.ActorId = apActor->formID; @@ -265,6 +265,7 @@ void* TP_MAKE_THISCALL(EquipShoutHook, EquipManager, Actor* apActor, TESForm* ap if (pExtension->IsRemote() && !ScopedEquipOverride::IsOverriden()) return nullptr; + // TODO: queue check? if (pExtension->IsLocal()) { EquipmentChangeEvent evt{}; @@ -289,6 +290,7 @@ void* TP_MAKE_THISCALL(UnEquipShoutHook, EquipManager, Actor* apActor, TESForm* if (pExtension->IsRemote() && !ScopedEquipOverride::IsOverriden()) return nullptr; + // TODO: queue check? if (pExtension->IsLocal() && !ScopedUnequipOverride::IsOverriden()) { EquipmentChangeEvent evt{}; diff --git a/Code/client/Games/Skyrim/ExtraData/ExtraContainerChanges.cpp b/Code/client/Games/Skyrim/ExtraData/ExtraContainerChanges.cpp index 6f17b9e9e..ec007e6b1 100644 --- a/Code/client/Games/Skyrim/ExtraData/ExtraContainerChanges.cpp +++ b/Code/client/Games/Skyrim/ExtraData/ExtraContainerChanges.cpp @@ -28,3 +28,12 @@ bool ExtraContainerChanges::Entry::IsQuestObject() noexcept return TiltedPhoques::ThisCall(s_isQuestObject, this); } + +TESObjectARMO* ExtraContainerChanges::Data::GetArmor(uint32_t aSlotId) noexcept +{ + TP_THIS_FUNCTION(TGetArmor, TESObjectARMO*, ExtraContainerChanges::Data, uint32_t); + + POINTER_SKYRIMSE(TGetArmor, s_getArmor, 16113); + + return TiltedPhoques::ThisCall(s_getArmor, this, aSlotId); +} diff --git a/Code/client/Games/Skyrim/ExtraData/ExtraContainerChanges.h b/Code/client/Games/Skyrim/ExtraData/ExtraContainerChanges.h index b851959d7..b7448857e 100644 --- a/Code/client/Games/Skyrim/ExtraData/ExtraContainerChanges.h +++ b/Code/client/Games/Skyrim/ExtraData/ExtraContainerChanges.h @@ -6,6 +6,7 @@ struct BGSLoadFormBuffer; struct BGSSaveFormBuffer; struct TESObjectREFR; struct TESForm; +struct TESObjectARMO; struct ExtraContainerChanges : BSExtraData { @@ -25,6 +26,7 @@ struct ExtraContainerChanges : BSExtraData { void Save(BGSSaveFormBuffer* apBuffer); void Load(BGSLoadFormBuffer* apBuffer); + TESObjectARMO* GetArmor(uint32_t aSlotId) noexcept; GameList* entries; TESObjectREFR* parent; diff --git a/Code/client/Games/Skyrim/Forms/BGSOutfit.h b/Code/client/Games/Skyrim/Forms/BGSOutfit.h index d4df8a487..cfbfc111d 100644 --- a/Code/client/Games/Skyrim/Forms/BGSOutfit.h +++ b/Code/client/Games/Skyrim/Forms/BGSOutfit.h @@ -4,4 +4,5 @@ struct BGSOutfit : TESForm { + GameArray outfitItems; }; diff --git a/Code/client/Games/Skyrim/Forms/TESForm.h b/Code/client/Games/Skyrim/Forms/TESForm.h index 74698014f..9772c7e12 100644 --- a/Code/client/Games/Skyrim/Forms/TESForm.h +++ b/Code/client/Games/Skyrim/Forms/TESForm.h @@ -14,6 +14,7 @@ enum class FormType : uint8_t Npc = 43, LeveledCharacter = 44, Alchemy = 46, + LeveledItem = 53, Character = 62, QuestItem = 77, Count = 0x87 diff --git a/Code/client/Games/Skyrim/Forms/TESLevItem.h b/Code/client/Games/Skyrim/Forms/TESLevItem.h new file mode 100644 index 000000000..3f7c9d1f3 --- /dev/null +++ b/Code/client/Games/Skyrim/Forms/TESLevItem.h @@ -0,0 +1,9 @@ +#pragma once + +#include "TESLeveledList.h" + +struct TESLevItem : TESBoundObject, TESLeveledList +{ +}; + +static_assert(sizeof(TESLevItem) == 0x58); diff --git a/Code/client/Games/Skyrim/Forms/TESLeveledList.h b/Code/client/Games/Skyrim/Forms/TESLeveledList.h new file mode 100644 index 000000000..6512f4300 --- /dev/null +++ b/Code/client/Games/Skyrim/Forms/TESLeveledList.h @@ -0,0 +1,18 @@ +#pragma once + +struct TESLeveledList : BaseFormComponent +{ + struct LEVELED_OBJECT + { + TESForm* pForm; + uint16_t count; + uint16_t level; + uint32_t padC; + void* pItemExtra; + }; + + LEVELED_OBJECT* pLeveledListA; // this array has count at offset -8 + uint8_t unk10[0x28 - 0x10]; +}; + +static_assert(sizeof(TESLeveledList) == 0x28); diff --git a/Code/client/Games/Skyrim/Forms/TESObjectARMO.h b/Code/client/Games/Skyrim/Forms/TESObjectARMO.h index fb2cf1af0..35b098ad0 100644 --- a/Code/client/Games/Skyrim/Forms/TESObjectARMO.h +++ b/Code/client/Games/Skyrim/Forms/TESObjectARMO.h @@ -4,4 +4,11 @@ struct TESObjectARMO : TESForm { + [[nodiscard]] bool IsBodyPiece() const noexcept + { + return (slotType & 0x4) != 0; // 0x4 is flag for body piece + } + + uint8_t unk20[0x1B8 - sizeof(TESForm)]; + uint32_t slotType; }; diff --git a/Code/client/Games/Skyrim/TESObjectREFR.h b/Code/client/Games/Skyrim/TESObjectREFR.h index 8598860b7..6e1d856d6 100644 --- a/Code/client/Games/Skyrim/TESObjectREFR.h +++ b/Code/client/Games/Skyrim/TESObjectREFR.h @@ -134,7 +134,7 @@ struct TESObjectREFR : TESForm virtual void sub_87(); virtual void sub_88(); virtual void DisableImpl(); - virtual void sub_8A(); + virtual void ResetInventory(bool abLeveledOnly); virtual void sub_8B(); virtual void sub_8C(); virtual void sub_8D(); diff --git a/Code/client/Games/TES.h b/Code/client/Games/TES.h index b7d8d611f..00f9162f1 100644 --- a/Code/client/Games/TES.h +++ b/Code/client/Games/TES.h @@ -192,6 +192,7 @@ struct INISettingCollection Entry* next; }; + // TODO(FT): Setting a setting should be followed by SettingHandlerMap::SettingUpdated() call Setting* GetSetting(const char* acpName) noexcept; uint8_t unk0[0x118]; diff --git a/Code/client/Services/Generic/DiscoveryService.cpp b/Code/client/Services/Generic/DiscoveryService.cpp index 7a04bae5e..ac8fa3416 100644 --- a/Code/client/Services/Generic/DiscoveryService.cpp +++ b/Code/client/Services/Generic/DiscoveryService.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include @@ -250,12 +251,54 @@ void DiscoveryService::OnUpdate(const PreUpdateEvent& acUpdateEvent) noexcept void DiscoveryService::OnConnected(const ConnectedEvent& acEvent) noexcept { + // uGridsToLoad should always be 5, as this is what the server enforces + auto* pSetting = INISettingCollection::Get()->GetSetting("uGridsToLoad:General"); + if (pSetting && pSetting->data != 5) + { + ConnectionErrorEvent errorEvent{}; + errorEvent.ErrorDetail = "{\"error\": \"bad_uGridsToLoad\"}"; + + m_world.GetRunner().Trigger(errorEvent); + } + VisitCell(true); } BSTEventResult DiscoveryService::OnEvent(const TESLoadGameEvent*, const EventDispatcher*) { spdlog::info("Finished loading, triggering visit cell"); + + const TiltedPhoques::String defaultModlist[7] = {"Skyrim.esm", "Update.esm", "Dawnguard.esm", + "HearthFires.esm", "Dragonborn.esm", "_ResourcePack.esl", + "SkyrimTogether.esp"}; + + auto& currentModlist = ModManager::Get()->mods; + + bool isModlistEqual = currentModlist.Size() == 7; + + if (isModlistEqual) + { + int i = 0; + for (const auto& currentMod : currentModlist) + { + if (currentMod->filename != defaultModlist[i]) + { + isModlistEqual = false; + break; + } + + i++; + } + } + + if (!isModlistEqual) + { + ConnectionErrorEvent errorEvent{}; + errorEvent.ErrorDetail = "{\"error\": \"non_default_install\"}"; + + m_world.GetRunner().Trigger(errorEvent); + } + VisitCell(true); return BSTEventResult::kOk; diff --git a/Code/client/Services/Generic/InventoryService.cpp b/Code/client/Services/Generic/InventoryService.cpp index a525e5415..02dc51639 100644 --- a/Code/client/Services/Generic/InventoryService.cpp +++ b/Code/client/Services/Generic/InventoryService.cpp @@ -23,6 +23,8 @@ #include #include #include +#include +#include #if TP_FALLOUT4 #include @@ -45,6 +47,7 @@ InventoryService::InventoryService(World& aWorld, entt::dispatcher& aDispatcher, void InventoryService::OnUpdate(const UpdateEvent& acUpdateEvent) noexcept { RunWeaponStateUpdates(); + RunNakedNPCBugChecks(); } void InventoryService::OnInventoryChangeEvent(const InventoryChangeEvent& acEvent) noexcept @@ -170,20 +173,10 @@ void InventoryService::OnNotifyEquipmentChanges(const NotifyEquipmentChanges& ac uint32_t equipSlotId = modSystem.GetGameId(acMessage.EquipSlotId); TESForm* pEquipSlot = TESForm::GetById(equipSlotId); - // TODO: ft, does it have the same problem? #if TP_SKYRIM64 uint32_t slotId = 0; if (pEquipSlot == DefaultObjectManager::Get().rightEquipSlot) slotId = 1; - - // There's a bug where double equipping something magically unequips something secretly. - // TODO: should this be done for armor as well? - // Also, find out why the client is sending two equip messages in the first place. - // TODO: this fix makes it so that weapons and spells are sometimes invisible :/ - #if 0 - if (!acMessage.Unequip && pActor->GetEquippedWeapon(slotId) == pItem) - return; - #endif #endif auto* pEquipManager = EquipManager::Get(); @@ -291,3 +284,49 @@ void InventoryService::RunWeaponStateUpdates() noexcept } } } + +void InventoryService::RunNakedNPCBugChecks() noexcept +{ +#if TP_SKYRIM64 + if (!m_transport.IsConnected()) + return; + + static std::chrono::steady_clock::time_point lastSendTimePoint; + constexpr auto cDelayBetweenUpdates = 1000ms; + + const auto now = std::chrono::steady_clock::now(); + if (now - lastSendTimePoint < cDelayBetweenUpdates) + return; + + lastSendTimePoint = now; + + auto view = m_world.view(); + + for (auto entity : view) + { + const auto& formIdComponent = view.get(entity); + Actor* pActor = Cast(TESForm::GetById(formIdComponent.Id)); + if (!pActor) + continue; + + if (pActor->GetExtension()->IsPlayer()) + continue; + + if (pActor->IsDead()) + continue; + + if (pActor->IsWearingBodyPiece()) + continue; + + if (!pActor->ShouldWearBodyPiece()) + continue; + + // Don't broadcast changes, it'll just make things messier. + // If all clients have this problem, they'll all fix it individually. + ScopedEquipOverride seo; + ScopedInventoryOverride sio; + + pActor->ResetInventory(false); + } +#endif +} diff --git a/Code/client/Services/InventoryService.h b/Code/client/Services/InventoryService.h index cff53bb5f..87235ce85 100644 --- a/Code/client/Services/InventoryService.h +++ b/Code/client/Services/InventoryService.h @@ -56,6 +56,11 @@ struct InventoryService * and if so, send the new states to the server. */ void RunWeaponStateUpdates() noexcept; + /** + * Checks whether an NPC's (local or remote) equipment is bugged (i.e. naked NPCS) + * and resets their inventory. + */ + void RunNakedNPCBugChecks() noexcept; World& m_world; entt::dispatcher& m_dispatcher; diff --git a/Code/components/imgui/ImGuiDriver.cpp b/Code/components/imgui/ImGuiDriver.cpp index e9c2370d1..98951c654 100644 --- a/Code/components/imgui/ImGuiDriver.cpp +++ b/Code/components/imgui/ImGuiDriver.cpp @@ -123,8 +123,11 @@ void ImGuiDriver::Initialize(void* apHandle) // 1920 = // https://github.com/ocornut/imgui/blob/master/docs/FAQ.md#q-how-should-i-handle-dpi-in-my-application auto& io = ImGui::GetIO(); + + auto* extraGlyphRanges = io.Fonts->GetGlyphRangesCyrillic(); // Includes Latin io.Fonts->AddFontFromMemoryCompressedBase85TTF(Roboto_compressed_data_base85, - 20.f * scaleFactor); //->Scale = scaleFactor; + 20.f * scaleFactor, //->Scale = scaleFactor; + nullptr, extraGlyphRanges); ImGui::GetStyle().ScaleAllSizes(scaleFactor); } diff --git a/Code/encoding/Structs/ServerSettings.cpp b/Code/encoding/Structs/ServerSettings.cpp index fe6e522c5..bdf8f77ad 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; + return Difficulty == acRhs.Difficulty && GreetingsEnabled == acRhs.GreetingsEnabled && PvpEnabled == acRhs.PvpEnabled && SyncPlayerHomes == acRhs.SyncPlayerHomes && DeathSystemEnabled == acRhs.DeathSystemEnabled && 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, AutoPartyJoin); } void ServerSettings::Deserialize(TiltedPhoques::Buffer::Reader& aReader) noexcept @@ -31,4 +32,5 @@ void ServerSettings::Deserialize(TiltedPhoques::Buffer::Reader& aReader) noexcep SyncPlayerHomes = Serialization::ReadBool(aReader); DeathSystemEnabled = Serialization::ReadBool(aReader); SyncPlayerCalendar = Serialization::ReadBool(aReader); + AutoPartyJoin = Serialization::ReadBool(aReader); } diff --git a/Code/encoding/Structs/ServerSettings.h b/Code/encoding/Structs/ServerSettings.h index da4e3d798..02a49302f 100644 --- a/Code/encoding/Structs/ServerSettings.h +++ b/Code/encoding/Structs/ServerSettings.h @@ -16,4 +16,5 @@ struct ServerSettings bool SyncPlayerHomes{}; bool DeathSystemEnabled{}; bool SyncPlayerCalendar{}; + bool AutoPartyJoin{}; }; diff --git a/Code/immersive_launcher/oobe/PathSelection.cpp b/Code/immersive_launcher/oobe/PathSelection.cpp index 20aee1b7a..d5428c3e3 100644 --- a/Code/immersive_launcher/oobe/PathSelection.cpp +++ b/Code/immersive_launcher/oobe/PathSelection.cpp @@ -38,7 +38,7 @@ std::optional OpenPathSelectionDialog2(const std::wstring& aPathSu pFileDialog->SetTitle(L"Select the game executable (" TARGET_NAME L".exe)"); - static constinit COMDLG_FILTERSPEC rgSpec[] = {{L"Executables", L"*.exe"}}; + static constinit COMDLG_FILTERSPEC rgSpec[] = {{L"Executables", TARGET_NAME L".exe"}}; pFileDialog->SetFileTypes(1, &rgSpec[0]); // pre select suggested folder & exe diff --git a/Code/immersive_launcher/stubs/FileMapping.cpp b/Code/immersive_launcher/stubs/FileMapping.cpp index 708d428cf..67b4f5d5a 100644 --- a/Code/immersive_launcher/stubs/FileMapping.cpp +++ b/Code/immersive_launcher/stubs/FileMapping.cpp @@ -218,7 +218,15 @@ DWORD WINAPI TP_GetModuleFileNameA(HMODULE aModule, char* alpFileName, DWORD aBu ScopedOSHeapItem wideBuffer((aBufferSize * sizeof(wchar_t)) + 1); wchar_t* pBuffer = static_cast(wideBuffer.m_pBlock); - DWORD result = RealGetModuleFileNameW(aModule, pBuffer, aBufferSize * sizeof(wchar_t)); + + // Under MO2, there's a bug caused when calling RealGetModuleFileNameW on XAudio2_7.dll, + // it crashes in usvfs. This only happens during the thread shutdown paths + // of quitting, so just avoid the bug. + // TODO: Further analysis of what is under MO2 USVFS and what is needed. + DWORD result = 0; + if (aModule != GetModuleHandleW(L"XAudio2_7.dll")) + result = RealGetModuleFileNameW(aModule, pBuffer, aBufferSize * sizeof(wchar_t)); + if (result == 0) { return result; diff --git a/Code/server/GameServer.cpp b/Code/server/GameServer.cpp index 87862c37a..f2350c581 100644 --- a/Code/server/GameServer.cpp +++ b/Code/server/GameServer.cpp @@ -52,6 +52,9 @@ Console::Setting uTimeScale{ Console::Setting bSyncPlayerCalendar{ "Gameplay:bSyncPlayerCalendar", "Syncs up all player calendars to be the same day, month, and year. This uses the date of the player with the furthest ahead date at connection.", false}; +Console::Setting bAutoPartyJoin{ + "Gameplay:bAutoPartyJoin", + "Join parties automatically, as long as there is only one party in the server", true}; // ModPolicy Stuff Console::Setting bEnableModCheck{"ModPolicy:bEnableModCheck", "Bypass the checking of mods on the server", false, Console::SettingsFlags::kLocked}; @@ -142,6 +145,7 @@ ServerSettings GetSettings() settings.SyncPlayerHomes = bSyncPlayerHomes; settings.DeathSystemEnabled = bEnableDeathSystem; settings.SyncPlayerCalendar = bSyncPlayerCalendar; + settings.AutoPartyJoin = bAutoPartyJoin; return settings; } diff --git a/Code/server/Services/PartyService.cpp b/Code/server/Services/PartyService.cpp index 63f325829..b3b525277 100644 --- a/Code/server/Services/PartyService.cpp +++ b/Code/server/Services/PartyService.cpp @@ -19,6 +19,11 @@ #include #include +namespace +{ +Console::Setting bAutoPartyJoin{"Gameplay:bAutoPartyJoin", "Join parties automatically, as long as there is only one party in the server", true}; +} + PartyService::PartyService(World& aWorld, entt::dispatcher& aDispatcher) noexcept : m_world(aWorld) , m_updateEvent(aDispatcher.sink().connect<&PartyService::OnUpdate>(this)) @@ -115,6 +120,22 @@ void PartyService::OnPartyCreate(const PacketEvent& acPacket spdlog::debug("[PartyService]: Created party for {}", player->GetId()); SendPartyJoinedEvent(party, player); + + if (m_parties.size() == 1 && bAutoPartyJoin) + { + for (Player* otherPlayer : m_world.GetPlayerManager()) + { + if (otherPlayer->GetId() != player->GetId()) + { + party.Members.push_back(otherPlayer); + otherPlayer->GetParty().JoinedPartyId = partyId; + + SendPartyJoinedEvent(party, otherPlayer); + } + } + + BroadcastPartyInfo(partyId); + } } } @@ -179,7 +200,7 @@ void PartyService::OnPartyKick(const PacketEvent& acPacket) no } } -void PartyService::OnPlayerJoin(const PlayerJoinEvent& acEvent) const noexcept +void PartyService::OnPlayerJoin(const PlayerJoinEvent& acEvent) noexcept { BroadcastPlayerList(); @@ -195,6 +216,28 @@ void PartyService::OnPlayerJoin(const PlayerJoinEvent& acEvent) const noexcept spdlog::debug("[Party] New notify player {:x} {}", notify.PlayerId, notify.Username.c_str()); GameServer::Get()->SendToPlayers(notify, acEvent.pPlayer); + + if (m_parties.size() == 1 && bAutoPartyJoin) + { + for (Player* player : m_world.GetPlayerManager()) + { + if (IsPlayerInParty(player)) + { + auto& playerPartyComponent = player->GetParty(); + Party& party = m_parties[*playerPartyComponent.JoinedPartyId]; + + party.Members.push_back(acEvent.pPlayer); + acEvent.pPlayer->GetParty().JoinedPartyId = *playerPartyComponent.JoinedPartyId; + + SendPartyJoinedEvent(party, acEvent.pPlayer); + + BroadcastPartyInfo(*playerPartyComponent.JoinedPartyId); + + break; + } + } + + } } void PartyService::OnPartyInvite(const PacketEvent& acPacket) noexcept diff --git a/Code/server/Services/PartyService.h b/Code/server/Services/PartyService.h index 2fab3f8e5..fbb0f7f7d 100644 --- a/Code/server/Services/PartyService.h +++ b/Code/server/Services/PartyService.h @@ -38,7 +38,7 @@ struct PartyService protected: void OnUpdate(const UpdateEvent& acEvent) noexcept; - void OnPlayerJoin(const PlayerJoinEvent& acEvent) const noexcept; + void OnPlayerJoin(const PlayerJoinEvent& acEvent) noexcept; void OnPlayerLeave(const PlayerLeaveEvent& acEvent) noexcept; void OnPartyInvite(const PacketEvent& acPacket) noexcept; void OnPartyAcceptInvite(const PacketEvent& acPacket) noexcept; diff --git a/Code/skyrim_ui/src/app/services/error.service.ts b/Code/skyrim_ui/src/app/services/error.service.ts index 2e4ccf510..fbd331daa 100644 --- a/Code/skyrim_ui/src/app/services/error.service.ts +++ b/Code/skyrim_ui/src/app/services/error.service.ts @@ -10,7 +10,9 @@ export interface ErrorEvent { | 'client_mods_disallowed' | 'wrong_password' | 'server_full' - | 'no_reason'; + | 'no_reason' + | 'bad_uGridsToLoad' + | 'non_default_install'; data?: Record; } diff --git a/Code/skyrim_ui/src/app/transloco-root.module.ts b/Code/skyrim_ui/src/app/transloco-root.module.ts index e94e3a769..1059cb7ff 100644 --- a/Code/skyrim_ui/src/app/transloco-root.module.ts +++ b/Code/skyrim_ui/src/app/transloco-root.module.ts @@ -31,8 +31,11 @@ export class TranslocoHttpLoader implements TranslocoLoader { { id: 'ru', label: 'Русский' }, { id: 'fr', label: 'Français' }, { id: 'zh-CN', label: '中文(中国)' }, + { id: 'ja', label: '日本語' }, { id: 'nl', label: 'Nederlands' }, { id: 'es', label: 'Español' }, + { id: 'ko', label: '한국어' }, + { id: 'tr', label: 'Türkçe' }, { id: 'cs', label: 'Čeština' }, { id: 'pl', label: 'Polski' }, { id: 'overwrite', label: 'Custom' }, diff --git a/Code/skyrim_ui/src/assets/i18n/en.json b/Code/skyrim_ui/src/assets/i18n/en.json index e64cbdd76..68e7594ea 100644 --- a/Code/skyrim_ui/src/assets/i18n/en.json +++ b/Code/skyrim_ui/src/assets/i18n/en.json @@ -165,7 +165,9 @@ "CLIENT_MODS_DISALLOWED": "This server disallows {{mods}}. Please remove them to join.", "WRONG_PASSWORD": "The password you entered is incorrect.", "NO_REASON": "The server refused connection without reason.", - "SERVER_FULL": "This server is full." + "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.", + "NON_DEFAULT_INSTALL": "It seems that your installation is not purely vanilla, i.e. it has Creation Club content (Anniversary Update or otherwise) or other mods.\nWe do NOT recommend playing with mods.\nWe recommend that you uninstall and disable these mods (instructions can be found on the wiki).\n\nFor the best experience, you should have the following mod list:\nSkyrim.esm\nUpdate.esm\nDawnguard.esm\nHearthFires.esm\nDragonborn.esm\n_ResourcePack.esl\nSkyrimTogether.esp" } }, "PLAYER_LIST": { diff --git a/Code/skyrim_ui/src/assets/i18n/es.json b/Code/skyrim_ui/src/assets/i18n/es.json index 4142bf6eb..b141f1133 100644 --- a/Code/skyrim_ui/src/assets/i18n/es.json +++ b/Code/skyrim_ui/src/assets/i18n/es.json @@ -161,6 +161,8 @@ "WRONG_PASSWORD": "La contraseña que has introducido es incorrecta.", "NO_REASON": "El servidor ha rechazado la conexión sin razón.", "SERVER_FULL": "El servidor está lleno." + "BAD_UGRIDSTOLOAD": "Parece que su Skyrim.ini tiene un valor no predeterminado para 'uGridsToLoad'. ESTA ES UNA MUY MALA IDEA y lo más probable es que rompa el mod. Por favor, restaurar esta configuración a su valor predeterminado (5) , o elimine el Skyrim.ini e lanzar Skyrim Vanilla una vez para generar un nuevo archivo. Las configuraciones 'uExterior Cell Buffer' y 'uInterior Cell Buffer' también deben dejarse en la configuración predeterminada. Si no sabe cómo hacerlo. , consulte nuestra wiki o solicite ayuda en nuestro servidor de Discord/subreddit.", + "NON_DEFAULT_INSTALL": "Parece que tu instalación no es puramente básica, en ejemplo, tiene contenido del Creation Club (De aniversario o no) o otras modificaciones.\nNO recomendamos jugar con modificaciones.\nRecomendamos que desinstales y deshabilites estas modificaciones (las instrucciones se pueden encontrar en la wiki).\n\nPara la mejor experiencia, debe tener la siguiente lista de mods:\nSkyrim.esm\nUpdate.esm\nDawnguard.esm\nHearthFires.esm\nDragonborn.esm\n_ResourcePack.esl\nSkyrimTogether.esp" } }, "PLAYER_LIST": { diff --git a/Code/skyrim_ui/src/assets/i18n/ja.json b/Code/skyrim_ui/src/assets/i18n/ja.json new file mode 100644 index 000000000..8727c1040 --- /dev/null +++ b/Code/skyrim_ui/src/assets/i18n/ja.json @@ -0,0 +1,177 @@ +{ + "COMPONENT": { + "CHAT": { + "SEND": "送信", + "MESSAGE": "メッセージ", + "MESSAGE_TOO_LONG": "{{chatMessageLengthLimit}}字以上のメッセージは送信不可能。" + }, + "CONNECT": { + "INFO": { + "SERVER_ADDRESS": "IPv4アドラスを入力してサーバーに入ります。 形式は、 IPアドレス:ポート。 デフォルトのポート番号は10578。", + "HELGEN": "ヘルゲンの脱出チュートリアルが終わってからサーバーに接続してください。脱出する前に接続するとバッグが発生します。" + }, + "ADDRESS": "IPアドレス", + "PASSWORD": "パスワード", + "SAVE_PASSWORD": "パスワードの保存", + "HIDE_PASSWORD": "パスワードを非表示にする", + "CONNECT": "接続", + "CANCEL": "キャンセル", + "PUBLIC_SERVERS": "公開サーバー", + "ERROR": { + "CONNECTION": "接続できませんでした。 IPアドレスとネットワークの安定を確認してから再度お試しください。", + "VERSION_MISMATCH": "サーバーの「Skyrim Together」バージョンが違う為、接続することができません。", + "INVALID_ADDRESS": "アドレスの形式が無効です。" + } + }, + "DISCONNECT": { + "PROMPT": "切断しますか?", + "PROCEED": "はい", + "CANCEL": "いいえ" + }, + "ERROR": { + "OK": "閉じる" + }, + "NOTIFICATIONS": { + "MESSAGE_FROM": "メッセージが届いていますー>", + "ACCEPT": "承認", + "DECLINE": "否認" + }, + "PARTY_MENU": { + "ACCEPT_INVITE": "{{name}}からの招待を承認する", + "LAUNCH_PARTY": "パーティーを組む", + "INVITES": "招待", + "NO_INVITES": "招待はありません。", + "EXPERIENCE_WARNING": "クエストがスムーズに進むよう、気を付けてサイトにある「プレイガイド」をお読みください。 skyrim-together.com. 以下がおおよそのルール:", + "GUIDELINES": { + "PARTY": "まずはパーティーを組むことで、中軸の「ホスト」、いわゆる「パーティーリーダー」になります。", + "PARTY_LEADER_TALK": "このパーティーリーダーだけがクエストを進めるためにNPCと話す。", + "PARTY_LEADER_LOOT_QUESTITEMS": "同じく、このパーティーリーダーだけがクエストアイテムを取る。", + "KEEP_PARTY_LEADER": "プレイする時は1周に1人、同じリーダーにする。", + "ONE_PARTY_PER_SERVER": "1つのサーバーには1つだけのパーティーを組む。" + }, + "NO_PLAYERS_IN_PARTY": "まだ誰もパーティーに入っていないようです。 プレイヤーリストから招待して組んでみましょう!", + "TABLE_HEADERS": { + "LEVEL": "Lv.", + "NAME": "名前", + "LOCATION": "位置" + }, + "ACTIONS": { + "KICK": "キック", + "TELEPORT": "テレポート", + "MAKE_LEADER": "リーダーにする" + }, + "LEAVE_PARTY": "パーティーを抜ける" + }, + "PARTY_LIST": { + "NO_OTHER_PLAYERS": "ニワトリしかいないね。", + "TABLE_HEADERS": { + "LEVEL": "Lv.", + "NAME": "名前", + "LOCATION": "位置" + }, + "INVITE": "招待" + }, + "PLAYER_MANAGER": { + "PLAYER_LIST": "プレイヤーメニュー", + "PARTY_MENU": "パーティーメニュー", + "BACK": "戻る" + }, + "ROOT": { + "CONNECT": "接続", + "DISCONNECT": "切断", + "RECONNECT": "再接続", + "PLAYER_MANAGER": "プレイヤーの管理", + "SETTINGS": "設定", + "CONNECTION_IN_PROGRESS": "接続しています…" + }, + "SERVER_LIST": { + "SEARCH": "検索", + "REFRESH": "リフレッシュ", + "BACK": "戻る", + "LOADING": "読み込んでいます…", + "NO_SERVERS": "公開サーバーはありません。", + "HIDE_MISMATCHED_SERVERS": "一致するバージョンでないサーバーを非表示", + "HIDE_FULL_SERVERS": "満員のサーバーを非表示", + "HIDE_PASSWORD_PROTECTED_SERVERS": "パスワード保護されたサーバーを非表示", + "SERVER_COUNT": "{{count}}の内から{{filteredCount}}を表示しています", + "TABLE_HEADERS": { + "NAME": "サーバーネーム", + "PLAYERS": "プレイヤー", + "COUNTRY": "国", + "FAVORITE": "お気に入り" + }, + "JOIN": "入る" + }, + "SETTINGS": { + "VERSION": { + "HEADER": "バージョン", + "INFO": "現バージョン:", + "OUTDATED": "バージョンが古い。 アップデートしてください。" + }, + "AUDIO": "オーディオ", + "MUTE_UI": "UI消音", + "UI_VOLUME": "UIボリューム", + "PARTY": "パーティー", + "SHOW_PARTY": "パーティーUIの表示", + "AUTO_HIDE_PARTY": "自動的にパーティーUIを非表示にする", + "AUTO_HIDE_PARTY_LENGTH": "自動非表示タイマー", + "PARTY_ANCHOR": "位置", + "PARTY_ANCHOR_POSITION": { + "TOP_LEFT": "左上", + "TOP_RIGHT": "右上", + "BOTTOM_RIGHT": "右下", + "BOTTOM_LEFT": "左下" + }, + "PARTY_ANCHOR_OFFSET_X": "水平オフセット", + "PARTY_ANCHOR_OFFSET_Y": "垂直オフセット", + "LENGTH": "{{length}}", + "UI": "ユーザーインターフェース", + "LANGUAGE": "言語", + "DEBUG": "デバッグ", + "SHOW_DEBUG_INFO": "デバッグ情報の表示", + "FONT_SIZE": "フォントサイズ", + "FONT_SIZES": { + "XS": "最小", + "S": "小", + "M": "普", + "L": "大", + "XL": "最大" + }, + "BACK": "戻る" + } + }, + "SERVICE": { + "CLIENT": { + "CONNECTION_LOST": "サーバーから切断されました。再接続しようとしています… 後{{remainingReconnectionAttempt}}回試します。", + "CONNECTED": "接続しました。", + "DISCONNECTED": "サーバーから切断されました。" + }, + "COMMANDS": { + "AVAILABLE_COMMANDS": "使用できるコマンド: {{cmds}}", + "COMMAND_NOT_FOUND": "'{{cmd}}' はコマンドではありません." + }, + "GROUP": { + "LEVEL_UP": "{{name}}がLv.{{level}}になりました。", + "ALREADY_IN_GROUP": "既にパーティーに入っています。 現在入っているパーティーを抜けてから他のパーティーに入りましょう。", + "KICK_NO_PARTY_LEADER": "パーティーリーダーでない為、他のプレイヤーをキックすることは出来ません。", + "MAKE_LEADER_NO_PARTY_LEADER": "パーティーリーダーでない為、他のプレイヤーをリーダーにすることは出来ません。" + }, + "ERROR": { + "ERRORS": { + "WRONG_VERSION": "サーバーバージョン「{{expectedVersion}}」 とクライアントバージョン「{{version}}」が違います。", + "MODS_MISMATCH": "このサーバーは 「ModPolicy」 がオンに設定してあります。{{mods}}", + "MODS_MISMATCH_REMOVE": "このサーバーに入るには以下のMODSを削除する必要があります:\n{{mods}}", + "MODS_MISMATCH_INSTALL": "このサーバーに入るには以下のMODSをインストールする必要があります:\n{{mods}}", + "CLIENT_MODS_DISALLOWED": "このサーバーでは、  {{mods}} が禁止とされています. 入るには削除する必要があります。", + "WRONG_PASSWORD": "パスワードが間違っています。", + "NO_REASON": "接続出来ませんでした。", + "SERVER_FULL": "このサーバーは満員です。", + "BAD_UGRIDSTOLOAD": "Skyrim.iniの「uGridsToLoad」値がデフォルトではないようです。これはイケマセン。「Skyrim Together」が故障してしまいます。この値をデフォルト(5)に戻すか、Skyrim.iniを削除し1回バニラで起動し新しいファイルをジェネレートしてください。「uExterior Cell Buffer」と「uInterior Cell Buffer」もデフォルトの値のままであったほうがマシです。やり方が分からない場合はSkyrim TogetherのRedditやDiscordをご参考にしてください。", + "NON_DEFAULT_INSTALL": "スカイリムが純粋なバニラではないようです[CCコンテンツなど]。\nMODSを使わなかったほうがマシ。\nAEのCCコンテンツコンテンツも含めて、インストールしたMODSを削除し無効にすることをお勧めします[詳細はWIKIに。]\n\n有効な順番はこの通り:\nSkyrim.esm\nUpdate.esm\nDawnguard.esm\nHearthFires.esm\nDragonborn.esm\n_ResourcePack.esl\nSkyrimTogether.esp" + } + }, + "PLAYER_LIST": { + "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 new file mode 100644 index 000000000..e1fd35c62 --- /dev/null +++ b/Code/skyrim_ui/src/assets/i18n/ko.json @@ -0,0 +1,175 @@ +{ + "COMPONENT": { + "CHAT": { + "SEND": "보내기", + "MESSAGE": "메시지", + "MESSAGE_TOO_LONG": "{{chatMessageLengthLimit}}자를 초과하는 메시지는 보낼 수 없습니다." + }, + "CONNECT": { + "INFO": { + "SERVER_ADDRESS": "전체 주소를 사용하여 서버에 연결합니다. 주소:포트 형식을 사용하세요. 포트의 기본값은 10578입니다.", + "HELGEN": "아직 헬겐 인트로 시퀀스에 있는 경우 서버에 연결하지 마세요. 먼저 헬겐에서 탈출했는지 확인하세요." + }, + "ADDRESS": "주소", + "PASSWORD": "암호", + "SAVE_PASSWORD": "암호 저장 여부?", + "HIDE_PASSWORD": "암호 숨김 여부?", + "CONNECT": "연결", + "CANCEL": "취소", + "PUBLIC_SERVERS": "공개 서버", + "ERROR": { + "CONNECTION": "지정된 서버에 연결할 수 없습니다. 올바른 주소를 입력했는지, 네트워크 문제가 발생하고 있지 않은지 확인하세요", + "VERSION_MISMATCH": "귀하와 동일한 버전의 Skyrim Together가 없기 때문에 서버에 연결할 수 없습니다.", + "INVALID_ADDRESS": "주소 형식이 잘못되었습니다." + } + }, + "DISCONNECT": { + "PROMPT": "정말로 연결을 끊으시겠어요?", + "PROCEED": "진행", + "CANCEL": "취소" + }, + "ERROR": { + "OK": "Ok" + }, + "NOTIFICATIONS": { + "MESSAGE_FROM": "메시지 보낸 사람:", + "ACCEPT": "승인", + "DECLINE": "거절" + }, + "PARTY_MENU": { + "ACCEPT_INVITE": "{{name}}님의 초대 수락", + "LAUNCH_PARTY": "파티 실행", + "INVITES": "초대", + "NO_INVITES": "초대장이 없습니다", + "EXPERIENCE_WARNING": "최적의 퀘스트 경험을 위해서는 우리 웹사이트 skyrim-together.com에서 찾을 수 있는 위키의 플레이 가이드를 꼭 읽어보세요. 다음은 지침을 간략히 요약한 것입니다:", + "GUIDELINES": { + "PARTY": "파티를 만들면 귀하가 일종의 \"호스트\", 즉 파티 리더가 됩니다.", + "PARTY_LEADER_TALK": "파티 리더만이 퀘스트 NPC와 대화하여 퀘스트를 진행할 수 있습니다.", + "PARTY_LEADER_LOOT_QUESTITEMS": "파티 리더만이 퀘스트 아이템을 전리품으로 얻을 수 있습니다.", + "KEEP_PARTY_LEADER": "플레이할 때마다 한 명의 파티 리더를 선택하세요.", + "ONE_PARTY_PER_SERVER": "서버 당 하나의 파티를 고수하세요." + }, + "NO_PLAYERS_IN_PARTY": "아직 파티에 아무도 없습니다. 플레이어 목록을 통해 초대해보세요!", + "TABLE_HEADERS": { + "LEVEL": "레벨", + "NAME": "이름", + "LOCATION": "위치" + }, + "ACTIONS": { + "KICK": "추방", + "TELEPORT": "순간 이동", + "MAKE_LEADER": "리더 임명" + }, + "LEAVE_PARTY": "파티에서 탈퇴" + }, + "PARTY_LIST": { + "NO_OTHER_PLAYERS": "여기엔 우리 닭들 외에는 아무도 없어", + "TABLE_HEADERS": { + "LEVEL": "레벨", + "NAME": "이름", + "LOCATION": "위치" + }, + "INVITE": "초대" + }, + "PLAYER_MANAGER": { + "PLAYER_LIST": "플레이어 목록", + "PARTY_MENU": "파티 메뉴", + "BACK": "뒤로" + }, + "ROOT": { + "CONNECT": "연결", + "DISCONNECT": "연결 해제", + "RECONNECT": "재연결", + "PLAYER_MANAGER": "플레이어 관리자", + "SETTINGS": "설정", + "CONNECTION_IN_PROGRESS": "연결 진행중..." + }, + "SERVER_LIST": { + "SEARCH": "검색...", + "REFRESH": "새로 고침", + "BACK": "뒤로", + "LOADING": "로드 중...", + "NO_SERVERS": "사용 가능한 공용 서버가 없습니다", + "HIDE_MISMATCHED_SERVERS": "호환되지 않는 버전의 서버 숨기기", + "HIDE_FULL_SERVERS": "만석 서버 숨기기", + "HIDE_PASSWORD_PROTECTED_SERVERS": "비밀번호로 보호된 서버 숨기기", + "SERVER_COUNT": "서버 {{count}}대 중 {{filteredCount}}대 표시 중", + "TABLE_HEADERS": { + "NAME": "서버 이름", + "PLAYERS": "플레이어 수 / 최대 수.", + "COUNTRY": "지역", + "FAVORITE": "즐겨찾기" + }, + "JOIN": "참가" + }, + "SETTINGS": { + "VERSION": { + "HEADER": "버전", + "INFO": "현재 설치된 버전:", + "OUTDATED": "버전이 오래되었습니다. 업데이트하세요." + }, + "AUDIO": "오디오", + "MUTE_UI": "사용자 인터페이스 음소거", + "UI_VOLUME": "사용자 인터페이스 음량", + "PARTY": "파티", + "SHOW_PARTY": "파티 표시", + "AUTO_HIDE_PARTY": "파티 자동 숨기기", + "AUTO_HIDE_PARTY_LENGTH": "자동 숨기기 타이머 크기", + "PARTY_ANCHOR": "위치", + "PARTY_ANCHOR_POSITION": { + "TOP_LEFT": "상단 왼쪽", + "TOP_RIGHT": "상단 오른쪽", + "BOTTOM_RIGHT": "하단 오른쪽", + "BOTTOM_LEFT": "하단 왼쪽" + }, + "PARTY_ANCHOR_OFFSET_X": "수평 오프셋", + "PARTY_ANCHOR_OFFSET_Y": "수직 오프셋", + "LENGTH": "{{length}}초", + "UI": "사용자 인터페이스", + "LANGUAGE": "언어", + "DEBUG": "디버그", + "SHOW_DEBUG_INFO": "디버그 정보 표시", + "FONT_SIZE": "글꼴 크기", + "FONT_SIZES": { + "XS": "매우 작게", + "S": "작게", + "M": "중간", + "L": "크게", + "XL": "매우 크게" + }, + "BACK": "뒤로" + } + }, + "SERVICE": { + "CLIENT": { + "CONNECTION_LOST": "연결이 끊어졌습니다. 다시 연결을 시도 중입니다. {{remainingReconnectionAttempt}}회 시도가 남았습니다.", + "CONNECTED": "서버에 성공적으로 연결되었습니다.", + "DISCONNECTED": "서버와의 연결이 끊어졌습니다." + }, + "COMMANDS": { + "AVAILABLE_COMMANDS": "다음과 같은 대화 명령어가 사용 가능합니다: {{cmds}}", + "COMMAND_NOT_FOUND": "'{{cmd}}'은(는) 명령어로 인식되지 않습니다." + }, + "GROUP": { + "LEVEL_UP": "{{name}}님이 {{level}} 레벨에 도달했습니다.", + "ALREADY_IN_GROUP": "귀하는 이미 그룹에 속해 있습니다. 다른 그룹에 참가하려면 현재 그룹을 탈퇴하세요.", + "KICK_NO_PARTY_LEADER": "귀하는 파티 리더가 아니므로 다른 멤버를 추방할 수 없습니다.", + "MAKE_LEADER_NO_PARTY_LEADER": "귀하는 파티 리더가 아니므로 다른 멤버를 리더로 임명할 수 없습니다." + }, + "ERROR": { + "ERRORS": { + "WRONG_VERSION": "이 서버는 {{expectedVersion}} 버전을 예상하지만 현재 버전은 {{version}}입니다.", + "MODS_MISMATCH": "이 서버에는 ModPolicy가 활성화되어 있습니다. {{mods}}", + "MODS_MISMATCH_REMOVE": "참가하려면 다음 모드를 제거하세요:\n{{mods}}", + "MODS_MISMATCH_INSTALL": "참가하려면 다음 모드를 설치하세요:\n{{mods}}", + "CLIENT_MODS_DISALLOWED": "이 서버는 {{mods}}를 허용하지 않습니다. 가입하려면 해당 항목을 삭제하세요.", + "WRONG_PASSWORD": "입력한 암호가 올바르지 않습니다.", + "NO_REASON": "서버가 이유 없이 연결을 거부했습니다.", + "SERVER_FULL": "이 서버는 만석입니다." + } + }, + "PLAYER_LIST": { + "PARTY_INVITE": "{{from}}님이 귀하를 파티에 초대했습니다" + } + } +} diff --git a/Code/skyrim_ui/src/assets/i18n/ru.json b/Code/skyrim_ui/src/assets/i18n/ru.json index bb775fb20..4263c4f3f 100644 --- a/Code/skyrim_ui/src/assets/i18n/ru.json +++ b/Code/skyrim_ui/src/assets/i18n/ru.json @@ -3,7 +3,7 @@ "CHAT": { "SEND": "Отправить", "MESSAGE": "Сообщение", - "MESSAGE_TOO_LONG": "Нельзя отправлять сообщение длиной более {{chatMessageLengthLimit}} символов." + "MESSAGE_TOO_LONG": "Нельзя отправить сообщение длиной более {{chatMessageLengthLimit}} символов." }, "CONNECT": { "INFO": { @@ -41,9 +41,9 @@ "LAUNCH_PARTY": "Создать группу", "INVITES": "Приглашения", "NO_INVITES": "Вас никто не пригласил.", - "EXPERIENCE_WARNING": "Для получения наилучшего опыта от прохождения игры, следуйте рекомендациям, которые вы можете найти на официальном сайте: skyrim-together.com или В НЕОФИЦИАЛЬНОМ РУ-сообществе: vk.com/skyrim_multiplayer Вот краткий список основных рекомендаций:", + "EXPERIENCE_WARNING": "Для получения наилучшего опыта от прохождения игры, следуйте рекомендациям, которые вы можете найти на официальном сайте: skyrim-together.com или в неофициальном русскоязычном сообществе ВКонтакте Skyrim Together: Reborn Вот краткий список основных рекомендаций:", "GUIDELINES": { - "PARTY": "Создавая группу, вы становитесь \"хозяином\" мира, то есть лидером группы.", + "PARTY": "Создавая группу, вы становитесь \"хостом\" мира, то есть лидером группы.", "PARTY_LEADER_TALK": "Только лидер группы должен разговаривать с NPC для выполнения квестов.", "PARTY_LEADER_LOOT_QUESTITEMS": "Только лидер группы должен подбирать квестовые предметы.", "KEEP_PARTY_LEADER": "Придерживайтесь одного лидера группы в каждом прохождении игры.", @@ -108,7 +108,7 @@ "INFO": "Сейчас установлена:", "OUTDATED": "Версия устарела. Пожалуйста, обновите клиент." }, - "AUDIO": "Аудио", + "AUDIO": "Звук", "MUTE_UI": "Заглушить интерфейс", "UI_VOLUME": "Громкость интерфейса", "PARTY": "Группа", @@ -165,11 +165,13 @@ "CLIENT_MODS_DISALLOWED": "Этот сервер запрещает использование {{mods}}. Пожалуйста, удалите их, чтобы присоединиться.", "WRONG_PASSWORD": "Введён неверный пароль.", "NO_REASON": "Сервер отказал в подключении без причины.", - "SERVER_FULL": "Сервер переполнен." + "SERVER_FULL": "Сервер переполнен.", + "BAD_UGRIDSTOLOAD": "Похоже, что в вашем Skyrim.ini установлено нестандартное значение для параметра 'uGridsToLoad'. ЭТО ОЧЕНЬ ПЛОХО и, скорее всего, нарушит работу модификации. Пожалуйста, установите этот параметр в значение по умолчанию (5) или удалите файл Skyrim.ini и запустите обычный Skyrim один раз (без модов), чтобы создался новый файл. Параметры 'uExterior Cell Buffer' и 'uInterior Cell Buffer' также должны иметь стандартное значение. Если вы не знаете, как это сделать, обратитесь в наше сообщество ВКонтакте Skyrim Together: Reborn или задайте вопрос на официальных серверах Discord/Reddit модификации.", + "NON_DEFAULT_INSTALL": "Похоже, что установленный Skyrim не является 'чистым' (ванильным), т.е. в игре присутствует контент Creation Club (Anniversary Update или другой) или другие моды.\nМы НЕ рекомендуем играть с модами.\nМы рекомендуем вам удалить и отключить эти моды (инструкции можно найти на официальном вики модификации или в сообществе ВКонтакте Skyrim Together: Reborn).\n\nДля получения наилучших впечатлений у вас должен быть следующий список модов:\nSkyrim.esm\nUpdate.esm\nDawnguard.esm\nHearthFires.esm\nDragonborn.esm\n_ResourcePack.esl\nSkyrimTogether.esp" } }, "PLAYER_LIST": { "PARTY_INVITE": "{{from}} предложил вам присоединиться к группе" } } -} \ No newline at end of file +} diff --git a/Code/skyrim_ui/src/assets/i18n/tr.json b/Code/skyrim_ui/src/assets/i18n/tr.json new file mode 100644 index 000000000..fa45ee874 --- /dev/null +++ b/Code/skyrim_ui/src/assets/i18n/tr.json @@ -0,0 +1,175 @@ +{ + "COMPONENT": { + "CHAT": { + "SEND": "Gönder", + "MESSAGE": "Mesaj", + "MESSAGE_TOO_LONG": "{{chatMessageLengthLimit}} harften daha uzun bir mesaj gönderemezsin." + }, + "CONNECT": { + "INFO": { + "SERVER_ADDRESS": "Sunucunun tam adresine bağlanın. address:port Formatını kullanın. Varsayılan port 10578.", + "HELGEN": "Helgen introsunu bitirmediyseniz hiçbir sunucuya BAĞLANMAYIN. İlk öne Helgen'den kaçtığınıza emin olun." + }, + "ADDRESS": "Adres", + "PASSWORD": "Şifre", + "SAVE_PASSWORD": "Şifreyi Kaydet?", + "HIDE_PASSWORD": "Şifreyi Gizle?", + "CONNECT": "Bağlan", + "CANCEL": "İptal", + "PUBLIC_SERVERS": "Herkese Açık Sunucular", + "ERROR": { + "CONNECTION": "Belirtilen sunucuya bağlanılamadı. Lütfen doğru adresi girdiğinize ve bağlantı sorununuzun olmadığına emin olun.", + "VERSION_MISMATCH": "Sunucuya bağlanamazsınız çünkü aynı Skyrim Together versiyonlarına sahip değilsiniz.", + "INVALID_ADDRESS": "Adres geçersiz formatta." + } + }, + "DISCONNECT": { + "PROMPT": "Bağlantıyı kesmek istediğinize emin misiniz??", + "PROCEED": "Devam et", + "CANCEL": "İptal" + }, + "ERROR": { + "OK": "Tamam" + }, + "NOTIFICATIONS": { + "MESSAGE_FROM": "Gelen mesaj", + "ACCEPT": "Kabul Et", + "DECLINE": "Reddet" + }, + "PARTY_MENU": { + "ACCEPT_INVITE": "{{name}} oyuncusundan parti daveti", + "LAUNCH_PARTY": "Partiyi başlat", + "INVITES": "Davetler", + "NO_INVITES": "Hiç davetiniz yok", + "EXPERIENCE_WARNING": "Optimal bir quest deneyimi için, wikimiz'deki oynama rehberimize bakınız, skyrim-together.com. Rehberin kısa bir özeti:", + "GUIDELINES": { + "PARTY": "Parti oluşturduğunuzda, \"host\" olurdunuz, parti lideri olarak da bilinir.", + "PARTY_LEADER_TALK": "Quest'te ilerlemek için sadece parti liderleri quest NPCleri ile konuşabilir.", + "PARTY_LEADER_LOOT_QUESTITEMS": "Sadece parti lideri quest eşyalarını alabilir.", + "KEEP_PARTY_LEADER": "Oynanış başına bir parti liderine uyun.", + "ONE_PARTY_PER_SERVER": "Sunucu başına bir parti liderine uyun." + }, + "NO_PLAYERS_IN_PARTY": "Partinde kimse yok. Oyuncu listesinden davet ediniz!", + "TABLE_HEADERS": { + "LEVEL": "Lv.", + "NAME": "İsim", + "LOCATION": "Konum" + }, + "ACTIONS": { + "KICK": "At", + "TELEPORT": "Işınla", + "MAKE_LEADER": "Lider yap" + }, + "LEAVE_PARTY": "Partiden çık" + }, + "PARTY_LIST": { + "NO_OTHER_PLAYERS": "Kimsecikler yok.", + "TABLE_HEADERS": { + "LEVEL": "Lv.", + "NAME": "İsim", + "LOCATION": "Konum" + }, + "INVITE": "Davet Et" + }, + "PLAYER_MANAGER": { + "PLAYER_LIST": "Oyuncu listesi", + "PARTY_MENU": "Parti Menüsü", + "BACK": "Geri" + }, + "ROOT": { + "CONNECT": "Bağlan", + "DISCONNECT": "Bağlantıyı Kopar", + "RECONNECT": "Geri Bağlan", + "PLAYER_MANAGER": "Oyuncu Yöneticisi", + "SETTINGS": "Ayarlar", + "CONNECTION_IN_PROGRESS": "Bağlanılıyor..." + }, + "SERVER_LIST": { + "SEARCH": "Ara...", + "REFRESH": "Yenile", + "BACK": "Geri", + "LOADING": "Yükleniyor...", + "NO_SERVERS": "Hiçbir Herkese Açık Sunucu Mevcut Değil", + "HIDE_MISMATCHED_SERVERS": "Uyumsuz sürümlü sunucuları gizle", + "HIDE_FULL_SERVERS": "Full sunucuları gizle", + "HIDE_PASSWORD_PROTECTED_SERVERS": "Şifreli sunucuları gizle", + "SERVER_COUNT": "{{count}} sunucudan {{filteredCount}} tanesi gösteriliyor", + "TABLE_HEADERS": { + "NAME": "Sunucu ismi", + "PLAYERS": "Oyuncular / Max.", + "COUNTRY": "Ülke", + "FAVORITE": "Favori" + }, + "JOIN": "Katıl" + }, + "SETTINGS": { + "VERSION": { + "HEADER": "Versiyon", + "INFO": "Şuan yüklü:", + "OUTDATED": "Eski versiyon. Lütfen güncelleyiniz" + }, + "AUDIO": "Ses", + "MUTE_UI": "Arayüzü sustur", + "UI_VOLUME": "Araüz sesi", + "PARTY": "Party", + "SHOW_PARTY": "Partiyi göster", + "AUTO_HIDE_PARTY": "Partiyi otomatik gizle", + "AUTO_HIDE_PARTY_LENGTH": "Otomatik zamanlayıcıyı gizle", + "PARTY_ANCHOR": "Pozisyon", + "PARTY_ANCHOR_POSITION": { + "TOP_LEFT": "Sol üst", + "TOP_RIGHT": "Sağ üst", + "BOTTOM_RIGHT": "Sağ alt", + "BOTTOM_LEFT": "Sol alt" + }, + "PARTY_ANCHOR_OFFSET_X": "Yatay Denge", + "PARTY_ANCHOR_OFFSET_Y": "Dikey Denge", + "LENGTH": "{{length}}s", + "UI": "Arayüz", + "LANGUAGE": "Dil", + "DEBUG": "Hata Ayıklama", + "SHOW_DEBUG_INFO": "Hata Ayıklama Bilgisini Göster", + "FONT_SIZE": "Yazı boyutu", + "FONT_SIZES": { + "XS": "Ekstra Küçük", + "S": "Küçük", + "M": "Orta", + "L": "Büyük", + "XL": "Ekstra Büyük" + }, + "BACK": "Geri" + } + }, + "SERVICE": { + "CLIENT": { + "CONNECTION_LOST": "Bağlantı kaybedildi, geri bağlanmaya çalışılıyor. {{remainingReconnectionAttempt}} deneme kaldı.", + "CONNECTED": "Sunucuya bağlanıldı.", + "DISCONNECTED": "Sunucudan kopuldu.." + }, + "COMMANDS": { + "AVAILABLE_COMMANDS": "Mevcut chat komutları: {{cmds}}", + "COMMAND_NOT_FOUND": "'{{cmd}}' geçerli bir komut değil." + }, + "GROUP": { + "LEVEL_UP": "{{name}} {{level}} oldu.", + "ALREADY_IN_GROUP": "Zaten bir gruptasın. Başka bir gruba katılmak için lütfen şuankinden çıkın.", + "KICK_NO_PARTY_LEADER": "Parti lideri olmadığın için diğer oyuncuları atamazsın.", + "MAKE_LEADER_NO_PARTY_LEADER": "Parti lideri olmadığın için diğer oyuncuları parti lideri yapamazsın." + }, + "ERROR": { + "ERRORS": { + "WRONG_VERSION": "Bu sunucu {{expectedVersion}} versiyonu ama sizdeki {{version}}.", + "MODS_MISMATCH": "Sunucuda ModPolicy aktif.{{mods}}", + "MODS_MISMATCH_REMOVE": "Katılmak için lütfen bu modları silin:\n{{mods}}", + "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ış.", + "NO_REASON": "Sunucu sebep vermeden bağlantıyı reddetti.", + "SERVER_FULL": "Sunucu Dolu." + } + }, + "PLAYER_LIST": { + "PARTY_INVITE": "{{from}} parti daveti attı!" + } + } +} diff --git a/Code/tests/encoding.cpp b/Code/tests/encoding.cpp index 2570e2afe..bd35f5160 100644 --- a/Code/tests/encoding.cpp +++ b/Code/tests/encoding.cpp @@ -481,8 +481,8 @@ TEST_CASE("StringCache", "[encoding.string_cache]") SECTION("Messages") { StringCacheUpdate update; - update.Values[0] = "Hello"; - update.Values[1] = "Bye"; + update.Values.push_back("Hello"); + update.Values.push_back("Bye"); Buffer buff(1000); Buffer::Writer writer(&buff); diff --git a/GameFiles/Skyrim/scripts/C00TrainerScript.pex b/GameFiles/Skyrim/scripts/C00TrainerScript.pex new file mode 100644 index 000000000..a89d363b4 Binary files /dev/null and b/GameFiles/Skyrim/scripts/C00TrainerScript.pex differ diff --git a/GameFiles/Skyrim/scripts/source/C00TrainerScript.psc b/GameFiles/Skyrim/scripts/source/C00TrainerScript.psc new file mode 100644 index 000000000..a52afdab1 --- /dev/null +++ b/GameFiles/Skyrim/scripts/source/C00TrainerScript.psc @@ -0,0 +1,55 @@ +Scriptname C00TrainerScript extends ReferenceAlias + +int numHits = 0 + +Event OnMagicEffectApply(ObjectReference akCaster, MagicEffect akEffect) +; Debug.Trace("C00: Vilkas hit by magic.") + if (Game.GetPlayer() == akCaster) + GetOwningQuest().SetStage(100) + endif +EndEvent + +Event OnEnterBleedout() + BleedoutChecks() +EndEvent + +Function BleedoutChecks() + if (!GetOwningQuest().IsRunning()) + return + endif +; Debug.Trace("C00: Vilkas reached bleedout.") + int currStage = GetOwningQuest().GetStage() + if (currStage != 100 && currStage != 110) ; don't let it through if he's trying to + ; razz you about using magic, for instance + GetOwningQuest().SetStage(125) + endif +EndFunction + +Event OnHit(ObjectReference akAggressor, Form akSource, Projectile akProjectile, bool abPowerAttack, bool abSneakAttack, bool abBashAttack, bool abHitBlocked) +; Debug.Trace("C00: Vilkas was hit.") +; Debug.Trace("C00: Vilkas hit datums -- " + akAggressor + " " + akSource + " " + akProjectile + " " + abPowerAttack + " " + abSneakAttack + " " + abBashAttack + " " + abHitBlocked) + if (akSource as Spell) +; Debug.Trace("C00: Vilkas hit by spell; bailing out and handling it in the magic effect handler.") + return + elseif (akSource as Explosion) +; Debug.Trace("C00: Vilkas hit by explosion; bailing out and handling it in the other handlers.") + return + endif +; if (Game.GetPlayer() == akAggressor) ; The original script didn't account for remote STR bystanders not hitting + if ((akSource as Spell) != None) + GetOwningQuest().SetStage(100) + return + endif + numHits += 1 + if (numHits >= 3) + GetOwningQuest().SetStage(150) + endif +; else; Removing this part lets STR remote or pacifist players get the same quest stage and stopcombat at 3 hits +; ; someone else hit him, stop the quest and have him berate you +; GetOwningQuest().SetStage(110) +; endif; No STR methods needed to reach stopcombat solution suggested in tiltedphoques/TiltedEvolution/issues/598 +EndEvent + +Function ResetHits() + numHits = 0 +EndFunction