diff --git a/.github/workflows/pr-ubuntu-docker.yml b/.github/workflows/pr-ubuntu-docker.yml index 12f5dcb404..7073767f5b 100644 --- a/.github/workflows/pr-ubuntu-docker.yml +++ b/.github/workflows/pr-ubuntu-docker.yml @@ -66,6 +66,7 @@ jobs: chown -R skymp:skymp /src /home/skymp/.cmake-js - name: CMake Configure + id: cmake_configure uses: addnab/docker-run-action@v3 with: image: ${{ env.SKYMP_VCPKG_DEPS_IMAGE }} @@ -73,12 +74,25 @@ jobs: -v ${{github.workspace}}:/src -v ${{github.workspace}}/.cmake-js:/home/skymp/.cmake-js -u skymp + --name configure_container run: | cd /src \ && ./build.sh --configure \ -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \ -DUNIT_DATA_DIR="/src/skyrim_data_files" + - name: Copy log file from container to workspace + if: failure() + run: | + sudo docker cp configure_container:/src/vcpkg/buildtrees/rsm-bsa/install-x64-linux-dbg-out.log ${{github.workspace}}/install-x64-linux-dbg-out.log + + - name: Upload vcpkg failure logs + if: failure() + uses: actions/upload-artifact@v2 + with: + name: install-x64-linux-dbg-out.log + path: ${{github.workspace}}/install-x64-linux-dbg-out.log + - name: Upload compile_commands.json uses: actions/upload-artifact@v3 with: diff --git a/.linelint.yml b/.linelint.yml index 3ca16a0aa1..cbbc338913 100644 --- a/.linelint.yml +++ b/.linelint.yml @@ -2,6 +2,7 @@ ignore: - .git/ - '**/third_party' - 'skymp5-scripts/' + - 'overlay_ports/rsm-bsa/*.patch' rules: end-of-file: diff --git a/CMakeLists.txt b/CMakeLists.txt index 043048c73c..accb68ca7c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,7 +12,12 @@ endif() set(VCPKG_OVERLAY_TRIPLETS "${CMAKE_CURRENT_LIST_DIR}/overlay_triplets") set(VCPKG_OVERLAY_PORTS "${CMAKE_CURRENT_LIST_DIR}/overlay_ports") -set(VCPKG_INSTALL_OPTIONS --no-print-usage --clean-after-build) + +if("$ENV{CI}" STREQUAL "true") + set(VCPKG_INSTALL_OPTIONS --no-print-usage) +else() + set(VCPKG_INSTALL_OPTIONS --no-print-usage --clean-after-build) +endif() if("$ENV{CI}" STREQUAL "true" AND WIN32) # The same submodule but moved to a larger disk in Windows CI. See action files: diff --git a/cmake/link_vcpkg_dependencies.cmake b/cmake/link_vcpkg_dependencies.cmake index b7c36c5fb8..94f0b4c332 100644 --- a/cmake/link_vcpkg_dependencies.cmake +++ b/cmake/link_vcpkg_dependencies.cmake @@ -57,5 +57,8 @@ function(link_vcpkg_dependencies) find_package(OpenSSL REQUIRED) target_link_libraries(${target} PUBLIC OpenSSL::SSL OpenSSL::Crypto) + + find_package(bsa CONFIG REQUIRED) + target_link_libraries(${target} PUBLIC bsa::bsa) endforeach() endfunction() diff --git a/docs/docs_server_configuration_reference.md b/docs/docs_server_configuration_reference.md index 99ea14c621..f7256f5290 100644 --- a/docs/docs_server_configuration_reference.md +++ b/docs/docs_server_configuration_reference.md @@ -93,6 +93,24 @@ Absolute paths work but aren't accessible via `uiPort`. External tooling wouldn' } ``` +## archives + +Specify BSA archives that will be loaded by the server. + +At this moment, used only for compiled Papyrus scripts. + +Relative/absolute paths work similar to esp/esm. + +```json5 +{ + // ... + "archives": [ + "Skyrim - Misc.bsa" + ] + // ... +} +``` + ## lang The language, the translation of which will be obtained from the string files located in Data/strings diff --git a/docs/release/dev/sp-added-evaluate-lvl-character.md b/docs/release/dev/sp-added-evaluate-lvl-character.md new file mode 100644 index 0000000000..4469b3da68 --- /dev/null +++ b/docs/release/dev/sp-added-evaluate-lvl-character.md @@ -0,0 +1 @@ +Added experimental `TESModPlatform.EvaluateLeveledNpc` native. It is unstable and shouldn't be used in user plugins. This native is required for SkyMP. diff --git a/libespm/include/libespm/ACHR.h b/libespm/include/libespm/ACHR.h index 73b499ea31..e0f03c687e 100644 --- a/libespm/include/libespm/ACHR.h +++ b/libespm/include/libespm/ACHR.h @@ -1,4 +1,5 @@ #pragma once +#include "REFR.h" #include "RecordHeader.h" #pragma pack(push, 1) @@ -11,6 +12,12 @@ class ACHR final : public RecordHeader static constexpr auto kType = "ACHR"; bool StartsDead() const noexcept; + + struct Data : public REFR::Data + { + }; + + Data GetData(CompressedFieldsCache& compressedFieldsCache) const noexcept; }; static_assert(sizeof(ACHR) == sizeof(RecordHeader)); diff --git a/libespm/include/libespm/LVLI.h b/libespm/include/libespm/LVLI.h index f1c9ebc2f2..c157e0b849 100644 --- a/libespm/include/libespm/LVLI.h +++ b/libespm/include/libespm/LVLI.h @@ -1,50 +1,17 @@ #pragma once -#include "RecordHeader.h" +#include "LeveledListBase.h" #pragma pack(push, 1) namespace espm { -class LVLI final : public RecordHeader +class LVLI final : public LeveledListBase { public: static constexpr auto kType = "LVLI"; - - enum LeveledItemFlags - { - AllLevels = 0x01, //(sets it to calculate for all entries < player level, - // choosing randomly from all the entries under) - Each = 0x02, // (sets it to repeat a check every time the list is called - // (if it's called multiple times), otherwise it will use the - // same result for all counts.) - UseAll = 0x04, // (use all entries when the list is called) - SpecialLoot = 0x08, - }; - - struct Entry - { - char type[4] = { 'L', 'V', 'L', 'O' }; - uint16_t dataSize = 0; - uint32_t level = 0; - uint32_t formId = 0; - uint32_t count = 0; - }; - - struct Data - { - const char* editorId = ""; - uint8_t chanceNone = 0; - uint8_t leveledItemFlags = 0; - uint32_t chanceNoneGlobalId = 0; - uint8_t numEntries = 0; - const Entry* entries = nullptr; - }; - - Data GetData(CompressedFieldsCache& compressedFieldsCache) const noexcept; }; static_assert(sizeof(LVLI) == sizeof(RecordHeader)); -static_assert(sizeof(LVLI::Entry) == 18); } diff --git a/libespm/include/libespm/LVLN.h b/libespm/include/libespm/LVLN.h new file mode 100644 index 0000000000..b1d6947144 --- /dev/null +++ b/libespm/include/libespm/LVLN.h @@ -0,0 +1,18 @@ +#pragma once +#include "LeveledListBase.h" + +#pragma pack(push, 1) + +namespace espm { + +class LVLN final : public LeveledListBase +{ +public: + static constexpr auto kType = "LVLN"; +}; + +static_assert(sizeof(LVLN) == sizeof(RecordHeader)); + +} + +#pragma pack(pop) diff --git a/libespm/include/libespm/LeveledListBase.h b/libespm/include/libespm/LeveledListBase.h new file mode 100644 index 0000000000..fd68136d70 --- /dev/null +++ b/libespm/include/libespm/LeveledListBase.h @@ -0,0 +1,49 @@ +#pragma once +#include "RecordHeader.h" + +#pragma pack(push, 1) + +namespace espm { + +class LeveledListBase : public RecordHeader +{ +public: + enum LeveledItemFlags + { + AllLevels = 0x01, //(sets it to calculate for all entries < player level, + // choosing randomly from all the entries under) + Each = 0x02, // (sets it to repeat a check every time the list is called + // (if it's called multiple times), otherwise it will use the + // same result for all counts.) + UseAll = 0x04, // (use all entries when the list is called) + SpecialLoot = 0x08, + }; + + struct Entry + { + char type[4] = { 'L', 'V', 'L', 'O' }; + uint16_t dataSize = 0; + uint32_t level = 0; + uint32_t formId = 0; + uint32_t count = 0; + }; + + struct Data + { + const char* editorId = ""; + uint8_t chanceNone = 0; + uint8_t leveledItemFlags = 0; + uint32_t chanceNoneGlobalId = 0; + uint8_t numEntries = 0; + const Entry* entries = nullptr; + }; + + Data GetData(CompressedFieldsCache& compressedFieldsCache) const noexcept; +}; + +static_assert(sizeof(LeveledListBase) == sizeof(RecordHeader)); +static_assert(sizeof(LeveledListBase::Entry) == 18); + +} + +#pragma pack(pop) diff --git a/libespm/include/libespm/NPC_.h b/libespm/include/libespm/NPC_.h index 39819178d1..d8a8b3a0a0 100644 --- a/libespm/include/libespm/NPC_.h +++ b/libespm/include/libespm/NPC_.h @@ -19,6 +19,44 @@ class NPC_ final : public RecordHeader int8_t rank = 0; }; + enum TemplateFlags : uint16_t + { + // (Destructible Object; Traits tab, including race, gender, height, + // weight, voice type, death item; Sounds tab; Animation tab; Character Gen + // tabs) + UseTraits = 0x01, + // (Stats tab, including level, autocalc, skills, health/magicka/stamina, + // speed, bleedout, class) + UseStats = 0x02, + // (both factions and assigned crime faction) + UseFactions = 0x04, + // (both spells and perks) + UseSpelllist = 0x08, + // (AI Data tab, including aggression/confidence/morality, combat style and + // gift filter) + UseAIData = 0x10, + // (only the basic Packages listed on the AI Packages tab; rest of tab + // controlled by Def Pack List) + UseAIPackages = 0x20, + // Unused? + Unused = 0x40, + // (including name and short name, and flags for Essential, Protected, + // Respawn, Summonable, Simple Actor, and Doesn't affect stealth meter) + UseBaseData = 0x80, + // (Inventory tab, including all outfits and geared-up item -- but not + // death item) + UseInventory = 0x100, + // Scripts + UseScript = 0x200, + // (the dropdown-selected package lists on the AI Packages tab) + UseDefPackList = 0x400, + // (Attack Data tab, including override from behavior graph race, events, + // and data) + UseAttackData = 0x800, + // Keywords + UseKeywords = 0x1000 + }; + struct Data { uint32_t defaultOutfitId = 0; @@ -30,6 +68,7 @@ class NPC_ final : public RecordHeader std::vector factions; bool isEssential = false; + bool isUnique = false; bool isProtected = false; uint32_t race = 0; @@ -37,6 +76,8 @@ class NPC_ final : public RecordHeader int16_t magickaOffset = 0; int16_t staminaOffset = 0; ObjectBounds objectBounds = {}; + uint32_t baseTemplate = 0; + uint16_t templateDataFlags = 0; }; Data GetData(CompressedFieldsCache& compressedFieldsCache) const noexcept; diff --git a/libespm/include/libespm/REFR.h b/libespm/include/libespm/REFR.h index e1c557b97b..994c29caa3 100644 --- a/libespm/include/libespm/REFR.h +++ b/libespm/include/libespm/REFR.h @@ -5,7 +5,7 @@ namespace espm { -class REFR final : public RecordHeader +class REFR : public RecordHeader { public: static constexpr auto kType = "REFR"; @@ -23,6 +23,12 @@ class REFR final : public RecordHeader float rotRadians[3]; }; + struct ActivationParentInfo + { + uint32_t refrId = 0; + float delay = 0.f; + }; + struct Data { uint32_t baseId = 0; @@ -31,6 +37,10 @@ class REFR final : public RecordHeader const DoorTeleport* teleport = nullptr; const float* boundsDiv2 = nullptr; uint32_t count = 0; + uint8_t isParentActivationOnly = 0; + std::vector activationParents; + uint32_t linkedRefKeywordId = 0; + uint32_t linkedRefId = 0; }; Data GetData(CompressedFieldsCache& compressedFieldsCache) const noexcept; diff --git a/libespm/include/libespm/Records.h b/libespm/include/libespm/Records.h index 2710cfa810..12fc118da9 100644 --- a/libespm/include/libespm/Records.h +++ b/libespm/include/libespm/Records.h @@ -16,6 +16,7 @@ #include "KYWD.h" #include "LIGH.h" #include "LVLI.h" +#include "LVLN.h" #include "MGEF.h" #include "NAVM.h" #include "NPC_.h" diff --git a/libespm/src/ACHR.cpp b/libespm/src/ACHR.cpp index c0f36b0937..4bda8ccd93 100644 --- a/libespm/src/ACHR.cpp +++ b/libespm/src/ACHR.cpp @@ -9,4 +9,16 @@ bool ACHR::StartsDead() const noexcept return this->flags & 0x200; } +ACHR::Data ACHR::GetData( + CompressedFieldsCache& compressedFieldsCache) const noexcept +{ + REFR::Data data = + reinterpret_cast(this)->GetData(compressedFieldsCache); + + ACHR::Data res; + static_cast(res) = data; + + return res; +} + } diff --git a/libespm/src/LVLI.cpp b/libespm/src/LeveledListBase.cpp similarity index 91% rename from libespm/src/LVLI.cpp rename to libespm/src/LeveledListBase.cpp index 67f7fa1414..c9d8663e05 100644 --- a/libespm/src/LVLI.cpp +++ b/libespm/src/LeveledListBase.cpp @@ -1,10 +1,10 @@ -#include "libespm/LVLI.h" +#include "libespm/LeveledListBase.h" #include "libespm/RecordHeaderAccess.h" #include namespace espm { -LVLI::Data LVLI::GetData( +LeveledListBase::Data LeveledListBase::GetData( CompressedFieldsCache& compressedFieldsCache) const noexcept { Data result; diff --git a/libespm/src/NPC_.cpp b/libespm/src/NPC_.cpp index 0309ec71e7..7cf9f77cf3 100644 --- a/libespm/src/NPC_.cpp +++ b/libespm/src/NPC_.cpp @@ -24,11 +24,14 @@ NPC_::Data NPC_::GetData( } else if (!std::memcmp(type, "ACBS", 4)) { const uint32_t flags = *reinterpret_cast(data); - result.isEssential = !!(flags & 0x02); + result.isEssential = !!(flags & 0x2); + result.isUnique = !!(flags & 0x20); result.isProtected = !!(flags & 0x800); result.magickaOffset = *reinterpret_cast(data + 4); result.staminaOffset = *reinterpret_cast(data + 6); result.healthOffset = *reinterpret_cast(data + 20); + result.templateDataFlags = + *reinterpret_cast(data + 18); } else if (!std::memcmp(type, "RNAM", 4)) { result.race = *reinterpret_cast(data); @@ -39,6 +42,8 @@ NPC_::Data NPC_::GetData( } } else if (!std::memcmp(type, "SPLO", 4)) { result.spells.emplace(*reinterpret_cast(data)); + } else if (!std::memcmp(type, "TPLT", 4)) { + result.baseTemplate = (*reinterpret_cast(data)); } }, compressedFieldsCache); diff --git a/libespm/src/REFR.cpp b/libespm/src/REFR.cpp index 6b9988379e..ef74689fbe 100644 --- a/libespm/src/REFR.cpp +++ b/libespm/src/REFR.cpp @@ -22,6 +22,20 @@ REFR::Data REFR::GetData( result.boundsDiv2 = reinterpret_cast(data); } else if (!std::memcmp(type, "XCNT", 4)) { result.count = *reinterpret_cast(data); + } else if (!std::memcmp(type, "XAPD", 4)) { + result.isParentActivationOnly = + *reinterpret_cast(data); + } else if (!std::memcmp(type, "XAPR", 4)) { + ActivationParentInfo info; + info = *reinterpret_cast(data); + result.activationParents.push_back(info); + } else if (!std::memcmp(type, "XLKR", 4)) { + [[likely]] if (dataSize == 8) { + result.linkedRefKeywordId = *reinterpret_cast(data); + result.linkedRefId = *reinterpret_cast(data + 4); + } else if (dataSize == 4) { + result.linkedRefId = *reinterpret_cast(data); + } } }, compressedFieldsCache); diff --git a/overlay_ports/rsm-bsa/portfile.cmake b/overlay_ports/rsm-bsa/portfile.cmake new file mode 100644 index 0000000000..f267c0e47b --- /dev/null +++ b/overlay_ports/rsm-bsa/portfile.cmake @@ -0,0 +1,40 @@ +vcpkg_check_linkage(ONLY_STATIC_LIBRARY) +vcpkg_from_github( + OUT_SOURCE_PATH SOURCE_PATH + REPO Ryan-rsm-McKenzie/bsa + REF 4.1.0 + SHA512 c488a4f7cffa59064baafd429cf118a8f8a7b5594a0bd49a0ed468572b37af2e7428a83ad83cc7b13b556744a444cb7b8a4591c7018e49cadb1c5d42ae780f51 + HEAD_REF master + PATCHES + variant-emplace-fix.patch + structural-binding.patch +) + +if (VCPKG_TARGET_IS_LINUX) + message(WARNING "Build ${PORT} requires at least gcc 10.") +endif() + +vcpkg_check_features( + OUT_FEATURE_OPTIONS FEATURE_OPTIONS + FEATURES + xmem BSA_SUPPORT_XMEM +) + +vcpkg_cmake_configure( + SOURCE_PATH "${SOURCE_PATH}" + OPTIONS + -DBUILD_TESTING=OFF + ${FEATURE_OPTIONS} +) +vcpkg_cmake_install() +vcpkg_cmake_config_fixup( + PACKAGE_NAME bsa + CONFIG_PATH "lib/cmake/bsa" +) + +file(REMOVE_RECURSE + ${CURRENT_PACKAGES_DIR}/debug/include + ${CURRENT_PACKAGES_DIR}/debug/share +) + +file(INSTALL "${SOURCE_PATH}/LICENSE" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}" RENAME copyright) diff --git a/overlay_ports/rsm-bsa/structural-binding.patch b/overlay_ports/rsm-bsa/structural-binding.patch new file mode 100644 index 0000000000..555356b0dd --- /dev/null +++ b/overlay_ports/rsm-bsa/structural-binding.patch @@ -0,0 +1,43 @@ +From 9efa9f4853b84c0b9f56c57dcb18df5e71c9ac9b Mon Sep 17 00:00:00 2001 +From: Leonid Pospelov +Date: Sun, 15 Oct 2023 03:32:09 +0600 +Subject: [PATCH] Update tes4.cpp + +--- + src/bsa/tes4.cpp | 8 ++++++-- + 1 file changed, 6 insertions(+), 2 deletions(-) + +diff --git a/src/bsa/tes4.cpp b/src/bsa/tes4.cpp +index 0e98dce..70330d5 100644 +--- a/src/bsa/tes4.cpp ++++ b/src/bsa/tes4.cpp +@@ -486,7 +486,7 @@ namespace bsa::tes4 + } + const std::string_view pview{ a_path }; + +- const auto [stem, extension] = [&]() noexcept ++ const auto p = [&]() noexcept + -> std::pair { + const auto split = pview.find_last_of('.'); + if (split != std::string_view::npos) { +@@ -501,6 +501,8 @@ namespace bsa::tes4 + }; + } + }(); ++ std::string_view stem = p.first; ++ std::string_view extension = p.second; + + if (!stem.empty() && + stem.length() < 260 && +@@ -1074,7 +1076,9 @@ namespace bsa::tes4 + hashing::hash hash; + hash.read(a_in, a_header.endian()); + +- auto [size, offset] = a_in->read(); ++ auto [size_, offset_] = a_in->read(); ++ auto size = size_; ++ auto offset = offset_; + + const detail::restore_point _{ a_in }; + a_in->seek_absolute(offset & ~file::isecondary_archive); + \ No newline at end of file diff --git a/overlay_ports/rsm-bsa/variant-emplace-fix.patch b/overlay_ports/rsm-bsa/variant-emplace-fix.patch new file mode 100644 index 0000000000..50a7f46880 --- /dev/null +++ b/overlay_ports/rsm-bsa/variant-emplace-fix.patch @@ -0,0 +1,23 @@ +From 5a9a5d0854b1fbf92ea0f0c710bacd195a2342b0 Mon Sep 17 00:00:00 2001 +From: Leonid Pospelov +Date: Sun, 15 Oct 2023 02:33:51 +0600 +Subject: [PATCH] Update common.hpp + +--- + include/bsa/detail/common.hpp | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/include/bsa/detail/common.hpp b/include/bsa/detail/common.hpp +index 8998d38..66e449f 100644 +--- a/include/bsa/detail/common.hpp ++++ b/include/bsa/detail/common.hpp +@@ -1074,7 +1074,7 @@ namespace bsa::components + _hash(a_hash) + { + if (a_in.has_file() && a_in.shallow_copy()) { +- _name.emplace(a_name, a_in.file()); ++ _name = name_proxy{ a_name, a_in.file() }; + } else { + if (a_in.deep_copy()) { + _name.emplace(a_name); + \ No newline at end of file diff --git a/overlay_ports/rsm-bsa/vcpkg.json b/overlay_ports/rsm-bsa/vcpkg.json new file mode 100644 index 0000000000..0bf1038e16 --- /dev/null +++ b/overlay_ports/rsm-bsa/vcpkg.json @@ -0,0 +1,33 @@ +{ + "name": "rsm-bsa", + "version-semver": "4.1.0", + "description": "A C++ library for working with the Bethesda archive file format", + "homepage": "https://github.com/Ryan-rsm-McKenzie/bsa", + "documentation": "https://ryan-rsm-mckenzie.github.io/bsa/", + "license": "MIT", + "supports": "!x86 & !osx & !uwp", + "dependencies": [ + "directxtex", + "lz4", + "rsm-binary-io", + "rsm-mmio", + { + "name": "vcpkg-cmake", + "host": true + }, + { + "name": "vcpkg-cmake-config", + "host": true + }, + "zlib" + ], + "features": { + "xmem": { + "description": "Compression support for the xmem codec", + "supports": "windows", + "dependencies": [ + "reproc" + ] + } + } +} diff --git a/papyrus-vm/include/papyrus-vm/Structures.h b/papyrus-vm/include/papyrus-vm/Structures.h index 8bb32fad22..c61c944616 100644 --- a/papyrus-vm/include/papyrus-vm/Structures.h +++ b/papyrus-vm/include/papyrus-vm/Structures.h @@ -31,8 +31,12 @@ class IGameObject bool HasScript(const char* name) const; -private: - std::vector> activePexInstances; +protected: + virtual const std::vector>& + ListActivePexInstances() const = 0; + + virtual void AddScript( + std::shared_ptr sctipt) noexcept = 0; }; enum class FunctionType @@ -100,15 +104,15 @@ struct VarValue static VarValue None() { return VarValue(); } - explicit operator bool() const { return this->CastToBool().data.b; } + explicit operator bool() const { return CastToBool().data.b; } - explicit operator IGameObject*() const { return this->data.id; } + explicit operator IGameObject*() const { return data.id; } - explicit operator int() const { return this->CastToInt().data.i; } + explicit operator int() const { return CastToInt().data.i; } - explicit operator double() const { return this->CastToFloat().data.f; } + explicit operator double() const { return CastToFloat().data.f; } - explicit operator const char*() const { return this->data.string; } + explicit operator const char*() const { return data.string; } std::shared_ptr> pArray; @@ -462,6 +466,16 @@ class ActivePexInstance std::string nameNeedScript, VarValue activeInstanceOwner, const std::shared_ptr& mapForFillProperties); + static VarValue TryCastToBaseClass( + VirtualMachine& vm, const std::string& resultTypeName, + VarValue* scriptToCastOwner, std::vector& locals, + std::vector& outClassesStack); + + static VarValue TryCastMultipleInheritance( + VirtualMachine& vm, const std::string& resultTypeName, + VarValue* scriptToCastOwner, + std::vector& locals); + bool _IsValid = false; std::string childrenName; diff --git a/papyrus-vm/src/papyrus-vm-lib/ActivePexInstance.cpp b/papyrus-vm/src/papyrus-vm-lib/ActivePexInstance.cpp index 46707c412c..b694e9c9e0 100644 --- a/papyrus-vm/src/papyrus-vm-lib/ActivePexInstance.cpp +++ b/papyrus-vm/src/papyrus-vm-lib/ActivePexInstance.cpp @@ -398,6 +398,8 @@ void ActivePexInstance::ExecuteOpCode(ExecutionContext* ctx, uint8_t op, try { if (functionName == nameOnBeginState || functionName == nameOnEndState) { + // TODO: consider using CallMethod here. I'm afraid that this event + // will pollute other scripts attached to an object parentVM->SendEvent(this, functionName.c_str(), argsForCall); break; } else { @@ -405,8 +407,11 @@ void ActivePexInstance::ExecuteOpCode(ExecutionContext* ctx, uint8_t op, auto res = parentVM->CallMethod(nullableGameObject, functionName.c_str(), argsForCall, ctx->stackIdHolder); - if (EnsureCallResultIsSynchronous(res, ctx)) + spdlog::trace("callmethod object={} funcName={} result={}", + object->ToString(), functionName, res.ToString()); + if (EnsureCallResultIsSynchronous(res, ctx)) { *args[2] = res; + } } } catch (std::exception& e) { if (auto handler = parentVM->GetExceptionHandler()) @@ -447,14 +452,39 @@ void ActivePexInstance::ExecuteOpCode(ExecutionContext* ctx, uint8_t op, IsSelfStr(*args[1]) ? activeInstanceOwner : *args[1]); if (!object) object = static_cast(activeInstanceOwner); - if (object && object->activePexInstances.size() > 0) { - auto inst = object->activePexInstances.back(); + if (object && object->ListActivePexInstances().size() > 0) { + auto inst = object->ListActivePexInstances().back(); Object::PropInfo* runProperty = GetProperty(*inst, nameProperty, Object::PropInfo::kFlags_Read); if (runProperty != nullptr) { *args[2] = inst->StartFunction(runProperty->readHandler, argsForCall, ctx->stackIdHolder); + spdlog::trace("propget function called"); + } else { + auto& instProps = inst->sourcePex.fn()->objectTable[0].properties; + auto it = + std::find_if(instProps.begin(), instProps.end(), + [&](const Object::PropInfo& propInfo) { + return !Utils::stricmp(propInfo.name.data(), + nameProperty.data()); + }); + if (it == instProps.end()) { + spdlog::trace("propget do nothing: prop {} not found", + nameProperty); + } else { + VarValue* var = inst->variables->GetVariableByName( + it->autoVarName.data(), *inst->sourcePex.fn()); + if (var) { + *args[2] = *var; + } else { + spdlog::trace("propget do nothing: variable {} not found", + it->autoVarName); + } + } } + spdlog::trace("propget propName={} object={} result={}", + args[0]->ToString(), args[1]->ToString(), + args[2]->ToString()); } } else { throw std::runtime_error( @@ -469,14 +499,39 @@ void ActivePexInstance::ExecuteOpCode(ExecutionContext* ctx, uint8_t op, IsSelfStr(*args[1]) ? activeInstanceOwner : *args[1]); if (!object) object = static_cast(activeInstanceOwner); - if (object && object->activePexInstances.size() > 0) { - auto inst = object->activePexInstances.back(); + if (object && object->ListActivePexInstances().size() > 0) { + auto inst = object->ListActivePexInstances().back(); Object::PropInfo* runProperty = GetProperty(*inst, nameProperty, Object::PropInfo::kFlags_Write); if (runProperty != nullptr) { inst->StartFunction(runProperty->writeHandler, argsForCall, ctx->stackIdHolder); + spdlog::trace("propset function called"); + } else { + auto& instProps = inst->sourcePex.fn()->objectTable[0].properties; + auto it = + std::find_if(instProps.begin(), instProps.end(), + [&](const Object::PropInfo& propInfo) { + return !Utils::stricmp(propInfo.name.data(), + nameProperty.data()); + }); + if (it == instProps.end()) { + spdlog::trace("propset do nothing: prop {} not found", + nameProperty); + } else { + VarValue* var = inst->variables->GetVariableByName( + it->autoVarName.data(), *inst->sourcePex.fn()); + if (var) { + *var = *args[2]; + } else { + spdlog::trace("propset do nothing: variable {} not found", + it->autoVarName); + } + } } + spdlog::trace("propset propName={} object={} result={}", + args[0]->ToString(), args[1]->ToString(), + args[2]->ToString()); } } else { throw std::runtime_error( @@ -674,20 +729,10 @@ VarValue& ActivePexInstance::GetIndentifierValue( if (treatStringsAsIdentifiers && value.GetType() == VarValue::kType_String) { auto& res = GetVariableValueByName(&locals, valueAsString); - if (spdlog::should_log(spdlog::level::trace)) { - spdlog::trace("GetIndentifierValue {}: {} = {}", - this->sourcePex.fn()->source, valueAsString, - res.ToString()); - } return res; } if (value.GetType() == VarValue::kType_Identifier) { auto& res = GetVariableValueByName(&locals, valueAsString); - if (spdlog::should_log(spdlog::level::trace)) { - spdlog::trace("GetIndentifierValue {}: {} = {}", - this->sourcePex.fn()->source, valueAsString, - res.ToString()); - } return res; } } @@ -802,67 +847,120 @@ uint8_t ActivePexInstance::GetArrayTypeByElementType(uint8_t type) return returnType; } -void ActivePexInstance::CastObjectToObject(VarValue* result, - VarValue* scriptToCastOwner, - std::vector& locals) +VarValue ActivePexInstance::TryCastToBaseClass( + VirtualMachine& vm, const std::string& resultTypeName, + VarValue* scriptToCastOwner, std::vector& locals, + std::vector& outClassesStack) { - std::string objectToCastTypeName = scriptToCastOwner->objectType; - const std::string& resultTypeName = result->objectType; + auto object = static_cast(*scriptToCastOwner); + if (!object) { + return VarValue::None(); + } + + std::string scriptName = object->GetParentNativeScript(); + outClassesStack.push_back(scriptName); + while (true) { + if (scriptName.empty()) { + break; + } + + if (!Utils::stricmp(resultTypeName.data(), scriptName.data())) { + return *scriptToCastOwner; + } - if (scriptToCastOwner->GetType() != VarValue::kType_Object || - *scriptToCastOwner == VarValue::None()) { - *result = VarValue::None(); - if (spdlog::should_log(spdlog::level::trace)) { - spdlog::trace("CastObjectToObject {} -> {} (object is null)", - scriptToCastOwner->ToString(), result->ToString()); + // TODO: Test this with attention + // Here is the case when i.e. variable with type 'Form' casts to + // 'ObjectReference' while it's actually an Actor + + auto myScriptPex = vm.GetPexByName(scriptName); + + if (!myScriptPex.fn) { + spdlog::error("Script not found: {}", scriptName); + break; } - return; + + scriptName = myScriptPex.fn()->objectTable[0].parentClassName; + outClassesStack.push_back(scriptName); } - std::vector classesStack; + return VarValue::None(); +} +VarValue ActivePexInstance::TryCastMultipleInheritance( + VirtualMachine& vm, const std::string& resultTypeName, + VarValue* scriptToCastOwner, std::vector& locals) +{ auto object = static_cast(*scriptToCastOwner); - if (object) { - std::string scriptName = object->GetParentNativeScript(); - classesStack.push_back(scriptName); - while (1) { - if (scriptName.empty()) { - break; - } + if (!object) { + return VarValue::None(); + } - if (!Utils::stricmp(resultTypeName.data(), scriptName.data())) { - *result = *scriptToCastOwner; - if (spdlog::should_log(spdlog::level::trace)) { - spdlog::trace("CastObjectToObject {} -> {} (match found: {})", - scriptToCastOwner->ToString(), result->ToString(), - resultTypeName); - } - return; - } + // TODO: support cast to base class in parallel inheritance chain + // i.e. X extends Y, in script Z we cast smth to X while it is Y + // Current code would fail in this case. I guess it'd be better to find such + // cases in game, then implement. + for (auto& script : object->ListActivePexInstances()) { + if (Utils::stricmp(script->GetSourcePexName().data(), + resultTypeName.data()) == 0) { + return script->activeInstanceOwner; + } + } - // TODO: Test this with attention - // Here is the case when i.e. variable with type 'Form' casts to - // 'ObjectReference' while it's actually an Actor + return VarValue::None(); +} - auto myScriptPex = parentVM->GetPexByName(scriptName); +void ActivePexInstance::CastObjectToObject(VarValue* result, + VarValue* scriptToCastOwner, + std::vector& locals) +{ + static const VarValue kNone = VarValue::None(); - if (!myScriptPex.fn) { - spdlog::error("Script not found: {}", scriptName); - break; - } + if (scriptToCastOwner->GetType() != VarValue::kType_Object) { + *result = kNone; + return spdlog::trace( + "CastObjectToObject {} -> {} (object is not an object)", + scriptToCastOwner->ToString(), result->ToString()); + } + + if (*scriptToCastOwner == kNone) { + *result = kNone; + return spdlog::trace("CastObjectToObject {} -> {} (object is None)", + scriptToCastOwner->ToString(), result->ToString()); + } + + const std::string& resultTypeName = result->objectType; - scriptName = myScriptPex.fn()->objectTable[0].parentClassName; - classesStack.push_back(scriptName); + VarValue tmp; + std::vector outClassesStack; + + if (tmp == kNone) { + tmp = TryCastToBaseClass(*parentVM, resultTypeName, scriptToCastOwner, + locals, outClassesStack); + if (tmp != kNone) { + spdlog::trace("CastObjectToObject {} -> {} (base class found: {})", + scriptToCastOwner->ToString(), tmp.ToString(), + resultTypeName); } } - *result = VarValue::None(); - if (spdlog::should_log(spdlog::level::trace)) { + if (tmp == kNone) { + tmp = TryCastMultipleInheritance(*parentVM, resultTypeName, + scriptToCastOwner, locals); + if (tmp != kNone) { + spdlog::trace( + "CastObjectToObject {} -> {} (multiple inheritance found: {})", + scriptToCastOwner->ToString(), tmp.ToString(), resultTypeName); + } + } + + if (tmp == kNone) { spdlog::trace( "CastObjectToObject {} -> {} (match not found, wanted {}, stack is {})", - scriptToCastOwner->ToString(), result->ToString(), resultTypeName, - fmt::join(classesStack, ", ")); + scriptToCastOwner->ToString(), tmp.ToString(), resultTypeName, + fmt::join(outClassesStack, ", ")); } + + *result = tmp; } bool ActivePexInstance::HasParent(ActivePexInstance* script, diff --git a/papyrus-vm/src/papyrus-vm-lib/IGameObject.cpp b/papyrus-vm/src/papyrus-vm-lib/IGameObject.cpp index 436f5b0a4e..55283f0daa 100644 --- a/papyrus-vm/src/papyrus-vm-lib/IGameObject.cpp +++ b/papyrus-vm/src/papyrus-vm-lib/IGameObject.cpp @@ -3,7 +3,7 @@ bool IGameObject::HasScript(const char* scriptName) const { - for (auto& instance : activePexInstances) { + for (auto& instance : ListActivePexInstances()) { const std::string& sourcePexName = instance->GetSourcePexName(); if (!Utils::stricmp(sourcePexName.data(), scriptName)) { return true; diff --git a/papyrus-vm/src/papyrus-vm-lib/VarValue.cpp b/papyrus-vm/src/papyrus-vm-lib/VarValue.cpp index da1788dad9..5693d96f50 100644 --- a/papyrus-vm/src/papyrus-vm-lib/VarValue.cpp +++ b/papyrus-vm/src/papyrus-vm-lib/VarValue.cpp @@ -574,6 +574,7 @@ VarValue& VarValue::operator=(const VarValue& arg2) data.string = stringHolder->data(); } else { stringHolder.reset(); + // data.string ptr is copied by 'data = arg2.data;' line in this case } return *this; diff --git a/papyrus-vm/src/papyrus-vm-lib/VirtualMachine.cpp b/papyrus-vm/src/papyrus-vm-lib/VirtualMachine.cpp index fe6496a1ce..1d11d6720f 100644 --- a/papyrus-vm/src/papyrus-vm-lib/VirtualMachine.cpp +++ b/papyrus-vm/src/papyrus-vm-lib/VirtualMachine.cpp @@ -80,7 +80,9 @@ void VirtualMachine::AddObject(std::shared_ptr self, } } - self->activePexInstances = scriptsForObject; + for (auto& script : scriptsForObject) { + self->AddScript(script); + } gameObjectsHolder.insert(self); } @@ -89,7 +91,7 @@ void VirtualMachine::SendEvent(std::shared_ptr self, const std::vector& arguments, OnEnter enter) { - for (auto& scriptInstance : self->activePexInstances) { + for (auto& scriptInstance : self->ListActivePexInstances()) { auto name = scriptInstance->GetActiveStateName(); auto fn = scriptInstance->GetFunctionByName( @@ -150,23 +152,7 @@ VarValue VirtualMachine::CallMethod( return VarValue::None(); } - const char* nativeClass = selfObj->GetParentNativeScript(); - const char* base = nativeClass; - while (1) { - if (auto f = nativeFunctions[ToLower(base)][ToLower(methodName)]) { - auto self = VarValue(selfObj); - self.SetMetaStackIdHolder(stackIdHolder); - return f(self, arguments); - } - auto it = allLoadedScripts.find(base); - if (it == allLoadedScripts.end()) - break; - base = it->second.fn()->objectTable[0].parentClassName.data(); - if (!base[0]) - break; - } - - for (auto& activeScript : selfObj->activePexInstances) { + for (auto& activeScript : selfObj->ListActivePexInstances()) { FunctionInfo functionInfo; if (!Utils::stricmp(methodName, "GotoState") || @@ -185,6 +171,24 @@ VarValue VirtualMachine::CallMethod( } } + // natives have to be after non-natives (Bethesda overrides native functions + // in some scripts) + const char* nativeClass = selfObj->GetParentNativeScript(); + const char* base = nativeClass; + while (1) { + if (auto f = nativeFunctions[ToLower(base)][ToLower(methodName)]) { + auto self = VarValue(selfObj); + self.SetMetaStackIdHolder(stackIdHolder); + return f(self, arguments); + } + auto it = allLoadedScripts.find(base); + if (it == allLoadedScripts.end()) + break; + base = it->second.fn()->objectTable[0].parentClassName.data(); + if (!base[0]) + break; + } + std::string e = "Method not found - '"; e += base; e += (base[0] ? "." : "") + std::string(methodName) + "'"; diff --git a/skymp5-client/src/extensions/objectReferenceEx.ts b/skymp5-client/src/extensions/objectReferenceEx.ts index 72f59adce5..62d8f1c78a 100644 --- a/skymp5-client/src/extensions/objectReferenceEx.ts +++ b/skymp5-client/src/extensions/objectReferenceEx.ts @@ -34,8 +34,13 @@ export class ObjectReferenceEx { const t = base.getType(); const isItem = FormTypeEx.isItem(t); + // BlackFallsBarrow02, door isn't opening via SetOpen so we're hacking it. + // Not blocking activation & asking parent to activate until will be in the correct state + // See also modelApplyUtils.ts + const caveGSecretDoor01 = 0x6f703; + // You can also block for t === FormType.Flora || t === FormType.Tree, but I don't think it's necessary. - if (t === FormType.Container || isItem || t === FormType.NPC || t === FormType.Door) { + if (t === FormType.Container || isItem || t === FormType.NPC || (t === FormType.Door && self.getBaseObject()?.getFormID() !== caveGSecretDoor01)) { self.blockActivation(true); } else { self.blockActivation(false); diff --git a/skymp5-client/src/modelSource/model.ts b/skymp5-client/src/modelSource/model.ts index eb3c0f6701..64bd679eb7 100644 --- a/skymp5-client/src/modelSource/model.ts +++ b/skymp5-client/src/modelSource/model.ts @@ -19,6 +19,8 @@ export interface FormModel { inventory?: Inventory; isHostedByOther?: boolean; isDead?: boolean; + templateChain?: number[]; + lastAnimation?: string; // Assigned locally isMyClone?: boolean; diff --git a/skymp5-client/src/modelSource/remoteServer.ts b/skymp5-client/src/modelSource/remoteServer.ts index 97bf0f19c6..996453ee2e 100644 --- a/skymp5-client/src/modelSource/remoteServer.ts +++ b/skymp5-client/src/modelSource/remoteServer.ts @@ -289,6 +289,19 @@ export class RemoteServer implements MsgHandler, ModelSource, SendTarget { refr, !!msg.props['isHarvested'], ); + const animation = msg.props.lastAnimation; + if (typeof animation === "string") { + const refrid = refr.getFormID(); + + (async () => { + for (let i = 0; i < 5; i ++) { + // retry. pillars in bleakfalls are not reliable for some reason + let res2 = ObjectReference.from(Game.getFormEx(refrid))?.playAnimation(animation); + if (res2) break; + await Utility.wait(2); + } + })(); + } } } else { printConsole('Failed to apply model to', refrId.toString(16)); @@ -303,6 +316,7 @@ export class RemoteServer implements MsgHandler, ModelSource, SendTarget { if (this.worldModel.forms.length <= i) this.worldModel.forms.length = i + 1; let movement: Movement = null as unknown as Movement; + // TODO: better check if it is an npc (not an object reference) if ((msg.refrId as number) >= 0xff000000) { movement = { pos: msg.transform.pos, @@ -704,7 +718,7 @@ export class RemoteServer implements MsgHandler, ModelSource, SendTarget { // TODO: emit event instead of sending directly to avoid type cast and dependency on network module this.send(message as unknown as Record, true); }) - .catch((e) => printConsole('!!! SpSnippet failed', e)); + .catch((e) => printConsole('!!! SpSnippet ' + msg.class + ' ' + msg.function + ' failed', e)); }); } diff --git a/skymp5-client/src/services/services/blockPapyrusEventsService.ts b/skymp5-client/src/services/services/blockPapyrusEventsService.ts index cfd8e74561..0643559d8e 100644 --- a/skymp5-client/src/services/services/blockPapyrusEventsService.ts +++ b/skymp5-client/src/services/services/blockPapyrusEventsService.ts @@ -8,9 +8,7 @@ export class BlockPapyrusEventsService extends ClientListener { } private onceTick() { - if (typeof this.sp.blockPapyrusEvents === "function") { - this.sp.blockPapyrusEvents(true); - } + this.sp.blockPapyrusEvents(true); } private onceUpdate() { diff --git a/skymp5-client/src/services/services/sendInputsService.ts b/skymp5-client/src/services/services/sendInputsService.ts index c0e8172621..a378cc0592 100644 --- a/skymp5-client/src/services/services/sendInputsService.ts +++ b/skymp5-client/src/services/services/sendInputsService.ts @@ -49,7 +49,7 @@ export class SendInputsService extends ClientListener { this.controller.emitter.emit("sendMessage", { message: { t: MsgType.OnEquip, baseId: event.baseObj.getFormID() }, - reliability: "unreliable" + reliability: "unreliable" }); } } diff --git a/skymp5-client/src/spSnippet.ts b/skymp5-client/src/spSnippet.ts index 4e93a3f2df..2b39d8adbe 100644 --- a/skymp5-client/src/spSnippet.ts +++ b/skymp5-client/src/spSnippet.ts @@ -32,7 +32,7 @@ const runMethod = async (snippet: Snippet): Promise => { const selfCasted = spAny[snippet.class].from(self); if (!selfCasted) throw new Error( - `Form ${selfId.toString(16)} is not instance of ${snippet.class}` + `Form ${selfId.toString(16)} is not instance of ${snippet.class}, form type is ${self.getType()}` ); const f = selfCasted[snippet.function]; return await f.apply( diff --git a/skymp5-client/src/view/formView.ts b/skymp5-client/src/view/formView.ts index b3880d1f63..cac3873d5b 100644 --- a/skymp5-client/src/view/formView.ts +++ b/skymp5-client/src/view/formView.ts @@ -54,6 +54,13 @@ export class FormView implements View { } } + // Don't spawn dead actors if not already + if (model.isDead) { + if (this.refrId === 0) { + return; + } + } + // Players with different worldOrCell should be invisible if (model.movement) { const worldOrCell = ObjectReferenceEx.getWorldOrCell(Game.getPlayer() as Actor); @@ -93,16 +100,32 @@ export class FormView implements View { } } } else { - const base = - Game.getFormEx(+(model.baseId as number)) || - Game.getFormEx(this.getAppearanceBasedBase()); - if (!base) return; + let templateChain = model.templateChain; + + // There is no place for random/leveling in 1-sized chain + // Just spawn an NPC, do not generate a temporary TESNPC form + if (templateChain?.length === 1) { + templateChain = undefined; + } + + // TODO: getLeveledBase crashes too often ATM + let base = null; //Game.getFormEx(this.getLeveledBase(templateChain)); + if (base === null) base = Game.getFormEx(model.baseId || NaN); + if (base === null) base = Game.getFormEx(this.getAppearanceBasedBase()); + if (base === null) return; let refr = ObjectReference.from(Game.getFormEx(this.refrId)); - const respawnRequired = - !refr || - !refr.getBaseObject() || - (refr.getBaseObject() as Form).getFormID() !== base.getFormID(); + + let respawnRequired = false; + if (!refr) { + respawnRequired = true; + } + else if (!refr.getBaseObject()) { + respawnRequired = true; + } + else if ((refr.getBaseObject() as Form).getFormID() !== base.getFormID()) { + respawnRequired = true; + } if (respawnRequired) { this.destroy(); @@ -112,6 +135,7 @@ export class FormView implements View { true, true ) as ObjectReference; + this.state = {}; delete this.wasHostedByOther; if (base.getType() !== FormType.NPC) { @@ -121,17 +145,48 @@ export class FormView implements View { model.movement?.rot[2] || 0 ); } + else { + const race = Actor.from(refr)?.getRace()?.getFormID(); + const draugrRace = 0xd53; + const falmerRace = 0x131f4; + const chaurusRace = 0x131eb; + const frostbiteSpiderRaceGiant = 0x4e507; + const frostbiteSpiderRaceLarge = 0x53477; + const dwarvenCenturionRace = 0x131f1; + const dwarvenSphereRace = 0x131f2; + const dwarvenSpiderRace = 0x131f3; + + // potential masterambushscript + if (race === draugrRace + || race === falmerRace + || race === chaurusRace + || race === frostbiteSpiderRaceGiant + || race === frostbiteSpiderRaceLarge + || race === dwarvenCenturionRace + || race === dwarvenSphereRace + || race === dwarvenSpiderRace) { + Actor.from(refr)?.setActorValue("Aggression", 2); + } + } modWcProtection(refr.getFormID(), 1); // TODO: reset all states? this.eqState = this.getDefaultEquipState(); this.ready = false; + + let spawnPos; + if (model.movement) { + spawnPos = model.movement.pos; + // printConsole("Spawn NPC at movement.pos"); + } + else { + spawnPos = ObjectReferenceEx.getPos(Game.getPlayer() as Actor); + printConsole("Spawn NPC at player pos"); + } new SpawnProcess( this.appearanceState.appearance, - model.movement - ? model.movement.pos - : ObjectReferenceEx.getPos(Game.getPlayer() as Actor), + spawnPos, refr.getFormID(), () => { this.ready = true; @@ -228,20 +283,13 @@ export class FormView implements View { let alreadyHosted = false; if (Array.isArray(hosted)) { const remoteId = localIdToRemoteId(this.refrId); - - if (hosted.includes(remoteId) || hosted.includes(remoteId + 0x100000000)) { + + if (hosted.includes(remoteId) || hosted.includes(remoteId + 0x100000000)) { alreadyHosted = true; } - // printConsole("remoteId=", remoteId.toString(16), "hosted=", hosted.map(x => x.toString(16))); } setDefaultAnimsDisabled(this.refrId, alreadyHosted ? false : true); - // if (model.baseId === 0x7 || !model.baseId) { - // setDefaultAnimsDisabled(this.refrId, true); - // } - // else { - // setDefaultAnimsDisabled(this.refrId, false); - // } if (alreadyHosted) { Actor.from(refr)?.clearKeepOffsetFromActor(); } @@ -300,7 +348,7 @@ export class FormView implements View { let alreadyHosted = false; if (Array.isArray(hosted)) { const remoteId = localIdToRemoteId(ac.getFormID()); - if (hosted.includes(remoteId)) { + if (hosted.includes(remoteId) || hosted.includes(remoteId + 0x100000000)) { alreadyHosted = true; } } @@ -436,6 +484,22 @@ export class FormView implements View { return this.appearanceBasedBaseId; } + private getLeveledBase(templateChain: number[] | undefined): number { + if (templateChain === undefined) return 0; + + const str = templateChain.join(','); + + if (this.leveledBaseId === 0) { + const leveledBase = TESModPlatform.evaluateLeveledNpc(str); + if (!leveledBase) { + printConsole("Failed to evaluate leveled npc", str); + } + this.leveledBaseId = leveledBase?.getFormID() || 0; + } + + return this.leveledBaseId; + } + private getDefaultEquipState() { return { lastNumChanges: 0, lastEqMoment: 0 }; }; @@ -480,6 +544,7 @@ export class FormView implements View { private appearanceState = this.getDefaultAppearanceState(); private eqState = this.getDefaultEquipState(); private appearanceBasedBaseId = 0; + private leveledBaseId = 0; private isOnScreen = false; private lastPcWorldOrCell = 0; private lastWorldOrCell = 0; diff --git a/skymp5-client/src/view/modelApplyUtils.ts b/skymp5-client/src/view/modelApplyUtils.ts index 15b8af8d22..816b38b843 100644 --- a/skymp5-client/src/view/modelApplyUtils.ts +++ b/skymp5-client/src/view/modelApplyUtils.ts @@ -7,11 +7,31 @@ export class ModelApplyUtils { static applyModelInventory(refr: ObjectReference, inventory: Inventory) { applyInventory(refr, inventory, false, true); } - + static applyModelIsOpen(refr: ObjectReference, isOpen: boolean) { refr.setOpen(isOpen); + + // See also objectReferenceEx.ts + const caveGSecretDoor01 = 0x6f703; + + // TODO: add more activators to support more cells + const parentActivatorId = 0x460ca; + + if (refr.getBaseObject()?.getFormID() === caveGSecretDoor01) { + const openOrOpening = [1, 2].includes(refr.getOpenState()); + if (openOrOpening) { + if (!isOpen) { + refr.activate(ObjectReference.from(Game.getForm(parentActivatorId)), false); + } + } + if (!openOrOpening) { + if (isOpen) { + refr.activate(ObjectReference.from(Game.getForm(parentActivatorId)), false); + } + } + } } - + static applyModelIsHarvested(refr: ObjectReference, isHarvested: boolean) { const base = refr.getBaseObject(); if (base) { diff --git a/skymp5-client/webpack.config.js b/skymp5-client/webpack.config.js index cb32e4be07..1b5497ba9f 100644 --- a/skymp5-client/webpack.config.js +++ b/skymp5-client/webpack.config.js @@ -9,6 +9,8 @@ module.exports = { entry: [ "./src/index.ts" ], + // SkyrimPlatform ignores embedded source maps at this moment + // devtool: "inline-source-map", devtool: false, output: { path: outDirPath, diff --git a/skymp5-scripts/pex/ActiveMagicEffect.pex b/skymp5-scripts/pex/ActiveMagicEffect.pex index 0904cb6fcf..3e36ce0a66 100644 Binary files a/skymp5-scripts/pex/ActiveMagicEffect.pex and b/skymp5-scripts/pex/ActiveMagicEffect.pex differ diff --git a/skymp5-server/cpp/addon/PapyrusUtils.h b/skymp5-server/cpp/addon/PapyrusUtils.h index 865ce994c8..9408877ddd 100644 --- a/skymp5-server/cpp/addon/PapyrusUtils.h +++ b/skymp5-server/cpp/addon/PapyrusUtils.h @@ -1,10 +1,10 @@ #pragma once -#include "EspmGameObject.h" #include "FormDesc.h" -#include "MpFormGameObject.h" #include "NapiHelper.h" #include "WorldState.h" #include "papyrus-vm/Structures.h" +#include "script_objects/EspmGameObject.h" +#include "script_objects/MpFormGameObject.h" #include #include diff --git a/skymp5-server/cpp/addon/ScampServer.cpp b/skymp5-server/cpp/addon/ScampServer.cpp index e016e7e485..a526962ff4 100644 --- a/skymp5-server/cpp/addon/ScampServer.cpp +++ b/skymp5-server/cpp/addon/ScampServer.cpp @@ -1,25 +1,21 @@ #include "ScampServer.h" -#include "AsyncSaveStorage.h" #include "Bot.h" -#include "EspmGameObject.h" -#include "FileDatabase.h" #include "FormCallbacks.h" #include "GamemodeApi.h" -#include "MigrationDatabase.h" -#include "MongoDatabase.h" -#include "MpFormGameObject.h" #include "NapiHelper.h" #include "NetworkingCombined.h" #include "PacketHistoryWrapper.h" #include "PapyrusUtils.h" #include "ScampServerListener.h" -#include "ScriptStorage.h" -#include "SettingsUtils.h" +#include "database_drivers/DatabaseFactory.h" #include "formulas/SweetPieDamageFormula.h" #include "formulas/TES5DamageFormula.h" #include "libespm/IterateFields.h" #include "property_bindings/PropertyBindingFactory.h" +#include "save_storages/SaveStorageFactory.h" +#include "script_objects/EspmGameObject.h" +#include "script_storages/ScriptStorageFactory.h" #include #include #include @@ -37,12 +33,6 @@ std::shared_ptr& GetLogger() return g_logger; } -std::shared_ptr CreateSaveStorage( - std::shared_ptr db, std::shared_ptr logger) -{ - return std::make_shared(db, logger); -} - std::string GetPropertyAlphabet() { std::string alphabet; @@ -306,13 +296,8 @@ ScampServer::ScampServer(const Napi::CallbackInfo& info) partOne->SetDamageFormula(std::make_unique()); } - std::vector> scriptStorages; - scriptStorages.push_back(std::make_shared( - (espm::fs::path(dataDir) / "scripts").string())); - scriptStorages.push_back(std::make_shared()); - auto scriptStorage = - std::make_shared(scriptStorages); - partOne->worldState.AttachScriptStorage(scriptStorage); + partOne->worldState.AttachScriptStorage( + ScriptStorageFactory::Create(serverSettings)); partOne->AttachEspm(espm); partOne->animationSystem.Init(&partOne->worldState); @@ -351,8 +336,9 @@ ScampServer::ScampServer(const Napi::CallbackInfo& info) Napi::Value ScampServer::AttachSaveStorage(const Napi::CallbackInfo& info) { try { - partOne->AttachSaveStorage(CreateSaveStorage( - SettingsUtils::CreateDatabase(serverSettings, logger), logger)); + auto db = DatabaseFactory::Create(serverSettings, logger); + auto saveStorage = SaveStorageFactory::Create(db, logger); + partOne->AttachSaveStorage(saveStorage); } catch (std::exception& e) { throw Napi::Error::New(info.Env(), (std::string)e.what()); } diff --git a/skymp5-server/cpp/addon/SettingsUtils.h b/skymp5-server/cpp/addon/SettingsUtils.h deleted file mode 100644 index 98ef6755ca..0000000000 --- a/skymp5-server/cpp/addon/SettingsUtils.h +++ /dev/null @@ -1,48 +0,0 @@ -#include -#include -#include - -#include "FileDatabase.h" -#include "MigrationDatabase.h" -#include "MongoDatabase.h" - -class SettingsUtils -{ -public: - std::shared_ptr static CreateDatabase( - nlohmann::json settings, std::shared_ptr logger) - { - auto databaseDriver = settings.count("databaseDriver") - ? settings["databaseDriver"].get() - : std::string("file"); - - if (databaseDriver == "file") { - auto databaseName = settings.count("databaseName") - ? settings["databaseName"].get() - : std::string("world"); - - logger->info("Using file with name '" + databaseName + "'"); - return std::make_shared(databaseName, logger); - } - - if (databaseDriver == "mongodb") { - auto databaseName = settings.count("databaseName") - ? settings["databaseName"].get() - : std::string("db"); - - auto databaseUri = settings["databaseUri"].get(); - logger->info("Using mongodb with name '" + databaseName + "'"); - return std::make_shared(databaseUri, databaseName); - } - - if (databaseDriver == "migration") { - auto from = settings.at("databaseOld"); - auto to = settings.at("databaseNew"); - auto oldDatabase = CreateDatabase(from, logger); - auto newDatabase = CreateDatabase(to, logger); - return std::make_shared(newDatabase, oldDatabase); - } - - throw std::runtime_error("Unrecognized databaseDriver: " + databaseDriver); - } -}; diff --git a/skymp5-server/cpp/server_guest_lib/ActionListener.cpp b/skymp5-server/cpp/server_guest_lib/ActionListener.cpp index 3a98e1f88a..52748519be 100644 --- a/skymp5-server/cpp/server_guest_lib/ActionListener.cpp +++ b/skymp5-server/cpp/server_guest_lib/ActionListener.cpp @@ -3,7 +3,6 @@ #include "ConsoleCommands.h" #include "CropRegeneration.h" #include "DummyMessageOutput.h" -#include "EspmGameObject.h" #include "Exceptions.h" #include "FindRecipe.h" #include "GetBaseActorValues.h" @@ -15,6 +14,7 @@ #include "UserMessageOutput.h" #include "WorldState.h" #include "papyrus-vm/Utils.h" +#include "script_objects/EspmGameObject.h" #include #include #include @@ -226,67 +226,6 @@ void ActionListener::OnUpdateEquipment( actor->SetEquipment(simdjson::minify(data)); } -void RecalculateWorn(MpObjectReference& refr) -{ - if (!refr.GetParent()->HasEspm()) { - return; - } - auto& loader = refr.GetParent()->GetEspm(); - auto& cache = refr.GetParent()->GetEspmCache(); - - auto ac = dynamic_cast(&refr); - if (!ac) { - return; - } - - const Equipment eq = ac->GetEquipment(); - - Equipment newEq; - newEq.numChanges = eq.numChanges + 1; - for (auto& entry : eq.inv.entries) { - bool isEquipped = entry.extra.worn != Inventory::Worn::None; - bool isWeap = - espm::GetRecordType(entry.baseId, refr.GetParent()) == espm::WEAP::kType; - if (isEquipped && isWeap) { - continue; - } - newEq.inv.AddItems({ entry }); - } - - const Inventory inv = ac->GetInventory(); - Inventory::Entry bestEntry; - int16_t bestDamage = -1; - for (auto& entry : inv.entries) { - if (entry.baseId) { - auto lookupRes = loader.GetBrowser().LookupById(entry.baseId); - if (auto weap = espm::Convert(lookupRes.rec)) { - if (!bestEntry.count || - weap->GetData(cache).weapData->damage > bestDamage) { - bestEntry = entry; - bestDamage = weap->GetData(cache).weapData->damage; - } - } - } - } - - if (bestEntry.count > 0) { - bestEntry.extra.worn = Inventory::Worn::Right; - newEq.inv.AddItems({ bestEntry }); - } - - ac->SetEquipment(newEq.ToJson().dump()); - for (auto listener : ac->GetListeners()) { - auto actor = dynamic_cast(listener); - if (!actor) { - continue; - } - UpdateEquipmentMessage msg; - msg.data = newEq.ToJson(); - msg.idx = ac->GetIdx(); - actor->SendToUser(msg, true); - } -} - void ActionListener::OnActivate(const RawMessageData& rawMsgData, uint32_t caster, uint32_t target) { @@ -318,7 +257,11 @@ void ActionListener::OnActivate(const RawMessageData& rawMsgData, caster == 0x14 ? *ac : partOne.worldState.GetFormAt(caster)); if (hosterId) { - RecalculateWorn(partOne.worldState.GetFormAt(caster)); + auto actor = std::dynamic_pointer_cast( + partOne.worldState.LookupFormById(caster)); + if (actor) { + actor->EquipBestWeapon(); + } } } @@ -360,19 +303,19 @@ namespace { VarValue VarValueFromJson(const simdjson::dom::element& parentMsg, const simdjson::dom::element& element) { - static const auto key = JsonPointer("returnValue"); + static const auto kKey = JsonPointer("returnValue"); // TODO: DOUBLE, STRING ... switch (element.type()) { case simdjson::dom::element_type::INT64: case simdjson::dom::element_type::UINT64: { int32_t v; - ReadEx(parentMsg, key, &v); + ReadEx(parentMsg, kKey, &v); return VarValue(v); } case simdjson::dom::element_type::BOOL: { bool v; - ReadEx(parentMsg, key, &v); + ReadEx(parentMsg, kKey, &v); return VarValue(v); } case simdjson::dom::element_type::NULL_VALUE: @@ -517,10 +460,15 @@ void ActionListener::OnHostAttempt(const RawMessageData& rawMsgData, me->GetFormId()); hoster = me->GetFormId(); remote.UpdateHoster(hoster); - RecalculateWorn(remote); + + auto remoteAsActor = dynamic_cast(&remote); + + if (remoteAsActor) { + remoteAsActor->EquipBestWeapon(); + } uint64_t longFormId = remote.GetFormId(); - if (dynamic_cast(&remote) && longFormId < 0xff000000) { + if (remoteAsActor && longFormId < 0xff000000) { longFormId += 0x100000000; } @@ -634,7 +582,9 @@ float CalculateCurrentHealthPercentage(const MpActor& actor, float damage, uint32_t baseId = actor.GetBaseId(); uint32_t raceId = actor.GetRaceId(); WorldState* espmProvider = actor.GetParent(); - float baseHealth = GetBaseActorValues(espmProvider, baseId, raceId).health; + float baseHealth = + GetBaseActorValues(espmProvider, baseId, raceId, actor.GetTemplateChain()) + .health; if (outBaseHealth) { *outBaseHealth = baseHealth; diff --git a/skymp5-server/cpp/server_guest_lib/ConsoleCommands.cpp b/skymp5-server/cpp/server_guest_lib/ConsoleCommands.cpp index fd88cd8201..79273f0954 100644 --- a/skymp5-server/cpp/server_guest_lib/ConsoleCommands.cpp +++ b/skymp5-server/cpp/server_guest_lib/ConsoleCommands.cpp @@ -1,9 +1,9 @@ #include "ConsoleCommands.h" -#include "EspmGameObject.h" #include "MpActor.h" -#include "PapyrusObjectReference.h" #include "WorldState.h" #include "papyrus-vm/Utils.h" +#include "script_classes/PapyrusObjectReference.h" +#include "script_objects/EspmGameObject.h" ConsoleCommands::Argument::Argument() { @@ -124,8 +124,10 @@ void ExecuteDisable(MpActor& caller, ? caller : caller.GetParent()->GetFormAt(targetId); - if (target.GetFormId() >= 0xff000000) + if (target.GetFormId() >= 0xff000000 || + dynamic_cast(&target) != nullptr) { target.Disable(); + } } void ExecuteMp(MpActor& caller, diff --git a/skymp5-server/cpp/server_guest_lib/CropRegeneration.cpp b/skymp5-server/cpp/server_guest_lib/CropRegeneration.cpp index 69bca8cfe8..4ec68f0c08 100644 --- a/skymp5-server/cpp/server_guest_lib/CropRegeneration.cpp +++ b/skymp5-server/cpp/server_guest_lib/CropRegeneration.cpp @@ -12,7 +12,8 @@ BaseActorValues GetValues(MpActor* actor) auto appearance = actor->GetAppearance(); uint32_t raceId = appearance ? appearance->raceId : 0; auto worldState = actor->GetParent(); - return GetBaseActorValues(worldState, baseId, raceId); + return GetBaseActorValues(worldState, baseId, raceId, + actor->GetTemplateChain()); } } diff --git a/skymp5-server/cpp/server_guest_lib/EvaluateTemplate.h b/skymp5-server/cpp/server_guest_lib/EvaluateTemplate.h new file mode 100644 index 0000000000..b13e40fdf4 --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/EvaluateTemplate.h @@ -0,0 +1,49 @@ +#pragma once +#include "FormDesc.h" +#include "WorldState.h" +#include "libespm/espm.h" +#include +#include +#include + +template +auto EvaluateTemplate(WorldState* worldState, uint32_t baseId, + const std::vector& templateChain, + const Callback& callback) +{ + const std::vector chainDefault = { FormDesc::FromFormId( + baseId, worldState->espmFiles) }; + const std::vector& chain = + templateChain.size() > 0 ? templateChain : chainDefault; + + for (auto it = chain.begin(); it != chain.end(); it++) { + auto templateChainElement = it->ToFormId(worldState->espmFiles); + auto npcLookupResult = + worldState->GetEspm().GetBrowser().LookupById(templateChainElement); + auto npc = espm::Convert(npcLookupResult.rec); + auto npcData = npc->GetData(worldState->GetEspmCache()); + + if (npcData.baseTemplate == 0) { + return callback(npcLookupResult, npcData); + } + + if (!(npcData.templateDataFlags & TemplateFlag)) { + return callback(npcLookupResult, npcData); + } + } + + std::stringstream ss; + ss << "EvaluateTemplate failed: baseId=" << std::hex << baseId + << ", templateChain="; + + for (size_t i = 0; i < templateChain.size(); ++i) { + ss << templateChain[i].ToString(); + if (i != templateChain.size() - 1) { + ss << ","; + } + } + + ss << ", templateFlag=" << TemplateFlag; + + throw std::runtime_error(ss.str()); +} diff --git a/skymp5-server/cpp/server_guest_lib/GetBaseActorValues.cpp b/skymp5-server/cpp/server_guest_lib/GetBaseActorValues.cpp index afe2d2a134..73d0932cfa 100644 --- a/skymp5-server/cpp/server_guest_lib/GetBaseActorValues.cpp +++ b/skymp5-server/cpp/server_guest_lib/GetBaseActorValues.cpp @@ -1,6 +1,6 @@ #include "GetBaseActorValues.h" +#include "EvaluateTemplate.h" #include "WorldState.h" - #include void BaseActorValues::VisitBaseActorValues(BaseActorValues& baseActorValues, @@ -27,39 +27,56 @@ void BaseActorValues::VisitBaseActorValues(BaseActorValues& baseActorValues, std::to_string(changeForm.actorValues.magickaPercentage).c_str()); } +// TODO: implement auto-calc flag BaseActorValues GetBaseActorValues(WorldState* worldState, uint32_t baseId, - uint32_t raceIdOverride) + uint32_t raceIdOverride, + const std::vector& templateChain) { + auto npcData = espm::GetData(baseId, worldState); - uint32_t raceID = raceIdOverride ? raceIdOverride : npcData.race; - auto raceData = espm::GetData(raceID, worldState); + + uint32_t raceId = raceIdOverride + ? raceIdOverride + : EvaluateTemplate( + worldState, baseId, templateChain, + [](const auto& npcLookupResult, const auto& npcData) { + return npcLookupResult.ToGlobalId(npcData.race); + }); + auto raceData = espm::GetData(raceId, worldState); + + espm::NPC_::Data attributesNpcData = EvaluateTemplate( + worldState, baseId, templateChain, + [](const auto&, const auto& npcData) { return npcData; }); BaseActorValues actorValues; - actorValues.health = raceData.startingHealth + npcData.healthOffset; + actorValues.health = + raceData.startingHealth + attributesNpcData.healthOffset; if (actorValues.health <= 0) { spdlog::warn("GetBaseActorValues {:x} {:x} - Negative Health found: " "startingHealth={}, healthOffset={}, defaulting to 100", baseId, raceIdOverride, raceData.startingHealth, - npcData.healthOffset); + attributesNpcData.healthOffset); actorValues.health = 100.f; } - actorValues.magicka = raceData.startingMagicka + npcData.magickaOffset; - if (actorValues.magicka <= 0) { + actorValues.magicka = + raceData.startingMagicka + attributesNpcData.magickaOffset; + if (actorValues.magicka < 0) { // zero magicka is ok, negative isn't spdlog::warn("GetBaseActorValues {:x} {:x} - Negative Magicka found: " "startingMagicka={}, magickaOffset={}, defaulting to 100", baseId, raceIdOverride, raceData.startingMagicka, - npcData.magickaOffset); + attributesNpcData.magickaOffset); actorValues.magicka = 100.f; } - actorValues.stamina = raceData.startingStamina + npcData.staminaOffset; + actorValues.stamina = + raceData.startingStamina + attributesNpcData.staminaOffset; if (actorValues.stamina <= 0) { spdlog::warn("GetBaseActorValues {:x} {:x} - Negative Stamina found: " "startingStamina={}, staminaOffset={}, defaulting to 100", baseId, raceIdOverride, raceData.startingStamina, - npcData.staminaOffset); + attributesNpcData.staminaOffset); actorValues.stamina = 100.f; } @@ -69,7 +86,8 @@ BaseActorValues GetBaseActorValues(WorldState* worldState, uint32_t baseId, spdlog::trace( "GetBaseActorValues {:x} {:x} - startingHealth={}, healthOffset={}", - baseId, raceIdOverride, raceData.startingHealth, npcData.healthOffset); + baseId, raceIdOverride, raceData.startingHealth, + attributesNpcData.healthOffset); return actorValues; } diff --git a/skymp5-server/cpp/server_guest_lib/GetBaseActorValues.h b/skymp5-server/cpp/server_guest_lib/GetBaseActorValues.h index 4a683b49ca..8cf96c48b4 100644 --- a/skymp5-server/cpp/server_guest_lib/GetBaseActorValues.h +++ b/skymp5-server/cpp/server_guest_lib/GetBaseActorValues.h @@ -17,4 +17,5 @@ struct BaseActorValues : public ActorValues }; BaseActorValues GetBaseActorValues(WorldState* worldState, uint32_t baseId, - uint32_t raceIdOverride); + uint32_t raceIdOverride, + const std::vector& templateChain); diff --git a/skymp5-server/cpp/server_guest_lib/Inventory.cpp b/skymp5-server/cpp/server_guest_lib/Inventory.cpp index 1669622036..7294888e49 100644 --- a/skymp5-server/cpp/server_guest_lib/Inventory.cpp +++ b/skymp5-server/cpp/server_guest_lib/Inventory.cpp @@ -80,26 +80,31 @@ Inventory& Inventory::RemoveItems(const std::vector& entries) bool Inventory::HasItem(uint32_t baseId) const { - for (auto& entry : entries) - if (entry.baseId == baseId) + for (auto& entry : entries) { + if (entry.baseId == baseId) { return true; + } + } return false; } uint32_t Inventory::GetItemCount(uint32_t baseId) const { uint32_t sum = 0; - for (auto& entry : entries) - if (entry.baseId == baseId) + for (auto& entry : entries) { + if (entry.baseId == baseId) { sum += entry.count; + } + } return sum; } uint32_t Inventory::GetTotalItemCount() const { uint32_t sum = 0; - for (auto& entry : entries) + for (auto& entry : entries) { sum += entry.count; + } return sum; } diff --git a/skymp5-server/cpp/server_guest_lib/LeveledListUtils.cpp b/skymp5-server/cpp/server_guest_lib/LeveledListUtils.cpp index 8ef8a5544e..06013aaa02 100644 --- a/skymp5-server/cpp/server_guest_lib/LeveledListUtils.cpp +++ b/skymp5-server/cpp/server_guest_lib/LeveledListUtils.cpp @@ -1,5 +1,17 @@ #include "LeveledListUtils.h" +#include #include +#include +#include + +namespace { +bool IsLeveledType(const espm::LookupResult& lookupRes) noexcept +{ + espm::Type type = lookupRes.rec->GetType(); + return type == espm::LVLI::kType || type == espm::LVLN::kType || + type == "LVSP" /* for the future leveled spell implementation */; +} +} std::vector LeveledListUtils::EvaluateList( const espm::CombineBrowser& br, const espm::LookupResult& lookupRes, @@ -7,7 +19,11 @@ std::vector LeveledListUtils::EvaluateList( { espm::CompressedFieldsCache dummyCache; - auto leveledList = espm::Convert(lookupRes.rec); + const espm::LeveledListBase* leveledList = nullptr; + if (IsLeveledType(lookupRes)) { + leveledList = + reinterpret_cast(lookupRes.rec); + } if (!leveledList) { return {}; } @@ -16,22 +32,25 @@ std::vector LeveledListUtils::EvaluateList( std::vector res; int chanceNone = data.chanceNoneGlobalId ? 100 : data.chanceNone; - if (chanceNoneOverride) + if (chanceNoneOverride) { chanceNone = *chanceNoneOverride; + } - std::random_device rd; - std::mt19937 mt(rd()); std::uniform_real_distribution dist(0.0, 100.0); + std::random_device rd; + std::mt19937 mt(rd()); bool none = dist(mt) < chanceNone; if (!none) { - std::vector entriesAllowed; + std::vector entriesAllowed; for (size_t i = 0; i < data.numEntries; ++i) { - if (!pcLevel || data.entries[i].level <= pcLevel) + if (!pcLevel || data.entries[i].level <= pcLevel) { entriesAllowed.push_back(&data.entries[i]); + } } - bool useRandomEntry = !(data.leveledItemFlags & espm::LVLI::UseAll); + bool useRandomEntry = + !(data.leveledItemFlags & espm::LeveledListBase::UseAll); if (useRandomEntry && !entriesAllowed.empty()) { std::uniform_int_distribution dist(0, entriesAllowed.size() - 1); auto entry = entriesAllowed[dist(mt)]; @@ -54,9 +73,15 @@ std::map LeveledListUtils::EvaluateListRecurse( { espm::CompressedFieldsCache dummyCache; - auto leveledList = espm::Convert(lookupRes.rec); + const espm::LeveledListBase* leveledList = nullptr; + if (IsLeveledType(lookupRes)) { + leveledList = + reinterpret_cast(lookupRes.rec); + } + bool calcForEach = leveledList && - (leveledList->GetData(dummyCache).leveledItemFlags & espm::LVLI::Each); + (leveledList->GetData(dummyCache).leveledItemFlags & + espm::LeveledListBase::Each); if (calcForEach && countMult != 1) { std::map res; @@ -76,7 +101,7 @@ std::map LeveledListUtils::EvaluateListRecurse( if (!eLookupRes.rec) { continue; } - if (eLookupRes.rec->GetType() == espm::LVLI::kType) { + if (IsLeveledType(eLookupRes)) { auto childRes = EvaluateListRecurse(br, eLookupRes, 1, pcLevel); for (auto& p : childRes) { res[p.first] += p.second; @@ -95,3 +120,105 @@ std::map LeveledListUtils::EvaluateListRecurse( return res; } + +std::vector LeveledListUtils::EvaluateTemplateChain( + const espm::CombineBrowser& browser, const espm::LookupResult& headNpc, + uint32_t pcLevel) +{ + std::vector result; + auto npcCursor = ConvertToNpc(headNpc); + if (!npcCursor) { + spdlog::error("EvaluateTemplateChain: NPC_ expected"); + return result; + } + + uint32_t cursorFileIdx = headNpc.fileIdx; + result.push_back(headNpc.ToGlobalId(headNpc.rec->GetId())); + + while (true) { + uint32_t templateId = GetBaseTemplateId(npcCursor, cursorFileIdx, browser); + if (templateId == 0) { + break; // End if no base template + } + + espm::LookupResult templateResult = browser.LookupById(templateId); + + if (!templateResult.rec) { + spdlog::error("EvaluateTemplateChain: Not found template record"); + return result; + } + + UpdateCursorAndResult(templateId, templateResult, npcCursor, cursorFileIdx, + result, browser, pcLevel); + } + + return result; +} + +const espm::NPC_* LeveledListUtils::ConvertToNpc( + const espm::LookupResult& lookupResult) +{ + return espm::Convert(lookupResult.rec); +} + +uint32_t LeveledListUtils::GetBaseTemplateId( + const espm::NPC_* cursor, uint32_t fileIdx, + const espm::CombineBrowser& browser) +{ + espm::CompressedFieldsCache dummyCache; + auto data = cursor->GetData(dummyCache); + if (!data.baseTemplate) { + return 0; // Indicates there's no base template + } + + auto lookupRes = espm::LookupResult(&browser, cursor, fileIdx); + return lookupRes.ToGlobalId(data.baseTemplate); +} + +void LeveledListUtils::UpdateCursorAndResult( + uint32_t templateId, espm::LookupResult& templateResult, + const espm::NPC_*& cursor, uint32_t& cursorFileIdx, + std::vector& result, const espm::CombineBrowser& browser, + uint32_t pcLevel) +{ + if (auto npc = espm::Convert(templateResult.rec)) { + result.push_back(templateId); + cursor = npc; + cursorFileIdx = templateResult.fileIdx; + } else if (auto lvln = espm::Convert(templateResult.rec)) { + auto selectedNpcId = + EvaluateAndSelectNpcId(browser, templateResult, pcLevel); + if (selectedNpcId == 0) { + return; + } + + auto npcLookupRes = browser.LookupById(selectedNpcId); + cursor = espm::Convert(npcLookupRes.rec); + result.push_back(selectedNpcId); + cursorFileIdx = npcLookupRes.fileIdx; + } else { + spdlog::error("EvaluateTemplateChain: Not found template record"); + } +} + +uint32_t LeveledListUtils::EvaluateAndSelectNpcId( + const espm::CombineBrowser& browser, + const espm::LookupResult& templateResult, uint32_t pcLevel) +{ + auto countByFormId = + LeveledListUtils::EvaluateListRecurse(browser, templateResult, 1, pcLevel); + + if (countByFormId.empty()) { + spdlog::error( + "EvaluateTemplateChain: EvaluateListRecurse returned empty map"); + return 0; + } + + if (countByFormId.size() > 1) { + spdlog::warn("EvaluateTemplateChain: EvaluateListRecurse returned more " + "than 1 result, omitting other results"); + } + + // Return the id of the first npc in the map + return countByFormId.begin()->first; +} diff --git a/skymp5-server/cpp/server_guest_lib/LeveledListUtils.h b/skymp5-server/cpp/server_guest_lib/LeveledListUtils.h index 84f85f3b5d..744f2cb6fb 100644 --- a/skymp5-server/cpp/server_guest_lib/LeveledListUtils.h +++ b/skymp5-server/cpp/server_guest_lib/LeveledListUtils.h @@ -5,20 +5,46 @@ #include #include -namespace LeveledListUtils { -struct Entry +class LeveledListUtils { - uint32_t formId = 0; - uint32_t count = 0; -}; +public: + struct Entry + { + uint32_t formId = 0; + uint32_t count = 0; + }; + + // It seems that pcLevel=0 makes it thinking that pcLevel=maximum possible pc + // level + static std::vector EvaluateList( + const espm::CombineBrowser& br, const espm::LookupResult& lookupRes, + uint32_t pcLevel = 0, uint8_t* chanceNoneOverride = nullptr); + + static std::map EvaluateListRecurse( + const espm::CombineBrowser& br, const espm::LookupResult& lookupRes, + uint32_t countMult = 1, uint32_t pcLevel = 0, + uint8_t* chanceNoneOverride = nullptr); + + static std::vector EvaluateTemplateChain( + const espm::CombineBrowser& br, const espm::LookupResult& headNpc, + uint32_t pcLevel); -std::vector EvaluateList(const espm::CombineBrowser& br, - const espm::LookupResult& lookupRes, - uint32_t pcLevel = 0, - uint8_t* chanceNoneOverride = nullptr); +private: + static const espm::NPC_* ConvertToNpc( + const espm::LookupResult& lookupResult); -std::map EvaluateListRecurse( - const espm::CombineBrowser& br, const espm::LookupResult& lookupRes, - uint32_t countMult = 1, uint32_t pcLevel = 0, - uint8_t* chanceNoneOverride = nullptr); -} + static uint32_t GetBaseTemplateId(const espm::NPC_* cursor, uint32_t fileIdx, + const espm::CombineBrowser& browser); + + static void UpdateCursorAndResult(uint32_t templateId, + espm::LookupResult& templateResult, + const espm::NPC_*& cursor, + uint32_t& cursorFileIdx, + std::vector& result, + const espm::CombineBrowser& browser, + uint32_t pcLevel); + + static uint32_t EvaluateAndSelectNpcId( + const espm::CombineBrowser& browser, + const espm::LookupResult& templateResult, uint32_t pcLevel); +}; diff --git a/skymp5-server/cpp/server_guest_lib/LocationalDataUtils.cpp b/skymp5-server/cpp/server_guest_lib/LocationalDataUtils.cpp new file mode 100644 index 0000000000..c4748b2aad --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/LocationalDataUtils.cpp @@ -0,0 +1,29 @@ +#include "LocationalDataUtils.h" +#include "NiPoint3.h" + +#include "libespm/GroupUtils.h" +#include "libespm/Utils.h" + +const NiPoint3& LocationalDataUtils::GetPos( + const espm::REFR::LocationalData* locationalData) +{ + return *reinterpret_cast(locationalData->pos); +} + +NiPoint3 LocationalDataUtils::GetRot( + const espm::REFR::LocationalData* locationalData) +{ + static const auto kPi = std::acos(-1.f); + return { locationalData->rotRadians[0] / kPi * 180.f, + locationalData->rotRadians[1] / kPi * 180.f, + locationalData->rotRadians[2] / kPi * 180.f }; +} + +uint32_t LocationalDataUtils::GetWorldOrCell( + const espm::CombineBrowser& br, const espm::LookupResult& refrLookupRes) +{ + auto mapping = br.GetCombMapping(refrLookupRes.fileIdx); + uint32_t worldOrCell = espm::utils::GetMappedId( + espm::GetWorldOrCell(br, refrLookupRes.rec), *mapping); + return worldOrCell; +} diff --git a/skymp5-server/cpp/server_guest_lib/LocationalDataUtils.h b/skymp5-server/cpp/server_guest_lib/LocationalDataUtils.h new file mode 100644 index 0000000000..b8f1a23206 --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/LocationalDataUtils.h @@ -0,0 +1,14 @@ +#pragma once +#include "libespm/CombineBrowser.h" +#include "libespm/LookupResult.h" +#include "libespm/REFR.h" +#include + +class NiPoint3; + +namespace LocationalDataUtils { +const NiPoint3& GetPos(const espm::REFR::LocationalData* locationalData); +NiPoint3 GetRot(const espm::REFR::LocationalData* locationalData); +uint32_t GetWorldOrCell(const espm::CombineBrowser& br, + const espm::LookupResult& refrLookupRes); +} diff --git a/skymp5-server/cpp/server_guest_lib/MpActor.cpp b/skymp5-server/cpp/server_guest_lib/MpActor.cpp index ec21a4f928..271d527eac 100644 --- a/skymp5-server/cpp/server_guest_lib/MpActor.cpp +++ b/skymp5-server/cpp/server_guest_lib/MpActor.cpp @@ -3,18 +3,19 @@ #include "ActorValues.h" #include "ChangeFormGuard.h" #include "CropRegeneration.h" -#include "EspmGameObject.h" #include "FormCallbacks.h" #include "GetBaseActorValues.h" +#include "LeveledListUtils.h" +#include "LocationalDataUtils.h" #include "MathUtils.h" #include "MpChangeForms.h" #include "MsgType.h" -#include "PapyrusObjectReference.h" #include "ServerState.h" #include "SweetPieScript.h" #include "TimeUtils.h" #include "WorldState.h" #include "libespm/espm.h" +#include "script_objects/EspmGameObject.h" #include #include #include @@ -24,6 +25,7 @@ #include "ChangeValuesMessage.h" #include "TeleportMessage.h" +#include "UpdateEquipmentMessage.h" struct MpActor::Impl { @@ -89,6 +91,63 @@ void MpActor::SetConsoleCommandsAllowedFlag(bool newValue) }); } +void MpActor::EquipBestWeapon() +{ + if (!GetParent()->HasEspm()) { + return; + } + + auto& loader = GetParent()->GetEspm(); + auto& cache = GetParent()->GetEspmCache(); + + const Equipment eq = GetEquipment(); + + Equipment newEq; + newEq.numChanges = eq.numChanges + 1; + for (auto& entry : eq.inv.entries) { + bool isEquipped = entry.extra.worn != Inventory::Worn::None; + bool isWeap = + espm::GetRecordType(entry.baseId, GetParent()) == espm::WEAP::kType; + if (isEquipped && isWeap) { + continue; + } + newEq.inv.AddItems({ entry }); + } + + const Inventory& inv = GetInventory(); + Inventory::Entry bestEntry; + int16_t bestDamage = -1; + for (auto& entry : inv.entries) { + if (entry.baseId) { + auto lookupRes = loader.GetBrowser().LookupById(entry.baseId); + if (auto weap = espm::Convert(lookupRes.rec)) { + if (!bestEntry.count || + weap->GetData(cache).weapData->damage > bestDamage) { + bestEntry = entry; + bestDamage = weap->GetData(cache).weapData->damage; + } + } + } + } + + if (bestEntry.count > 0) { + bestEntry.extra.worn = Inventory::Worn::Right; + newEq.inv.AddItems({ bestEntry }); + } + + SetEquipment(newEq.ToJson().dump()); + for (auto listener : GetListeners()) { + auto actor = dynamic_cast(listener); + if (!actor) { + continue; + } + UpdateEquipmentMessage msg; + msg.data = newEq.ToJson(); + msg.idx = GetIdx(); + actor->SendToUser(msg, true); + } +} + void MpActor::SetRaceMenuOpen(bool isOpen) { EditChangeForm( @@ -122,7 +181,8 @@ void MpActor::VisitProperties(const PropertiesVisitor& visitor, // this "if" is needed for unit testing: tests can call VisitProperties // without espm attached, which will cause tests to fail if (worldState && worldState->HasEspm()) { - baseActorValues = GetBaseActorValues(worldState, baseId, raceId); + baseActorValues = GetBaseActorValues(worldState, baseId, raceId, + ChangeForm().templateChain); } MpChangeForm changeForm = GetChangeForm(); @@ -141,6 +201,18 @@ void MpActor::VisitProperties(const PropertiesVisitor& visitor, nlohmann::json(changeForm.learnedSpells.GetLearnedSpells()) .dump() .c_str()); + + if (!changeForm.templateChain.empty()) { + // should be faster than nlohmann::json + std::string jsonDump = "["; + for (auto& element : changeForm.templateChain) { + jsonDump += std::to_string(element.ToFormId(GetParent()->espmFiles)); + jsonDump += ','; + } + jsonDump.pop_back(); // comma + jsonDump += "]"; + visitor("templateChain", jsonDump.data()); + } } void MpActor::Disable() @@ -282,8 +354,8 @@ void MpActor::ApplyChangeForm(const MpChangeForm& newChangeForm) // this check is added only for test as a workaround. It is to be redone // in the nearest future. TODO if (GetParent() && GetParent()->HasEspm()) { - changeForm.actorValues = - GetBaseActorValues(GetParent(), GetBaseId(), GetRaceId()); + changeForm.actorValues = GetBaseActorValues( + GetParent(), GetBaseId(), GetRaceId(), changeForm.templateChain); } }, Mode::NoRequestSave); @@ -451,6 +523,16 @@ espm::ObjectBounds MpActor::GetBounds() const return espm::GetData(GetBaseId(), GetParent()).objectBounds; } +const std::vector& MpActor::GetTemplateChain() const +{ + return ChangeForm().templateChain; +} + +bool MpActor::IsCreatedAsPlayer() const +{ + return GetFormId() >= 0xff000000 && GetBaseId() <= 0x7; +} + void MpActor::SendAndSetDeathState(bool isDead, bool shouldTeleport) { float attribute = isDead ? 0.f : 1.f; @@ -564,6 +646,37 @@ bool MpActor::CanActorValueBeRestored(espm::ActorValue av) return true; } +void MpActor::EnsureTemplateChainEvaluated(espm::Loader& loader) +{ + constexpr auto kPcLevel = 0; + + auto worldState = GetParent(); + if (!worldState) { + return; + } + + auto baseId = GetBaseId(); + if (baseId == 0x7 || baseId == 0) { + return; + } + + if (!ChangeForm().templateChain.empty()) { + return; + } + + EditChangeForm([&](MpChangeFormREFR& changeForm) { + auto headNpc = loader.GetBrowser().LookupById(baseId); + std::vector res = LeveledListUtils::EvaluateTemplateChain( + loader.GetBrowser(), headNpc, kPcLevel); + std::vector templateChain(res.size()); + std::transform( + res.begin(), res.end(), templateChain.begin(), [&](uint32_t formId) { + return FormDesc::FromFormId(formId, worldState->espmFiles); + }); + changeForm.templateChain = std::move(templateChain); + }); +} + std::chrono::steady_clock::time_point MpActor::GetLastRestorationTime( espm::ActorValue av) const noexcept { @@ -614,7 +727,12 @@ void MpActor::Init(WorldState* worldState, uint32_t formId, bool hasChangeForm) MpObjectReference::Init(worldState, formId, hasChangeForm); if (worldState->HasEspm()) { - EnsureBaseContainerAdded(GetParent()->GetEspm()); + auto& espm = worldState->GetEspm(); + EnsureTemplateChainEvaluated(espm); + EnsureBaseContainerAdded(espm); // template chain needed here + + // TODO: implement "gearedUpWeapons" flag + EquipBestWeapon(); } } @@ -675,11 +793,43 @@ void MpActor::SetSpawnPoint(const LocationalData& position) LocationalData MpActor::GetSpawnPoint() const { + auto formId = GetFormId(); + + if (!IsCreatedAsPlayer()) { + return GetEditorLocationalData(); + } return ChangeForm().spawnPoint; } +LocationalData MpActor::GetEditorLocationalData() const +{ + auto formId = GetFormId(); + auto worldState = GetParent(); + + if (!worldState || !worldState->HasEspm()) { + throw std::runtime_error("MpActor::GetEditorLocation can only be used " + "with actors attached to a valid world state"); + } + + auto data = espm::GetData(formId, worldState); + auto lookupRes = worldState->GetEspm().GetBrowser().LookupById(formId); + + const NiPoint3& pos = LocationalDataUtils::GetPos(data.loc); + NiPoint3 rot = LocationalDataUtils::GetRot(data.loc); + uint32_t worldOrCell = LocationalDataUtils::GetWorldOrCell( + worldState->GetEspm().GetBrowser(), lookupRes); + + return LocationalData{ + pos, rot, FormDesc::FromFormId(worldOrCell, worldState->espmFiles) + }; +} + const float MpActor::GetRespawnTime() const { + if (!IsCreatedAsPlayer()) { + static const auto kNpcSpawnDelay = 6 * 60.f * 60.f; + return kNpcSpawnDelay; + } return ChangeForm().spawnDelay; } @@ -717,7 +867,8 @@ void MpActor::DamageActorValue(espm::ActorValue av, float value) BaseActorValues MpActor::GetBaseValues() { - return GetBaseActorValues(GetParent(), GetBaseId(), GetRaceId()); + return GetBaseActorValues(GetParent(), GetBaseId(), GetRaceId(), + ChangeForm().templateChain); } BaseActorValues MpActor::GetMaximumValues() @@ -836,8 +987,8 @@ void MpActor::ApplyMagicEffect(espm::Effects::Effect& effect, bool hasSweetpie, return; } MpChangeForm changeForm = GetChangeForm(); - BaseActorValues baseValues = - GetBaseActorValues(GetParent(), GetBaseId(), GetRaceId()); + BaseActorValues baseValues = GetBaseActorValues( + GetParent(), GetBaseId(), GetRaceId(), changeForm.templateChain); const ActiveMagicEffectsMap& activeEffects = changeForm.activeMagicEffects; const float baseValue = baseValues.GetValue(av); const uint32_t formId = GetFormId(); @@ -913,8 +1064,8 @@ void MpActor::ApplyMagicEffects(std::vector& effects, void MpActor::RemoveMagicEffect(const espm::ActorValue actorValue) noexcept { - const ActorValues baseActorValues = - GetBaseActorValues(GetParent(), GetBaseId(), GetRaceId()); + const ActorValues baseActorValues = GetBaseActorValues( + GetParent(), GetBaseId(), GetRaceId(), ChangeForm().templateChain); const float baseActorValue = baseActorValues.GetValue(actorValue); SetActorValue(actorValue, baseActorValue); EditChangeForm([actorValue](MpChangeForm& changeForm) { @@ -924,8 +1075,8 @@ void MpActor::RemoveMagicEffect(const espm::ActorValue actorValue) noexcept void MpActor::RemoveAllMagicEffects() noexcept { - const ActorValues baseActorValues = - GetBaseActorValues(GetParent(), GetBaseId(), GetRaceId()); + const ActorValues baseActorValues = GetBaseActorValues( + GetParent(), GetBaseId(), GetRaceId(), ChangeForm().templateChain); SetActorValues(baseActorValues); EditChangeForm( [](MpChangeForm& changeForm) { changeForm.activeMagicEffects.Clear(); }); diff --git a/skymp5-server/cpp/server_guest_lib/MpActor.h b/skymp5-server/cpp/server_guest_lib/MpActor.h index 98eb7d7464..c3ec7d06cf 100644 --- a/skymp5-server/cpp/server_guest_lib/MpActor.h +++ b/skymp5-server/cpp/server_guest_lib/MpActor.h @@ -35,6 +35,8 @@ class MpActor : public MpObjectReference uint32_t GetRaceId() const; bool IsWeaponDrawn() const; espm::ObjectBounds GetBounds() const; + const std::vector& GetTemplateChain() const; + bool IsCreatedAsPlayer() const; void SetRaceMenuOpen(bool isOpen); void SetAppearance(const Appearance* newAppearance); @@ -92,6 +94,7 @@ class MpActor : public MpObjectReference void Teleport(const LocationalData& position); void SetSpawnPoint(const LocationalData& position); LocationalData GetSpawnPoint() const; + LocationalData GetEditorLocationalData() const; const float GetRespawnTime() const; void SetRespawnTime(float time); @@ -125,6 +128,8 @@ class MpActor : public MpObjectReference bool GetConsoleCommandsAllowedFlag() const; void SetConsoleCommandsAllowedFlag(bool newValue); + void EquipBestWeapon(); + private: struct Impl; std::shared_ptr pImpl; @@ -149,6 +154,8 @@ class MpActor : public MpObjectReference std::chrono::steady_clock::time_point timePoint); bool CanActorValueBeRestored(espm::ActorValue av); + void EnsureTemplateChainEvaluated(espm::Loader& loader); + protected: void BeforeDestroy() override; void Init(WorldState* parent, uint32_t formId, bool hasChangeForm) override; diff --git a/skymp5-server/cpp/server_guest_lib/MpChangeForms.cpp b/skymp5-server/cpp/server_guest_lib/MpChangeForms.cpp index b7d78d0f84..ec275a355d 100644 --- a/skymp5-server/cpp/server_guest_lib/MpChangeForms.cpp +++ b/skymp5-server/cpp/server_guest_lib/MpChangeForms.cpp @@ -1,6 +1,23 @@ #include "MpChangeForms.h" #include "JsonUtils.h" +namespace { +std::vector ToStringArray(const std::vector& formDescs) +{ + std::vector res(formDescs.size()); + std::transform(formDescs.begin(), formDescs.end(), res.begin(), + [](const FormDesc& v) { return v.ToString(); }); + return res; +} +std::vector ToFormDescsArray(const std::vector& strings) +{ + std::vector res(strings.size()); + std::transform(strings.begin(), strings.end(), res.begin(), + [](const std::string& v) { return FormDesc::FromString(v); }); + return res; +} +} + nlohmann::json MpChangeForm::ToJson(const MpChangeForm& changeForm) { auto res = nlohmann::json::object(); @@ -55,6 +72,16 @@ nlohmann::json MpChangeForm::ToJson(const MpChangeForm& changeForm) res["spawnDelay"] = changeForm.spawnDelay; res["effects"] = changeForm.activeMagicEffects.ToJson(); + + if (!changeForm.templateChain.empty()) { + res["templateChain"] = ToStringArray(changeForm.templateChain); + } + + // TODO: uncomment when add script vars save feature + // if (changeForm.lastAnimation.has_value()) { + // res["lastAnimation"] = *changeForm.lastAnimation; + // } + return res; } @@ -74,7 +101,8 @@ MpChangeForm MpChangeForm::JsonToChangeForm(simdjson::dom::element& element) consoleCommandsAllowed("consoleCommandsAllowed"), spawnPointPos("spawnPoint_pos"), spawnPointRot("spawnPoint_rot"), spawnPointCellOrWorldDesc("spawnPoint_cellOrWorldDesc"), - spawnDelay("spawnDelay"), effects("effects"); + spawnDelay("spawnDelay"), effects("effects"), + templateChain("templateChain"), lastAnimation("lastAnimation"); MpChangeForm res; ReadEx(element, recType, &res.recType); @@ -172,6 +200,21 @@ MpChangeForm MpChangeForm::JsonToChangeForm(simdjson::dom::element& element) ReadEx(element, effects, &jTmp); res.activeMagicEffects = ActiveMagicEffectsMap::FromJson(jTmp); } + + if (element.at_pointer(templateChain.GetData()).error() == + simdjson::error_code::SUCCESS) { + std::vector data; + ReadVector(element, templateChain, &data); + res.templateChain = ToFormDescsArray(data); + } + + if (element.at_pointer(lastAnimation.GetData()).error() == + simdjson::error_code::SUCCESS) { + const char* tmp; + ReadEx(element, lastAnimation, &tmp); + res.lastAnimation = tmp; + } + return res; } @@ -187,7 +230,7 @@ size_t LearnedSpells::Count() const noexcept bool LearnedSpells::IsSpellLearned(const Data::key_type baseId) const { - return _learnedSpellIds.contains(baseId); + return _learnedSpellIds.count(baseId) != 0; } std::vector LearnedSpells::GetLearnedSpells() diff --git a/skymp5-server/cpp/server_guest_lib/MpChangeForms.h b/skymp5-server/cpp/server_guest_lib/MpChangeForms.h index e5f0c1fd08..9ad5fb9ba7 100644 --- a/skymp5-server/cpp/server_guest_lib/MpChangeForms.h +++ b/skymp5-server/cpp/server_guest_lib/MpChangeForms.h @@ -84,11 +84,20 @@ class MpChangeFormREFR // values in skymp due to poor design std::string appearanceDump, equipmentDump; ActorValues actorValues; + + // Used only for player characters. See GetSpawnPoint LocationalData spawnPoint = { { 133857, -61130, 14662 }, { 0.f, 0.f, 72.f }, FormDesc::Tamriel() }; + + // Used only for player characters. See GetSpawnDelay float spawnDelay = 25.0f; + std::vector templateChain; + + // Used for PlayAnimation (object reference) + std::optional lastAnimation; + // Please update 'ActorTest.cpp' when adding new Actor-related rows DynamicFields dynamicFields; @@ -101,7 +110,7 @@ class MpChangeFormREFR baseContainerAdded, nextRelootDatetime, isDisabled, profileId, isRaceMenuOpen, isDead, consoleCommandsAllowed, appearanceDump, equipmentDump, actorValues.ToTuple(), spawnPoint, dynamicFields, - spawnDelay, learnedSpells); + spawnDelay, learnedSpells, templateChain, lastAnimation); } static nlohmann::json ToJson(const MpChangeFormREFR& changeForm); diff --git a/skymp5-server/cpp/server_guest_lib/MpForm.cpp b/skymp5-server/cpp/server_guest_lib/MpForm.cpp index 7236a8e28a..a3299a56cb 100644 --- a/skymp5-server/cpp/server_guest_lib/MpForm.cpp +++ b/skymp5-server/cpp/server_guest_lib/MpForm.cpp @@ -1,7 +1,7 @@ #include "MpForm.h" -#include "MpFormGameObject.h" #include "WorldState.h" +#include "script_objects/MpFormGameObject.h" MpForm::MpForm() { @@ -26,12 +26,36 @@ void MpForm::SendPapyrusEvent(const char* eventName, const VarValue* arguments, VarValue MpForm::ToVarValue() const { + // TODO: consider using owning VarValue instead of non-owning return VarValue(ToGameObject().get()); } std::shared_ptr MpForm::ToGameObject() const { - if (!gameObject) + if (!gameObject) { gameObject.reset(new MpFormGameObject(const_cast(this))); + } return gameObject; } + +const std::vector>& +MpForm::ListActivePexInstances() const +{ + return activePexInstances; +} + +void MpForm::AddScript( + const std::shared_ptr& script) noexcept +{ + const std::string& name = script->GetSourcePexName(); + auto it = std::find_if(activePexInstances.begin(), activePexInstances.end(), + [&](const std::shared_ptr& item) { + return item->GetSourcePexName() == name; + }); + if (it != activePexInstances.end()) { + spdlog::warn("MpForm::AddScript {:x} - Already added script name {}", + GetFormId(), name); + return; + } + activePexInstances.push_back(script); +} diff --git a/skymp5-server/cpp/server_guest_lib/MpForm.h b/skymp5-server/cpp/server_guest_lib/MpForm.h index 48ceaf5d0f..8da2fe534c 100644 --- a/skymp5-server/cpp/server_guest_lib/MpForm.h +++ b/skymp5-server/cpp/server_guest_lib/MpForm.h @@ -1,17 +1,20 @@ #pragma once #include "NiPoint3.h" -#include "papyrus-vm/Structures.h" #include #include #include #include +#include class WorldState; class IGameObject; +class ActivePexInstance; +struct VarValue; class MpForm { friend class WorldState; + friend class MpFormGameObject; public: MpForm(); @@ -68,7 +71,13 @@ class MpForm uint32_t id = 0; WorldState* parent = nullptr; mutable GameObjectPtr gameObject; + std::vector> activePexInstances; protected: virtual void BeforeDestroy(){}; + + const std::vector>& + ListActivePexInstances() const; + + void AddScript(const std::shared_ptr& script) noexcept; }; diff --git a/skymp5-server/cpp/server_guest_lib/MpObjectReference.cpp b/skymp5-server/cpp/server_guest_lib/MpObjectReference.cpp index 8b781372a3..a4811206fe 100644 --- a/skymp5-server/cpp/server_guest_lib/MpObjectReference.cpp +++ b/skymp5-server/cpp/server_guest_lib/MpObjectReference.cpp @@ -1,22 +1,22 @@ #include "MpObjectReference.h" #include "ChangeFormGuard.h" -#include "EspmGameObject.h" +#include "EvaluateTemplate.h" #include "FormCallbacks.h" #include "LeveledListUtils.h" #include "MpActor.h" #include "MpChangeForms.h" #include "MsgType.h" -#include "PapyrusGame.h" -#include "PapyrusObjectReference.h" #include "Primitive.h" #include "ScopedTask.h" -#include "ScriptStorage.h" #include "ScriptVariablesHolder.h" +#include "TimeUtils.h" #include "WorldState.h" #include "libespm/GroupUtils.h" #include "libespm/Utils.h" #include "papyrus-vm/Reader.h" #include "papyrus-vm/VirtualMachine.h" +#include "script_objects/EspmGameObject.h" +#include "script_storages/IScriptStorage.h" #include #include @@ -269,15 +269,29 @@ bool MpObjectReference::GetTeleportFlag() const void MpObjectReference::VisitProperties(const PropertiesVisitor& visitor, VisitPropertiesMode mode) { - if (IsHarvested()) + if (IsHarvested()) { visitor("isHarvested", "true"); - if (IsOpen()) + } + + if (IsOpen()) { visitor("isOpen", "true"); + } + + if (auto actor = dynamic_cast(this); actor && actor->IsDead()) { + visitor("isDead", "true"); + } + if (mode == VisitPropertiesMode::All && !GetInventory().IsEmpty()) { auto inventoryDump = GetInventory().ToJson().dump(); visitor("inventory", inventoryDump.data()); } + if (ChangeForm().lastAnimation.has_value()) { + std::string lastAnimationAsJson = + "\"" + *ChangeForm().lastAnimation + "\""; + visitor("lastAnimation", lastAnimationAsJson.data()); + } + // Property flags (isVisibleByOwner, isVisibleByNeighbor) should be checked // by a visitor auto& dynamicFields = ChangeForm().dynamicFields.GetAsJson(); @@ -290,8 +304,38 @@ void MpObjectReference::VisitProperties(const PropertiesVisitor& visitor, void MpObjectReference::Activate(MpObjectReference& activationSource, bool defaultProcessingOnly) { + if (spdlog::should_log(spdlog::level::trace)) { + for (auto& script : ListActivePexInstances()) { + spdlog::trace("MpObjectReference::Activate {:x} - found script {}", + GetFormId(), script->GetSourcePexName()); + } + } + if (auto worldState = activationSource.GetParent(); worldState->HasEspm()) { CheckInteractionAbility(activationSource); + + // Pillars puzzle Bleak Falls Barrow + bool workaroundBypassParentsCheck = &activationSource == this; + + // Block if only activation parents can activate this + auto refrId = GetFormId(); + if (!workaroundBypassParentsCheck && refrId < 0xff000000 && + !dynamic_cast(this)) { + auto lookupRes = worldState->GetEspm().GetBrowser().LookupById(refrId); + auto data = espm::GetData(refrId, worldState); + auto it = std::find_if( + data.activationParents.begin(), data.activationParents.end(), + [&](const espm::REFR::ActivationParentInfo& info) { + return lookupRes.ToGlobalId(info.refrId) == + activationSource.GetFormId(); + }); + if (it == data.activationParents.end()) { + if (data.isParentActivationOnly) { + throw std::runtime_error( + "Only activation parents can activate this object"); + } + } + } } bool activationBlockedByMpApi = MpApiOnActivate(activationSource); @@ -299,6 +343,7 @@ void MpObjectReference::Activate(MpObjectReference& activationSource, if (!activationBlockedByMpApi && (!activationBlocked || defaultProcessingOnly)) { ProcessActivate(activationSource); + ActivateChilds(); } else { spdlog::trace( "Activation of form {:#x} has been blocked. Reasons: " @@ -616,12 +661,14 @@ void MpObjectReference::AddItem(uint32_t baseId, uint32_t count) }); SendInventoryUpdate(); - auto baseItem = VarValue(static_cast(baseId)); - auto itemCount = VarValue(static_cast(count)); - auto itemReference = VarValue((IGameObject*)nullptr); - auto sourceContainer = VarValue((IGameObject*)nullptr); - VarValue args[4] = { baseItem, itemCount, itemReference, sourceContainer }; - SendPapyrusEvent("OnItemAdded", args, 4); + // TODO: No one used it due to incorrect baseItem which should be object, + // not id. Needs to be revised. Seems to also be buggy + // auto baseItem = VarValue(static_cast(baseId)); + // auto itemCount = VarValue(static_cast(count)); + // auto itemReference = VarValue((IGameObject*)nullptr); + // auto sourceContainer = VarValue((IGameObject*)nullptr); + // VarValue args[4] = { baseItem, itemCount, itemReference, sourceContainer + // }; SendPapyrusEvent("OnItemAdded", args, 4); } void MpObjectReference::AddItems(const std::vector& entries) @@ -634,14 +681,14 @@ void MpObjectReference::AddItems(const std::vector& entries) SendInventoryUpdate(); } - for (const auto& entri : entries) { - auto baseItem = VarValue(static_cast(entri.baseId)); - auto itemCount = VarValue(static_cast(entri.count)); - auto itemReference = VarValue((IGameObject*)nullptr); - auto sourceContainer = VarValue((IGameObject*)nullptr); - VarValue args[4] = { baseItem, itemCount, itemReference, sourceContainer }; - SendPapyrusEvent("OnItemAdded", args, 4); - } + // for (const auto& entri : entries) { + // auto baseItem = VarValue(static_cast(entri.baseId)); + // auto itemCount = VarValue(static_cast(entri.count)); + // auto itemReference = VarValue((IGameObject*)nullptr); + // auto sourceContainer = VarValue((IGameObject*)nullptr); + // VarValue args[4] = { baseItem, itemCount, itemReference, sourceContainer + // }; SendPapyrusEvent("OnItemAdded", args, 4); + // } } void MpObjectReference::RemoveItem(uint32_t baseId, uint32_t count, @@ -759,8 +806,9 @@ void MpObjectReference::Subscribe(MpObjectReference* emitter, { auto actorEmitter = dynamic_cast(emitter); auto actorListener = dynamic_cast(listener); - if (!actorEmitter && !actorListener) + if (!actorEmitter && !actorListener) { return; + } // I don't know how often Subscrbe is called but I suppose // it is to be invoked quite frequently. In this case, each @@ -770,6 +818,8 @@ void MpObjectReference::Subscribe(MpObjectReference* emitter, listener->GetChangeForm().profileId != -1) { emitter->pImpl->onInitEventSent = true; emitter->SendPapyrusEvent("OnInit"); + emitter->SendPapyrusEvent("OnCellLoad"); + emitter->SendPapyrusEvent("OnLoad"); } const bool hasPrimitive = emitter->HasPrimitive(); @@ -781,12 +831,14 @@ void MpObjectReference::Subscribe(MpObjectReference* emitter, emitter->actorListeners.insert(actorListener); } listener->emitters->insert(emitter); - if (!hasPrimitive) + if (!hasPrimitive) { emitter->callbacks->subscribe(emitter, listener); + } if (hasPrimitive) { - if (!listener->emittersWithPrimitives) + if (!listener->emittersWithPrimitives) { listener->emittersWithPrimitives.reset(new std::map); + } listener->emittersWithPrimitives->insert({ emitter->GetFormId(), false }); } } @@ -803,8 +855,9 @@ void MpObjectReference::Unsubscribe(MpObjectReference* emitter, const bool hasPrimitive = emitter->HasPrimitive(); - if (!hasPrimitive) + if (!hasPrimitive) { emitter->callbacks->unsubscribe(emitter, listener); + } emitter->listeners->erase(listener); if (actorListener) { emitter->actorListeners.erase(actorListener); @@ -816,10 +869,17 @@ void MpObjectReference::Unsubscribe(MpObjectReference* emitter, } } +void MpObjectReference::SetLastAnimation(const std::string& lastAnimation) +{ + EditChangeForm([&](MpChangeForm& changeForm) { + changeForm.lastAnimation = lastAnimation; + }); +} + const std::set& MpObjectReference::GetListeners() const { - static const std::set g_emptyListeners; - return listeners ? *listeners : g_emptyListeners; + static const std::set kEmptyListeners; + return listeners ? *listeners : kEmptyListeners; } const std::set& MpObjectReference::GetActorListeners() const noexcept @@ -829,8 +889,8 @@ const std::set& MpObjectReference::GetActorListeners() const noexcept const std::set& MpObjectReference::GetEmitters() const { - static const std::set g_emptyEmitters; - return emitters ? *emitters : g_emptyEmitters; + static const std::set kEmptyEmitters; + return emitters ? *emitters : kEmptyEmitters; } void MpObjectReference::RequestReloot( @@ -873,17 +933,17 @@ std::shared_ptr> MpObjectReference::GetNextRelootMoment() const { std::shared_ptr> res; - if (ChangeForm().nextRelootDatetime) + if (ChangeForm().nextRelootDatetime) { res.reset(new std::chrono::time_point( std::chrono::system_clock::from_time_t( ChangeForm().nextRelootDatetime))); + } return res; } MpChangeForm MpObjectReference::GetChangeForm() const { - MpChangeForm res; - static_cast(res) = ChangeForm(); + MpChangeForm res = ChangeForm(); if (GetParent() && !GetParent()->espmFiles.empty()) { res.formDesc = FormDesc::FromFormId(GetFormId(), GetParent()->espmFiles); @@ -1031,14 +1091,6 @@ void MpObjectReference::Init(WorldState* parent, uint32_t formId, { MpForm::Init(parent, formId, hasChangeForm); - // It crashed during sparsepp hashmap indexing. - // Not sure why. And not sure why this code actually been here. - // It seems that MoveOnGrid will be caled later. - /*if (!IsDisabled()) { - auto& gridInfo = GetParent()->grids[ChangeForm().worldOrCell]; - MoveOnGrid(*gridInfo.grid); - }*/ - // We should queue created form for saving as soon as it is initialized const auto mode = (!hasChangeForm && formId >= 0xff000000) ? Mode::RequestSave @@ -1050,6 +1102,21 @@ void MpObjectReference::Init(WorldState* parent, uint32_t formId, FormDesc::FromFormId(formId, GetParent()->espmFiles); }, mode); + + auto refrId = GetFormId(); + if (parent->HasEspm() && refrId < 0xff000000 && + !dynamic_cast(this)) { + auto lookupRes = parent->GetEspm().GetBrowser().LookupById(refrId); + auto data = espm::GetData(refrId, parent); + for (auto& info : data.activationParents) { + auto activationParent = lookupRes.ToGlobalId(info.refrId); + + // Using WorldState for that, because we don't want search (potentially + // load) other references during OnInit + parent->activationChildsByActivationParent[activationParent].insert( + { refrId, info.delay }); + } + } } bool MpObjectReference::IsLocationSavingNeeded() const @@ -1147,11 +1214,11 @@ void MpObjectReference::ProcessActivate(MpObjectReference& activationSource) auto teleportWorldOrCell = destination.ToGlobalId( GetWorldOrCell(loader.GetBrowser(), destinationRecord)); - static const auto g_pi = std::acos(-1.f); + static const auto kPi = std::acos(-1.f); const auto& pos = teleport->pos; - const float rot[] = { teleport->rotRadians[0] / g_pi * 180, - teleport->rotRadians[1] / g_pi * 180, - teleport->rotRadians[2] / g_pi * 180 }; + const float rot[] = { teleport->rotRadians[0] / kPi * 180, + teleport->rotRadians[1] / kPi * 180, + teleport->rotRadians[2] / kPi * 180 }; TeleportMessage msg; msg.idx = activationSource.GetIdx(); @@ -1204,6 +1271,38 @@ void MpObjectReference::ProcessActivate(MpObjectReference& activationSource) } } +void MpObjectReference::ActivateChilds() +{ + auto worldState = GetParent(); + if (!worldState) { + return; + } + + auto myFormId = GetFormId(); + + for (auto& pair : worldState->activationChildsByActivationParent[myFormId]) { + auto childRefrId = pair.first; + auto delay = pair.second; + + auto delayMs = Viet::TimeUtils::To(delay); + worldState->SetTimer(delayMs).Then([worldState, childRefrId, + myFormId](Viet::Void) { + auto childRefr = std::dynamic_pointer_cast( + worldState->LookupFormById(childRefrId)); + if (!childRefr) { + spdlog::warn("MpObjectReference::ActivateChilds {:x} - Bad/missing " + "activation child {:x}", + myFormId, childRefrId); + return; + } + + // Not sure about activationSource and defaultProcessingOnly in this + // case I'll try to keep vanilla scripts working + childRefr->Activate(worldState->GetFormAt(myFormId)); + }); + } +} + bool MpObjectReference::MpApiOnActivate(MpObjectReference& caster) { simdjson::dom::parser parser; @@ -1251,12 +1350,14 @@ void MpObjectReference::UnsubscribeFromAll() void MpObjectReference::InitScripts() { auto baseId = GetBaseId(); - if (!baseId || !GetParent()->espm) + if (!baseId || !GetParent()->espm) { return; + } auto scriptStorage = GetParent()->GetScriptStorage(); - if (!scriptStorage) + if (!scriptStorage) { return; + } auto& compressedFieldsCache = GetParent()->GetEspmCache(); @@ -1266,14 +1367,36 @@ void MpObjectReference::InitScripts() auto base = br.LookupById(baseId); auto refr = br.LookupById(GetFormId()); for (auto record : { base.rec, refr.rec }) { - if (!record) + if (!record) { continue; - espm::ScriptData scriptData; - record->GetScriptData(&scriptData, compressedFieldsCache); + } + + std::optional scriptData; + + if (record == base.rec && record->GetType() == "NPC_") { + auto baseId = base.ToGlobalId(base.rec->GetId()); + if (auto actor = dynamic_cast(this)) { + auto& templateChain = actor->GetTemplateChain(); + scriptData = EvaluateTemplate( + GetParent(), baseId, templateChain, + [&compressedFieldsCache](const auto& npcLookupRes, + const auto& npcData) { + espm::ScriptData scriptData; + npcLookupRes.rec->GetScriptData(&scriptData, + compressedFieldsCache); + return scriptData; + }); + } + } + + if (!scriptData) { + scriptData = espm::ScriptData(); + record->GetScriptData(&*scriptData, compressedFieldsCache); + } auto& scriptsInStorage = GetParent()->GetScriptStorage()->ListScripts(false); - for (auto& script : scriptData.scripts) { + for (auto& script : scriptData->scripts) { if (scriptsInStorage.count( { script.scriptName.begin(), script.scriptName.end() })) { @@ -1287,8 +1410,28 @@ void MpObjectReference::InitScripts() } } + // A hardcoded hack to remove all scripts except SweetPie scripts from + // exterior objects + if (GetParent() && GetParent()->disableVanillaScriptsInExterior && + GetFormId() < 0x05000000) { + auto cellOrWorld = GetCellOrWorld().ToFormId(GetParent()->espmFiles); + auto lookupRes = + GetParent()->GetEspm().GetBrowser().LookupById(cellOrWorld); + if (lookupRes.rec && lookupRes.rec->GetType() == "WRLD") { + spdlog::info("Skipping non-Sweet scripts for exterior form {:x}"); + scriptNames.erase(std::remove_if(scriptNames.begin(), scriptNames.end(), + [](const std::string& val) { + auto kPrefix = "Sweet"; + bool startsWith = val.size() >= 5 && + !memcmp(kPrefix, val.data(), 5); + return !startsWith; + }), + scriptNames.end()); + } + } + if (!scriptNames.empty()) { - pImpl->scriptState.reset(new ScriptState); + pImpl->scriptState = std::make_unique(); std::vector scriptInfo; for (auto& scriptName : scriptNames) { @@ -1344,16 +1487,22 @@ void MpObjectReference::SendOpenContainer(uint32_t targetId) } std::vector GetOutfitObjects( - const espm::CombineBrowser& br, const espm::LookupResult& lookupRes, - espm::CompressedFieldsCache& compressedFieldsCache) + WorldState* worldState, const std::vector& templateChain, + const espm::LookupResult& lookupRes) { + auto& compressedFieldsCache = worldState->GetEspmCache(); + std::vector res; if (auto baseNpc = espm::Convert(lookupRes.rec)) { - auto data = baseNpc->GetData(compressedFieldsCache); - - auto outfitId = lookupRes.ToGlobalId(data.defaultOutfitId); - auto outfit = espm::Convert(br.LookupById(outfitId).rec); + auto baseId = lookupRes.ToGlobalId(lookupRes.rec->GetId()); + auto outfitId = EvaluateTemplate( + worldState, baseId, templateChain, + [](const auto& npcLookupRes, const auto& npcData) { + return npcLookupRes.ToGlobalId(npcData.defaultOutfitId); + }); + auto outfit = espm::Convert( + worldState->GetEspm().GetBrowser().LookupById(outfitId).rec); auto outfitData = outfit ? outfit->GetData(compressedFieldsCache) : espm::OTFT::Data(); @@ -1366,16 +1515,22 @@ std::vector GetOutfitObjects( } std::vector GetInventoryObjects( - const espm::CombineBrowser& br, const espm::LookupResult& lookupRes, - espm::CompressedFieldsCache& compressedFieldsCache) + WorldState* worldState, const std::vector& templateChain, + const espm::LookupResult& lookupRes) { + auto& compressedFieldsCache = worldState->GetEspmCache(); + auto baseContainer = espm::Convert(lookupRes.rec); - if (baseContainer) + if (baseContainer) { return baseContainer->GetData(compressedFieldsCache).objects; + } auto baseNpc = espm::Convert(lookupRes.rec); if (baseNpc) { - return baseNpc->GetData(compressedFieldsCache).objects; + auto baseId = lookupRes.ToGlobalId(lookupRes.rec->GetId()); + return EvaluateTemplate( + worldState, baseId, templateChain, + [](const auto&, const auto& npcData) { return npcData.objects; }); } return {}; @@ -1393,8 +1548,9 @@ void MpObjectReference::AddContainerObject( auto map = LeveledListUtils::EvaluateListRecurse( espm.GetBrowser(), formLookupRes, kCountMult, kPlayerCharacterLevel, chanceNoneOverride.get()); - for (auto& p : map) + for (auto& p : map) { (*itemsToAdd)[p.first] += p.second; + } } else { (*itemsToAdd)[entry.formId] += entry.count; } @@ -1402,32 +1558,38 @@ void MpObjectReference::AddContainerObject( void MpObjectReference::EnsureBaseContainerAdded(espm::Loader& espm) { - if (ChangeForm().baseContainerAdded) + if (ChangeForm().baseContainerAdded) { return; + } auto worldState = GetParent(); - if (!worldState) + if (!worldState) { return; + } + + auto actor = dynamic_cast(this); + const std::vector kEmptyTemplateChain; + const std::vector& templateChain = + actor ? actor->GetTemplateChain() : kEmptyTemplateChain; auto lookupRes = espm.GetBrowser().LookupById(GetBaseId()); std::map itemsToAdd, itemsToEquip; - auto inventoryObjects = GetInventoryObjects(espm.GetBrowser(), lookupRes, - worldState->GetEspmCache()); + auto inventoryObjects = + GetInventoryObjects(GetParent(), templateChain, lookupRes); for (auto& entry : inventoryObjects) { AddContainerObject(entry, &itemsToAdd); } - auto outfitObjects = - GetOutfitObjects(espm.GetBrowser(), lookupRes, worldState->GetEspmCache()); + auto outfitObjects = GetOutfitObjects(GetParent(), templateChain, lookupRes); for (auto& entry : outfitObjects) { AddContainerObject(entry, &itemsToAdd); AddContainerObject(entry, &itemsToEquip); } - if (auto actor = dynamic_cast(this)) { + if (actor) { Equipment eq; - for (auto p : itemsToEquip) { + for (auto& p : itemsToEquip) { Inventory::Entry e; e.baseId = p.first; e.count = p.second; @@ -1438,8 +1600,9 @@ void MpObjectReference::EnsureBaseContainerAdded(espm::Loader& espm) } std::vector entries; - for (auto& p : itemsToAdd) + for (auto& p : itemsToAdd) { entries.push_back({ p.first, p.second }); + } AddItems(entries); if (!ChangeForm().baseContainerAdded) { @@ -1502,8 +1665,9 @@ void MpObjectReference::SendPropertyTo(const IMessageBase& preparedPropMsg, void MpObjectReference::BeforeDestroy() { - if (this->occupant && this->occupantDestroySink) + if (this->occupant && this->occupantDestroySink) { this->occupant->RemoveEventSink(this->occupantDestroySink); + } // Move far far away calling OnTriggerExit, unsubscribing, etc SetPos({ -1'000'000'000, 0, 0 }); diff --git a/skymp5-server/cpp/server_guest_lib/MpObjectReference.h b/skymp5-server/cpp/server_guest_lib/MpObjectReference.h index 6a3eb07e0f..fa9c96af90 100644 --- a/skymp5-server/cpp/server_guest_lib/MpObjectReference.h +++ b/skymp5-server/cpp/server_guest_lib/MpObjectReference.h @@ -139,6 +139,8 @@ class MpObjectReference static void Unsubscribe(MpObjectReference* emitter, MpObjectReference* listener); + void SetLastAnimation(const std::string& lastAnimation); + const std::set& GetListeners() const; const std::set& GetEmitters() const; @@ -193,6 +195,7 @@ class MpObjectReference void CheckInteractionAbility(MpObjectReference& ac); bool IsLocationSavingNeeded() const; void ProcessActivate(MpObjectReference& activationSource); + void ActivateChilds(); bool MpApiOnActivate(MpObjectReference& caster); bool everSubscribedOrListened = false; diff --git a/skymp5-server/cpp/server_guest_lib/PapyrusDebug.cpp b/skymp5-server/cpp/server_guest_lib/PapyrusDebug.cpp deleted file mode 100644 index dee2038f64..0000000000 --- a/skymp5-server/cpp/server_guest_lib/PapyrusDebug.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include "PapyrusDebug.h" - -#include "MpActor.h" -#include "MpFormGameObject.h" - -VarValue PapyrusDebug::SendAnimationEvent( - VarValue self, const std::vector& arguments) -{ - auto targetActor = GetFormPtr(arguments[0]); - if (targetActor) { - auto funcName = "SendAnimationEvent"; - auto s = SpSnippetFunctionGen::SerializeArguments(arguments, targetActor); - SpSnippet(GetName(), funcName, s.data()).Execute(targetActor); - } - return VarValue::None(); -} diff --git a/skymp5-server/cpp/server_guest_lib/PapyrusMessage.cpp b/skymp5-server/cpp/server_guest_lib/PapyrusMessage.cpp deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/skymp5-server/cpp/server_guest_lib/PapyrusSkymp.cpp b/skymp5-server/cpp/server_guest_lib/PapyrusSkymp.cpp deleted file mode 100644 index 648d65a79e..0000000000 --- a/skymp5-server/cpp/server_guest_lib/PapyrusSkymp.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include "PapyrusSkymp.h" - -#include "HeuristicPolicy.h" -#include "MpFormGameObject.h" - -VarValue PapyrusSkymp::SetDefaultActor(VarValue self, - const std::vector& arguments) -{ - auto heuristicPolicy = std::dynamic_pointer_cast(policy); - if (!heuristicPolicy) - throw std::runtime_error( - "Current compatibility policy doesn't support SetDefaultActor"); - - if (arguments.size() >= 1) - heuristicPolicy->SetDefaultActor(self.GetMetaStackId(), - GetFormPtr(arguments[0])); - - return VarValue::None(); -} diff --git a/skymp5-server/cpp/server_guest_lib/PartOne.h b/skymp5-server/cpp/server_guest_lib/PartOne.h index 59344aee40..52c702029b 100644 --- a/skymp5-server/cpp/server_guest_lib/PartOne.h +++ b/skymp5-server/cpp/server_guest_lib/PartOne.h @@ -1,7 +1,6 @@ #pragma once #include "AnimationSystem.h" #include "GamemodeApi.h" -#include "ISaveStorage.h" #include "MpActor.h" #include "Networking.h" #include "NiPoint3.h" @@ -10,6 +9,7 @@ #include "WorldState.h" #include "formulas/IDamageFormula.h" #include "libespm/Loader.h" +#include "save_storages/ISaveStorage.h" #include #include #include diff --git a/skymp5-server/cpp/server_guest_lib/ScriptStorage.cpp b/skymp5-server/cpp/server_guest_lib/ScriptStorage.cpp deleted file mode 100644 index 52eb446406..0000000000 --- a/skymp5-server/cpp/server_guest_lib/ScriptStorage.cpp +++ /dev/null @@ -1,169 +0,0 @@ -#include "ScriptStorage.h" -#include -#include -#include -#include -#include - -namespace { -std::string GetFileName(const std::string& path) -{ - std::string s = path; - while (s.find('/') != s.npos || s.find('\\') != s.npos) { - while (s.find('/') != s.npos) - s = { s.begin() + s.find('/') + 1, s.end() }; - while (s.find('\\') != s.npos) - s = { s.begin() + s.find('\\') + 1, s.end() }; - } - return s; -} - -std::string RemoveExtension(std::string s) -{ - const std::regex e(".*\\.pex"); - if (std::regex_match(s, e)) { - s = { s.begin(), s.end() - strlen(".pex") }; - return s; - } - return ""; -} - -std::set GetScriptsInDirectory(std::string pexDir) -{ - std::set scripts; - - for (auto& p : std::filesystem::directory_iterator(pexDir)) { - if (p.is_directory()) - continue; - - std::string s = GetFileName(p.path().string()); - if (auto fileNameWe = RemoveExtension(s); !fileNameWe.empty()) - scripts.insert({ fileNameWe.begin(), fileNameWe.end() }); - } - - return scripts; -} -} - -DirectoryScriptStorage::DirectoryScriptStorage(const std::string& pexDirPath_) - : pexDir(pexDirPath_) -{ - scripts = GetScriptsInDirectory(pexDir); -} - -std::vector DirectoryScriptStorage::GetScriptPex( - const char* scriptName) -{ - const auto path = - std::filesystem::path(pexDir) / (scriptName + std::string(".pex")); - - if (!std::filesystem::exists(path)) { - spdlog::trace("DirectoryScriptStorage::GetScriptPex - Not found {} (file " - "doesn't exist)", - scriptName); - return {}; - } - - std::ifstream f(path, std::ios::binary); - if (!f.is_open()) { - throw std::runtime_error(path.string() + " is failed to open"); - } - std::vector buffer(std::istreambuf_iterator(f), {}); - - if (buffer.empty()) { - spdlog::trace( - "DirectoryScriptStorage::GetScriptPex - Not found {} (file is empty)", - scriptName); - return {}; - } - - spdlog::trace("DirectoryScriptStorage::GetScriptPex - Found {}", scriptName); - return buffer; -} - -const std::set& DirectoryScriptStorage::ListScripts( - bool forceReloadScripts) -{ - if (forceReloadScripts) { - scripts = GetScriptsInDirectory(pexDir); - } - return scripts; -} - -CMRC_DECLARE(server_standard_scripts); - -AssetsScriptStorage::AssetsScriptStorage() -{ - try { - auto fileSystem = cmrc::server_standard_scripts::get_filesystem(); - for (auto&& entry : fileSystem.iterate_directory("standard_scripts")) { - cmrc::file file = - fileSystem.open("standard_scripts/" + entry.filename()); - const uint8_t* begin = reinterpret_cast(file.begin()); - const uint8_t* end = begin + file.size(); - std::vector pex(begin, end); - auto nameWithoutExtension = - std::filesystem::path(entry.filename()).stem().string(); - scripts.insert( - { nameWithoutExtension.begin(), nameWithoutExtension.end() }); - scriptPex[{ nameWithoutExtension.begin(), nameWithoutExtension.end() }] = - pex; - } - } catch (std::exception& e) { - auto dir = - cmrc::server_standard_scripts::get_filesystem().iterate_directory(""); - std::stringstream ss; - ss << e.what() << std::endl << std::endl; - ss << "Root directory contents is: " << std::endl; - for (auto&& entry : dir) { - ss << entry.filename() << std::endl; - } - throw std::runtime_error(ss.str()); - } -} - -std::vector AssetsScriptStorage::GetScriptPex(const char* scriptName) -{ - auto it = scriptPex.find(scriptName); - if (it == scriptPex.end()) { - spdlog::trace("AssetsScriptStorage::GetScriptPex - Not found {}", - scriptName); - return {}; - } - spdlog::trace("AssetsScriptStorage::GetScriptPex - Found {}", scriptName); - return it->second; -} - -const std::set& AssetsScriptStorage::ListScripts(bool) -{ - return scripts; -} - -CombinedScriptStorage::CombinedScriptStorage( - std::vector> scriptStorages) -{ - this->scriptStorages = std::move(scriptStorages); -} - -std::vector CombinedScriptStorage::GetScriptPex( - const char* scriptName) -{ - for (auto& storage : scriptStorages) { - auto result = storage->GetScriptPex(scriptName); - if (!result.empty()) { - return result; - } - } - return {}; -} - -const std::set& CombinedScriptStorage::ListScripts( - bool forceReloadScripts) -{ - scripts.clear(); - for (auto& storage : scriptStorages) { - auto& result = storage->ListScripts(forceReloadScripts); - scripts.insert(result.begin(), result.end()); - } - return scripts; -} diff --git a/skymp5-server/cpp/server_guest_lib/ScriptStorage.h b/skymp5-server/cpp/server_guest_lib/ScriptStorage.h deleted file mode 100644 index 9c882ca1d5..0000000000 --- a/skymp5-server/cpp/server_guest_lib/ScriptStorage.h +++ /dev/null @@ -1,63 +0,0 @@ -#pragma once -#include "papyrus-vm/CIString.h" -#include -#include -#include -#include -#include -#include - -class IScriptStorage -{ -public: - virtual ~IScriptStorage() = default; - - virtual std::vector GetScriptPex(const char* scriptName) = 0; - - virtual const std::set& ListScripts(bool forceReloadScripts) = 0; -}; - -class DirectoryScriptStorage : public IScriptStorage -{ -public: - DirectoryScriptStorage(const std::string& pexDir_); - - std::vector GetScriptPex(const char* scriptName) override; - - const std::set& ListScripts(bool forceReloadScripts) override; - -private: - const std::string pexDir; - std::set scripts; -}; - -class AssetsScriptStorage : public IScriptStorage -{ -public: - AssetsScriptStorage(); - - std::vector GetScriptPex(const char* scriptName) override; - - const std::set& ListScripts(bool forceReloadScripts) override; - -private: - std::set scripts; - CIMap> scriptPex; -}; - -class CombinedScriptStorage : public IScriptStorage -{ -public: - // Load order matters. But unlike mods, scriptStorages.front() will be - // checked first - CombinedScriptStorage( - std::vector> scriptStorages); - - std::vector GetScriptPex(const char* scriptName) override; - - const std::set& ListScripts(bool forceReloadScripts) override; - -private: - std::vector> scriptStorages; - std::set scripts; -}; diff --git a/skymp5-server/cpp/server_guest_lib/ScriptVariablesHolder.cpp b/skymp5-server/cpp/server_guest_lib/ScriptVariablesHolder.cpp index 728f87e643..2d234ad2e5 100644 --- a/skymp5-server/cpp/server_guest_lib/ScriptVariablesHolder.cpp +++ b/skymp5-server/cpp/server_guest_lib/ScriptVariablesHolder.cpp @@ -1,10 +1,10 @@ #include "ScriptVariablesHolder.h" -#include "EspmGameObject.h" -#include "MpFormGameObject.h" #include "WorldState.h" #include "libespm/Property.h" #include "papyrus-vm/Utils.h" +#include "script_objects/EspmGameObject.h" +#include "script_objects/MpFormGameObject.h" #include @@ -135,43 +135,49 @@ VarValue ScriptVariablesHolder::CastPrimitivePropertyValue( } auto propValueFormIdGlobal = toGlobalId(propValue.formId); spdlog::trace( - "CastPrimitivePropertyValue - Prop to global id {:x} -> {:x}", - propValue.formId, propValueFormIdGlobal); + "CastPrimitivePropertyValue {} - Prop to global id {:x} -> {:x}", + myScriptName, propValue.formId, propValueFormIdGlobal); auto& gameObject = st.objectsHolder[propValueFormIdGlobal]; if (!gameObject) { auto lookupResult = br.LookupById(propValueFormIdGlobal); if (!lookupResult.rec) { spdlog::error( - "CastPrimitivePropertyValue - Record with id {:x} not found", - propValueFormIdGlobal); + "CastPrimitivePropertyValue {} - Record with id {:x} not found", + myScriptName, propValueFormIdGlobal); } else { auto type = lookupResult.rec->GetType(); if (type == espm::REFR::kType || type == espm::ACHR::kType) { if (worldState) { - auto& form = worldState->LookupFormById(propValueFormIdGlobal); + std::stringstream traceStream; + auto& form = worldState->LookupFormById(propValueFormIdGlobal, + &traceStream); if (form != nullptr) { gameObject = std::make_shared(form.get()); - spdlog::trace("CastPrimitivePropertyValue - Created {} " + spdlog::trace("CastPrimitivePropertyValue {} - Created {} " "(MpFormGameObject) property with id {:x}", - type.ToString(), propValueFormIdGlobal); + myScriptName, type.ToString(), + propValueFormIdGlobal); } else { spdlog::warn( - "CastPrimitivePropertyValue - Unable to create {} " + "CastPrimitivePropertyValue {} - Unable to create {} " "(MpFormGameObject) property with id {:x}, form " - "not found in the world", - type.ToString(), propValueFormIdGlobal); + "not found in the world. LookupFormById trace:\n{}", + myScriptName, type.ToString(), propValueFormIdGlobal, + traceStream.str()); } } else { - spdlog::error("CastPrimitivePropertyValue - Unable to create {} " - "(MpFormGameObject) property with id {:x}, null " - "WorldState", - type.ToString(), propValueFormIdGlobal); + spdlog::error( + "CastPrimitivePropertyValue {} - Unable to create {} " + "(MpFormGameObject) property with id {:x}, null " + "WorldState", + myScriptName, type.ToString(), propValueFormIdGlobal); } } else { gameObject = std::make_shared(lookupResult); - spdlog::trace("CastPrimitivePropertyValue - Created {} " + spdlog::trace("CastPrimitivePropertyValue {} - Created {} " "(EspmGameObject) property with id {:x}", - type.ToString(), propValueFormIdGlobal); + myScriptName, type.ToString(), + propValueFormIdGlobal); } } } diff --git a/skymp5-server/cpp/server_guest_lib/ScriptVariablesHolder.h b/skymp5-server/cpp/server_guest_lib/ScriptVariablesHolder.h index 5c2e8cea4d..7dfb245265 100644 --- a/skymp5-server/cpp/server_guest_lib/ScriptVariablesHolder.h +++ b/skymp5-server/cpp/server_guest_lib/ScriptVariablesHolder.h @@ -41,23 +41,22 @@ class ScriptVariablesHolder : public IVariablesHolder std::unordered_map> objectsHolder; }; - static VarValue CastPrimitivePropertyValue( + VarValue CastPrimitivePropertyValue( const espm::CombineBrowser& br, ScriptsCache& st, const espm::Property::Value& propValue, espm::Property::Type type, const std::function& toGlobalId, WorldState* worldState); - static void CastProperty(const espm::CombineBrowser& br, - const espm::Property& prop, VarValue* out, - ScriptsCache* scriptsCache, - const std::function& toGlobalId, - WorldState* worldState); + void CastProperty(const espm::CombineBrowser& br, const espm::Property& prop, + VarValue* out, ScriptsCache* scriptsCache, + const std::function& toGlobalId, + WorldState* worldState); static espm::Property::Type GetElementType(espm::Property::Type arrayType); espm::LookupResult baseRecordWithScripts; espm::LookupResult refrRecordWithScripts; - const std::string myScriptName; + std::string myScriptName; const espm::CombineBrowser* const browser; std::unique_ptr> vars; VarValue state; diff --git a/skymp5-server/cpp/server_guest_lib/SpSnippet.cpp b/skymp5-server/cpp/server_guest_lib/SpSnippet.cpp index 26618b18c6..a9f99b4417 100644 --- a/skymp5-server/cpp/server_guest_lib/SpSnippet.cpp +++ b/skymp5-server/cpp/server_guest_lib/SpSnippet.cpp @@ -14,6 +14,14 @@ SpSnippet::SpSnippet(const char* cl_, const char* func_, const char* args_, Viet::Promise SpSnippet::Execute(MpActor* actor) { + auto worldState = actor->GetParent(); + if (!actor->IsCreatedAsPlayer()) { + // Return promise that never resolves in this case + // TODO: somehow detect user instead as this breaks potential feature of + // transferring user into an npc actor + return Viet::Promise(); + } + Viet::Promise promise; auto snippetIdx = actor->NextSnippetIndex(promise); diff --git a/skymp5-server/cpp/server_guest_lib/SpSnippet.h b/skymp5-server/cpp/server_guest_lib/SpSnippet.h index c53e43169e..ff39b3bb74 100644 --- a/skymp5-server/cpp/server_guest_lib/SpSnippet.h +++ b/skymp5-server/cpp/server_guest_lib/SpSnippet.h @@ -11,6 +11,7 @@ class SpSnippet public: SpSnippet(const char* cl_, const char* func_, const char* args_, uint32_t selfId_ = 0); + Viet::Promise Execute(MpActor* actor); private: diff --git a/skymp5-server/cpp/server_guest_lib/SpSnippetFunctionGen.cpp b/skymp5-server/cpp/server_guest_lib/SpSnippetFunctionGen.cpp index 9cc55a0e58..054f176a9f 100644 --- a/skymp5-server/cpp/server_guest_lib/SpSnippetFunctionGen.cpp +++ b/skymp5-server/cpp/server_guest_lib/SpSnippetFunctionGen.cpp @@ -1,9 +1,9 @@ #include "SpSnippetFunctionGen.h" -#include "EspmGameObject.h" -#include "MpFormGameObject.h" #include "SpSnippet.h" #include "WorldState.h" +#include "script_objects/EspmGameObject.h" +#include "script_objects/MpFormGameObject.h" #include #include diff --git a/skymp5-server/cpp/server_guest_lib/WorldState.cpp b/skymp5-server/cpp/server_guest_lib/WorldState.cpp index fe6605caab..8f10bcdc0f 100644 --- a/skymp5-server/cpp/server_guest_lib/WorldState.cpp +++ b/skymp5-server/cpp/server_guest_lib/WorldState.cpp @@ -1,47 +1,22 @@ #include "WorldState.h" #include "FormCallbacks.h" -#include "HeuristicPolicy.h" -#include "ISaveStorage.h" +#include "LocationalDataUtils.h" #include "MpActor.h" #include "MpChangeForms.h" -#include "MpFormGameObject.h" #include "MpObjectReference.h" -#include "PapyrusActor.h" -#include "PapyrusDebug.h" -#include "PapyrusEffectShader.h" -#include "PapyrusForm.h" -#include "PapyrusFormList.h" -#include "PapyrusGame.h" -#include "PapyrusKeyword.h" -#include "PapyrusMessage.h" -#include "PapyrusObjectReference.h" -#include "PapyrusSkymp.h" -#include "PapyrusUtility.h" #include "ScopedTask.h" -#include "ScriptStorage.h" #include "Timer.h" #include "libespm/GroupUtils.h" #include "papyrus-vm/Reader.h" +#include "save_storages/ISaveStorage.h" +#include "script_classes/PapyrusClassesFactory.h" +#include "script_compatibility_policies/PapyrusCompatibilityPolicyFactory.h" +#include "script_storages/IScriptStorage.h" #include #include #include #include -namespace { -inline const NiPoint3& GetPos(const espm::REFR::LocationalData* locationalData) -{ - return *reinterpret_cast(locationalData->pos); -} - -inline NiPoint3 GetRot(const espm::REFR::LocationalData* locationalData) -{ - static const auto g_pi = std::acos(-1.f); - return { locationalData->rotRadians[0] / g_pi * 180.f, - locationalData->rotRadians[1] / g_pi * 180.f, - locationalData->rotRadians[2] / g_pi * 180.f }; -} -} - struct WorldState::Impl { std::unordered_map changes; @@ -50,7 +25,7 @@ struct WorldState::Impl bool saveStorageBusy = false; std::shared_ptr vm; uint32_t nextId = 0xff000000; - std::shared_ptr policy; + std::shared_ptr policy; std::unordered_map changeFormsForDeferredLoad; bool chunkLoadingInProgress = false; bool formLoadingInProgress = false; @@ -65,7 +40,7 @@ WorldState::WorldState() logger.reset(new spdlog::logger("empty logger")); pImpl.reset(new Impl); - pImpl->policy.reset(new HeuristicPolicy(logger, this)); + pImpl->policy = PapyrusCompatibilityPolicyFactory::Create(this); } void WorldState::Clear() @@ -219,26 +194,55 @@ void WorldState::RequestSave(MpObjectReference& ref) } } -const std::shared_ptr& WorldState::LookupFormById(uint32_t formId) +const std::shared_ptr& WorldState::LookupFormById( + uint32_t formId, std::stringstream* optionalOutTrace) { static const std::shared_ptr kNullForm; + if (optionalOutTrace) { + *optionalOutTrace << "searching for " << std::hex << formId << std::endl; + } + auto it = forms.find(formId); if (it == forms.end()) { if (formId < 0xff000000) { - if (LoadForm(formId)) { + if (LoadForm(formId, optionalOutTrace)) { it = forms.find(formId); - return it == forms.end() ? kNullForm : it->second; + if (it != forms.end()) { + if (optionalOutTrace) { + *optionalOutTrace << "found after successful LoadForm" << std::hex + << formId << std::endl; + } + return it->second; + } + if (optionalOutTrace) { + *optionalOutTrace << "not found after successful LoadForm" + << std::hex << formId << std::endl; + } + return kNullForm; + } else { + if (optionalOutTrace) { + *optionalOutTrace << "LoadForm returned false " << std::hex << formId + << std::endl; + } } } + if (optionalOutTrace) { + *optionalOutTrace << "not found " << std::hex << formId << std::endl; + } return kNullForm; } + + if (optionalOutTrace) { + *optionalOutTrace << "found " << std::hex << formId << std::endl; + } return it->second; } bool WorldState::AttachEspmRecord(const espm::CombineBrowser& br, const espm::RecordHeader* record, - const espm::IdMapping& mapping) + const espm::IdMapping& mapping, + std::stringstream* optionalOutTrace) { auto& cache = GetEspmCache(); // this place is a hotpath. @@ -251,6 +255,10 @@ bool WorldState::AttachEspmRecord(const espm::CombineBrowser& br, espm::LookupResult base = br.LookupById(baseId); if (!base.rec) { logger->info("baseId {} {}", baseId, static_cast(base.rec)); + if (optionalOutTrace) { + *optionalOutTrace << fmt::format( + "AttachEspmRecord - base record not found {:x} \n", baseId); + } return false; } @@ -267,6 +275,10 @@ bool WorldState::AttachEspmRecord(const espm::CombineBrowser& br, if (!isNpc && !isFurniture && !isActivator && !espm::utils::IsItem(t) && !isDoor && !isContainer && !isFlor && !isTree) { + if (optionalOutTrace) { + *optionalOutTrace << fmt::format( + "AttachEspmRecord - the server skips base type {} \n", t.ToString()); + } return false; } @@ -275,6 +287,10 @@ bool WorldState::AttachEspmRecord(const espm::CombineBrowser& br, auto* achr = reinterpret_cast(record); startsDead = achr->StartsDead(); if (startsDead) { + if (optionalOutTrace) { + *optionalOutTrace << fmt::format( + "AttachEspmRecord - the server skips dead actors\n"); + } return false; // TODO: Load dead references } } @@ -287,14 +303,26 @@ bool WorldState::AttachEspmRecord(const espm::CombineBrowser& br, }; if (refr->GetFlags() & InitiallyDisabled) { + if (optionalOutTrace) { + *optionalOutTrace << fmt::format( + "AttachEspmRecord - the server skips initially disabled references\n"); + } return false; } if (refr->GetFlags() & DeletedRecord) { + if (optionalOutTrace) { + *optionalOutTrace << fmt::format( + "AttachEspmRecord - the server skips deleted references\n"); + } return false; } if (!npcEnabled && isNpc) { + if (optionalOutTrace) { + *optionalOutTrace << fmt::format( + "AttachEspmRecord - the server skips npcs: npcEnabled = false\n"); + } return false; } @@ -302,12 +330,22 @@ bool WorldState::AttachEspmRecord(const espm::CombineBrowser& br, if (NpcSourceFilesOverriden() && !IsNpcAllowed(baseId)) { spdlog::trace("Skip NPC loading, it is not allowed. baseId {:#x}", baseId); + if (optionalOutTrace) { + *optionalOutTrace + << fmt::format("Skip NPC loading, it is not allowed. baseId {:#x}", + baseId) + << std::endl; + } return false; } auto npcData = reinterpret_cast(base.rec)->GetData(cache); - if (npcData.isEssential || npcData.isProtected) { + if (npcData.isEssential || npcData.isProtected || npcData.isUnique) { + if (optionalOutTrace) { + *optionalOutTrace << fmt::format("Skip NPC due to its flags") + << std::endl; + } return false; } @@ -331,6 +369,10 @@ bool WorldState::AttachEspmRecord(const espm::CombineBrowser& br, if (it != factionFormIds.end()) { logger->info("Skipping actor {:#x} because it's in faction {:#x}", record->GetId(), *it); + if (optionalOutTrace) { + *optionalOutTrace << fmt::format("Skip NPC due to faction") + << std::endl; + } return false; } } @@ -343,6 +385,10 @@ bool WorldState::AttachEspmRecord(const espm::CombineBrowser& br, espm::utils::GetMappedId(GetWorldOrCell(br, record), mapping); if (!worldOrCell) { logger->error("Anomaly: refr without world/cell"); + if (optionalOutTrace) { + *optionalOutTrace << fmt::format("Anomaly: refr without world/cell") + << std::endl; + } return false; } @@ -365,6 +411,14 @@ bool WorldState::AttachEspmRecord(const espm::CombineBrowser& br, "baseId " "{:#x}, espmFiles size: {}", baseId, espmFiles.size()); + if (optionalOutTrace) { + *optionalOutTrace + << fmt::format("NPC's idx is greater than espmFiles.size(). NPC's" + "baseId " + "{:#x}, espmFiles size: {}", + baseId, espmFiles.size()) + << std::endl; + } return false; } auto it = npcSettings.find(espmFiles[npcFileIdx]); @@ -390,6 +444,15 @@ bool WorldState::AttachEspmRecord(const espm::CombineBrowser& br, "rules applied in server settings: spanwInInterior={}, " "spawnInExterior={}, NPC location: exterior={}, interior={}", spawnInInterior, spawnInExterior, isExterior, isInterior); + if (optionalOutTrace) { + *optionalOutTrace + << fmt::format( + "Unable to spawn npc because of " + "rules applied in server settings: spanwInInterior={}, " + "spawnInExterior={}, NPC location: exterior={}, interior={}", + spawnInInterior, spawnInExterior, isExterior, isInterior) + << std::endl; + } return false; } } @@ -402,15 +465,22 @@ bool WorldState::AttachEspmRecord(const espm::CombineBrowser& br, reinterpret_cast(existing->second.get()); if (locationalData) { - existingAsRefr->SetPosAndAngleSilent(GetPos(locationalData), - GetRot(locationalData)); + existingAsRefr->SetPosAndAngleSilent( + LocationalDataUtils::GetPos(locationalData), + LocationalDataUtils::GetRot(locationalData)); - assert(existingAsRefr->GetPos() == NiPoint3(GetPos(locationalData))); + assert(existingAsRefr->GetPos() == + NiPoint3(LocationalDataUtils::GetPos(locationalData))); } } else { if (!locationalData) { logger->error("Anomaly: refr without locationalData"); + if (optionalOutTrace) { + *optionalOutTrace << fmt::format( + "Anomaly: refr without locationalData") + << std::endl; + } return false; } @@ -423,7 +493,8 @@ bool WorldState::AttachEspmRecord(const espm::CombineBrowser& br, auto typeStr = t.ToString(); std::unique_ptr form; LocationalData formLocationalData = { - GetPos(locationalData), GetRot(locationalData), + LocationalDataUtils::GetPos(locationalData), + LocationalDataUtils::GetRot(locationalData), FormDesc::FromFormId(worldOrCell, espmFiles) }; if (!isNpc) { @@ -435,22 +506,33 @@ bool WorldState::AttachEspmRecord(const espm::CombineBrowser& br, new MpActor(formLocationalData, formCallbacksFactory(), baseId)); } AddForm(std::move(form), formId, true); + // Do not TriggerFormInitEvent here, doing it later after changeForm apply } + if (optionalOutTrace) { + *optionalOutTrace << fmt::format("AttachEspmRecord returned true") + << std::endl; + } return true; } -bool WorldState::LoadForm(uint32_t formId) +bool WorldState::LoadForm(uint32_t formId, std::stringstream* optionalOutTrace) { bool atLeastOneLoaded = false; auto& br = GetEspm().GetBrowser(); auto lookupResults = br.LookupByIdAll(formId); for (auto& lookupRes : lookupResults) { auto mapping = br.GetCombMapping(lookupRes.fileIdx); - if (AttachEspmRecord(br, lookupRes.rec, *mapping)) { + bool attached = + AttachEspmRecord(br, lookupRes.rec, *mapping, optionalOutTrace); + if (attached) { atLeastOneLoaded = true; } + if (optionalOutTrace) { + *optionalOutTrace << "AttachEspmRecord " << (attached ? "true" : "false") + << std::endl; + } } if (atLeastOneLoaded) { @@ -727,20 +809,8 @@ VirtualMachine& WorldState::GetPapyrusVm() } }); - pImpl->classes.emplace_back(std::make_unique()); - pImpl->classes.emplace_back(std::make_unique()); - pImpl->classes.emplace_back(std::make_unique()); - pImpl->classes.emplace_back(std::make_unique()); - pImpl->classes.emplace_back(std::make_unique()); - pImpl->classes.emplace_back(std::make_unique()); - pImpl->classes.emplace_back(std::make_unique()); - pImpl->classes.emplace_back(std::make_unique()); - pImpl->classes.emplace_back(std::make_unique()); - pImpl->classes.emplace_back(std::make_unique()); - pImpl->classes.emplace_back(std::make_unique()); - for (auto& cl : pImpl->classes) { - cl->Register(*pImpl->vm, pImpl->policy); - } + pImpl->classes = + PapyrusClassesFactory::CreateAndRegister(*pImpl->vm, pImpl->policy); } } diff --git a/skymp5-server/cpp/server_guest_lib/WorldState.h b/skymp5-server/cpp/server_guest_lib/WorldState.h index cfa285f5d4..10d1ddda96 100644 --- a/skymp5-server/cpp/server_guest_lib/WorldState.h +++ b/skymp5-server/cpp/server_guest_lib/WorldState.h @@ -4,13 +4,13 @@ #include "GridElement.h" #include "MpChangeForms.h" #include "MpForm.h" -#include "MpFormGameObject.h" #include "MpObjectReference.h" #include "NiPoint3.h" #include "PartOneListener.h" #include "Timer.h" #include "libespm/Loader.h" #include "papyrus-vm/VirtualMachine.h" +#include "script_objects/MpFormGameObject.h" #include #include #include @@ -109,7 +109,8 @@ class WorldState wrapper); bool RemoveEffectTimer(uint32_t timerId); - const std::shared_ptr& LookupFormById(uint32_t formId); + const std::shared_ptr& LookupFormById( + uint32_t formId, std::stringstream* optionalOutTrace = nullptr); MpForm* LookupFormByIdx(int idx); @@ -217,6 +218,8 @@ class WorldState std::shared_ptr logger; std::vector> listeners; std::unordered_map hosters; + std::unordered_map> + activationChildsByActivationParent; std::vector> lastMovUpdateByIdx; @@ -227,12 +230,16 @@ class WorldState NpcSettingsEntry defaultSetting; bool enableConsoleCommandsForAll = false; + bool disableVanillaScriptsInExterior = true; + private: bool AttachEspmRecord(const espm::CombineBrowser& br, const espm::RecordHeader* record, - const espm::IdMapping& mapping); + const espm::IdMapping& mapping, + std::stringstream* optionalOutTrace = nullptr); - bool LoadForm(uint32_t formId); + bool LoadForm(uint32_t formId, + std::stringstream* optionalOutTrace = nullptr); void TickReloot(const std::chrono::system_clock::time_point& now); void TickSaveStorage(const std::chrono::system_clock::time_point& now); void TickTimers(const std::chrono::system_clock::time_point& now); diff --git a/skymp5-server/cpp/server_guest_lib/database_drivers/DatabaseFactory.cpp b/skymp5-server/cpp/server_guest_lib/database_drivers/DatabaseFactory.cpp new file mode 100644 index 0000000000..4fdca7941f --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/database_drivers/DatabaseFactory.cpp @@ -0,0 +1,46 @@ +#include "DatabaseFactory.h" + +#include +#include +#include + +#include "FileDatabase.h" +#include "MigrationDatabase.h" +#include "MongoDatabase.h" + +std::shared_ptr DatabaseFactory::Create( + nlohmann::json settings, std::shared_ptr logger) +{ + auto databaseDriver = settings.count("databaseDriver") + ? settings["databaseDriver"].get() + : std::string("file"); + + if (databaseDriver == "file") { + auto databaseName = settings.count("databaseName") + ? settings["databaseName"].get() + : std::string("world"); + + logger->info("Using file with name '" + databaseName + "'"); + return std::make_shared(databaseName, logger); + } + + if (databaseDriver == "mongodb") { + auto databaseName = settings.count("databaseName") + ? settings["databaseName"].get() + : std::string("db"); + + auto databaseUri = settings["databaseUri"].get(); + logger->info("Using mongodb with name '" + databaseName + "'"); + return std::make_shared(databaseUri, databaseName); + } + + if (databaseDriver == "migration") { + auto from = settings.at("databaseOld"); + auto to = settings.at("databaseNew"); + auto oldDatabase = Create(from, logger); + auto newDatabase = Create(to, logger); + return std::make_shared(newDatabase, oldDatabase); + } + + throw std::runtime_error("Unrecognized databaseDriver: " + databaseDriver); +} diff --git a/skymp5-server/cpp/server_guest_lib/database_drivers/DatabaseFactory.h b/skymp5-server/cpp/server_guest_lib/database_drivers/DatabaseFactory.h new file mode 100644 index 0000000000..d92aebd8d7 --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/database_drivers/DatabaseFactory.h @@ -0,0 +1,11 @@ +#pragma once +#include "IDatabase.h" +#include +#include + +class DatabaseFactory +{ +public: + static std::shared_ptr Create( + nlohmann::json settings, std::shared_ptr logger); +}; diff --git a/skymp5-server/cpp/server_guest_lib/FileDatabase.cpp b/skymp5-server/cpp/server_guest_lib/database_drivers/FileDatabase.cpp similarity index 100% rename from skymp5-server/cpp/server_guest_lib/FileDatabase.cpp rename to skymp5-server/cpp/server_guest_lib/database_drivers/FileDatabase.cpp diff --git a/skymp5-server/cpp/server_guest_lib/FileDatabase.h b/skymp5-server/cpp/server_guest_lib/database_drivers/FileDatabase.h similarity index 100% rename from skymp5-server/cpp/server_guest_lib/FileDatabase.h rename to skymp5-server/cpp/server_guest_lib/database_drivers/FileDatabase.h diff --git a/skymp5-server/cpp/server_guest_lib/IDatabase.h b/skymp5-server/cpp/server_guest_lib/database_drivers/IDatabase.h similarity index 100% rename from skymp5-server/cpp/server_guest_lib/IDatabase.h rename to skymp5-server/cpp/server_guest_lib/database_drivers/IDatabase.h diff --git a/skymp5-server/cpp/server_guest_lib/MigrationDatabase.cpp b/skymp5-server/cpp/server_guest_lib/database_drivers/MigrationDatabase.cpp similarity index 100% rename from skymp5-server/cpp/server_guest_lib/MigrationDatabase.cpp rename to skymp5-server/cpp/server_guest_lib/database_drivers/MigrationDatabase.cpp diff --git a/skymp5-server/cpp/server_guest_lib/MigrationDatabase.h b/skymp5-server/cpp/server_guest_lib/database_drivers/MigrationDatabase.h similarity index 100% rename from skymp5-server/cpp/server_guest_lib/MigrationDatabase.h rename to skymp5-server/cpp/server_guest_lib/database_drivers/MigrationDatabase.h diff --git a/skymp5-server/cpp/server_guest_lib/MongoDatabase.cpp b/skymp5-server/cpp/server_guest_lib/database_drivers/MongoDatabase.cpp similarity index 100% rename from skymp5-server/cpp/server_guest_lib/MongoDatabase.cpp rename to skymp5-server/cpp/server_guest_lib/database_drivers/MongoDatabase.cpp diff --git a/skymp5-server/cpp/server_guest_lib/MongoDatabase.h b/skymp5-server/cpp/server_guest_lib/database_drivers/MongoDatabase.h similarity index 100% rename from skymp5-server/cpp/server_guest_lib/MongoDatabase.h rename to skymp5-server/cpp/server_guest_lib/database_drivers/MongoDatabase.h diff --git a/skymp5-server/cpp/server_guest_lib/AsyncSaveStorage.cpp b/skymp5-server/cpp/server_guest_lib/save_storages/AsyncSaveStorage.cpp similarity index 100% rename from skymp5-server/cpp/server_guest_lib/AsyncSaveStorage.cpp rename to skymp5-server/cpp/server_guest_lib/save_storages/AsyncSaveStorage.cpp diff --git a/skymp5-server/cpp/server_guest_lib/AsyncSaveStorage.h b/skymp5-server/cpp/server_guest_lib/save_storages/AsyncSaveStorage.h similarity index 94% rename from skymp5-server/cpp/server_guest_lib/AsyncSaveStorage.h rename to skymp5-server/cpp/server_guest_lib/save_storages/AsyncSaveStorage.h index 176bb26b45..0966be19a4 100644 --- a/skymp5-server/cpp/server_guest_lib/AsyncSaveStorage.h +++ b/skymp5-server/cpp/server_guest_lib/save_storages/AsyncSaveStorage.h @@ -1,6 +1,6 @@ #pragma once -#include "IDatabase.h" #include "ISaveStorage.h" +#include "database_drivers/IDatabase.h" #include #include diff --git a/skymp5-server/cpp/server_guest_lib/ISaveStorage.h b/skymp5-server/cpp/server_guest_lib/save_storages/ISaveStorage.h similarity index 100% rename from skymp5-server/cpp/server_guest_lib/ISaveStorage.h rename to skymp5-server/cpp/server_guest_lib/save_storages/ISaveStorage.h diff --git a/skymp5-server/cpp/server_guest_lib/save_storages/SaveStorageFactory.cpp b/skymp5-server/cpp/server_guest_lib/save_storages/SaveStorageFactory.cpp new file mode 100644 index 0000000000..7b34d4b16b --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/save_storages/SaveStorageFactory.cpp @@ -0,0 +1,9 @@ +#include "SaveStorageFactory.h" + +#include "AsyncSaveStorage.h" + +std::shared_ptr SaveStorageFactory::Create( + std::shared_ptr db, std::shared_ptr logger) +{ + return std::make_shared(db, logger); +} diff --git a/skymp5-server/cpp/server_guest_lib/save_storages/SaveStorageFactory.h b/skymp5-server/cpp/server_guest_lib/save_storages/SaveStorageFactory.h new file mode 100644 index 0000000000..cb5589b047 --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/save_storages/SaveStorageFactory.h @@ -0,0 +1,12 @@ +#pragma once +#include "ISaveStorage.h" +#include "database_drivers/IDatabase.h" +#include +#include + +class SaveStorageFactory +{ +public: + static std::shared_ptr Create( + std::shared_ptr db, std::shared_ptr logger); +}; diff --git a/skymp5-server/cpp/server_guest_lib/IPapyrusClass.h b/skymp5-server/cpp/server_guest_lib/script_classes/IPapyrusClass.h similarity index 94% rename from skymp5-server/cpp/server_guest_lib/IPapyrusClass.h rename to skymp5-server/cpp/server_guest_lib/script_classes/IPapyrusClass.h index 702b0727b0..4ed87aee0a 100644 --- a/skymp5-server/cpp/server_guest_lib/IPapyrusClass.h +++ b/skymp5-server/cpp/server_guest_lib/script_classes/IPapyrusClass.h @@ -1,6 +1,6 @@ #pragma once -#include "IPapyrusCompatibilityPolicy.h" #include "papyrus-vm/VirtualMachine.h" +#include "script_compatibility_policies/IPapyrusCompatibilityPolicy.h" class IPapyrusClassBase { diff --git a/skymp5-server/cpp/server_guest_lib/PapyrusActor.cpp b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusActor.cpp similarity index 86% rename from skymp5-server/cpp/server_guest_lib/PapyrusActor.cpp rename to skymp5-server/cpp/server_guest_lib/script_classes/PapyrusActor.cpp index d20ff5b36b..e2297763e6 100644 --- a/skymp5-server/cpp/server_guest_lib/PapyrusActor.cpp +++ b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusActor.cpp @@ -1,7 +1,7 @@ #include "PapyrusActor.h" #include "MpActor.h" -#include "MpFormGameObject.h" +#include "script_objects/MpFormGameObject.h" #include "SpSnippetFunctionGen.h" #include "papyrus-vm/CIString.h" @@ -37,9 +37,35 @@ VarValue PapyrusActor::RestoreActorValue( { espm::ActorValue attributeName = ConvertToAV(static_cast(arguments[0])); - float modifire = static_cast(arguments[1]); + float modifier = static_cast(arguments[1]); + if (auto actor = GetFormPtr(self)) { + actor->RestoreActorValue(attributeName, modifier); + } + return VarValue(); +} + +VarValue PapyrusActor::SetActorValue(VarValue self, + const std::vector& arguments) +{ if (auto actor = GetFormPtr(self)) { - actor->RestoreActorValue(attributeName, modifire); + + // TODO: fix that at least for important AVs like attributes + // SpSnippet impl helps scripted draugrs attack, nothing more (Aggression + // var) + spdlog::warn("SetActorValue executes locally at this moment. Results will " + "not affect server calculations"); + + auto it = actor->GetParent()->hosters.find(actor->GetFormId()); + + auto serializedArgs = SpSnippetFunctionGen::SerializeArguments(arguments); + + // spsnippet don't support auto sending to host. so determining current + // hoster explicitly + SpSnippet(GetName(), "SetActorValue", serializedArgs.data(), + actor->GetFormId()) + .Execute(it == actor->GetParent()->hosters.end() + ? actor + : &actor->GetParent()->GetFormAt(it->second)); } return VarValue(); } @@ -240,6 +266,7 @@ void PapyrusActor::Register( AddMethod(vm, "PlayIdle", &PapyrusActor::PlayIdle); AddMethod(vm, "GetSitState", &PapyrusActor::GetSitState); AddMethod(vm, "RestoreActorValue", &PapyrusActor::RestoreActorValue); + AddMethod(vm, "SetActorValue", &PapyrusActor::SetActorValue); AddMethod(vm, "DamageActorValue", &PapyrusActor::DamageActorValue); AddMethod(vm, "IsEquipped", &PapyrusActor::IsEquipped); AddMethod(vm, "GetActorValuePercentage", diff --git a/skymp5-server/cpp/server_guest_lib/PapyrusActor.h b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusActor.h similarity index 91% rename from skymp5-server/cpp/server_guest_lib/PapyrusActor.h rename to skymp5-server/cpp/server_guest_lib/script_classes/PapyrusActor.h index 5967f60854..b2fd5869e8 100644 --- a/skymp5-server/cpp/server_guest_lib/PapyrusActor.h +++ b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusActor.h @@ -1,7 +1,7 @@ #pragma once -#include "EspmGameObject.h" #include "IPapyrusClass.h" #include "SpSnippetFunctionGen.h" +#include "script_objects/EspmGameObject.h" class PapyrusActor final : public IPapyrusClass { @@ -19,6 +19,9 @@ class PapyrusActor final : public IPapyrusClass VarValue RestoreActorValue(VarValue self, const std::vector& arguments); + VarValue SetActorValue(VarValue self, + const std::vector& arguments); + VarValue DamageActorValue(VarValue self, const std::vector& arguments); diff --git a/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusCell.cpp b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusCell.cpp new file mode 100644 index 0000000000..473c4eec81 --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusCell.cpp @@ -0,0 +1,15 @@ +#include "PapyrusCell.h" + +VarValue PapyrusCell::IsAttached(VarValue self, + const std::vector& arguments) +{ + return VarValue(true); // stub +} + +void PapyrusCell::Register(VirtualMachine& vm, + std::shared_ptr policy) +{ + compatibilityPolicy = policy; + + AddMethod(vm, "IsAttached", &PapyrusCell::IsAttached); +} diff --git a/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusCell.h b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusCell.h new file mode 100644 index 0000000000..d7f9ce8e91 --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusCell.h @@ -0,0 +1,17 @@ +#pragma once +#include "IPapyrusClass.h" +#include "SpSnippetFunctionGen.h" +#include "script_objects/EspmGameObject.h" + +class PapyrusCell final : public IPapyrusClass +{ +public: + const char* GetName() override { return "cell"; } + + VarValue IsAttached(VarValue self, const std::vector& arguments); + + void Register(VirtualMachine& vm, + std::shared_ptr policy) override; + + std::shared_ptr compatibilityPolicy; +}; diff --git a/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusClassesFactory.cpp b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusClassesFactory.cpp new file mode 100644 index 0000000000..f8f246182d --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusClassesFactory.cpp @@ -0,0 +1,45 @@ +#include "PapyrusClassesFactory.h" + +#include "PapyrusActor.h" +#include "PapyrusCell.h" +#include "PapyrusDebug.h" +#include "PapyrusEffectShader.h" +#include "PapyrusForm.h" +#include "PapyrusFormList.h" +#include "PapyrusGame.h" +#include "PapyrusKeyword.h" +#include "PapyrusMessage.h" +#include "PapyrusNetImmerse.h" +#include "PapyrusObjectReference.h" +#include "PapyrusSkymp.h" +#include "PapyrusSound.h" +#include "PapyrusUtility.h" + +std::vector> +PapyrusClassesFactory::CreateAndRegister( + VirtualMachine& vm, + const std::shared_ptr& compatibilityPolicy) +{ + std::vector> result; + + result.emplace_back(std::make_unique()); + result.emplace_back(std::make_unique()); + result.emplace_back(std::make_unique()); + result.emplace_back(std::make_unique()); + result.emplace_back(std::make_unique()); + result.emplace_back(std::make_unique()); + result.emplace_back(std::make_unique()); + result.emplace_back(std::make_unique()); + result.emplace_back(std::make_unique()); + result.emplace_back(std::make_unique()); + result.emplace_back(std::make_unique()); + result.emplace_back(std::make_unique()); + result.emplace_back(std::make_unique()); + result.emplace_back(std::make_unique()); + + for (auto& papyrusClass : result) { + papyrusClass->Register(vm, compatibilityPolicy); + } + + return result; +} diff --git a/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusClassesFactory.h b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusClassesFactory.h new file mode 100644 index 0000000000..955e506d5f --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusClassesFactory.h @@ -0,0 +1,15 @@ +#pragma once +#include "IPapyrusClass.h" +#include +#include + +class IPapyrusCompatibilityPolicy; +class VirtualMachine; + +class PapyrusClassesFactory +{ +public: + static std::vector> CreateAndRegister( + VirtualMachine& vm, + const std::shared_ptr& compatibilityPolicy); +}; diff --git a/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusDebug.cpp b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusDebug.cpp new file mode 100644 index 0000000000..ba9a1ba896 --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusDebug.cpp @@ -0,0 +1,39 @@ +#include "PapyrusDebug.h" + +#include "MpActor.h" +#include "script_objects/MpFormGameObject.h" + +VarValue PapyrusDebug::SendAnimationEvent( + VarValue, const std::vector& arguments) +{ + auto targetActor = GetFormPtr(arguments[0]); + if (targetActor) { + auto funcName = "SendAnimationEvent"; + auto s = SpSnippetFunctionGen::SerializeArguments(arguments, targetActor); + SpSnippet(GetName(), funcName, s.data()).Execute(targetActor); + } + return VarValue::None(); +} + +VarValue PapyrusDebug::Trace(VarValue, const std::vector& arguments) +{ + const char* asTextToPrint = static_cast(arguments[0]); + int aiSeverity = static_cast(arguments[1]); + + std::ignore = aiSeverity; + + spdlog::info("{}", asTextToPrint); + + return VarValue::None(); +} + +void PapyrusDebug::Register( + VirtualMachine& vm, std::shared_ptr policy) +{ + compatibilityPolicy = policy; + + AddStatic(vm, "Notification", &PapyrusDebug::Notification); + AddStatic(vm, "MessageBox", &PapyrusDebug::MessageBox); + AddStatic(vm, "SendAnimationEvent", &PapyrusDebug::SendAnimationEvent); + AddStatic(vm, "Trace", &PapyrusDebug::Trace); +} diff --git a/skymp5-server/cpp/server_guest_lib/PapyrusDebug.h b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusDebug.h similarity index 69% rename from skymp5-server/cpp/server_guest_lib/PapyrusDebug.h rename to skymp5-server/cpp/server_guest_lib/script_classes/PapyrusDebug.h index b9a6bd6ef4..554da0bc19 100644 --- a/skymp5-server/cpp/server_guest_lib/PapyrusDebug.h +++ b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusDebug.h @@ -13,15 +13,10 @@ class PapyrusDebug final : public IPapyrusClass VarValue SendAnimationEvent(VarValue self, const std::vector& arguments); - void Register(VirtualMachine& vm, - std::shared_ptr policy) override - { - compatibilityPolicy = policy; + VarValue Trace(VarValue self, const std::vector& arguments); - AddStatic(vm, "Notification", &PapyrusDebug::Notification); - AddStatic(vm, "MessageBox", &PapyrusDebug::MessageBox); - AddStatic(vm, "SendAnimationEvent", &PapyrusDebug::SendAnimationEvent); - } + void Register(VirtualMachine& vm, + std::shared_ptr policy) override; std::shared_ptr compatibilityPolicy; }; diff --git a/skymp5-server/cpp/server_guest_lib/PapyrusEffectShader.cpp b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusEffectShader.cpp similarity index 95% rename from skymp5-server/cpp/server_guest_lib/PapyrusEffectShader.cpp rename to skymp5-server/cpp/server_guest_lib/script_classes/PapyrusEffectShader.cpp index fae3ec0116..0cb3d45025 100644 --- a/skymp5-server/cpp/server_guest_lib/PapyrusEffectShader.cpp +++ b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusEffectShader.cpp @@ -1,8 +1,8 @@ #include "PapyrusEffectShader.h" -#include "EspmGameObject.h" -#include "MpFormGameObject.h" #include "WorldState.h" +#include "script_objects/EspmGameObject.h" +#include "script_objects/MpFormGameObject.h" VarValue PapyrusEffectShader::Play(VarValue self, const std::vector& arguments) diff --git a/skymp5-server/cpp/server_guest_lib/PapyrusEffectShader.h b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusEffectShader.h similarity index 100% rename from skymp5-server/cpp/server_guest_lib/PapyrusEffectShader.h rename to skymp5-server/cpp/server_guest_lib/script_classes/PapyrusEffectShader.h diff --git a/skymp5-server/cpp/server_guest_lib/PapyrusForm.cpp b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusForm.cpp similarity index 98% rename from skymp5-server/cpp/server_guest_lib/PapyrusForm.cpp rename to skymp5-server/cpp/server_guest_lib/script_classes/PapyrusForm.cpp index 74105a98aa..c3539f1935 100644 --- a/skymp5-server/cpp/server_guest_lib/PapyrusForm.cpp +++ b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusForm.cpp @@ -1,10 +1,10 @@ #include "PapyrusForm.h" -#include "EspmGameObject.h" #include "MpActor.h" -#include "MpFormGameObject.h" #include "TimeUtils.h" #include "WorldState.h" +#include "script_objects/EspmGameObject.h" +#include "script_objects/MpFormGameObject.h" #include #include diff --git a/skymp5-server/cpp/server_guest_lib/PapyrusForm.h b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusForm.h similarity index 100% rename from skymp5-server/cpp/server_guest_lib/PapyrusForm.h rename to skymp5-server/cpp/server_guest_lib/script_classes/PapyrusForm.h diff --git a/skymp5-server/cpp/server_guest_lib/PapyrusFormList.cpp b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusFormList.cpp similarity index 97% rename from skymp5-server/cpp/server_guest_lib/PapyrusFormList.cpp rename to skymp5-server/cpp/server_guest_lib/script_classes/PapyrusFormList.cpp index aadf9266b5..fcef502ffc 100644 --- a/skymp5-server/cpp/server_guest_lib/PapyrusFormList.cpp +++ b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusFormList.cpp @@ -1,6 +1,6 @@ #include "PapyrusFormList.h" -#include "EspmGameObject.h" +#include "script_objects/EspmGameObject.h" VarValue PapyrusFormList::GetSize(VarValue self, const std::vector& arguments) diff --git a/skymp5-server/cpp/server_guest_lib/PapyrusFormList.h b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusFormList.h similarity index 100% rename from skymp5-server/cpp/server_guest_lib/PapyrusFormList.h rename to skymp5-server/cpp/server_guest_lib/script_classes/PapyrusFormList.h diff --git a/skymp5-server/cpp/server_guest_lib/PapyrusGame.cpp b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusGame.cpp similarity index 98% rename from skymp5-server/cpp/server_guest_lib/PapyrusGame.cpp rename to skymp5-server/cpp/server_guest_lib/script_classes/PapyrusGame.cpp index cc9ad85bac..a502c6104b 100644 --- a/skymp5-server/cpp/server_guest_lib/PapyrusGame.cpp +++ b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusGame.cpp @@ -1,10 +1,10 @@ #include "PapyrusGame.h" #include "PapyrusFormList.h" -#include "EspmGameObject.h" -#include "MpFormGameObject.h" #include "WorldState.h" #include "libespm/Combiner.h" +#include "script_objects/EspmGameObject.h" +#include "script_objects/MpFormGameObject.h" VarValue PapyrusGame::IncrementStat(VarValue self, const std::vector& arguments) diff --git a/skymp5-server/cpp/server_guest_lib/PapyrusGame.h b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusGame.h similarity index 100% rename from skymp5-server/cpp/server_guest_lib/PapyrusGame.h rename to skymp5-server/cpp/server_guest_lib/script_classes/PapyrusGame.h diff --git a/skymp5-server/cpp/server_guest_lib/PapyrusKeyword.cpp b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusKeyword.cpp similarity index 100% rename from skymp5-server/cpp/server_guest_lib/PapyrusKeyword.cpp rename to skymp5-server/cpp/server_guest_lib/script_classes/PapyrusKeyword.cpp diff --git a/skymp5-server/cpp/server_guest_lib/PapyrusKeyword.h b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusKeyword.h similarity index 94% rename from skymp5-server/cpp/server_guest_lib/PapyrusKeyword.h rename to skymp5-server/cpp/server_guest_lib/script_classes/PapyrusKeyword.h index 7a5b823f93..3a249f35f0 100644 --- a/skymp5-server/cpp/server_guest_lib/PapyrusKeyword.h +++ b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusKeyword.h @@ -1,7 +1,7 @@ #pragma once -#include "EspmGameObject.h" #include "IPapyrusClass.h" #include "WorldState.h" +#include "script_objects/EspmGameObject.h" class PapyrusKeyword final : public IPapyrusClass { diff --git a/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusMessage.cpp b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusMessage.cpp new file mode 100644 index 0000000000..bb6735a93c --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusMessage.cpp @@ -0,0 +1,9 @@ +#include "PapyrusMessage.h" + +void PapyrusMessage::Register( + VirtualMachine& vm, std::shared_ptr policy) +{ + compatibilityPolicy = policy; + + AddMethod(vm, "Show", &PapyrusMessage::Show); +} diff --git a/skymp5-server/cpp/server_guest_lib/PapyrusMessage.h b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusMessage.h similarity index 79% rename from skymp5-server/cpp/server_guest_lib/PapyrusMessage.h rename to skymp5-server/cpp/server_guest_lib/script_classes/PapyrusMessage.h index d388716ed0..90122658e6 100644 --- a/skymp5-server/cpp/server_guest_lib/PapyrusMessage.h +++ b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusMessage.h @@ -10,12 +10,7 @@ class PapyrusMessage final : public IPapyrusClass DEFINE_METHOD_SPSNIPPET(Show); void Register(VirtualMachine& vm, - std::shared_ptr policy) override - { - compatibilityPolicy = policy; - - AddMethod(vm, "Show", &PapyrusMessage::Show); - } + std::shared_ptr policy) override; std::shared_ptr compatibilityPolicy; }; diff --git a/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusNetImmerse.cpp b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusNetImmerse.cpp new file mode 100644 index 0000000000..1eff6e63a9 --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusNetImmerse.cpp @@ -0,0 +1,48 @@ +#include "PapyrusNetImmerse.h" +#include "script_objects/EspmGameObject.h" +#include "script_objects/MpFormGameObject.h" + +VarValue PapyrusNetImmerse::SetNodeTextureSet( + VarValue, const std::vector& arguments) +{ + if (arguments.size() != 4) { + throw std::runtime_error( + "PapyrusNetImmerse::SetNodeTextureSet - expected 4 arguments"); + } + + MpObjectReference* ref = GetFormPtr(arguments[0]); + const char* node = static_cast(arguments[1]); + espm::LookupResult tSet = GetRecordPtr(arguments[2]); + bool firstPerson = static_cast(arguments[3]); + + std::ignore = firstPerson; + + // for (auto listener : selfRefr->GetListeners()) { + // auto targetRefr = dynamic_cast(listener); + // if (targetRefr) { + // SpSnippet(GetName(), funcName, serializedArgs.data(), + // selfRefr->GetFormId()) + // .Execute(targetRefr); + // } + // } + + // TODO + + return VarValue::None(); +} + +VarValue PapyrusNetImmerse::SetNodeScale( + VarValue, const std::vector& arguments) +{ + // TODO + return VarValue::None(); +} + +void PapyrusNetImmerse::Register( + VirtualMachine& vm, std::shared_ptr policy) +{ + compatibilityPolicy = policy; + + AddStatic(vm, "SetNodeTextureSet", &PapyrusNetImmerse::SetNodeTextureSet); + AddStatic(vm, "SetNodeScale", &PapyrusNetImmerse::SetNodeScale); +} diff --git a/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusNetImmerse.h b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusNetImmerse.h new file mode 100644 index 0000000000..309b98492c --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusNetImmerse.h @@ -0,0 +1,19 @@ +#pragma once +#include "IPapyrusClass.h" +#include "SpSnippetFunctionGen.h" + +class PapyrusNetImmerse final : public IPapyrusClass +{ +public: + const char* GetName() override { return "netimmerse"; } + + VarValue SetNodeTextureSet(VarValue self, + const std::vector& arguments); + + VarValue SetNodeScale(VarValue self, const std::vector& arguments); + + void Register(VirtualMachine& vm, + std::shared_ptr policy) override; + + std::shared_ptr compatibilityPolicy; +}; diff --git a/skymp5-server/cpp/server_guest_lib/PapyrusObjectReference.cpp b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusObjectReference.cpp similarity index 66% rename from skymp5-server/cpp/server_guest_lib/PapyrusObjectReference.cpp rename to skymp5-server/cpp/server_guest_lib/script_classes/PapyrusObjectReference.cpp index b86682524d..3d36de864a 100644 --- a/skymp5-server/cpp/server_guest_lib/PapyrusObjectReference.cpp +++ b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusObjectReference.cpp @@ -1,14 +1,15 @@ #include "PapyrusObjectReference.h" -#include "EspmGameObject.h" #include "FormCallbacks.h" #include "LocationalData.h" #include "MpActor.h" -#include "MpFormGameObject.h" #include "MpObjectReference.h" #include "SpSnippetFunctionGen.h" +#include "TimeUtils.h" #include "WorldState.h" #include "papyrus-vm/Structures.h" +#include "script_objects/EspmGameObject.h" +#include "script_objects/MpFormGameObject.h" #include VarValue PapyrusObjectReference::IsHarvested( @@ -187,48 +188,84 @@ VarValue PapyrusObjectReference::GetAnimationVariableBool( return VarValue(false); } +namespace { +void PlaceAtMeSpSnippet(MpObjectReference* self, + const std::vector& arguments) +{ + auto funcName = "PlaceAtMe"; + auto serializedArgs = SpSnippetFunctionGen::SerializeArguments(arguments); + for (auto listener : self->GetListeners()) { + auto targetRefr = dynamic_cast(listener); + if (targetRefr) { + SpSnippet("ObjectReference", funcName, serializedArgs.data(), + self->GetFormId()) + .Execute(targetRefr); + } + } +} +} + VarValue PapyrusObjectReference::PlaceAtMe( VarValue self, const std::vector& arguments) { auto selfRefr = GetFormPtr(self); - if (selfRefr && arguments.size() >= 4) { - auto akFormToPlace = GetRecordPtr(arguments[0]); - int aiCount = static_cast(arguments[1].CastToInt()); - bool abForcePersist = static_cast(arguments[2].CastToBool()); - bool abInitiallyDisabled = static_cast(arguments[3].CastToBool()); - - if (akFormToPlace.rec) { - auto baseId = akFormToPlace.ToGlobalId(akFormToPlace.rec->GetId()); - - LocationalData locationalData = { - selfRefr->GetPos(), - { 0, 0, selfRefr->GetAngle().z }, // TODO: fix Degrees/radians mismatch - selfRefr->GetCellOrWorld() - }; - FormCallbacks callbacks = selfRefr->GetCallbacks(); - std::string type = akFormToPlace.rec->GetType().ToString(); - - std::unique_ptr newRefr; - - if (akFormToPlace.rec->GetType() == "NPC_") { - auto actor = new MpActor(locationalData, callbacks, baseId); - newRefr.reset(actor); - } else { - newRefr.reset( - new MpObjectReference(locationalData, callbacks, baseId, type)); - } + if (!selfRefr || arguments.size() < 4) { + return VarValue::None(); + } - auto worldState = selfRefr->GetParent(); - auto newRefrId = worldState->GenerateFormId(); - worldState->AddForm(std::move(newRefr), newRefrId); + auto akFormToPlace = GetRecordPtr(arguments[0]); + int aiCount = static_cast(arguments[1].CastToInt()); + bool abForcePersist = static_cast(arguments[2].CastToBool()); + bool abInitiallyDisabled = static_cast(arguments[3].CastToBool()); - auto& refr = worldState->GetFormAt(newRefrId); - refr.ForceSubscriptionsUpdate(); - return VarValue(std::make_shared(&refr)); - } + if (!akFormToPlace.rec) { + return VarValue::None(); } - return VarValue::None(); + + bool isExplosion = akFormToPlace.rec->GetType() == "EXPL"; + if (isExplosion) { + // Well sp snippet fails ATM. and I don't want to overpollute clients and + // network with those placeatme s for now + + // PlaceAtMeSpSnippet(selfRefr, arguments); + + // TODO: return pseudo-reference or even create real server-side form? + return VarValue::None(); + } + + auto baseId = akFormToPlace.ToGlobalId(akFormToPlace.rec->GetId()); + + float angleZDegrees = selfRefr->GetAngle().z; + float angleZRadians = angleZDegrees; // TODO: fix Degrees/radians mismatch + // TODO: support angleX, angleY + LocationalData locationalData = { selfRefr->GetPos(), + { 0, 0, angleZRadians }, + selfRefr->GetCellOrWorld() }; + FormCallbacks callbacks = selfRefr->GetCallbacks(); + std::string type = akFormToPlace.rec->GetType().ToString(); + + std::unique_ptr newRefr; + + if (akFormToPlace.rec->GetType() == "NPC_") { + auto actor = new MpActor(locationalData, callbacks, baseId); + newRefr.reset(actor); + } else { + newRefr.reset( + new MpObjectReference(locationalData, callbacks, baseId, type)); + } + + auto worldState = selfRefr->GetParent(); + auto newRefrId = worldState->GenerateFormId(); + worldState->AddForm(std::move(newRefr), newRefrId); + + auto& refr = worldState->GetFormAt(newRefrId); + refr.ForceSubscriptionsUpdate(); + + if (abInitiallyDisabled) { + refr.Disable(); + } + return VarValue(std::make_shared(&refr)); } VarValue PapyrusObjectReference::SetAngle( @@ -293,7 +330,9 @@ VarValue PapyrusObjectReference::Activate( } auto akActivator = GetFormPtr(arguments[0]); if (!akActivator) { - throw std::runtime_error("Activate didn't recognize akActivator"); + spdlog::warn("Activate didn't recognize akActivator"); + // workaround for defaultPillarPuzzlelever script + akActivator = selfRefr; } bool abDefaultProcessingOnly = static_cast(arguments[1].CastToBool()); @@ -387,6 +426,9 @@ VarValue PapyrusObjectReference::PlayAnimation( if (arguments.size() < 1) { throw std::runtime_error("PlayAnimation requires at least 1 argument"); } + const char* animation = static_cast(arguments[0]); + selfRefr->SetLastAnimation(animation); + auto funcName = "PlayAnimation"; auto serializedArgs = SpSnippetFunctionGen::SerializeArguments(arguments); for (auto listener : selfRefr->GetListeners()) { @@ -401,6 +443,54 @@ VarValue PapyrusObjectReference::PlayAnimation( return VarValue::None(); } +VarValue PapyrusObjectReference::PlayAnimationAndWait( + VarValue self, const std::vector& arguments) +{ + if (auto selfRefr = GetFormPtr(self)) { + if (arguments.size() < 1) { + throw std::runtime_error("PlayAnimation requires at least 2 arguments"); + } + + const char* animation = static_cast(arguments[0]); + selfRefr->SetLastAnimation(animation); + + auto funcName = "PlayAnimationAndWait"; + auto serializedArgs = SpSnippetFunctionGen::SerializeArguments(arguments); + + std::vector> promises; + + for (auto listener : selfRefr->GetListeners()) { + auto targetRefr = dynamic_cast(listener); + if (targetRefr) { + auto promise = SpSnippet(GetName(), funcName, serializedArgs.data(), + selfRefr->GetFormId()) + .Execute(targetRefr); + promises.push_back(promise); + } + } + + if (promises.empty()) { + // No listeners found + return VarValue::None(); + } + + // Add 15 seconds timeout (protection against script freeze by user) + // Code based on PapyrusUtility::Wait implementation + auto time = Viet::TimeUtils::To(15.f); + auto timerPromise = selfRefr->GetParent()->SetTimer(time); + auto resultPromise = Viet::Promise(); + timerPromise + .Then([resultPromise](Viet::Void) { + resultPromise.Resolve(VarValue::None()); + }) + .Catch([resultPromise](const char* e) { resultPromise.Reject(e); }); + promises.push_back(resultPromise); + + return VarValue(Viet::Promise::Any(promises)); + } + return VarValue::None(); +} + VarValue PapyrusObjectReference::PlayGamebryoAnimation( VarValue self, const std::vector& arguments) { @@ -481,6 +571,99 @@ VarValue PapyrusObjectReference::SetOpen( return VarValue::None(); } +VarValue PapyrusObjectReference::Is3DLoaded(VarValue, + const std::vector&) +{ + // stub + return VarValue(true); +} + +namespace LinkedRefUtils { +static VarValue GetLinkedRef(VarValue self) +{ + if (auto selfRefr = GetFormPtr(self)) { + auto lookupRes = selfRefr->GetParent()->GetEspm().GetBrowser().LookupById( + selfRefr->GetFormId()); + auto data = + espm::GetData(selfRefr->GetFormId(), selfRefr->GetParent()); + if (data.linkedRefId) { + auto& linkedRef = selfRefr->GetParent()->GetFormAt( + lookupRes.ToGlobalId(data.linkedRefId)); + return VarValue(std::make_shared(&linkedRef)); + } + } + + return VarValue::None(); +} +} + +VarValue PapyrusObjectReference::GetLinkedRef( + VarValue self, const std::vector& arguments) +{ + // TODO: implement keyword argument + // https://ck.uesp.net/wiki/GetLinkedRef_-_ObjectReference + if (arguments.size() > 0 && arguments[0] != VarValue::None()) { + spdlog::warn( + "GetLinkedRef doesn't support Keyword argument at this moment"); + } + + return LinkedRefUtils::GetLinkedRef(self); +} + +VarValue PapyrusObjectReference::GetNthLinkedRef( + VarValue self, const std::vector& arguments) +{ + if (arguments.empty()) { + spdlog::error("GetNthLinkedRef - expected at least 1 argument"); + return VarValue::None(); + } + int n = static_cast(arguments[0]); + if (n < 0) { + return VarValue::None(); + } + + VarValue cursor = self; + for (int i = 0; i < n; ++i) { + cursor = LinkedRefUtils::GetLinkedRef(cursor); + } + return cursor; +} + +VarValue PapyrusObjectReference::GetParentCell(VarValue self, + const std::vector&) +{ + if (auto selfRefr = GetFormPtr(self)) { + auto cellOrWorld = + selfRefr->GetCellOrWorld().ToFormId(selfRefr->GetParent()->espmFiles); + auto lookupRes = + selfRefr->GetParent()->GetEspm().GetBrowser().LookupById(cellOrWorld); + if (lookupRes.rec == nullptr) { + spdlog::warn("GetParentCell - nullptr cell/world found"); + } else if (lookupRes.rec->GetType() == espm::WRLD::kType) { + // TODO: support. at least you can use WRLD + x/y to find exterior cell + spdlog::warn( + "GetParentCell - exterior cells are not supported at this moment"); + } else if (lookupRes.rec->GetType() == espm::CELL::kType) { + return VarValue(std::make_shared(lookupRes)); + } else { + spdlog::warn("GetParentCell - bad record type {}", + lookupRes.rec->GetType().ToString()); + } + } + return VarValue::None(); +} + +VarValue PapyrusObjectReference::GetOpenState(VarValue self, + const std::vector&) +{ + if (auto selfRefr = GetFormPtr(self)) { + if (selfRefr->GetBaseType() == "DOOR") { + return selfRefr->IsOpen() ? VarValue(1) : VarValue(3); + } + } + return VarValue(0); +} + void PapyrusObjectReference::Register( VirtualMachine& vm, std::shared_ptr policy) { @@ -510,8 +693,15 @@ void PapyrusObjectReference::Register( AddMethod(vm, "SetPosition", &PapyrusObjectReference::SetPosition); AddMethod(vm, "GetBaseObject", &PapyrusObjectReference::GetBaseObject); AddMethod(vm, "PlayAnimation", &PapyrusObjectReference::PlayAnimation); + AddMethod(vm, "PlayAnimationAndWait", + &PapyrusObjectReference::PlayAnimationAndWait); AddMethod(vm, "PlayGamebryoAnimation", &PapyrusObjectReference::PlayGamebryoAnimation); AddMethod(vm, "MoveTo", &PapyrusObjectReference::MoveTo); AddMethod(vm, "SetOpen", &PapyrusObjectReference::SetOpen); + AddMethod(vm, "Is3DLoaded", &PapyrusObjectReference::Is3DLoaded); + AddMethod(vm, "GetLinkedRef", &PapyrusObjectReference::GetLinkedRef); + AddMethod(vm, "GetNthLinkedRef", &PapyrusObjectReference::GetNthLinkedRef); + AddMethod(vm, "GetParentCell", &PapyrusObjectReference::GetParentCell); + AddMethod(vm, "GetOpenState", &PapyrusObjectReference::GetOpenState); } diff --git a/skymp5-server/cpp/server_guest_lib/PapyrusObjectReference.h b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusObjectReference.h similarity index 82% rename from skymp5-server/cpp/server_guest_lib/PapyrusObjectReference.h rename to skymp5-server/cpp/server_guest_lib/script_classes/PapyrusObjectReference.h index 65974a9d8f..8a5def1d00 100644 --- a/skymp5-server/cpp/server_guest_lib/PapyrusObjectReference.h +++ b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusObjectReference.h @@ -38,12 +38,26 @@ class PapyrusObjectReference final const std::vector& arguments); VarValue PlayAnimation(VarValue self, const std::vector& arguments); + VarValue PlayAnimationAndWait(VarValue self, + const std::vector& arguments); VarValue PlayGamebryoAnimation(VarValue self, const std::vector& arguments); VarValue MoveTo(VarValue self, const std::vector& arguments); VarValue SetOpen(VarValue self, const std::vector& arguments); + VarValue Is3DLoaded(VarValue self, const std::vector& arguments); + + VarValue GetLinkedRef(VarValue self, const std::vector& arguments); + + VarValue GetNthLinkedRef(VarValue self, + const std::vector& arguments); + + VarValue GetParentCell(VarValue self, + const std::vector& arguments); + + VarValue GetOpenState(VarValue self, const std::vector& arguments); + void Register(VirtualMachine& vm, std::shared_ptr policy) override; }; diff --git a/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusSkymp.cpp b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusSkymp.cpp new file mode 100644 index 0000000000..9d550b1b65 --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusSkymp.cpp @@ -0,0 +1,15 @@ +#include "PapyrusSkymp.h" + +#include "MpActor.h" +#include "script_objects/MpFormGameObject.h" + +VarValue PapyrusSkymp::SetDefaultActor(VarValue self, + const std::vector& arguments) +{ + if (arguments.size() >= 1) { + policy->SetDefaultActor(self.GetMetaStackId(), + GetFormPtr(arguments[0])); + } + + return VarValue::None(); +} diff --git a/skymp5-server/cpp/server_guest_lib/PapyrusSkymp.h b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusSkymp.h similarity index 100% rename from skymp5-server/cpp/server_guest_lib/PapyrusSkymp.h rename to skymp5-server/cpp/server_guest_lib/script_classes/PapyrusSkymp.h diff --git a/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusSound.cpp b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusSound.cpp new file mode 100644 index 0000000000..edd7b4e133 --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusSound.cpp @@ -0,0 +1,41 @@ +#include "PapyrusSound.h" + +#include "MpObjectReference.h" +#include "SpSnippetFunctionGen.h" +#include "script_objects/EspmGameObject.h" +#include "script_objects/MpFormGameObject.h" + +VarValue PapyrusSound::Play(VarValue self, + const std::vector& arguments) +{ + auto sound = GetRecordPtr(self); + if (!sound.rec) { + throw std::runtime_error("Self not found"); + } + + auto selfId = sound.ToGlobalId(sound.rec->GetId()); + + if (auto refr = GetFormPtr(arguments[0])) { + if (arguments.size() < 1) { + throw std::runtime_error("Play requires at least 1 argument"); + } + auto funcName = "Play"; + auto serializedArgs = SpSnippetFunctionGen::SerializeArguments(arguments); + for (auto listener : refr->GetListeners()) { + auto targetRefr = dynamic_cast(listener); + if (targetRefr) { + SpSnippet(GetName(), funcName, serializedArgs.data(), selfId) + .Execute(targetRefr); + } + } + } + return VarValue::None(); +} + +void PapyrusSound::Register( + VirtualMachine& vm, std::shared_ptr policy) +{ + compatibilityPolicy = policy; + + AddMethod(vm, "Play", &PapyrusSound::Play); +} diff --git a/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusSound.h b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusSound.h new file mode 100644 index 0000000000..feb28c5329 --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusSound.h @@ -0,0 +1,15 @@ +#pragma once +#include "IPapyrusClass.h" + +class PapyrusSound final : public IPapyrusClass +{ +public: + const char* GetName() override { return "sound"; } + + VarValue Play(VarValue slef, const std::vector& arguments); + + void Register(VirtualMachine& vm, + std::shared_ptr policy) override; + + std::shared_ptr compatibilityPolicy; +}; diff --git a/skymp5-server/cpp/server_guest_lib/PapyrusUtility.cpp b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusUtility.cpp similarity index 67% rename from skymp5-server/cpp/server_guest_lib/PapyrusUtility.cpp rename to skymp5-server/cpp/server_guest_lib/script_classes/PapyrusUtility.cpp index 6ed143726f..c112bb11c8 100644 --- a/skymp5-server/cpp/server_guest_lib/PapyrusUtility.cpp +++ b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusUtility.cpp @@ -28,15 +28,24 @@ VarValue PapyrusUtility::Wait(VarValue self, return VarValue(resultPromise); } -static std::mt19937 kGenerator{ std::random_device{}() }; +static std::mt19937 g_generator{ std::random_device{}() }; -VarValue PapyrusUtility::RandomInt( - VarValue self, const std::vector& arguments) const noexcept +VarValue PapyrusUtility::RandomInt(VarValue self, + const std::vector& arguments) { int32_t min = static_cast(arguments[0].CastToInt()); int32_t max = static_cast(arguments[1].CastToInt()); std::uniform_int_distribution<> distribute{ min, max }; - return VarValue(distribute(kGenerator)); + return VarValue(distribute(g_generator)); +} + +VarValue PapyrusUtility::RandomFloat(VarValue self, + const std::vector& arguments) +{ + double min = static_cast(arguments[0].CastToFloat()); + double max = static_cast(arguments[1].CastToFloat()); + std::uniform_real_distribution<> distribute{ min, max }; + return VarValue(distribute(g_generator)); } void PapyrusUtility::Register( @@ -47,4 +56,5 @@ void PapyrusUtility::Register( AddStatic(vm, "Wait", &PapyrusUtility::Wait); AddStatic(vm, "RandomInt", &PapyrusUtility::RandomInt); + AddStatic(vm, "RandomFloat", &PapyrusUtility::RandomFloat); } diff --git a/skymp5-server/cpp/server_guest_lib/PapyrusUtility.h b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusUtility.h similarity index 73% rename from skymp5-server/cpp/server_guest_lib/PapyrusUtility.h rename to skymp5-server/cpp/server_guest_lib/script_classes/PapyrusUtility.h index bac9930b01..c95d7d8ed0 100644 --- a/skymp5-server/cpp/server_guest_lib/PapyrusUtility.h +++ b/skymp5-server/cpp/server_guest_lib/script_classes/PapyrusUtility.h @@ -7,8 +7,8 @@ class PapyrusUtility final : public IPapyrusClass const char* GetName() override { return "utility"; } VarValue Wait(VarValue self, const std::vector& arguments); - VarValue RandomInt(VarValue slef, - const std::vector& arguments) const noexcept; + VarValue RandomInt(VarValue slef, const std::vector& arguments); + VarValue RandomFloat(VarValue slef, const std::vector& arguments); void Register(VirtualMachine& vm, std::shared_ptr policy) override; diff --git a/skymp5-server/cpp/server_guest_lib/HeuristicPolicy.cpp b/skymp5-server/cpp/server_guest_lib/script_compatibility_policies/HeuristicPolicy.cpp similarity index 82% rename from skymp5-server/cpp/server_guest_lib/HeuristicPolicy.cpp rename to skymp5-server/cpp/server_guest_lib/script_compatibility_policies/HeuristicPolicy.cpp index 3292207f7a..43fa70ab0a 100644 --- a/skymp5-server/cpp/server_guest_lib/HeuristicPolicy.cpp +++ b/skymp5-server/cpp/server_guest_lib/script_compatibility_policies/HeuristicPolicy.cpp @@ -1,21 +1,21 @@ #include "HeuristicPolicy.h" -#include "MpFormGameObject.h" +#include "script_objects/MpFormGameObject.h" -HeuristicPolicy::HeuristicPolicy( - const std::shared_ptr& logger_, WorldState* worldState_) - : logger(logger_) - , worldState(worldState_) +HeuristicPolicy::HeuristicPolicy(WorldState* worldState_) + : worldState(worldState_) { } void HeuristicPolicy::SetDefaultActor(int32_t stackId, MpActor* newActor) { - if (stackId < 0) + if (stackId < 0) { throw std::runtime_error("Invalid stackId was passed to SetDefaultActor"); + } - if (stackInfo.size() <= stackId) + if (stackInfo.size() <= stackId) { stackInfo.resize(stackId + 1); + } stackInfo[stackId].ac = newActor; } @@ -23,19 +23,21 @@ MpActor* HeuristicPolicy::GetDefaultActor(const char* className, const char* funcName, int32_t stackId) const { - if (stackId < 0 || stackId >= stackInfo.size()) + if (stackId < 0 || stackId >= stackInfo.size()) { throw std::runtime_error( "Invalid stackId was passed to GetDefaultActor (" + std::to_string(stackId) + ")"); + } auto& info = stackInfo[stackId]; MpActor* actor = stackId < stackInfo.size() ? stackInfo[stackId].ac : static_cast(nullptr); - if (!actor && logger) - logger->warn("Unable to determine Actor for '{}.{}' in '{}'", className, + if (!actor) { + spdlog::warn("Unable to determine Actor for '{}.{}' in '{}'", className, funcName, info.currentEventName); + } return actor; } @@ -65,7 +67,8 @@ void HeuristicPolicy::BeforeSendPapyrusEvent(MpForm* form, actor = GetFormPtr(arguments[0]); } - if (stackInfo.size() <= stackId) + if (stackInfo.size() <= stackId) { stackInfo.resize(stackId + 1); + } stackInfo[stackId] = { actor, eventName }; } diff --git a/skymp5-server/cpp/server_guest_lib/HeuristicPolicy.h b/skymp5-server/cpp/server_guest_lib/script_compatibility_policies/HeuristicPolicy.h similarity index 68% rename from skymp5-server/cpp/server_guest_lib/HeuristicPolicy.h rename to skymp5-server/cpp/server_guest_lib/script_compatibility_policies/HeuristicPolicy.h index 3746b91d2a..1ccda337c1 100644 --- a/skymp5-server/cpp/server_guest_lib/HeuristicPolicy.h +++ b/skymp5-server/cpp/server_guest_lib/script_compatibility_policies/HeuristicPolicy.h @@ -2,28 +2,26 @@ #include "IPapyrusCompatibilityPolicy.h" #include "MpActor.h" -#include "MpFormGameObject.h" #include "papyrus-vm/Utils.h" #include "papyrus-vm/VirtualMachine.h" -#include +#include "script_objects/MpFormGameObject.h" #include class HeuristicPolicy : public IPapyrusCompatibilityPolicy { public: - explicit HeuristicPolicy(const std::shared_ptr& logger, - WorldState* worldState_); + explicit HeuristicPolicy(WorldState* worldState_); MpActor* GetDefaultActor(const char* className, const char* funcName, int32_t stackId) const override; WorldState* GetWorldState() const override; - void SetDefaultActor(int32_t stackId, MpActor* actor); + void SetDefaultActor(int32_t stackId, MpActor* actor) override; void BeforeSendPapyrusEvent(MpForm* form, const char* eventName, const VarValue* arguments, size_t argumentsCount, - int32_t stackId); + int32_t stackId) override; private: struct StackInfo @@ -32,6 +30,5 @@ class HeuristicPolicy : public IPapyrusCompatibilityPolicy const char* currentEventName = ""; }; std::vector stackInfo; - const std::shared_ptr& logger; WorldState* const worldState; }; diff --git a/skymp5-server/cpp/server_guest_lib/IPapyrusCompatibilityPolicy.h b/skymp5-server/cpp/server_guest_lib/script_compatibility_policies/IPapyrusCompatibilityPolicy.h similarity index 60% rename from skymp5-server/cpp/server_guest_lib/IPapyrusCompatibilityPolicy.h rename to skymp5-server/cpp/server_guest_lib/script_compatibility_policies/IPapyrusCompatibilityPolicy.h index 4e96e2c46b..8692e18344 100644 --- a/skymp5-server/cpp/server_guest_lib/IPapyrusCompatibilityPolicy.h +++ b/skymp5-server/cpp/server_guest_lib/script_compatibility_policies/IPapyrusCompatibilityPolicy.h @@ -1,8 +1,11 @@ #pragma once +#include #include +class MpForm; class MpActor; class WorldState; +struct VarValue; class IPapyrusCompatibilityPolicy { @@ -16,4 +19,11 @@ class IPapyrusCompatibilityPolicy int32_t stackId) const = 0; virtual WorldState* GetWorldState() const { return nullptr; } + + virtual void SetDefaultActor(int32_t stackId, MpActor* actor) = 0; + + virtual void BeforeSendPapyrusEvent(MpForm* form, const char* eventName, + const VarValue* arguments, + size_t argumentsCount, + int32_t stackId) = 0; }; diff --git a/skymp5-server/cpp/server_guest_lib/script_compatibility_policies/PapyrusCompatibilityPolicyFactory.cpp b/skymp5-server/cpp/server_guest_lib/script_compatibility_policies/PapyrusCompatibilityPolicyFactory.cpp new file mode 100644 index 0000000000..d12abb5a69 --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/script_compatibility_policies/PapyrusCompatibilityPolicyFactory.cpp @@ -0,0 +1,9 @@ +#include "PapyrusCompatibilityPolicyFactory.h" + +#include "HeuristicPolicy.h" + +std::shared_ptr +PapyrusCompatibilityPolicyFactory::Create(WorldState* worldState) +{ + return std::make_shared(worldState); +} diff --git a/skymp5-server/cpp/server_guest_lib/script_compatibility_policies/PapyrusCompatibilityPolicyFactory.h b/skymp5-server/cpp/server_guest_lib/script_compatibility_policies/PapyrusCompatibilityPolicyFactory.h new file mode 100644 index 0000000000..1538039c86 --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/script_compatibility_policies/PapyrusCompatibilityPolicyFactory.h @@ -0,0 +1,12 @@ +#pragma once +#include "IPapyrusCompatibilityPolicy.h" +#include + +class WorldState; + +class PapyrusCompatibilityPolicyFactory +{ +public: + static std::shared_ptr Create( + WorldState* worldState); +}; diff --git a/skymp5-server/cpp/server_guest_lib/EspmGameObject.cpp b/skymp5-server/cpp/server_guest_lib/script_objects/EspmGameObject.cpp similarity index 95% rename from skymp5-server/cpp/server_guest_lib/EspmGameObject.cpp rename to skymp5-server/cpp/server_guest_lib/script_objects/EspmGameObject.cpp index 85f74c72dc..93aa797d05 100644 --- a/skymp5-server/cpp/server_guest_lib/EspmGameObject.cpp +++ b/skymp5-server/cpp/server_guest_lib/script_objects/EspmGameObject.cpp @@ -2,6 +2,7 @@ #include #include +#include EspmGameObject::EspmGameObject(const espm::LookupResult& record_) : record(record_) @@ -336,3 +337,18 @@ const char* EspmGameObject::GetStringID() } return v->data(); } + +const std::vector>& +EspmGameObject::ListActivePexInstances() const +{ + static const std::vector> + kEmptyScriptsArray; + return kEmptyScriptsArray; +} + +void EspmGameObject::AddScript( + std::shared_ptr sctipt) noexcept +{ + spdlog::critical("EspmGameObject::AddScript is not supported"); + std::terminate(); +} diff --git a/skymp5-server/cpp/server_guest_lib/EspmGameObject.h b/skymp5-server/cpp/server_guest_lib/script_objects/EspmGameObject.h similarity index 70% rename from skymp5-server/cpp/server_guest_lib/EspmGameObject.h rename to skymp5-server/cpp/server_guest_lib/script_objects/EspmGameObject.h index d9beb2d942..d2448e4245 100644 --- a/skymp5-server/cpp/server_guest_lib/EspmGameObject.h +++ b/skymp5-server/cpp/server_guest_lib/script_objects/EspmGameObject.h @@ -12,6 +12,12 @@ class EspmGameObject : public IGameObject const char* GetStringID() override; const espm::LookupResult record; + +protected: + const std::vector>& + ListActivePexInstances() const override; + + void AddScript(std::shared_ptr sctipt) noexcept override; }; const espm::LookupResult& GetRecordPtr(const VarValue& papyrusObject); diff --git a/skymp5-server/cpp/server_guest_lib/MpFormGameObject.cpp b/skymp5-server/cpp/server_guest_lib/script_objects/MpFormGameObject.cpp similarity index 63% rename from skymp5-server/cpp/server_guest_lib/MpFormGameObject.cpp rename to skymp5-server/cpp/server_guest_lib/script_objects/MpFormGameObject.cpp index e3e120829d..119c567ada 100644 --- a/skymp5-server/cpp/server_guest_lib/MpFormGameObject.cpp +++ b/skymp5-server/cpp/server_guest_lib/script_objects/MpFormGameObject.cpp @@ -16,8 +16,9 @@ MpForm* MpFormGameObject::GetFormPtr() const noexcept { if (parent) { bool formStillValid = parent->LookupFormById(formId).get() == form; - if (!formStillValid) + if (!formStillValid) { return nullptr; + } } return form; } @@ -47,3 +48,27 @@ const char* MpFormGameObject::GetStringID() } return v->data(); } + +const std::vector>& +MpFormGameObject::ListActivePexInstances() const +{ + static const std::vector> + kEmptyScriptsArray; + + auto form = GetFormPtr(); + if (!form) { + return kEmptyScriptsArray; + } + return form->ListActivePexInstances(); +} + +void MpFormGameObject::AddScript( + std::shared_ptr script) noexcept +{ + auto form = GetFormPtr(); + if (!form) { + spdlog::critical("MpFormGameObject::AddScript - Invalid form"); + std::terminate(); + } + return form->AddScript(script); +} diff --git a/skymp5-server/cpp/server_guest_lib/MpFormGameObject.h b/skymp5-server/cpp/server_guest_lib/script_objects/MpFormGameObject.h similarity index 68% rename from skymp5-server/cpp/server_guest_lib/MpFormGameObject.h rename to skymp5-server/cpp/server_guest_lib/script_objects/MpFormGameObject.h index e4f141f107..d768ee39b5 100644 --- a/skymp5-server/cpp/server_guest_lib/MpFormGameObject.h +++ b/skymp5-server/cpp/server_guest_lib/script_objects/MpFormGameObject.h @@ -13,6 +13,11 @@ class MpFormGameObject : public IGameObject const char* GetStringID() override; + const std::vector>& + ListActivePexInstances() const override; + + void AddScript(std::shared_ptr script) noexcept override; + private: WorldState* const parent; MpForm* const form; @@ -20,13 +25,18 @@ class MpFormGameObject : public IGameObject }; template -inline T* GetFormPtr(const VarValue& papyrusObject) +T* GetFormPtr(const VarValue& papyrusObject) { - if (papyrusObject.GetType() != VarValue::kType_Object) + if (papyrusObject.GetType() != VarValue::kType_Object) { return nullptr; + } + auto gameObject = static_cast(papyrusObject); + auto mpFormGameObject = dynamic_cast(gameObject); - if (!mpFormGameObject) + if (!mpFormGameObject) { return nullptr; + } + return dynamic_cast(mpFormGameObject->GetFormPtr()); } diff --git a/skymp5-server/cpp/server_guest_lib/script_storages/AssetsScriptStorage.cpp b/skymp5-server/cpp/server_guest_lib/script_storages/AssetsScriptStorage.cpp new file mode 100644 index 0000000000..de4774a054 --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/script_storages/AssetsScriptStorage.cpp @@ -0,0 +1,56 @@ +#include "AssetsScriptStorage.h" + +#include +#include +#include +#include + +CMRC_DECLARE(server_standard_scripts); + +AssetsScriptStorage::AssetsScriptStorage() +{ + try { + auto fileSystem = cmrc::server_standard_scripts::get_filesystem(); + for (auto&& entry : fileSystem.iterate_directory("standard_scripts")) { + cmrc::file file = + fileSystem.open("standard_scripts/" + entry.filename()); + const uint8_t* begin = reinterpret_cast(file.begin()); + const uint8_t* end = begin + file.size(); + std::vector pex(begin, end); + + auto nameWithoutExtension = + std::filesystem::path(entry.filename()).stem().string(); + scripts.insert( + { nameWithoutExtension.begin(), nameWithoutExtension.end() }); + scriptPex[{ nameWithoutExtension.begin(), nameWithoutExtension.end() }] = + pex; + } + } catch (std::exception& e) { + auto dir = + cmrc::server_standard_scripts::get_filesystem().iterate_directory(""); + std::stringstream ss; + ss << e.what() << std::endl << std::endl; + ss << "Root directory contents is: " << std::endl; + for (auto&& entry : dir) { + ss << entry.filename() << std::endl; + } + throw std::runtime_error(ss.str()); + } +} + +std::vector AssetsScriptStorage::GetScriptPex(const char* scriptName) +{ + auto it = scriptPex.find(scriptName); + if (it == scriptPex.end()) { + spdlog::trace("AssetsScriptStorage::GetScriptPex - Not found {}", + scriptName); + return {}; + } + spdlog::trace("AssetsScriptStorage::GetScriptPex - Found {}", scriptName); + return it->second; +} + +const std::set& AssetsScriptStorage::ListScripts(bool) +{ + return scripts; +} diff --git a/skymp5-server/cpp/server_guest_lib/script_storages/AssetsScriptStorage.h b/skymp5-server/cpp/server_guest_lib/script_storages/AssetsScriptStorage.h new file mode 100644 index 0000000000..b8cd5678d0 --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/script_storages/AssetsScriptStorage.h @@ -0,0 +1,16 @@ +#pragma once +#include "IScriptStorage.h" + +class AssetsScriptStorage : public IScriptStorage +{ +public: + AssetsScriptStorage(); + + std::vector GetScriptPex(const char* scriptName) override; + + const std::set& ListScripts(bool forceReloadScripts) override; + +private: + std::set scripts; + CIMap> scriptPex; +}; diff --git a/skymp5-server/cpp/server_guest_lib/script_storages/BsaArchiveScriptStorage.cpp b/skymp5-server/cpp/server_guest_lib/script_storages/BsaArchiveScriptStorage.cpp new file mode 100644 index 0000000000..a0691a0a3b --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/script_storages/BsaArchiveScriptStorage.cpp @@ -0,0 +1,57 @@ +#include "BsaArchiveScriptStorage.h" + +#include +#include +#include +#include + +BsaArchiveScriptStorage::BsaArchiveScriptStorage(const char* bsaPath_) +{ + this->bsaPath = bsaPath_; + if (!std::filesystem::exists(bsaPath_)) { + throw std::runtime_error( + fmt::format("BSA Archive '{}' doesn't exist", bsaPath_)); + } +} + +std::vector BsaArchiveScriptStorage::GetScriptPex( + const char* scriptName) +{ + auto it = scriptPex.find(scriptName); + if (it == scriptPex.end()) { + spdlog::trace("BsaArchiveScriptStorage::GetScriptPex - Not found {}", + scriptName); + return {}; + } + spdlog::trace("BsaArchiveScriptStorage::GetScriptPex - Found {}", + scriptName); + return it->second; +} + +const std::set& BsaArchiveScriptStorage::ListScripts( + bool forceReloadScripts) +{ + if (scripts.empty() || forceReloadScripts) { + scripts.clear(); + bsa::tes4::archive bsa; + bsa.read(bsaPath); + auto bsaScripts = *bsa["scripts"]; + for (auto it = bsaScripts.begin(); it != bsaScripts.end(); it++) { + auto fileName = it->first.name(); + + const std::byte* data = it->second.data(); + size_t size = it->second.size(); + auto pex = + std::vector(reinterpret_cast(data), + reinterpret_cast(data) + size); + + auto nameWithoutExtension = + std::filesystem::path(fileName).stem().string(); + scripts.insert( + { nameWithoutExtension.begin(), nameWithoutExtension.end() }); + scriptPex[{ nameWithoutExtension.begin(), nameWithoutExtension.end() }] = + pex; + } + } + return scripts; +} diff --git a/skymp5-server/cpp/server_guest_lib/script_storages/BsaArchiveScriptStorage.h b/skymp5-server/cpp/server_guest_lib/script_storages/BsaArchiveScriptStorage.h new file mode 100644 index 0000000000..9ae084c16e --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/script_storages/BsaArchiveScriptStorage.h @@ -0,0 +1,17 @@ +#pragma once +#include "IScriptStorage.h" + +class BsaArchiveScriptStorage : public IScriptStorage +{ +public: + BsaArchiveScriptStorage(const char* pathToBsa); + + std::vector GetScriptPex(const char* scriptName) override; + + const std::set& ListScripts(bool forceReloadScripts) override; + +private: + std::set scripts; + CIMap> scriptPex; + std::string bsaPath; +}; diff --git a/skymp5-server/cpp/server_guest_lib/script_storages/CombinedScriptStorage.cpp b/skymp5-server/cpp/server_guest_lib/script_storages/CombinedScriptStorage.cpp new file mode 100644 index 0000000000..a8c5e08f61 --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/script_storages/CombinedScriptStorage.cpp @@ -0,0 +1,32 @@ +#include "CombinedScriptStorage.h" + +CombinedScriptStorage::CombinedScriptStorage( + std::vector> scriptStorages) +{ + this->scriptStorages = std::move(scriptStorages); +} + +std::vector CombinedScriptStorage::GetScriptPex( + const char* scriptName) +{ + for (auto& storage : scriptStorages) { + auto result = storage->GetScriptPex(scriptName); + if (!result.empty()) { + return result; + } + } + return {}; +} + +const std::set& CombinedScriptStorage::ListScripts( + bool forceReloadScripts) +{ + if (scripts.empty() || forceReloadScripts) { + scripts.clear(); + for (auto& storage : scriptStorages) { + auto& result = storage->ListScripts(forceReloadScripts); + scripts.insert(result.begin(), result.end()); + } + } + return scripts; +} diff --git a/skymp5-server/cpp/server_guest_lib/script_storages/CombinedScriptStorage.h b/skymp5-server/cpp/server_guest_lib/script_storages/CombinedScriptStorage.h new file mode 100644 index 0000000000..c9e1f54f62 --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/script_storages/CombinedScriptStorage.h @@ -0,0 +1,21 @@ +#pragma once +#include "IScriptStorage.h" + +#include + +class CombinedScriptStorage : public IScriptStorage +{ +public: + // Load order matters. But unlike mods, scriptStorages.front() will be + // checked first + CombinedScriptStorage( + std::vector> scriptStorages); + + std::vector GetScriptPex(const char* scriptName) override; + + const std::set& ListScripts(bool forceReloadScripts) override; + +private: + std::vector> scriptStorages; + std::set scripts; +}; diff --git a/skymp5-server/cpp/server_guest_lib/script_storages/DirectoryScriptStorage.cpp b/skymp5-server/cpp/server_guest_lib/script_storages/DirectoryScriptStorage.cpp new file mode 100644 index 0000000000..b54c6a7fe7 --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/script_storages/DirectoryScriptStorage.cpp @@ -0,0 +1,52 @@ +#include "DirectoryScriptStorage.h" +#include "ScriptStorageUtils.h" + +#include +#include +#include +#include + +DirectoryScriptStorage::DirectoryScriptStorage(const std::string& pexDirPath_) + : pexDir(pexDirPath_) +{ + scripts = ScriptStorageUtils::GetScriptsInDirectory(pexDir); +} + +std::vector DirectoryScriptStorage::GetScriptPex( + const char* scriptName) +{ + const auto path = + std::filesystem::path(pexDir) / (scriptName + std::string(".pex")); + + if (!std::filesystem::exists(path)) { + spdlog::trace("DirectoryScriptStorage::GetScriptPex - Not found {} (file " + "doesn't exist)", + scriptName); + return {}; + } + + std::ifstream f(path, std::ios::binary); + if (!f.is_open()) { + throw std::runtime_error(path.string() + " is failed to open"); + } + std::vector buffer(std::istreambuf_iterator(f), {}); + + if (buffer.empty()) { + spdlog::trace( + "DirectoryScriptStorage::GetScriptPex - Not found {} (file is empty)", + scriptName); + return {}; + } + + spdlog::trace("DirectoryScriptStorage::GetScriptPex - Found {}", scriptName); + return buffer; +} + +const std::set& DirectoryScriptStorage::ListScripts( + bool forceReloadScripts) +{ + if (forceReloadScripts) { + scripts = ScriptStorageUtils::GetScriptsInDirectory(pexDir); + } + return scripts; +} diff --git a/skymp5-server/cpp/server_guest_lib/script_storages/DirectoryScriptStorage.h b/skymp5-server/cpp/server_guest_lib/script_storages/DirectoryScriptStorage.h new file mode 100644 index 0000000000..d46ed75969 --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/script_storages/DirectoryScriptStorage.h @@ -0,0 +1,16 @@ +#pragma once +#include "IScriptStorage.h" + +class DirectoryScriptStorage : public IScriptStorage +{ +public: + DirectoryScriptStorage(const std::string& pexDir_); + + std::vector GetScriptPex(const char* scriptName) override; + + const std::set& ListScripts(bool forceReloadScripts) override; + +private: + const std::string pexDir; + std::set scripts; +}; diff --git a/skymp5-server/cpp/server_guest_lib/script_storages/IScriptStorage.h b/skymp5-server/cpp/server_guest_lib/script_storages/IScriptStorage.h new file mode 100644 index 0000000000..2e261d7ebe --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/script_storages/IScriptStorage.h @@ -0,0 +1,15 @@ +#pragma once +#include "papyrus-vm/CIString.h" +#include +#include +#include + +class IScriptStorage +{ +public: + virtual ~IScriptStorage() = default; + + virtual std::vector GetScriptPex(const char* scriptName) = 0; + + virtual const std::set& ListScripts(bool forceReloadScripts) = 0; +}; diff --git a/skymp5-server/cpp/server_guest_lib/script_storages/ScriptStorageFactory.cpp b/skymp5-server/cpp/server_guest_lib/script_storages/ScriptStorageFactory.cpp new file mode 100644 index 0000000000..242b44b8bb --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/script_storages/ScriptStorageFactory.cpp @@ -0,0 +1,63 @@ +#include "ScriptStorageFactory.h" + +#include + +std::shared_ptr ScriptStorageFactory::Create( + nlohmann::json serverSettings) +{ + + std::vector> scriptStorages; + + // TODO: ensure this order is correct + AddDirectory(scriptStorages, serverSettings); + AddBsa(scriptStorages, serverSettings); + AddAssets(scriptStorages, serverSettings); + + return std::dynamic_pointer_cast( + std::make_shared(scriptStorages)); +} + +std::string ScriptStorageFactory::ResolveArchivePath( + const std::string& pathFromConfigStr, const std::string& dataDir) +{ + std::filesystem::path pathFromConfig = pathFromConfigStr; + + if (pathFromConfig.is_absolute()) { + return pathFromConfig.string(); + } else { + return (dataDir / pathFromConfig).string(); + } +} + +void ScriptStorageFactory::AddDirectory( + std::vector>& storages, + nlohmann::json& serverSettings) +{ + std::string dataDir = serverSettings["dataDir"]; + + storages.push_back(std::make_shared( + (std::filesystem::path(dataDir) / "scripts").string())); +} + +void ScriptStorageFactory::AddAssets( + std::vector>& storages, nlohmann::json&) +{ + storages.push_back(std::make_shared()); +} + +void ScriptStorageFactory::AddBsa( + std::vector>& storages, + nlohmann::json& serverSettings) +{ + std::string dataDir = serverSettings["dataDir"]; + + if (serverSettings.contains("archives") && + serverSettings.at("archives").is_array()) { + for (auto& archive : serverSettings.at("archives")) { + std::string archivePath = + ResolveArchivePath(archive.get(), dataDir); + storages.push_back( + std::make_shared(archivePath.data())); + } + } +} diff --git a/skymp5-server/cpp/server_guest_lib/script_storages/ScriptStorageFactory.h b/skymp5-server/cpp/server_guest_lib/script_storages/ScriptStorageFactory.h new file mode 100644 index 0000000000..a8b1505545 --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/script_storages/ScriptStorageFactory.h @@ -0,0 +1,26 @@ +#pragma once +#include "AssetsScriptStorage.h" +#include "BsaArchiveScriptStorage.h" +#include "CombinedScriptStorage.h" +#include "DirectoryScriptStorage.h" + +#include +#include + +class ScriptStorageFactory +{ +public: + static std::shared_ptr Create(nlohmann::json serverSettings); + +private: + static std::string ResolveArchivePath(const std::string& pathFromConfigStr, + const std::string& dataDir); + + static void AddDirectory( + std::vector>& storages, + nlohmann::json& serverSettings); + static void AddAssets(std::vector>& storages, + nlohmann::json& serverSettings); + static void AddBsa(std::vector>& storages, + nlohmann::json& serverSettings); +}; diff --git a/skymp5-server/cpp/server_guest_lib/script_storages/ScriptStorageUtils.cpp b/skymp5-server/cpp/server_guest_lib/script_storages/ScriptStorageUtils.cpp new file mode 100644 index 0000000000..951e190a60 --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/script_storages/ScriptStorageUtils.cpp @@ -0,0 +1,45 @@ +#include "ScriptStorageUtils.h" + +#include +#include + +std::string ScriptStorageUtils::GetFileName(const std::string& path) +{ + std::string s = path; + while (s.find('/') != s.npos || s.find('\\') != s.npos) { + while (s.find('/') != s.npos) + s = { s.begin() + s.find('/') + 1, s.end() }; + while (s.find('\\') != s.npos) + s = { s.begin() + s.find('\\') + 1, s.end() }; + } + return s; +} + +std::string ScriptStorageUtils::RemoveExtension(std::string s) +{ + const std::regex e(".*\\.pex"); + if (std::regex_match(s, e)) { + s = { s.begin(), s.end() - strlen(".pex") }; + return s; + } + return ""; +} + +std::set ScriptStorageUtils::GetScriptsInDirectory( + const std::string& pexDir) +{ + std::set scripts; + + for (auto& p : std::filesystem::directory_iterator(pexDir)) { + if (p.is_directory()) { + continue; + } + + std::string s = GetFileName(p.path().string()); + if (auto fileNameWe = RemoveExtension(s); !fileNameWe.empty()) { + scripts.insert({ fileNameWe.begin(), fileNameWe.end() }); + } + } + + return scripts; +} diff --git a/skymp5-server/cpp/server_guest_lib/script_storages/ScriptStorageUtils.h b/skymp5-server/cpp/server_guest_lib/script_storages/ScriptStorageUtils.h new file mode 100644 index 0000000000..56f9245f67 --- /dev/null +++ b/skymp5-server/cpp/server_guest_lib/script_storages/ScriptStorageUtils.h @@ -0,0 +1,14 @@ +#pragma once + +#include "papyrus-vm/CIString.h" +#include +#include + +namespace ScriptStorageUtils { + +std::string GetFileName(const std::string& path); + +std::string RemoveExtension(std::string s); + +std::set GetScriptsInDirectory(const std::string& pexDir); +} diff --git a/skyrim-platform/src/platform_se/codegen/convert-files/FunctionsDump.txt b/skyrim-platform/src/platform_se/codegen/convert-files/FunctionsDump.txt index c02b091bbf..b857664f6f 100644 --- a/skyrim-platform/src/platform_se/codegen/convert-files/FunctionsDump.txt +++ b/skyrim-platform/src/platform_se/codegen/convert-files/FunctionsDump.txt @@ -21451,6 +21451,25 @@ }, "useLongSignature": true }, + { + "$comment": "hand-crafted entry", + "arguments": [ + { + "name": "commaSeparatedListOfIds", + "type": { + "rawType": "String" + } + } + ], + "isLatent": false, + "name": "EvaluateLeveledNpc", + "offset": "25744236640", + "returnType": { + "objectTypeName": "ActorBase", + "rawType": "Object" + }, + "useLongSignature": true + }, { "arguments": [ { diff --git a/skyrim-platform/src/platform_se/codegen/convert-files/skyrimPlatform.ts b/skyrim-platform/src/platform_se/codegen/convert-files/skyrimPlatform.ts index 400d1eace5..ebc70d0866 100644 --- a/skyrim-platform/src/platform_se/codegen/convert-files/skyrimPlatform.ts +++ b/skyrim-platform/src/platform_se/codegen/convert-files/skyrimPlatform.ts @@ -3229,6 +3229,7 @@ export declare class TESModPlatform extends PapyrusObject { static addItemEx(containerRefr: ObjectReference | null, item: Form | null, countDelta: number, health: number, enchantment: Enchantment | null, maxCharge: number, removeEnchantmentOnUnequip: boolean, chargePercent: number, textDisplayData: string, soul: number, poison: Potion | null, poisonCount: number): void static clearTintMasks(targetActor: Actor | null): void static createNpc(): ActorBase | null + static evaluateLeveledNpc(commaSeparatedListOfIds: string): ActorBase | null static getNthVtableElement(pointer: Form | null, pointerOffset: number, elementIndex: number): number static getSkinColor(base: ActorBase | null): ColorForm | null static isPlayerRunningEnabled(): boolean diff --git a/skyrim-platform/src/platform_se/pex/TESModPlatform.pex b/skyrim-platform/src/platform_se/pex/TESModPlatform.pex index 26c93d27c8..1eb3eef8ee 100644 Binary files a/skyrim-platform/src/platform_se/pex/TESModPlatform.pex and b/skyrim-platform/src/platform_se/pex/TESModPlatform.pex differ diff --git a/skyrim-platform/src/platform_se/psc/TESModPlatform.psc b/skyrim-platform/src/platform_se/psc/TESModPlatform.psc index 6c61adf776..fabfb30818 100644 --- a/skyrim-platform/src/platform_se/psc/TESModPlatform.psc +++ b/skyrim-platform/src/platform_se/psc/TESModPlatform.psc @@ -14,6 +14,8 @@ ColorForm Function GetSkinColor(ActorBase base) global native ActorBase Function CreateNpc() global native +ActorBase Function EvaluateLeveledNpc(String commaSeparatedListOfIds) global native + Function SetNpcSex(ActorBase npc, Int sex) global native Function SetNpcRace(ActorBase npc, Race race) global native diff --git a/skyrim-platform/src/platform_se/skyrim_platform/LoadGame.cpp b/skyrim-platform/src/platform_se/skyrim_platform/LoadGame.cpp index 318f3ddd36..ca6b7e21a0 100644 --- a/skyrim-platform/src/platform_se/skyrim_platform/LoadGame.cpp +++ b/skyrim-platform/src/platform_se/skyrim_platform/LoadGame.cpp @@ -147,9 +147,9 @@ void LoadGame::ModifyPluginInfo(std::shared_ptr& save) throw NullPointerException("dataHandler"); } - for (auto it = dataHandler->files.begin(); it != dataHandler->files.end(); - ++it) - newPlugins.push_back(std::string((*it)->fileName)); + for (auto& file : dataHandler->files) { + newPlugins.push_back(std::string(file->fileName)); + } save->OverwritePluginInfo(newPlugins); } @@ -293,10 +293,10 @@ SaveFile_::PlayerLocation LoadGame::CreatePlayerLocation( const std::array& pos, const SaveFile_::RefID& world) { SaveFile_::PlayerLocation r; - r.nextObjectId = 4278195454; + r.nextObjectId = 0xFF0014FE; r.worldspace1 = world; - r.coorX = (int)pos[0] / 4096; - r.coorY = (int)pos[1] / 4096; + r.coorX = static_cast(pos[0]) / 4096; + r.coorY = static_cast(pos[1]) / 4096; r.worldspace2 = world; r.posX = pos[0]; r.posY = pos[1]; @@ -359,13 +359,14 @@ void LoadGame::WriteChangeForm(std::shared_ptr save, std::copy(compressed.begin(), compressed.end(), changeForm.data.begin()); // fix offsets - const auto diff = (int64_t)previousSize - (int64_t)compressed.size(); + const auto diff = static_cast(previousSize) - + static_cast(compressed.size()); save->fileLocationTable.formIDArrayCountOffset -= diff; save->fileLocationTable.unknownTable3Offset -= diff; save->fileLocationTable.globalDataTable3Offset -= diff; } -std::wstring LoadGame::StringToWstring(std::string s) +std::wstring LoadGame::StringToWstring(const std::string& s) { std::wstring ws(s.size(), L' '); auto n = std::mbstowcs(&ws[0], s.c_str(), s.size()); @@ -380,7 +381,7 @@ std::string LoadGame::GenerateGuid() throw std::runtime_error("CoCreateGuid failed"); } - char name[MAX_PATH] = { 0 }; + char name[37] = { 0 }; // Size adjusted for GUID string sprintf_s( name, "%08lX-%04hX-%04hX-%02hhX%02hhX-%02hhX%02hhX%02hhX%02hhX%02hhX%02hhX", diff --git a/skyrim-platform/src/platform_se/skyrim_platform/LoadGame.h b/skyrim-platform/src/platform_se/skyrim_platform/LoadGame.h index b93aca3380..94acefc249 100644 --- a/skyrim-platform/src/platform_se/skyrim_platform/LoadGame.h +++ b/skyrim-platform/src/platform_se/skyrim_platform/LoadGame.h @@ -46,7 +46,7 @@ class LoadGame static std::wstring GetPathToMyDocuments(); private: - static std::wstring StringToWstring(std::string s); + static std::wstring StringToWstring(const std::string& s); static std::string GenerateGuid(); diff --git a/skyrim-platform/src/platform_se/skyrim_platform/PapyrusTESModPlatform.cpp b/skyrim-platform/src/platform_se/skyrim_platform/PapyrusTESModPlatform.cpp index 17cb3cd9f9..485ebf7df3 100644 --- a/skyrim-platform/src/platform_se/skyrim_platform/PapyrusTESModPlatform.cpp +++ b/skyrim-platform/src/platform_se/skyrim_platform/PapyrusTESModPlatform.cpp @@ -217,22 +217,20 @@ RE::BGSColorForm* TESModPlatform::GetSkinColor(IVM* vm, StackID stackId, return col; } -RE::TESNPC* TESModPlatform::CreateNpc(IVM* vm, StackID stackId, - RE::StaticFunctionTag*) +enum class AiPackagesMode { - auto npc = CreateForm(); + KeepOriginal, + ReplaceWithDoNothing +}; + +static RE::TESNPC* CloneNpc(uint32_t npcId, AiPackagesMode aiPackagesMode) +{ + auto npc = TESModPlatform::CreateForm(); if (!npc) { return nullptr; } - enum - { - AADeleteWhenDoneTestJeremyRegular = 0x0010D13E - }; - const auto srcNpc = - RE::TESForm::LookupByID(AADeleteWhenDoneTestJeremyRegular); - assert(srcNpc); - assert(srcNpc->formType.get() == RE::FormType::NPC); + const auto srcNpc = RE::TESForm::LookupByID(npcId); if (!srcNpc || srcNpc->formType != RE::FormType::NPC) { return nullptr; } @@ -249,21 +247,29 @@ RE::TESNPC* TESModPlatform::CreateNpc(IVM* vm, StackID stackId, npc_->actorData.actorBaseFlags.set(RE::ACTOR_BASE_DATA::Flag::kUnique); npc_->actorData.actorBaseFlags.set(RE::ACTOR_BASE_DATA::Flag::kSimpleActor); - // Clear AI Packages to prevent idle animations with Furniture - enum - { - DoNothing = 0x654e2, - DefaultMoveToCustom02IgnoreCombat = 0x6af62 - }; - // ignore combat && no - auto doNothing = RE::TESForm::LookupByID(DoNothing); - // combat alert - auto flagsSource = - RE::TESForm::LookupByID(DefaultMoveToCustom02IgnoreCombat); - - doNothing->packData = flagsSource->packData; - npc_->aiPackages.packages.clear(); - npc_->aiPackages.packages.push_front(doNothing); + switch (aiPackagesMode) { + case AiPackagesMode::ReplaceWithDoNothing: { + // Clear AI Packages to prevent idle animations with Furniture + enum + { + DoNothing = 0x654e2, + DefaultMoveToCustom02IgnoreCombat = 0x6af62 + }; + // ignore combat && no combat alert + auto doNothing = RE::TESForm::LookupByID(DoNothing); + auto flagsSource = RE::TESForm::LookupByID( + DefaultMoveToCustom02IgnoreCombat); + + doNothing->packData = flagsSource->packData; + npc_->aiPackages.packages.clear(); + npc_->aiPackages.packages.push_front(doNothing); + break; + } + case AiPackagesMode::KeepOriginal: + break; + default: + break; + } auto sourceFaceData = npc_->faceData; npc_->faceData = new RE::TESNPC::FaceData; @@ -275,6 +281,110 @@ RE::TESNPC* TESModPlatform::CreateNpc(IVM* vm, StackID stackId, return npc; } +template +static void FillFormsArray(VectorT& leak, std::vector cursorStack) +{ + leak.reserve(cursorStack.size()); + for (auto it = cursorStack.begin(); it != cursorStack.end(); ++it) { + leak.push_back(*it); + } +} + +RE::TESNPC* TESModPlatform::CreateNpc(IVM* vm, StackID stackId, + RE::StaticFunctionTag*) +{ + constexpr uint32_t kAADeleteWhenDoneTestJeremyRegular = 0x0010D13E; + return CloneNpc(kAADeleteWhenDoneTestJeremyRegular, + AiPackagesMode::ReplaceWithDoNothing); +} + +RE::TESNPC* TESModPlatform::EvaluateLeveledNpc( + IVM* vm, StackID stackId, RE::StaticFunctionTag*, + FixedString commaSeparatedListOfIds) +{ + auto str = std::string(commaSeparatedListOfIds.data()); + + thread_local std::unordered_map g_cache; + auto& cachedNpc = g_cache[str]; + if (cachedNpc) { + return cachedNpc; + } + + std::vector formIds; + formIds.reserve(10); + + std::istringstream iss(str); + std::string id; + while (std::getline(iss, id, ',')) { + auto formId = static_cast(atoll(id.data())); + const auto srcNpc = RE::TESForm::LookupByID(formId); + if (!srcNpc || srcNpc->formType != RE::FormType::NPC) { + return nullptr; + } + formIds.push_back(formId); + } + + std::vector cursorStack; + cursorStack.reserve(10); + + for (auto it = formIds.rbegin(); it != formIds.rend(); ++it) { + uint32_t formId = *it; + // TODO: replace with DoNothing for humanoids? and how to determine + // humanoids in case of leveled? + auto copiedNpc = CloneNpc(formId, AiPackagesMode::KeepOriginal); + if (!copiedNpc) { + return nullptr; + } + + { + std::stringstream ss; + ss << std::hex << "working on " << formId << "\n"; + auto str = ss.str(); + OutputDebugStringA(str.data()); + } + + if (cursorStack.size() > 0) { + OutputDebugStringA("cursorStack.size() > 0\n"); + str = + "cursorStack.size() is " + std::to_string(cursorStack.size()) + "\n"; + OutputDebugStringA(str.data()); + for (auto v : cursorStack) { + std::stringstream ss; + ss << " - " << v << std::endl; + str = ss.str(); + OutputDebugStringA(str.data()); + } + copiedNpc->baseTemplateForm = cursorStack.back(); + // copiedNpc->baseTemplateForm = nullptr; + + // auto leak = new std::vector(); + // leak->reserve(10); + auto leak = new RE::BSTArray(); + FillFormsArray(*leak, cursorStack); + + // copiedNpc->CopyFromTemplateForms(leak->data()); + + auto leak2 = new RE::BSTArray(); + leak2->resize(100, 0); + + // copiedNpc->templateForms = reinterpret_cast(leak2); + + copiedNpc->CopyFromTemplateForms( + reinterpret_cast(leak)); + } else { + OutputDebugStringA("cursorStack.size() == 0\n"); + } + cursorStack.push_back(copiedNpc); + } + + if (cursorStack.empty()) { + return nullptr; + } + + cachedNpc = cursorStack.back(); + return cachedNpc; +} + void TESModPlatform::SetNpcSex(IVM* vm, StackID stackId, RE::StaticFunctionTag*, RE::TESNPC* npc, int32_t sex) @@ -914,6 +1024,12 @@ bool TESModPlatform::Register(IVM* vm) RE::StaticFunctionTag*>( "CreateNpc", "TESModPlatform", CreateNpc)); + vm->BindNativeMethod( + new RE::BSScript::NativeFunction( + "EvaluateLeveledNpc", "TESModPlatform", EvaluateLeveledNpc)); + vm->BindNativeMethod( new RE::BSScript::NativeFunction +#include + +PartOne& GetPartOne(); + +TEST_CASE( + "trapwallwood (54b15) in BleakFalls shouldn't be activatable by actors", + "[ActivateParentTest]") +{ + PartOne& p = GetPartOne(); + + p.worldState.npcSettings["Skyrim.esm"].spawnInInterior = true; + p.worldState.npcEnabled = true; + + auto& trapWallWood = p.worldState.GetFormAt(0x54b15); + + // Bandit will try to activate trapwallwood + uint32_t actorId = 0x39fe4; + auto& bandit = p.worldState.GetFormAt(actorId); + bandit.SetPos(trapWallWood.GetPos()); + + REQUIRE_THROWS_WITH(trapWallWood.Activate(bandit), + "Only activation parents can activate this object"); +} +namespace { +class MyListener : public PartOneListener +{ +public: + void OnConnect(Networking::UserId userId) {} + void OnDisconnect(Networking::UserId userId) {} + void OnCustomPacket(Networking::UserId userId, + const simdjson::dom::element& content) + { + } + bool OnMpApiEvent(const char* eventName, + std::optional, + std::optional formId) + { + if (eventName == std::string("onActivate")) { + if (!formId) { + throw std::runtime_error("ActivateParentTest.cpp - null formId"); + } + numActivates[*formId]++; + } + return true; + } + + std::map numActivates; +}; +} + +TEST_CASE("trapwallwood (54b15) in BleakFalls should be activatable by " + "activation parents", + "[ActivateParentTest]") +{ + PartOne& p = GetPartOne(); + + auto listener = std::make_shared(); + p.AddListener(listener); + + p.worldState.npcSettings["Skyrim.esm"].spawnInInterior = true; + p.worldState.npcEnabled = true; + + auto& trapWallWood = p.worldState.GetFormAt(0x54b15); + auto& plate = p.worldState.GetFormAt(0x567f2); + + auto activationParents = + espm::GetData(0x54b15, &p.worldState).activationParents; + REQUIRE(activationParents.size() == 1); + REQUIRE(activationParents[0].refrId == plate.GetFormId()); + REQUIRE(activationParents[0].delay == 0); + + REQUIRE(listener->numActivates[trapWallWood.GetFormId()] == 0); + REQUIRE(listener->numActivates[plate.GetFormId()] == 0); + + plate.Activate(plate); + + REQUIRE(listener->numActivates[trapWallWood.GetFormId()] == 0); + REQUIRE(listener->numActivates[plate.GetFormId()] == 1); + + p.Tick(); // tick timers, child activations are deferred + + REQUIRE(listener->numActivates[trapWallWood.GetFormId()] == 1); + REQUIRE(listener->numActivates[plate.GetFormId()] == 1); +} diff --git a/unit/ChangeValuesTest.cpp b/unit/ChangeValuesTest.cpp index 77af1b9f46..258f424af2 100644 --- a/unit/ChangeValuesTest.cpp +++ b/unit/ChangeValuesTest.cpp @@ -54,7 +54,7 @@ TEST_CASE("OnChangeValues call is cropping percentage values", auto appearance = ac.GetAppearance(); uint32_t raceId = appearance ? appearance->raceId : 0; BaseActorValues baseValues = - GetBaseActorValues(&p.worldState, baseId, raceId); + GetBaseActorValues(&p.worldState, baseId, raceId, {}); ActionListener::RawMessageData msgData; msgData.userId = 0; diff --git a/unit/CropRegenerationTest.cpp b/unit/CropRegenerationTest.cpp index 7aa207b825..85790a27cb 100644 --- a/unit/CropRegenerationTest.cpp +++ b/unit/CropRegenerationTest.cpp @@ -104,7 +104,7 @@ TEST_CASE("CropHealthRegeneration, CropMagickaRegeneration and " auto appearance = ac.GetAppearance(); uint32_t raceId = appearance ? appearance->raceId : 0; BaseActorValues baseValues = - GetBaseActorValues(&p.worldState, baseId, raceId); + GetBaseActorValues(&p.worldState, baseId, raceId, {}); ac.SetPercentages({ 0.0f, 0.0f, 0.0f }); diff --git a/unit/EspmTest.cpp b/unit/EspmTest.cpp index 2099f4101a..b6b22bd3ef 100644 --- a/unit/EspmTest.cpp +++ b/unit/EspmTest.cpp @@ -278,6 +278,7 @@ TEST_CASE("Loads Npc", "[espm]") REQUIRE(npc->GetData(compressedFieldsCache).defaultOutfitId == 0x1697c); REQUIRE(npc->GetData(compressedFieldsCache).sleepOutfitId == 0x0); REQUIRE(npc->GetData(compressedFieldsCache).objects.size() == 16); + REQUIRE(npc->GetData(compressedFieldsCache).templateDataFlags == 0); } TEST_CASE("Loads Weapon", "[espm]") diff --git a/unit/GetBaseActorValuesTest.cpp b/unit/GetBaseActorValuesTest.cpp index 4d44b73b46..c399d1e7c3 100644 --- a/unit/GetBaseActorValuesTest.cpp +++ b/unit/GetBaseActorValuesTest.cpp @@ -23,7 +23,7 @@ TEST_CASE("GetBaseActorValues works correctly", "[GetBaseActorValues]") auto appearance = ac.GetAppearance(); uint32_t raceId = appearance ? appearance->raceId : 0; BaseActorValues baseValues = - GetBaseActorValues(&p.worldState, baseId, raceId); + GetBaseActorValues(&p.worldState, baseId, raceId, {}); REQUIRE(baseValues.health == 100.f); REQUIRE(baseValues.stamina == 100.f); diff --git a/unit/HeuristicPolicyTest.cpp b/unit/HeuristicPolicyTest.cpp index c255283de4..6affca9091 100644 --- a/unit/HeuristicPolicyTest.cpp +++ b/unit/HeuristicPolicyTest.cpp @@ -1,15 +1,15 @@ #include "TestUtils.hpp" #include -#include "HeuristicPolicy.h" #include "MpActor.h" -#include "MpFormGameObject.h" +#include "script_compatibility_policies/HeuristicPolicy.h" +#include "script_objects/MpFormGameObject.h" TEST_CASE("HeuristicPolicy", "[HeuristicPolicy]") { auto logger = std::make_shared("empty logger"); WorldState wst; - HeuristicPolicy policy(logger, &wst); + HeuristicPolicy policy(&wst); MpActor actor(LocationalData(), FormCallbacks::DoNothing()); MpFormGameObject actorGameObject(&actor); diff --git a/unit/LeveledListUtilsTest.cpp b/unit/LeveledListUtilsTest.cpp index c67a5ad0b0..7a9a33d50d 100644 --- a/unit/LeveledListUtilsTest.cpp +++ b/unit/LeveledListUtilsTest.cpp @@ -5,11 +5,11 @@ #include #include #include +#include #include extern espm::Loader l; -using namespace LeveledListUtils; using namespace std::chrono_literals; TEST_CASE("Bug", "[espm]") @@ -47,8 +47,8 @@ TEST_CASE("Bug", "[espm]") for (auto lvli : leveled) { for (int i = 0; i < 100; ++i) { const int pcLevel = 1, count = 1; - auto result = - EvaluateListRecurse(l.GetBrowser(), lvli, count, pcLevel, nullptr); + auto result = LeveledListUtils::EvaluateListRecurse( + l.GetBrowser(), lvli, count, pcLevel, nullptr); } } finished = true; @@ -64,7 +64,7 @@ TEST_CASE("Evaluate LItemFoodCabbage75", "[espm]") int totalItems = 0; for (int i = 0; i < 100000; ++i) { - auto res = EvaluateList(br, leveledList); + auto res = LeveledListUtils::EvaluateList(br, leveledList); if (res.size() > 0) { REQUIRE(res.size() == 1); REQUIRE(res[0].formId == FoodCabbage); @@ -85,14 +85,14 @@ TEST_CASE("Evaluate recurse LItemFoodCabbage", "[espm]") auto leveledList = br.LookupById(LItemFoodCabbage); for (int i = 0; i < 100; i++) { - auto res = EvaluateListRecurse(br, leveledList); + auto res = LeveledListUtils::EvaluateListRecurse(br, leveledList); REQUIRE(res.size() == 1); REQUIRE(res[FoodCabbage] >= 1); REQUIRE(res[FoodCabbage] <= 5); } // "Calculate for each" flag is not set - auto r = EvaluateListRecurse(br, leveledList, 10); + auto r = LeveledListUtils::EvaluateListRecurse(br, leveledList, 10); REQUIRE(r.size() == 1); REQUIRE(r[FoodCabbage] % 10 == 0); } @@ -107,7 +107,7 @@ TEST_CASE("Evaluate LootGoldChange25", "[espm]") int n[10] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; for (int i = 0; i < 100000; ++i) { - auto res = EvaluateList(br, leveledList); + auto res = LeveledListUtils::EvaluateList(br, leveledList); if (res.size() == 0) { n[0]++; } @@ -131,7 +131,7 @@ TEST_CASE("Evaluate DeathItemDraugr", "[espm]") auto& br = l.GetBrowser(); auto leveledList = br.LookupById(DeathItemDraugr); - auto res = EvaluateList(br, leveledList); + auto res = LeveledListUtils::EvaluateList(br, leveledList); REQUIRE(res.size() == 5); } @@ -142,12 +142,12 @@ TEST_CASE("Evaluate LItemWeaponDaggerTown", "[espm]") auto leveledList = br.LookupById(LItemWeaponDaggerTown); // Normally this leveled list returns one of daggers (iron, steel, etc) - auto res = EvaluateListRecurse(br, leveledList, 1); + auto res = LeveledListUtils::EvaluateListRecurse(br, leveledList, 1); REQUIRE(res.size() == 1); // But if we increase count, we will get a lot of different daggers // The reason is "Each" flag is set - res = EvaluateListRecurse(br, leveledList, 1000); + res = LeveledListUtils::EvaluateListRecurse(br, leveledList, 1000); REQUIRE(res.size() == 5); REQUIRE(res[0x1397e] > 100); @@ -157,7 +157,93 @@ TEST_CASE("Evaluate LItemWeaponDaggerTown", "[espm]") REQUIRE(res[0x1399e] > 30); // PlayerCharacter's level is 1. Only IronDagger should be generated - res = EvaluateListRecurse(br, leveledList, 1000, 1); + res = LeveledListUtils::EvaluateListRecurse(br, leveledList, 1000, 1); REQUIRE(res.size() == 1); REQUIRE(res[0x1397e] == 1000); } + +TEST_CASE("Evaluate LCharHorse", "[espm]") +{ + auto LCharHorse = 0x68d6f; + auto& br = l.GetBrowser(); + auto leveledList = br.LookupById(LCharHorse); + + std::map results; + + for (int i = 0; i < 1000000; ++i) { + auto res = LeveledListUtils::EvaluateListRecurse(br, leveledList, 1); + REQUIRE(res.size() == 1); + + auto pair = *res.begin(); + results[pair.first] += pair.second; + if (results.size() == 5) { + break; + } + } + + REQUIRE(results.size() == 5); + REQUIRE(results[0x68d6e] > 0); + REQUIRE(results[0x68d6d] > 0); + REQUIRE(results[0x68d6b] > 0); + REQUIRE(results[0x68d5b] > 0); + REQUIRE(results[0x68d07] > 0); +} + +TEST_CASE("Evaluate LvlHorse template", "[espm]") +{ + auto LvlHorse = 0x68d71; + auto& br = l.GetBrowser(); + auto horse = br.LookupById(LvlHorse); + + std::map results; + + for (int i = 0; i < 1000000; ++i) { + auto res = LeveledListUtils::EvaluateTemplateChain(br, horse, 1); + REQUIRE(res.size() == 2); + REQUIRE(res[0] == LvlHorse); + + results[res[1]] += 1; + if (results.size() == 5) { + break; + } + } + + REQUIRE(results.size() == 5); + REQUIRE(results[0x68d6e] > 0); + REQUIRE(results[0x68d6d] > 0); + REQUIRE(results[0x68d6b] > 0); + REQUIRE(results[0x68d5b] > 0); + REQUIRE(results[0x68d07] > 0); +} + +TEST_CASE("Evaluate LvlMudcrab template", "[espm]") +{ + auto LvlMudcrab = 0x8cacc; + auto& br = l.GetBrowser(); + auto mudcrab = br.LookupById(LvlMudcrab); + + std::map countByFormId; + + for (int i = 0; i < 100'000; i++) { + constexpr int kPcLevel = 5; + auto chain = + LeveledListUtils::EvaluateTemplateChain(br, mudcrab, kPcLevel); + REQUIRE(chain.size() == 2); + REQUIRE(chain[0] == 0x8cacc); + ++countByFormId[chain[1]]; + } + + // formId e4010 level 1 + // formId e4010 level 1 + // formId e4011 level 1 + // formId e4011 level 5 + // So with level 5 should be 50/50 + + REQUIRE(countByFormId.size() == 2); + + int64_t countA = static_cast(countByFormId.begin()->second); + int64_t countB = static_cast((--countByFormId.end())->second); + + // usually diff is less than 100, but we don't want to fail tests randomly + REQUIRE(std::abs(countA - countB) < 2000); +} diff --git a/unit/MigrationDatabaseTest.cpp b/unit/MigrationDatabaseTest.cpp index da72ca9abf..e771552739 100644 --- a/unit/MigrationDatabaseTest.cpp +++ b/unit/MigrationDatabaseTest.cpp @@ -1,6 +1,6 @@ -#include "MigrationDatabase.h" -#include "FileDatabase.h" +#include "database_drivers/MigrationDatabase.h" #include "TestUtils.hpp" +#include "database_drivers/FileDatabase.h" #include inline std::shared_ptr MakeDatabase(const char* directory) diff --git a/unit/PapyrusActorTest.cpp b/unit/PapyrusActorTest.cpp index 3c9e294d45..d5056e236e 100644 --- a/unit/PapyrusActorTest.cpp +++ b/unit/PapyrusActorTest.cpp @@ -1,7 +1,7 @@ #include "TestUtils.hpp" #include -#include "PapyrusActor.h" +#include "script_classes/PapyrusActor.h" PartOne& GetPartOne(); diff --git a/unit/PapyrusCompatibilityTest.cpp b/unit/PapyrusCompatibilityTest.cpp index 910ddeb0f3..bf9300256a 100644 --- a/unit/PapyrusCompatibilityTest.cpp +++ b/unit/PapyrusCompatibilityTest.cpp @@ -8,6 +8,8 @@ PartOne& GetPartOne(); TEST_CASE("Should be able to harvest a Nirnroot", "[Papyrus][espm]") { auto& partOne = GetPartOne(); + partOne.worldState.disableVanillaScriptsInExterior = false; + auto& nirnrootRef = partOne.worldState.GetFormAt(0xa4de9); partOne.worldState.AddForm( @@ -33,6 +35,8 @@ TEST_CASE("Should be able to harvest a Nirnroot", "[Papyrus][espm]") TEST_CASE("Server crash in CallMethod", "[Papyrus][espm]") { auto& partOne = GetPartOne(); + partOne.worldState.disableVanillaScriptsInExterior = false; + auto& ref = partOne.worldState.GetFormAt(0xd8995); partOne.worldState.AddForm( std::make_unique( @@ -50,6 +54,8 @@ TEST_CASE("Server crash in CallMethod", "[Papyrus][espm]") TEST_CASE("Server crash in PropGet", "[Papyrus][espm]") { auto& partOne = GetPartOne(); + partOne.worldState.disableVanillaScriptsInExterior = false; + auto& ref = partOne.worldState.GetFormAt(0xabb6f); partOne.worldState.AddForm( std::make_unique( @@ -68,6 +74,8 @@ TEST_CASE("Server crash in PropGet", "[Papyrus][espm]") TEST_CASE("Activate auto load door in BrokenOarGrotto01", "[PartOne][espm]") { auto& partOne = GetPartOne(); + partOne.worldState.disableVanillaScriptsInExterior = false; + auto& ref = partOne.worldState.GetFormAt(87048); partOne.worldState.AddForm( @@ -89,6 +97,8 @@ TEST_CASE("Activate auto load door in BrokenOarGrotto01", "[PartOne][espm]") TEST_CASE("OnTriggerEnter crash in MovarthsLairExterior01", "[PartOne][espm]") { auto& partOne = GetPartOne(); + partOne.worldState.disableVanillaScriptsInExterior = false; + auto& ref = partOne.worldState.GetFormAt(464472); partOne.worldState.AddForm( diff --git a/unit/PapyrusDebugTest.cpp b/unit/PapyrusDebugTest.cpp index b5bbe67b11..2ce58344ac 100644 --- a/unit/PapyrusDebugTest.cpp +++ b/unit/PapyrusDebugTest.cpp @@ -1,7 +1,8 @@ #include "TestUtils.hpp" #include -#include "PapyrusDebug.h" +#include "script_classes/PapyrusDebug.h" +#include "script_compatibility_policies/HeuristicPolicy.h" TEST_CASE("Notification", "[Papyrus][Debug]") { @@ -15,8 +16,11 @@ TEST_CASE("Notification", "[Papyrus][Debug]") auto& ac = p.worldState.GetFormAt(0xff000000); + auto policy = new HeuristicPolicy(&p.worldState); + policy->SetDefaultActor(VarValue::AttachTestStackId().GetMetaStackId(), &ac); + PapyrusDebug debug; - debug.compatibilityPolicy.reset(new PapyrusCompatibilityPolicy(&ac)); + debug.compatibilityPolicy.reset(policy); DoConnect(p, 3); p.SetUserActor(3, 0xff000000); diff --git a/unit/PapyrusFormListTest.cpp b/unit/PapyrusFormListTest.cpp index bf0f4ec6a2..c22eafe555 100644 --- a/unit/PapyrusFormListTest.cpp +++ b/unit/PapyrusFormListTest.cpp @@ -1,8 +1,8 @@ #include "TestUtils.hpp" #include -#include "EspmGameObject.h" -#include "PapyrusFormList.h" +#include "script_classes/PapyrusFormList.h" +#include "script_objects/EspmGameObject.h" extern espm::Loader l; diff --git a/unit/PapyrusFormTest.cpp b/unit/PapyrusFormTest.cpp index 67056879e9..ad5849106b 100644 --- a/unit/PapyrusFormTest.cpp +++ b/unit/PapyrusFormTest.cpp @@ -1,7 +1,7 @@ #include "TestUtils.hpp" #include -#include "PapyrusForm.h" +#include "script_classes/PapyrusForm.h" using namespace std::chrono_literals; diff --git a/unit/PapyrusGameTest.cpp b/unit/PapyrusGameTest.cpp index 0acc37682a..33bf742668 100644 --- a/unit/PapyrusGameTest.cpp +++ b/unit/PapyrusGameTest.cpp @@ -1,10 +1,10 @@ -#include "HeuristicPolicy.h" #include "PartOne.h" #include "TestUtils.hpp" +#include "script_compatibility_policies/HeuristicPolicy.h" #include -#include "PapyrusGame.h" #include "papyrus-vm/Structures.h" +#include "script_classes/PapyrusGame.h" PartOne& GetPartOne(); @@ -13,8 +13,7 @@ TEST_CASE("GetForm", "[Papyrus][Game][espm]") PartOne& partOne = GetPartOne(); PapyrusGame game; std::shared_ptr logger; - game.compatibilityPolicy.reset( - new HeuristicPolicy(logger, &partOne.worldState)); + game.compatibilityPolicy.reset(new HeuristicPolicy(&partOne.worldState)); constexpr const uint32_t foodBarrel = 0x20570; const auto& refer = @@ -32,8 +31,7 @@ TEST_CASE("GetFormEx", "[Papyrus][Game][espm]") PartOne& partOne = GetPartOne(); PapyrusGame game; std::shared_ptr logger; - game.compatibilityPolicy.reset( - new HeuristicPolicy(logger, &partOne.worldState)); + game.compatibilityPolicy.reset(new HeuristicPolicy(&partOne.worldState)); DoConnect(partOne, 0); const uint32_t formId = partOne.CreateActor(0xff000000, { 0, 0, 0 }, 0, 0x3c); diff --git a/unit/PapyrusObjectReferenceTest.cpp b/unit/PapyrusObjectReferenceTest.cpp index 5e5959cd15..98534e2d7a 100644 --- a/unit/PapyrusObjectReferenceTest.cpp +++ b/unit/PapyrusObjectReferenceTest.cpp @@ -2,10 +2,10 @@ #include #include "ActionListener.h" -#include "EspmGameObject.h" #include "MpObjectReference.h" -#include "PapyrusObjectReference.h" #include "papyrus-vm/Structures.h" +#include "script_classes/PapyrusObjectReference.h" +#include "script_objects/EspmGameObject.h" using Catch::Matchers::ContainsSubstring; diff --git a/unit/PapyrusSkympTest.cpp b/unit/PapyrusSkympTest.cpp index 4ff3d98420..70d0f42512 100644 --- a/unit/PapyrusSkympTest.cpp +++ b/unit/PapyrusSkympTest.cpp @@ -1,8 +1,8 @@ #include "TestUtils.hpp" #include -#include "HeuristicPolicy.h" -#include "PapyrusSkymp.h" +#include "script_classes/PapyrusSkymp.h" +#include "script_compatibility_policies/HeuristicPolicy.h" TEST_CASE("SetDefaultActor should store actor per stack", "[Papyrus][Skymp]") { @@ -12,7 +12,7 @@ TEST_CASE("SetDefaultActor should store actor per stack", "[Papyrus][Skymp]") auto logger = std::make_shared("empty logger"); PapyrusSkymp skymp; - skymp.policy = std::make_shared(logger, &p.worldState); + skymp.policy = std::make_shared(&p.worldState); skymp.SetDefaultActor(VarValue::AttachTestStackId(VarValue::None(), 0), { ac.ToVarValue() }); diff --git a/unit/PapyrusUtilityTest.cpp b/unit/PapyrusUtilityTest.cpp index 3c6a07ea1f..774ec3b76d 100644 --- a/unit/PapyrusUtilityTest.cpp +++ b/unit/PapyrusUtilityTest.cpp @@ -1,15 +1,15 @@ #include "TestUtils.hpp" #include -#include "HeuristicPolicy.h" -#include "PapyrusUtility.h" +#include "script_classes/PapyrusUtility.h" +#include "script_compatibility_policies/HeuristicPolicy.h" TEST_CASE("Wait", "[Papyrus][Utility]") { WorldState wst; PapyrusUtility utility; std::shared_ptr logger; - utility.compatibilityPolicy.reset(new HeuristicPolicy(logger, &wst)); + utility.compatibilityPolicy.reset(new HeuristicPolicy(&wst)); bool waitFinished = false; diff --git a/unit/PartOne_ActivateTest.cpp b/unit/PartOne_ActivateTest.cpp index f0d4791915..0b441c5f85 100644 --- a/unit/PartOne_ActivateTest.cpp +++ b/unit/PartOne_ActivateTest.cpp @@ -1,5 +1,5 @@ -#include "ScriptStorage.h" #include "TestUtils.hpp" +#include "script_storages/DirectoryScriptStorage.h" using Catch::Matchers::ContainsSubstring; diff --git a/unit/SaveStorageTest.cpp b/unit/SaveStorageTest.cpp index 254537e669..5fe990b624 100644 --- a/unit/SaveStorageTest.cpp +++ b/unit/SaveStorageTest.cpp @@ -1,8 +1,8 @@ #include "TestUtils.hpp" -#include "AsyncSaveStorage.h" -#include "FileDatabase.h" #include "MpChangeForms.h" +#include "database_drivers/FileDatabase.h" +#include "save_storages/AsyncSaveStorage.h" #include std::shared_ptr MakeSaveStorage() diff --git a/unit/TemplateInventoryTest.cpp b/unit/TemplateInventoryTest.cpp new file mode 100644 index 0000000000..e342f85689 --- /dev/null +++ b/unit/TemplateInventoryTest.cpp @@ -0,0 +1,31 @@ +#include "MsgType.h" +#include "ServerState.h" +#include "TestUtils.hpp" +#include +#include + +PartOne& GetPartOne(); + +TEST_CASE("MS13BanditCampfire01 in BleakFalls should have inventory/equipment " + "derived from NPC template", + "[TemplateInventory]") +{ + PartOne& p = GetPartOne(); + p.worldState.npcSettings["Skyrim.esm"].spawnInInterior = true; + p.worldState.npcEnabled = true; + uint32_t actorId = 0x39fe4; + auto& bandit = p.worldState.GetFormAt(actorId); + + REQUIRE(bandit.GetEquipment().inv.ToJson().size() > 0); + + int numWeaps = 0; + for (auto entry : bandit.GetEquipment().inv.entries) { + auto lookupRes = + p.worldState.GetEspm().GetBrowser().LookupById(entry.baseId); + if (lookupRes.rec && lookupRes.rec->GetType() == "WEAP") { + numWeaps++; + } + } + + REQUIRE(numWeaps == 1); +} diff --git a/unit/TemplateScriptTest.cpp b/unit/TemplateScriptTest.cpp new file mode 100644 index 0000000000..625ed5efa0 --- /dev/null +++ b/unit/TemplateScriptTest.cpp @@ -0,0 +1,50 @@ +#include "MsgType.h" +#include "ServerState.h" +#include "TestUtils.hpp" +#include "script_storages/IScriptStorage.h" +#include +#include + +PartOne& GetPartOne(); + +namespace { +class MyScriptStorage : public IScriptStorage +{ + std::vector GetScriptPex(const char* scriptName) override + { + if (scriptName == std::string("masterambushscript")) { + throw std::runtime_error("OK"); + } + return {}; + } + + const std::set& ListScripts(bool forceReloadScripts) override + { + static const std::set kSet = { "masterambushscript" }; + return kSet; + } +}; +} + +TEST_CASE("MS13FrostbiteSpiderREF in BleakFalls should have scripts " + "derived from NPC template", + "[TemplateScript]") +{ + PartOne& p = GetPartOne(); + + p.worldState.npcSettings["Skyrim.esm"].spawnInInterior = true; + p.worldState.npcEnabled = true; + + p.worldState.AttachScriptStorage(std::make_shared()); + + std::string what; + try { + uint32_t actorId = 0x3a1e1; + auto& spider = p.worldState.GetFormAt(actorId); + spider.SendPapyrusEvent("OnLoad"); + } catch (std::exception& e) { + what = e.what(); + } + + REQUIRE(what == "OK"); +} diff --git a/unit/TestUtils.cpp b/unit/TestUtils.cpp index 806e4581f2..f7012b245c 100644 --- a/unit/TestUtils.cpp +++ b/unit/TestUtils.cpp @@ -1,6 +1,5 @@ #include "TestUtils.hpp" #include "FormCallbacks.h" -#include "IPapyrusCompatibilityPolicy.h" #include "MpActor.h" #include "MsgType.h" #include "PartOne.h" @@ -104,14 +103,3 @@ void FakeListener::clear() { ss = std::stringstream(); } - -PapyrusCompatibilityPolicy::PapyrusCompatibilityPolicy(MpActor* ac_) - : ac(ac_) -{ -} - -MpActor* PapyrusCompatibilityPolicy::GetDefaultActor(const char*, const char*, - int32_t) const -{ - return ac; -} diff --git a/unit/TestUtils.hpp b/unit/TestUtils.hpp index 77d65faf48..0539c58c48 100644 --- a/unit/TestUtils.hpp +++ b/unit/TestUtils.hpp @@ -1,9 +1,9 @@ #pragma once #include "FormCallbacks.h" -#include "IPapyrusCompatibilityPolicy.h" #include "MpActor.h" #include "MsgType.h" #include "PartOne.h" +#include "script_compatibility_policies/IPapyrusCompatibilityPolicy.h" #include #include #include @@ -100,14 +100,3 @@ class FakeListener : public PartOne::Listener private: std::stringstream ss; }; - -class PapyrusCompatibilityPolicy : public IPapyrusCompatibilityPolicy -{ -public: - PapyrusCompatibilityPolicy(MpActor* ac_); - - MpActor* GetDefaultActor(const char*, const char*, int32_t) const override; - -private: - MpActor* const ac; -}; diff --git a/unit/VarValueTest.cpp b/unit/VarValueTest.cpp index 5efb4eacf3..65541226fa 100644 --- a/unit/VarValueTest.cpp +++ b/unit/VarValueTest.cpp @@ -20,6 +20,19 @@ TEST_CASE("operator= for owning objects", "[VarValue]") { class MyObject : public IGameObject { + const std::vector>& + ListActivePexInstances() const override + { + static const std::vector> + kEmptyScripts; + return kEmptyScripts; + } + + void AddScript(std::shared_ptr sctipt) noexcept override + { + spdlog::critical("VarValueTest.cpp: AddScript not implemented"); + std::terminate(); + } }; VarValue var; @@ -42,6 +55,20 @@ TEST_CASE("operator== for objects", "[VarValue]") return false; } + const std::vector>& + ListActivePexInstances() const override + { + static const std::vector> + kEmptyScripts; + return kEmptyScripts; + } + + void AddScript(std::shared_ptr sctipt) noexcept override + { + spdlog::critical("VarValueTest.cpp: AddScript not implemented"); + std::terminate(); + } + private: int i; }; diff --git a/unit/VirtualMachineTest.cpp b/unit/VirtualMachineTest.cpp index 70ce808f0d..f814390fc1 100644 --- a/unit/VirtualMachineTest.cpp +++ b/unit/VirtualMachineTest.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include namespace fs = std::filesystem; @@ -45,16 +46,20 @@ std::shared_ptr CreateVirtualMachine() auto vm = std::make_shared(vector); - std::shared_ptr assertId(new int(1)); + std::shared_ptr assertId = std::make_shared(1); + std::shared_ptr ss = + std::make_shared(); + vm->RegisterFunction("", "Print", FunctionType::GlobalFunction, [=](VarValue self, const std::vector args) { if (args.size() >= 1) { - std::string showString = (const char*)args[0]; - std::cout << std::endl - << "[!] Papyrus says: " << showString - << std::endl - << std::endl; - (*assertId) = 1; + std::string showString = + static_cast(args[0]); + *ss << std::endl + << "[!] Papyrus says: " << showString + << std::endl + << std::endl; + *assertId = 1; } return VarValue::None(); }); @@ -62,15 +67,16 @@ std::shared_ptr CreateVirtualMachine() vm->RegisterFunction("", "Assert", FunctionType::GlobalFunction, [=](VarValue self, std::vector args) { if (args.size() >= 1) { - bool success = (bool)args[0]; + bool success = static_cast(args[0]); std::string message = "\t Assertion " + std::string(success ? "succeed" : "failed") + " (" + std::to_string(*assertId) + ")"; - (*assertId)++; + ++*assertId; if (!success) { + std::cout << ss->str(); throw std::runtime_error(message); } - std::cout << message << std::endl; + *ss << message << std::endl; } return VarValue::None(); }); @@ -87,8 +93,20 @@ class TestObject : public IGameObject { public: std::string myId = "0x006AFF2E"; + std::vector> scripts; const char* GetStringID() override { return myId.c_str(); }; + + const std::vector>& + ListActivePexInstances() const override + { + return scripts; + } + + void AddScript(std::shared_ptr sctipt) noexcept override + { + scripts.push_back(sctipt); + } }; class MyScriptVariablesHolder : public ScriptVariablesHolder diff --git a/unit/papyrus_test_files/pex/AAATestObject.pex b/unit/papyrus_test_files/pex/AAATestObject.pex index 2cd7742b1e..78f8a142ed 100644 Binary files a/unit/papyrus_test_files/pex/AAATestObject.pex and b/unit/papyrus_test_files/pex/AAATestObject.pex differ diff --git a/unit/papyrus_test_files/pex/LatentTest.pex b/unit/papyrus_test_files/pex/LatentTest.pex index cd36ff7aa7..59f2863f56 100644 Binary files a/unit/papyrus_test_files/pex/LatentTest.pex and b/unit/papyrus_test_files/pex/LatentTest.pex differ diff --git a/unit/papyrus_test_files/pex/OpcodesTest.pex b/unit/papyrus_test_files/pex/OpcodesTest.pex index 9230dd081f..4d5218eb78 100644 Binary files a/unit/papyrus_test_files/pex/OpcodesTest.pex and b/unit/papyrus_test_files/pex/OpcodesTest.pex differ diff --git a/vcpkg.json b/vcpkg.json index 98a6af4e8a..5166218942 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -19,6 +19,7 @@ "node-addon-api", "bshoshany-thread-pool", "makeid", + "rsm-bsa", { "name": "frida-gum", "platform": "windows" diff --git a/viet/include/Promise.h b/viet/include/Promise.h index 86543828f9..12651c4615 100644 --- a/viet/include/Promise.h +++ b/viet/include/Promise.h @@ -99,6 +99,18 @@ class Promise return res; } + static Promise Any(const std::vector>& promises) + { + Promise res; + + for (const auto& promise : promises) { + promise.Then([res](const T& val) { res.Resolve(val); }) + .Catch([res](const char* err) { res.Reject(err); }); + } + + return res; + } + operator AnyPromise() { return AnyPromise(*this); } private: