From 294606e47c962dfaf72ca70452a9fdd7a5201f14 Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Fri, 14 Nov 2025 23:48:34 -0300 Subject: [PATCH] perf: optimize spectator retrieval and add unit tests (gtest) (#1319) Refactors the build system by introducing the static library otclient_core, separating core client code from platform-specific entry points. Centralizes compiler flags, precompiled headers, unity build, and sanitizer configuration to ensure consistent behavior across all platforms. Updates include paths and dependencies, ensuring that every platform links correctly against the new core target. Simplifies Discord RPC support by using a dedicated CMake option. Modernizes C++ code by making type-checking methods const and adding new silent state-update methods to Creature, along with a const getter for retrieving the previous position. Updates Lua bindings to expose overloaded const and non-const methods, preventing ambiguity correctly. Improves code hygiene in Map by reorganizing includes, adding a helper function to clean duplicated spectators, and optimizing spectator retrieval with memory reservations and more efficient data structures. Adds Google Test support and introduces a CMake option to enable or disable building unit tests. --- CMakeLists.txt | 10 + src/CMakeLists.txt | 201 +++++----- src/client/creature.h | 11 +- src/client/effect.h | 2 +- src/client/item.h | 4 +- src/client/localplayer.h | 2 +- src/client/luafunctions.cpp | 10 +- src/client/map.cpp | 75 +++- src/client/missile.h | 2 +- src/client/player.h | 2 +- src/client/thing.cpp | 514 +++++++++++++++++++----- src/client/thing.h | 18 +- src/client/tile.cpp | 80 +++- src/client/tile.h | 12 +- src/framework/global.h | 4 +- src/framework/luaengine/luabinder.h | 104 ++++- src/framework/luaengine/luainterface.h | 13 +- tests/CMakeLists.txt | 45 +++ tests/map/CMakeLists.txt | 5 + tests/map/map_spectators_test.cpp | 515 +++++++++++++++++++++++++ vcpkg.json | 1 + 21 files changed, 1395 insertions(+), 235 deletions(-) create mode 100644 tests/CMakeLists.txt create mode 100644 tests/map/CMakeLists.txt create mode 100644 tests/map/map_spectators_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index bff3500ce..0cd5c6c43 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,3 +104,13 @@ endif() add_subdirectory(src/protobuf) # Src add_subdirectory(src) + +option(OTCLIENT_BUILD_TESTS "Build unit tests" ON) + +if(OTCLIENT_BUILD_TESTS) + enable_testing() + add_subdirectory(tests) + log_option_enabled("Build tests") +else() + log_option_disabled("Build tests") +endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 07eab907e..7b61b270a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -75,71 +75,12 @@ if (ANDROID OR WASM) set(FRAMEWORK_DEFINITIONS ${FRAMEWORK_DEFINITIONS} -DOPENGL_ES=2) endif() -# Set for use precompiled header -if(TOGGLE_PRE_COMPILED_HEADER) - # === PRECOMPILED HEADER === - target_precompile_headers(${PROJECT_NAME} PRIVATE framework/pch.h) - message(STATUS "Use precompiled header: ON") -else() - message(STATUS "Use precompiled header: OFF") -endif(TOGGLE_PRE_COMPILED_HEADER) - -# === UNITY BUILD (compile time reducer) === -if(SPEED_UP_BUILD_UNITY) - set_target_properties(${PROJECT_NAME} PROPERTIES UNITY_BUILD ON) - log_option_enabled("Build unity for speed up compilation") -endif(SPEED_UP_BUILD_UNITY) - set(VERSION "1.0.0") -# ***************************************************************************** -# Build flags -# ***************************************************************************** -if (NOT MSVC) - if (CMAKE_COMPILER_IS_GNUCXX) - target_compile_options(${PROJECT_NAME} PRIVATE -Wno-deprecated-declarations) - endif() -endif() - -if(THREADS_HAVE_PTHREAD_ARG) - target_compile_options(${PROJECT_NAME} PUBLIC "-pthread") -endif() - -if(CMAKE_BUILD_TYPE STREQUAL "Release") - add_definitions(-DNDEBUG) -endif() - # ***************************************************************************** # Definitions code # ***************************************************************************** - -# === ASAN === -if(ASAN_ENABLED) - log_option_enabled("asan") - - if(MSVC) - string(REGEX REPLACE "/RTC(su|[1su])" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") - target_compile_options(${PROJECT_NAME} PUBLIC /fsanitize=address) - target_link_options(${PROJECT_NAME} PUBLIC /fsanitize=address) - else() - target_compile_options(${PROJECT_NAME} PUBLIC -fsanitize=address) - target_link_options(${PROJECT_NAME} PUBLIC -fsanitize=address) - endif() -else() - log_option_disabled("asan") -endif() - - -# === DEBUG LOG === -# cmake -DDEBUG_LOG=ON .. -if(CMAKE_BUILD_TYPE MATCHES Debug) - target_compile_definitions(${PROJECT_NAME} PRIVATE -DDEBUG_LOG=ON ) - log_option_enabled("DEBUG LOG") -else() - log_option_disabled("DEBUG LOG") -endif(CMAKE_BUILD_TYPE MATCHES Debug) - # ***************************************************************************** # Sanity Check # ***************************************************************************** @@ -212,12 +153,12 @@ if(NOT OPENSSL_FOUND) find_package(GMP REQUIRED) endif() if(ENABLE_DISCORD_RPC AND NOT ANDROID) - find_package(DiscordRPC REQUIRED) - target_compile_definitions(${PROJECT_NAME} PRIVATE ENABLE_DISCORD_RPC=1) - log_option_enabled("Discord Rich Presence") + find_package(DiscordRPC REQUIRED) + set(OTCLIENT_ENABLE_DISCORD_RPC 1) + log_option_enabled("Discord Rich Presence") else() - target_compile_definitions(${PROJECT_NAME} PRIVATE ENABLE_DISCORD_RPC=0) - log_option_disabled("Discord Rich Presence") + set(OTCLIENT_ENABLE_DISCORD_RPC 0) + log_option_disabled("Discord Rich Presence") endif() if(TOGGLE_DIRECTX) find_package(DirectX REQUIRED) @@ -368,9 +309,6 @@ set(SOURCE_FILES client/uiminimap.cpp client/uiprogressrect.cpp client/uisprite.cpp - - main.cpp - androidmain.cpp ) if (TOGGLE_FRAMEWORK_GRAPHICS) @@ -449,9 +387,79 @@ if (WASM) ) endif() +add_library(otclient_core STATIC ${SOURCE_FILES}) +if(NOT MSVC) + target_link_options(otclient_core PUBLIC -flto=auto) +endif() -target_sources(${PROJECT_NAME} PRIVATE ${SOURCE_FILES}) -target_link_options(${PROJECT_NAME} PUBLIC -flto=auto) +if(TOGGLE_PRE_COMPILED_HEADER) + # === PRECOMPILED HEADER === + target_precompile_headers(otclient_core PRIVATE framework/pch.h) + message(STATUS "Use precompiled header: ON") +else() + message(STATUS "Use precompiled header: OFF") +endif(TOGGLE_PRE_COMPILED_HEADER) + +if(SPEED_UP_BUILD_UNITY) + set_target_properties(otclient_core PROPERTIES UNITY_BUILD ON) + log_option_enabled("Build unity for speed up compilation") +endif(SPEED_UP_BUILD_UNITY) + +if(ANDROID) + target_sources(${PROJECT_NAME} PRIVATE androidmain.cpp) +else() + target_sources(${PROJECT_NAME} PRIVATE main.cpp) +endif() + +target_link_libraries(${PROJECT_NAME} PRIVATE otclient_core) + +# ***************************************************************************** +# Build flags +# ***************************************************************************** +if (NOT MSVC) + if (CMAKE_COMPILER_IS_GNUCXX) + target_compile_options(otclient_core PRIVATE -Wno-deprecated-declarations) + endif() +endif() + +if(THREADS_HAVE_PTHREAD_ARG) + target_compile_options(otclient_core PUBLIC "-pthread") +endif() + +if(CMAKE_BUILD_TYPE STREQUAL "Release") + add_definitions(-DNDEBUG) +endif() + +# ***************************************************************************** +# Definitions code +# ***************************************************************************** + +# === ASAN === +if(ASAN_ENABLED) + log_option_enabled("asan") + + if(MSVC) + string(REGEX REPLACE "/RTC(su|[1su])" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") + target_compile_options(otclient_core PUBLIC /fsanitize=address) + target_link_options(${PROJECT_NAME} PUBLIC /fsanitize=address) + else() + target_compile_options(otclient_core PUBLIC -fsanitize=address) + target_link_options(${PROJECT_NAME} PUBLIC -fsanitize=address) + endif() +else() + log_option_disabled("asan") +endif() + +# === DEBUG LOG === +# cmake -DDEBUG_LOG=ON .. +if(CMAKE_BUILD_TYPE MATCHES Debug) + target_compile_definitions(otclient_core PRIVATE -DDEBUG_LOG=ON ) + log_option_enabled("DEBUG LOG") +else() + log_option_disabled("DEBUG LOG") +endif(CMAKE_BUILD_TYPE MATCHES Debug) + +target_compile_definitions(otclient_core PRIVATE ENABLE_DISCORD_RPC=${OTCLIENT_ENABLE_DISCORD_RPC}) # ***************************************************************************** # Includes and librarys @@ -459,13 +467,13 @@ target_link_options(${PROJECT_NAME} PUBLIC -flto=auto) if(MSVC) # Set variables to have Windows Vista Value so httplib will build 'inet_pton' - target_compile_definitions(${PROJECT_NAME} - PRIVATE + target_compile_definitions(otclient_core + PUBLIC NTDDI_VERSION=0x06000000 _WIN32_WINNT=0x0600 ) - target_compile_options(${PROJECT_NAME} PUBLIC /MP /FS /Zf /EHsc /bigobj) + target_compile_options(otclient_core PUBLIC /MP /FS /Zf /EHsc /bigobj) if(CMAKE_BUILD_TYPE STREQUAL "Debug") string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}") @@ -485,15 +493,24 @@ if(MSVC) if(ANDROID) find_package(Vorbis CONFIG REQUIRED) endif() - set_property(TARGET ${PROJECT_NAME} PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + foreach(msvc_target IN ITEMS otclient_core ${PROJECT_NAME}) + if(TARGET ${msvc_target}) + set_property(TARGET ${msvc_target} PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + endif() + endforeach() set(VCPKG_TARGET_TRIPLET "x64-windows-static" CACHE STRING "") else() log_option_disabled("STATIC_LIBRARY") set(VCPKG_TARGET_TRIPLET "x64-windows" CACHE STRING "") + foreach(msvc_target IN ITEMS otclient_core ${PROJECT_NAME}) + if(TARGET ${msvc_target}) + set_property(TARGET ${msvc_target} PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") + endif() + endforeach() endif() - target_include_directories(${PROJECT_NAME} - PRIVATE + target_include_directories(otclient_core + PUBLIC $ $ ${LUAJIT_INCLUDE_DIR} @@ -506,8 +523,8 @@ if(MSVC) ${NLOHMANN_JSON_INCLUDE_DIR} ${CPPCODEC_INCLUDE_DIRS} ) - target_link_libraries(${PROJECT_NAME} - PRIVATE + target_link_libraries(otclient_core + PUBLIC ${LUAJIT_LIBRARY} ${CMAKE_THREAD_LIBS_INIT} ${PHYSFS_LIBRARY} @@ -536,8 +553,8 @@ if(MSVC) fmt::fmt-header-only ) elseif(ANDROID) - target_include_directories(${PROJECT_NAME} - PRIVATE + target_include_directories(otclient_core + PUBLIC $ $ ${LUAJIT_INCLUDE_DIR} @@ -552,8 +569,8 @@ elseif(ANDROID) ${MINIZIP_INCLUDE_DIR} ${CPPCODEC_INCLUDE_DIRS} ) - target_link_libraries(${PROJECT_NAME} - PRIVATE + target_link_libraries(otclient_core + PUBLIC ${LUA_LIBRARY} ${LUAJIT_LIBRARY} ${PHYSFS_LIBRARY} @@ -586,8 +603,8 @@ elseif(ANDROID) ) elseif(WASM) - target_include_directories(${PROJECT_NAME} - PRIVATE + target_include_directories(otclient_core + PUBLIC ${CMAKE_SOURCE_DIR}/src ${CMAKE_THREAD_LIBS_INIT} ${Protobuf_INCLUDE_DIRS} @@ -600,8 +617,8 @@ elseif(WASM) ${BROWSER_INCLUDE_DIR} ${CPPCODEC_INCLUDE_DIRS} ) - target_link_libraries(${PROJECT_NAME} - PRIVATE + target_link_libraries(otclient_core + PUBLIC ${LUA_LIBRARY} ${PHYSFS_LIBRARY} ${ZLIB_LIBRARY} @@ -644,7 +661,7 @@ elseif(WASM) get_property(linkflags TARGET ${PROJECT_NAME} PROPERTY LINK_FLAGS) if(CMAKE_BUILD_TYPE STREQUAL "Debug") - target_compile_options(${PROJECT_NAME} + target_compile_options(otclient_core PRIVATE -Wall -Wextra -Wpedantic ) @@ -670,8 +687,8 @@ elseif(WASM) set(VCPKG_TARGET_TRIPLET "wasm32-emscripten" CACHE STRING "") else() # Linux - target_include_directories(${PROJECT_NAME} - PRIVATE + target_include_directories(otclient_core + PUBLIC $ $ ${LUAJIT_INCLUDE_DIR} @@ -685,8 +702,8 @@ else() # Linux ${OPENSSL_INCLUDE_DIR} ${CPPCODEC_INCLUDE_DIRS} ) - target_link_libraries(${PROJECT_NAME} - PRIVATE + target_link_libraries(otclient_core + PUBLIC ${LUAJIT_LIBRARY} ${PHYSFS_LIBRARY} ${ZLIB_LIBRARY} @@ -725,7 +742,7 @@ else() # Linux ) if(CMAKE_BUILD_TYPE STREQUAL "Debug") - target_compile_options(${PROJECT_NAME} + target_compile_options(otclient_core PRIVATE -Wall -Wextra -Wpedantic ) @@ -733,7 +750,7 @@ else() # Linux endif() if(ENABLE_DISCORD_RPC AND NOT ANDROID) - target_link_libraries(${PROJECT_NAME} PRIVATE ${DISCORDRPC_LIBRARY}) + target_link_libraries(otclient_core PUBLIC ${DISCORDRPC_LIBRARY}) endif() # ***************************************************************************** diff --git a/src/client/creature.h b/src/client/creature.h index 1d204f078..97741a0f1 100644 --- a/src/client/creature.h +++ b/src/client/creature.h @@ -148,11 +148,13 @@ public: bool isWalking() { return m_walking; } bool isRemoved() { return m_removed; } + bool isRemoved() const { return m_removed; } + const Position& getOldPosition() const { return m_oldPosition; } bool isInvisible() { return m_outfit.isEffect() && m_outfit.getAuxId() == 13; } bool isDead() { return m_healthPercent <= 0; } bool isFullHealth() { return m_healthPercent == 100; } bool canBeSeen() { return !isInvisible() || isPlayer(); } - bool isCreature() override { return true; } + bool isCreature() const override { return true; } bool isCovered() { return m_isCovered; } void setCovered(bool covered); @@ -206,6 +208,9 @@ protected: void updateWalkOffset(uint8_t totalPixelsWalked); void updateWalk(); + void setOldPositionSilently(const Position& pos) { m_oldPosition = pos; } + void setRemovedSilently(const bool removed) { m_removed = removed; } + ThingType* getThingType() const override; ThingType* getMountThingType() const; @@ -357,12 +362,12 @@ private: class Npc final : public Creature { public: - bool isNpc() override { return true; } + bool isNpc() const override { return true; } }; // @bindclass class Monster final : public Creature { public: - bool isMonster() override { return true; } + bool isMonster() const override { return true; } }; diff --git a/src/client/effect.h b/src/client/effect.h index eef25ad76..2920b5901 100644 --- a/src/client/effect.h +++ b/src/client/effect.h @@ -33,7 +33,7 @@ public: void setId(uint32_t id) override; void setPosition(const Position& position, uint8_t stackPos = 0) override; - bool isEffect() override { return true; } + bool isEffect() const override { return true; } bool waitFor(const EffectPtr&); EffectPtr asEffect() { return static_self_cast(); } diff --git a/src/client/item.h b/src/client/item.h index 4e7cf3ddc..e8d803a43 100644 --- a/src/client/item.h +++ b/src/client/item.h @@ -109,7 +109,7 @@ public: ItemPtr clone(); ItemPtr asItem() { return static_self_cast(); } - bool isItem() override { return true; } + bool isItem() const override { return true; } void updatePatterns(); int calculateAnimationPhase(); @@ -148,7 +148,7 @@ public: bool isHouseDoor() { return m_attribs.has(ATTR_HOUSEDOORID); } bool isDepot() { return m_attribs.has(ATTR_DEPOT_ID); } - bool isContainer() override { return m_attribs.has(ATTR_CONTAINER_ITEMS) || Thing::isContainer(); } + bool isContainer() const override { return m_attribs.has(ATTR_CONTAINER_ITEMS) || Thing::isContainer(); } bool isDoor() { return m_attribs.has(ATTR_HOUSEDOORID); } bool isTeleport() { return m_attribs.has(ATTR_TELE_DEST); } diff --git a/src/client/localplayer.h b/src/client/localplayer.h index 175fafc60..c123e16c9 100644 --- a/src/client/localplayer.h +++ b/src/client/localplayer.h @@ -128,7 +128,7 @@ public: bool isParalyzed() const { return (m_states & Otc::IconParalyze) == Otc::IconParalyze; } LocalPlayerPtr asLocalPlayer() { return static_self_cast(); } - bool isLocalPlayer() override { return true; } + bool isLocalPlayer() const override { return true; } void onPositionChange(const Position& newPos, const Position& oldPos) override; diff --git a/src/client/luafunctions.cpp b/src/client/luafunctions.cpp index 7b0fe5fa2..4327a43e3 100644 --- a/src/client/luafunctions.cpp +++ b/src/client/luafunctions.cpp @@ -611,7 +611,10 @@ void Client::registerLuaFunctions() g_lua.bindClassMemberFunction("isWalking", &Creature::isWalking); g_lua.bindClassMemberFunction("isInvisible", &Creature::isInvisible); g_lua.bindClassMemberFunction("isDead", &Creature::isDead); - g_lua.bindClassMemberFunction("isRemoved", &Creature::isRemoved); + g_lua.bindClassMemberFunction( + "isRemoved", + static_cast(&Creature::isRemoved) + ); g_lua.bindClassMemberFunction("canBeSeen", &Creature::canBeSeen); g_lua.bindClassMemberFunction("jump", &Creature::jump); g_lua.bindClassMemberFunction("setMountShader", &Creature::setMountShader); @@ -933,7 +936,10 @@ void Client::registerLuaFunctions() g_lua.bindClassMemberFunction("isClickable", &Tile::isClickable); g_lua.bindClassMemberFunction("isPathable", &Tile::isPathable); - g_lua.bindClassMemberFunction("hasCreatures", &Tile::hasCreatures); + g_lua.bindClassMemberFunction( + "hasCreatures", + static_cast(&Tile::hasCreatures) + ); g_lua.bindClassMemberFunction("select", &Tile::select); g_lua.bindClassMemberFunction("unselect", &Tile::unselect); diff --git a/src/client/map.cpp b/src/client/map.cpp index 4696afbb2..42f7848f0 100644 --- a/src/client/map.cpp +++ b/src/client/map.cpp @@ -33,11 +33,27 @@ #include "missile.h" #include "thing.h" #include "tile.h" -#include "framework/core/asyncdispatcher.h" -#include "framework/core/eventdispatcher.h" + +#include +#include #include "framework/graphics/drawpoolmanager.h" #include "framework/graphics/painter.h" -#include "framework/ui/uiwidget.h" +#include + +namespace +{ +void cleanNewSpectators(std::vector& creatures, std::unordered_set& seenIds, const std::size_t startIndex) +{ + auto it = creatures.begin() + startIndex; + while (it != creatures.end()) { + if (const auto& creature = *it; !creature || !seenIds.insert(creature->getId()).second) { + it = creatures.erase(it); + continue; + } + ++it; + } +} +} #ifdef FRAMEWORK_EDITOR #include "houses.h" @@ -634,6 +650,10 @@ void Map::setLight(const Light& light) std::vector Map::getSpectatorsInRangeEx(const Position& centerPos, const bool multiFloor, const int32_t minXRange, const int32_t maxXRange, const int32_t minYRange, const int32_t maxYRange) { std::vector creatures; + creatures.reserve(m_knownCreatures.size()); + std::unordered_set seenIds; + seenIds.reserve(m_knownCreatures.size()); + uint8_t minZRange = 0; uint8_t maxZRange = 0; @@ -642,19 +662,31 @@ std::vector Map::getSpectatorsInRangeEx(const Position& centerPos, maxZRange = getLastAwareFloor() - centerPos.z; } - //TODO: optimize - //TODO: delivery creatures in distance order - for (int iz = -minZRange; iz <= maxZRange; ++iz) { - for (int iy = -minYRange; iy <= maxYRange; ++iy) { - for (int ix = -minXRange; ix <= maxXRange; ++ix) { - if (const auto& tile = getTile(centerPos.translated(ix, iy, iz))) { - const auto& tileCreatures = tile->getCreatures(); - creatures.insert(creatures.end(), tileCreatures.rbegin(), tileCreatures.rend()); + const int startZ = centerPos.z - minZRange; + const int endZ = centerPos.z + maxZRange; + const int startY = centerPos.y - minYRange; + const int endY = centerPos.y + maxYRange; + const int startX = centerPos.x - minXRange; + const int endX = centerPos.x + maxXRange; + + const auto appendSpectatorsFromLayer = [&](const int z) { + for (int y = startY; y <= endY; ++y) { + for (int x = startX; x <= endX; ++x) { + const auto tile = getTile(Position(x, y, z)); + if (!tile || !tile->hasCreatures()) { + continue; } + + const auto sizeBeforeAppend = creatures.size(); + tile->appendSpectators(creatures); + cleanNewSpectators(creatures, seenIds, sizeBeforeAppend); } } - } + }; + for (int z = startZ; z <= endZ; ++z) { + appendSpectatorsFromLayer(z); + } return creatures; } @@ -1471,15 +1503,24 @@ std::vector Map::getSpectatorsByPattern(const Position& centerPos, } p = 0; + std::unordered_set seenIds; + seenIds.reserve(m_knownCreatures.size()); for (int y = centerPos.y - height / 2, endy = centerPos.y + height / 2; y <= endy; ++y) { for (int x = centerPos.x - width / 2, endx = centerPos.x + width / 2; x <= endx; ++x) { - if (!finalPattern[p++]) + const auto enabled = finalPattern[p]; + ++p; + if (!enabled) { continue; - TilePtr tile = getTile(Position(x, y, centerPos.z)); - if (!tile) + } + + const auto tile = getTile(Position(x, y, centerPos.z)); + if (!tile || !tile->hasCreatures()) { continue; - auto tileCreatures = tile->getCreatures(); - creatures.insert(creatures.end(), tileCreatures.rbegin(), tileCreatures.rend()); + } + + const auto sizeBeforeAppend = creatures.size(); + tile->appendSpectators(creatures); + cleanNewSpectators(creatures, seenIds, sizeBeforeAppend); } } return creatures; diff --git a/src/client/missile.h b/src/client/missile.h index 2c9380068..a40c88cac 100644 --- a/src/client/missile.h +++ b/src/client/missile.h @@ -34,7 +34,7 @@ public: void setId(uint32_t id) override; void setPath(const Position& fromPosition, const Position& toPosition); - bool isMissile() override { return true; } + bool isMissile() const override { return true; } MissilePtr asMissile() { return static_self_cast(); } diff --git a/src/client/player.h b/src/client/player.h index eef77d4a9..35b4c6736 100644 --- a/src/client/player.h +++ b/src/client/player.h @@ -32,7 +32,7 @@ public: ~Player() override = default; PlayerPtr asPlayer() { return static_self_cast(); } - bool isPlayer() override { return true; } + bool isPlayer() const override { return true; } bool isMage() const; void setVocation(uint8_t vocation); uint8_t getVocation() { return m_vocation; } diff --git a/src/client/thing.cpp b/src/client/thing.cpp index 4f5ef004e..0b890bbbd 100644 --- a/src/client/thing.cpp +++ b/src/client/thing.cpp @@ -114,107 +114,447 @@ void Thing::setAttachedEffectDirection(const Otc::Direction dir) const } } -Animator* Thing::getAnimator() const { return getThingType()->getAnimator(); } -Animator* Thing::getIdleAnimator() const { return getThingType()->getIdleAnimator(); } - -Point Thing::getDisplacement() const { return getThingType()->getDisplacement(); } -int Thing::getDisplacementX() const { return getThingType()->getDisplacementX(); } -int Thing::getDisplacementY() const { return getThingType()->getDisplacementY(); } -int Thing::getExactSize(const int layer, const int xPattern, const int yPattern, const int zPattern, const int animationPhase) { - return getThingType()->getExactSize(layer, xPattern, yPattern, zPattern, animationPhase); +Animator* Thing::getAnimator() const { + if (const auto t = getThingType(); t) + return t->getAnimator(); + return nullptr; +} +Animator* Thing::getIdleAnimator() const { + if (const auto t = getThingType(); t) + return t->getIdleAnimator(); + return nullptr; } -const Light& Thing::getLight() const { return getThingType()->getLight(); } -bool Thing::hasLight() const { return getThingType()->hasLight(); } +Point Thing::getDisplacement() const { + if (const auto t = getThingType(); t) + return t->getDisplacement(); + return Point(); +} +int Thing::getDisplacementX() const { + if (const auto t = getThingType(); t) + return t->getDisplacementX(); + return 0; +} +int Thing::getDisplacementY() const { + if (const auto t = getThingType(); t) + return t->getDisplacementY(); + return 0; +} +int Thing::getExactSize(const int layer, const int xPattern, const int yPattern, const int zPattern, const int animationPhase) { + if (const auto t = getThingType(); t) + return t->getExactSize(layer, xPattern, yPattern, zPattern, animationPhase); + return 0; +} -const MarketData& Thing::getMarketData() { return getThingType()->getMarketData(); } -const std::vector& Thing::getNpcSaleData() { return getThingType()->getNpcSaleData(); } -int Thing::getMeanPrice() { return getThingType()->getMeanPrice(); } -const Size& Thing::getSize() const { return getThingType()->getSize(); } +const Light& Thing::getLight() const { + if (const auto t = getThingType(); t) + return t->getLight(); + static const Light kEmptyLight; + return kEmptyLight; +} +bool Thing::hasLight() const { + if (const auto t = getThingType(); t) + return t->hasLight(); + return false; +} -int Thing::getWidth() const { return getThingType()->getWidth(); } -int Thing::getHeight() const { return getThingType()->getHeight(); } -int Thing::getRealSize() const { return getThingType()->getRealSize(); } -int Thing::getLayers() const { return getThingType()->getLayers(); } -int Thing::getNumPatternX() const { return getThingType()->getNumPatternX(); } -int Thing::getNumPatternY() const { return getThingType()->getNumPatternY(); } -int Thing::getNumPatternZ() const { return getThingType()->getNumPatternZ(); } -int Thing::getAnimationPhases() const { return getThingType()->getAnimationPhases(); } -int Thing::getGroundSpeed() const { return getThingType()->getGroundSpeed(); } -int Thing::getMaxTextLength() const { return getThingType()->getMaxTextLength(); } -int Thing::getMinimapColor() const { return getThingType()->getMinimapColor(); } -int Thing::getLensHelp() const { return getThingType()->getLensHelp(); } -int Thing::getElevation() const { return getThingType()->getElevation(); } +const MarketData& Thing::getMarketData() { + if (const auto t = getThingType(); t) + return t->getMarketData(); + static const MarketData kEmptyMarketData{}; + return kEmptyMarketData; +} +const std::vector& Thing::getNpcSaleData() { + if (const auto t = getThingType(); t) + return t->getNpcSaleData(); + static const std::vector kEmptyNpcData; + return kEmptyNpcData; +} +int Thing::getMeanPrice() { + if (const auto t = getThingType(); t) + return t->getMeanPrice(); + return 0; +} +const Size& Thing::getSize() const { + if (const auto t = getThingType(); t) + return t->getSize(); + static const Size kEmptySize; + return kEmptySize; +} -int Thing::getClothSlot() { return getThingType()->getClothSlot(); } +int Thing::getWidth() const { + if (const auto t = getThingType(); t) + return t->getWidth(); + return 0; +} +int Thing::getHeight() const { + if (const auto t = getThingType(); t) + return t->getHeight(); + return 0; +} +int Thing::getRealSize() const { + if (const auto t = getThingType(); t) + return t->getRealSize(); + return 0; +} +int Thing::getLayers() const { + if (const auto t = getThingType(); t) + return t->getLayers(); + return 0; +} +int Thing::getNumPatternX() const { + if (const auto t = getThingType(); t) + return t->getNumPatternX(); + return 0; +} +int Thing::getNumPatternY() const { + if (const auto t = getThingType(); t) + return t->getNumPatternY(); + return 0; +} +int Thing::getNumPatternZ() const { + if (const auto t = getThingType(); t) + return t->getNumPatternZ(); + return 0; +} +int Thing::getAnimationPhases() const { + if (const auto t = getThingType(); t) + return t->getAnimationPhases(); + return 0; +} +int Thing::getGroundSpeed() const { + if (const auto t = getThingType(); t) + return t->getGroundSpeed(); + return 0; +} +int Thing::getMaxTextLength() const { + if (const auto t = getThingType(); t) + return t->getMaxTextLength(); + return 0; +} +int Thing::getMinimapColor() const { + if (const auto t = getThingType(); t) + return t->getMinimapColor(); + return 0; +} +int Thing::getLensHelp() const { + if (const auto t = getThingType(); t) + return t->getLensHelp(); + return 0; +} +int Thing::getElevation() const { + if (const auto t = getThingType(); t) + return t->getElevation(); + return 0; +} -bool Thing::blockProjectile() const { return getThingType()->blockProjectile(); } +int Thing::getClothSlot() { + if (const auto t = getThingType(); t) + return t->getClothSlot(); + return 0; +} -bool Thing::isContainer() { return getThingType()->isContainer(); } +bool Thing::blockProjectile() const { + if (const auto t = getThingType(); t) + return t->blockProjectile(); + return false; +} -bool Thing::isTopGround() { return !isCreature() && getThingType()->isTopGround(); } -bool Thing::isTopGroundBorder() { return !isCreature() && getThingType()->isTopGroundBorder(); } -bool Thing::isSingleGround() { return !isCreature() && getThingType()->isSingleGround(); } -bool Thing::isSingleGroundBorder() { return !isCreature() && getThingType()->isSingleGroundBorder(); } -bool Thing::isGround() { return !isCreature() && getThingType()->isGround(); } -bool Thing::isGroundBorder() { return !isCreature() && getThingType()->isGroundBorder(); } -bool Thing::isOnBottom() { return !isCreature() && getThingType()->isOnBottom(); } -bool Thing::isOnTop() { return !isCreature() && getThingType()->isOnTop(); } +bool Thing::isContainer() { + if (const auto t = getThingType(); t) + return t->isContainer(); + return false; +} -bool Thing::isMarketable() { return getThingType()->isMarketable(); } -bool Thing::isStackable() { return getThingType()->isStackable(); } -bool Thing::isFluidContainer() { return getThingType()->isFluidContainer(); } -bool Thing::isForceUse() { return getThingType()->isForceUse(); } -bool Thing::isMultiUse() { return getThingType()->isMultiUse(); } -bool Thing::isWritable() { return getThingType()->isWritable(); } -bool Thing::isChargeable() { return getThingType()->isChargeable(); } -bool Thing::isWritableOnce() { return getThingType()->isWritableOnce(); } -bool Thing::isSplash() { return getThingType()->isSplash(); } -bool Thing::isNotWalkable() { return getThingType()->isNotWalkable(); } -bool Thing::isNotMoveable() { return getThingType()->isNotMoveable(); } -bool Thing::isMoveable() { return !getThingType()->isNotMoveable(); } -bool Thing::isNotPathable() { return getThingType()->isNotPathable(); } -bool Thing::isPickupable() { return getThingType()->isPickupable(); } -bool Thing::isHangable() { return getThingType()->isHangable(); } -bool Thing::isHookSouth() { return getThingType()->isHookSouth(); } -bool Thing::isHookEast() { return getThingType()->isHookEast(); } -bool Thing::isRotateable() { return getThingType()->isRotateable(); } -bool Thing::isDontHide() { return getThingType()->isDontHide(); } -bool Thing::isTranslucent() { return getThingType()->isTranslucent(); } -bool Thing::isLyingCorpse() { return getThingType()->isLyingCorpse(); } -bool Thing::isAnimateAlways() { return getThingType()->isAnimateAlways(); } -bool Thing::isFullGround() { return getThingType()->isFullGround(); } -bool Thing::isIgnoreLook() { return getThingType()->isIgnoreLook(); } -bool Thing::isCloth() { return getThingType()->isCloth(); } -bool Thing::isUsable() { return getThingType()->isUsable(); } -bool Thing::isWrapable() { return getThingType()->isWrapable(); } -bool Thing::isUnwrapable() { return getThingType()->isUnwrapable(); } -bool Thing::isTopEffect() { return getThingType()->isTopEffect(); } -bool Thing::isPodium() const { return getThingType()->isPodium(); } -bool Thing::isOpaque() const { return getThingType()->isOpaque(); } -bool Thing::isLoading() const { return getThingType()->isLoading(); } -bool Thing::isSingleDimension() const { return getThingType()->isSingleDimension(); } -bool Thing::isTall(const bool useRealSize) const { return getThingType()->isTall(useRealSize); } +bool Thing::isTopGround() { + if (const auto t = getThingType(); !isCreature() && t) + return t->isTopGround(); + return false; +} +bool Thing::isTopGroundBorder() { + if (const auto t = getThingType(); !isCreature() && t) + return t->isTopGroundBorder(); + return false; +} +bool Thing::isSingleGround() { + if (const auto t = getThingType(); !isCreature() && t) + return t->isSingleGround(); + return false; +} +bool Thing::isSingleGroundBorder() { + if (const auto t = getThingType(); !isCreature() && t) + return t->isSingleGroundBorder(); + return false; +} +bool Thing::isGround() { + if (const auto t = getThingType(); !isCreature() && t) + return t->isGround(); + return false; +} +bool Thing::isGroundBorder() { + if (const auto t = getThingType(); !isCreature() && t) + return t->isGroundBorder(); + return false; +} +bool Thing::isOnBottom() { + if (const auto t = getThingType(); !isCreature() && t) + return t->isOnBottom(); + return false; +} +bool Thing::isOnTop() { + if (const auto t = getThingType(); !isCreature() && t) + return t->isOnTop(); + return false; +} -bool Thing::hasMiniMapColor() const { return getThingType()->hasMiniMapColor(); } -bool Thing::hasLensHelp() const { return getThingType()->hasLensHelp(); } -bool Thing::hasDisplacement() const { return getThingType()->hasDisplacement(); } -bool Thing::hasElevation() const { return getThingType()->hasElevation(); } -bool Thing::hasAction() const { return getThingType()->hasAction(); } -bool Thing::hasWearOut() const { return getThingType()->hasWearOut(); } -bool Thing::hasClockExpire() const { return getThingType()->hasClockExpire(); } -bool Thing::hasExpire() const { return getThingType()->hasExpire(); } -bool Thing::hasExpireStop() const { return getThingType()->hasExpireStop(); } -bool Thing::hasAnimationPhases() const { return getThingType()->getAnimationPhases() > 1; } -bool Thing::isDecoKit() const { return getThingType()->isDecoKit(); } -bool Thing::isAmmo() { return getThingType()->isAmmo(); } +bool Thing::isMarketable() { + if (const auto t = getThingType(); t) + return t->isMarketable(); + return false; +} +bool Thing::isStackable() { + if (const auto t = getThingType(); t) + return t->isStackable(); + return false; +} +bool Thing::isFluidContainer() { + if (const auto t = getThingType(); t) + return t->isFluidContainer(); + return false; +} +bool Thing::isForceUse() { + if (const auto t = getThingType(); t) + return t->isForceUse(); + return false; +} +bool Thing::isMultiUse() { + if (const auto t = getThingType(); t) + return t->isMultiUse(); + return false; +} +bool Thing::isWritable() { + if (const auto t = getThingType(); t) + return t->isWritable(); + return false; +} +bool Thing::isChargeable() { + if (const auto t = getThingType(); t) + return t->isChargeable(); + return false; +} +bool Thing::isWritableOnce() { + if (const auto t = getThingType(); t) + return t->isWritableOnce(); + return false; +} +bool Thing::isSplash() { + if (const auto t = getThingType(); t) + return t->isSplash(); + return false; +} +bool Thing::isNotWalkable() { + if (const auto t = getThingType(); t) + return t->isNotWalkable(); + return false; +} +bool Thing::isNotMoveable() { + if (const auto t = getThingType(); t) + return t->isNotMoveable(); + return false; +} +bool Thing::isMoveable() { + if (const auto t = getThingType(); t) + return !t->isNotMoveable(); + return false; +} +bool Thing::isNotPathable() { + if (const auto t = getThingType(); t) + return t->isNotPathable(); + return false; +} +bool Thing::isPickupable() { + if (const auto t = getThingType(); t) + return t->isPickupable(); + return false; +} +bool Thing::isHangable() { + if (const auto t = getThingType(); t) + return t->isHangable(); + return false; +} +bool Thing::isHookSouth() { + if (const auto t = getThingType(); t) + return t->isHookSouth(); + return false; +} +bool Thing::isHookEast() { + if (const auto t = getThingType(); t) + return t->isHookEast(); + return false; +} +bool Thing::isRotateable() { + if (const auto t = getThingType(); t) + return t->isRotateable(); + return false; +} +bool Thing::isDontHide() { + if (const auto t = getThingType(); t) + return t->isDontHide(); + return false; +} +bool Thing::isTranslucent() { + if (const auto t = getThingType(); t) + return t->isTranslucent(); + return false; +} +bool Thing::isLyingCorpse() { + if (const auto t = getThingType(); t) + return t->isLyingCorpse(); + return false; +} +bool Thing::isAnimateAlways() { + if (const auto t = getThingType(); t) + return t->isAnimateAlways(); + return false; +} +bool Thing::isFullGround() { + if (const auto t = getThingType(); t) + return t->isFullGround(); + return false; +} +bool Thing::isIgnoreLook() { + if (const auto t = getThingType(); t) + return t->isIgnoreLook(); + return false; +} +bool Thing::isCloth() { + if (const auto t = getThingType(); t) + return t->isCloth(); + return false; +} +bool Thing::isUsable() { + if (const auto t = getThingType(); t) + return t->isUsable(); + return false; +} +bool Thing::isWrapable() { + if (const auto t = getThingType(); t) + return t->isWrapable(); + return false; +} +bool Thing::isUnwrapable() { + if (const auto t = getThingType(); t) + return t->isUnwrapable(); + return false; +} +bool Thing::isTopEffect() { + if (const auto t = getThingType(); t) + return t->isTopEffect(); + return false; +} +bool Thing::isPodium() const { + if (const auto t = getThingType(); t) + return t->isPodium(); + return false; +} +bool Thing::isOpaque() const { + if (const auto t = getThingType(); t) + return t->isOpaque(); + return false; +} +bool Thing::isLoading() const { + if (const auto t = getThingType(); t) + return t->isLoading(); + return false; +} +bool Thing::isSingleDimension() const { + if (const auto t = getThingType(); t) + return t->isSingleDimension(); + return false; +} +bool Thing::isTall(const bool useRealSize) const { + if (const auto t = getThingType(); t) + return t->isTall(useRealSize); + return false; +} -PLAYER_ACTION Thing::getDefaultAction() { return getThingType()->getDefaultAction(); } +bool Thing::hasMiniMapColor() const { + if (const auto t = getThingType(); t) + return t->hasMiniMapColor(); + return false; +} +bool Thing::hasLensHelp() const { + if (const auto t = getThingType(); t) + return t->hasLensHelp(); + return false; +} +bool Thing::hasDisplacement() const { + if (const auto t = getThingType(); t) + return t->hasDisplacement(); + return false; +} +bool Thing::hasElevation() const { + if (const auto t = getThingType(); t) + return t->hasElevation(); + return false; +} +bool Thing::hasAction() const { + if (const auto t = getThingType(); t) + return t->hasAction(); + return false; +} +bool Thing::hasWearOut() const { + if (const auto t = getThingType(); t) + return t->hasWearOut(); + return false; +} +bool Thing::hasClockExpire() const { + if (const auto t = getThingType(); t) + return t->hasClockExpire(); + return false; +} +bool Thing::hasExpire() const { + if (const auto t = getThingType(); t) + return t->hasExpire(); + return false; +} +bool Thing::hasExpireStop() const { + if (const auto t = getThingType(); t) + return t->hasExpireStop(); + return false; +} +bool Thing::hasAnimationPhases() const { + if (const auto t = getThingType(); t) + return t->getAnimationPhases() > 1; + return false; +} +bool Thing::isDecoKit() const { + if (const auto t = getThingType(); t) + return t->isDecoKit(); + return false; +} +bool Thing::isAmmo() { + if (const auto t = getThingType(); t) + return t->isAmmo(); + return false; +} -uint16_t Thing::getClassification() { return getThingType()->getClassification(); } +PLAYER_ACTION Thing::getDefaultAction() { + if (const auto t = getThingType(); t) + return t->getDefaultAction(); + return static_cast(0); +} + +uint16_t Thing::getClassification() { + if (const auto t = getThingType(); t) + return t->getClassification(); + return 0; +} bool Thing::canDraw(const Color& color) const { - return m_canDraw && m_clientId > 0 && color.aF() > Fw::MIN_ALPHA && getThingType() && getThingType()->getOpacity() > Fw::MIN_ALPHA; + if (const auto t = getThingType(); t) + return m_canDraw && m_clientId > 0 && color.aF() > Fw::MIN_ALPHA && t->getOpacity() > Fw::MIN_ALPHA; + return false; } const Color& Thing::getMarkedColor() { diff --git a/src/client/thing.h b/src/client/thing.h index 8e034555a..9a885800e 100644 --- a/src/client/thing.h +++ b/src/client/thing.h @@ -26,7 +26,7 @@ #include "staticdata.h" #include - // @bindclass +// @bindclass #pragma pack(push,1) // disable memory alignment class Thing : public AttachableObject { @@ -52,15 +52,15 @@ public: int getStackPos(); int getStackPriority(); - virtual bool isItem() { return false; } - virtual bool isEffect() { return false; } - virtual bool isMissile() { return false; } - virtual bool isCreature() { return false; } + virtual bool isItem() const { return false; } + virtual bool isEffect() const { return false; } + virtual bool isMissile() const { return false; } + virtual bool isCreature() const { return false; } - virtual bool isNpc() { return false; } - virtual bool isMonster() { return false; } - virtual bool isPlayer() { return false; } - virtual bool isLocalPlayer() { return false; } + virtual bool isNpc() const { return false; } + virtual bool isMonster() const { return false; } + virtual bool isPlayer() const { return false; } + virtual bool isLocalPlayer() const { return false; } bool isCommon() { return !isGround() && !isGroundBorder() && !isOnTop() && !isCreature() && !isOnBottom(); } void canDraw(const bool canDraw) { m_canDraw = canDraw; } diff --git a/src/client/tile.cpp b/src/client/tile.cpp index 404f653c6..cd3253762 100644 --- a/src/client/tile.cpp +++ b/src/client/tile.cpp @@ -209,6 +209,9 @@ void Tile::clean() m_thingTypeFlag = 0; + m_firstCreatureIndex = -1; + m_lastCreatureIndex = -1; + #ifdef FRAMEWORK_EDITOR m_flags = 0; #endif @@ -313,6 +316,8 @@ void Tile::addThing(const ThingPtr& thing, int stackPos) m_things.insert(m_things.begin() + stackPos, thing); + updateCreatureRangeForInsert(static_cast(stackPos), thing); + setThingFlag(thing); if (size > g_gameConfig.getTileMaxThings()) @@ -385,16 +390,74 @@ ThingPtr Tile::getThing(const int stackPos) return nullptr; } +void Tile::updateCreatureRangeForInsert(const int16_t stackPos, const ThingPtr& thing) +{ + if (m_firstCreatureIndex != -1 && stackPos <= m_firstCreatureIndex) { + ++m_firstCreatureIndex; + } + + if (m_lastCreatureIndex != -1 && stackPos <= m_lastCreatureIndex) { + ++m_lastCreatureIndex; + } + + if (!thing->isCreature()) { + return; + } + + if (m_firstCreatureIndex == -1 || stackPos < m_firstCreatureIndex) { + m_firstCreatureIndex = stackPos; + } + + if (stackPos > m_lastCreatureIndex) { + m_lastCreatureIndex = stackPos; + } +} + +void Tile::rebuildCreatureRange() +{ + m_firstCreatureIndex = -1; + m_lastCreatureIndex = -1; + + const auto count = static_cast(m_things.size()); + for (int32_t i = 0; i < count; ++i) { + if (!m_things[i]->isCreature()) { + continue; + } + + if (m_firstCreatureIndex == -1) { + m_firstCreatureIndex = static_cast(i); + } + + m_lastCreatureIndex = static_cast(i); + } +} + +void Tile::appendSpectators(std::vector& out) const +{ + if (!hasCreatures() || m_lastCreatureIndex == -1) { + return; + } + + const auto size = static_cast(m_things.size()); + const auto beginOffset = size - 1 - static_cast(m_lastCreatureIndex); + const auto endOffset = size - static_cast(m_firstCreatureIndex); + + auto it = m_things.rbegin() + beginOffset; + const auto end = m_things.rbegin() + endOffset; + + for (; it != end; ++it) { + const auto& thing = *it; + if (thing->isCreature()) { + out.emplace_back(thing->static_self_cast()); + } + } +} + std::vector Tile::getCreatures() { std::vector creatures; - if (hasCreatures()) { - for (const auto& thing : m_things) { - if (thing->isCreature()) - creatures.emplace_back(thing->static_self_cast()); - } - } - + appendSpectators(creatures); + std::ranges::reverse(creatures); return creatures; } @@ -427,8 +490,9 @@ std::vector Tile::getItems() { std::vector items; for (const auto& thing : m_things) { - if (!thing->isItem()) + if (!thing->isItem()) { continue; + } items.emplace_back(thing->static_self_cast()); } diff --git a/src/client/tile.h b/src/client/tile.h index 81aaf3a91..3aa40b18e 100644 --- a/src/client/tile.h +++ b/src/client/tile.h @@ -91,7 +91,10 @@ public: bool hasGround(); bool hasTopGround(const bool ignoreBorder = false); - bool hasCreatures() { return m_thingTypeFlag & HAS_CREATURE; } + bool hasCreatures() const { return (m_thingTypeFlag & HAS_CREATURE) != 0; } + bool hasCreatures() { return static_cast(*this).hasCreatures(); } + + void appendSpectators(std::vector& out) const; bool hasTopItem() const { return m_thingTypeFlag & HAS_TOP_ITEM; } bool hasCommonItem() const { return m_thingTypeFlag & HAS_COMMON_ITEM; } @@ -160,11 +163,15 @@ private: void drawTop(const Point& dest, int flags, bool forceDraw, uint8_t drawElevation); void drawCreature(const Point& dest, int flags, bool forceDraw, uint8_t drawElevation, LightView* lightView = nullptr); + void updateCreatureRangeForInsert(int16_t stackPos, const ThingPtr& thing); + void rebuildCreatureRange(); + void setThingFlag(const ThingPtr& thing); void recalculateThingFlag() { m_thingTypeFlag = 0; + rebuildCreatureRange(); for (const auto& thing : m_things) setThingFlag(thing); } @@ -199,6 +206,9 @@ private: uint8_t m_minimapColor{ 0 }; uint8_t m_elevation{ 0 }; + int16_t m_firstCreatureIndex{ -1 }; + int16_t m_lastCreatureIndex{ -1 }; + int8_t m_highlightThingStackPos = -1; TileSelectType m_selectType{ TileSelectType::NONE }; diff --git a/src/framework/global.h b/src/framework/global.h index 19e63c458..85c2206a3 100644 --- a/src/framework/global.h +++ b/src/framework/global.h @@ -22,8 +22,6 @@ #pragma once -using namespace std::literals; - #include "config.h" #include "stdext/compiler.h" @@ -31,6 +29,8 @@ using namespace std::literals; // common C/C++ headers #include "pch.h" +using namespace std::literals; + // global constants #include "const.h" diff --git a/src/framework/luaengine/luabinder.h b/src/framework/luaengine/luabinder.h index 477aef059..069cc07d1 100644 --- a/src/framework/luaengine/luabinder.h +++ b/src/framework/luaengine/luabinder.h @@ -27,6 +27,7 @@ #include #include +#include /// This namespace contains some dirty metaprogamming that uses a lot of C++0x features /// The purpose here is to create templates that can bind any function from C++ @@ -37,6 +38,18 @@ /// pushes the result to lua. namespace luabinder { + template + Ret make_default_return_value() + { + if constexpr (std::is_reference_v) { + using ValueType = std::remove_cv_t>; + static ValueType value{}; + return value; + } else { + return std::decay_t{}; + } + } + /// Pack arguments from lua stack into a tuple recursively template struct pack_values_into_tuple @@ -169,8 +182,10 @@ namespace luabinder { auto mf = std::mem_fn(f); return [=](const std::shared_ptr& obj, const Args&... args) mutable -> Ret { - if (!obj) - throw LuaException("failed to call a member function because the passed object is nil"); + if (!obj) { + g_logger.warning("Lua warning: member function call skipped because the passed object is nil"); + return make_default_return_value(); + } return mf(obj.get(), args...); }; } @@ -179,8 +194,10 @@ namespace luabinder { auto mf = std::mem_fn(f); return [=](const std::shared_ptr& obj, const Args&... args) mutable { - if (!obj) - throw LuaException("failed to call a member function because the passed object is nil"); + if (!obj) { + g_logger.warning("Lua warning: member function call skipped because the passed object is nil"); + return; + } mf(obj.get(), args...); }; } @@ -233,4 +250,83 @@ namespace luabinder return mf(obj, lua); }; } + + template + std::function&, const Args&...)> + make_mem_func(Ret (C::*f)(Args...) const) + { + auto mf = std::mem_fn(f); + return [=](const std::shared_ptr& obj, const Args&... args) mutable -> Ret { + if (!obj) { + g_logger.warning("Lua warning: member function call skipped because the passed object is nil"); + if constexpr (std::is_void_v) { + return; + } else { + return make_default_return_value(); + } + } + if constexpr (std::is_void_v) { + mf(obj.get(), args...); + return; + } + return mf(obj.get(), args...); + }; + } + + template + std::function&, const Args&...)> + make_mem_func(void (C::*f)(Args...) const) + { + auto mf = std::mem_fn(f); + return [=](const std::shared_ptr& obj, const Args&... args) mutable { + if (!obj) { + g_logger.warning("Lua warning: member function call skipped because the passed object is nil"); + return; + } + mf(obj.get(), args...); + }; + } + + template + LuaCppFunction bind_mem_fun(Ret (FC::*f)(Args...) const) + { + using Tuple = std::tuple, typename stdext::remove_const_ref::type...>; + auto lambda = make_mem_func(f); + return bind_fun_specializer::type, + decltype(lambda), + Tuple>(lambda); + } + + template + std::function + make_mem_func_singleton(Ret (C::*f)(Args...) const, C* instance) + { + auto mf = std::mem_fn(f); + return [=](Args... args) mutable -> Ret { + if constexpr (std::is_void_v) { + mf(instance, args...); + return; + } + return mf(instance, args...); + }; + } + + template + std::function + make_mem_func_singleton(void (C::*f)(Args...) const, C* instance) + { + auto mf = std::mem_fn(f); + return [=](Args... args) mutable { mf(instance, args...); }; + } + + template + LuaCppFunction bind_singleton_mem_fun(Ret (FC::*f)(Args...) const, C* instance) + { + using Tuple = std::tuple::type...>; + assert(instance); + auto lambda = make_mem_func_singleton(f, static_cast(instance)); + return bind_fun_specializer::type, + decltype(lambda), + Tuple>(lambda); + } } diff --git a/src/framework/luaengine/luainterface.h b/src/framework/luaengine/luainterface.h index df2c9a0e9..b8e22f740 100644 --- a/src/framework/luaengine/luainterface.h +++ b/src/framework/luaengine/luainterface.h @@ -92,19 +92,23 @@ public: template void registerClass() { - registerClass(stdext::demangle_class(), stdext::demangle_class()); + const std::string className = stdext::demangle_class(); + const std::string baseClassName = stdext::demangle_class(); + registerClass(className, baseClassName); } template void registerClassStaticFunction(const std::string_view functionName, const LuaCppFunction& function) { - registerClassStaticFunction(stdext::demangle_class(), functionName, function); + const std::string className = stdext::demangle_class(); + registerClassStaticFunction(className, functionName, function); } template void registerClassMemberFunction(const std::string_view functionName, const LuaCppFunction& function) { - registerClassMemberFunction(stdext::demangle_class(), functionName, function); + const std::string className = stdext::demangle_class(); + registerClassMemberFunction(className, functionName, function); } template @@ -112,7 +116,8 @@ public: const LuaCppFunction& getFunction, const LuaCppFunction& setFunction) { - registerClassMemberField(stdext::demangle_class(), field, getFunction, setFunction); + const std::string className = stdext::demangle_class(); + registerClassMemberField(className, field, getFunction, setFunction); } // methods for binding functions diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 000000000..799c2fc02 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,45 @@ +find_package(GTest CONFIG REQUIRED) + +include(GoogleTest) + +function(otclient_add_gtest TARGET_NAME) + add_executable(${TARGET_NAME} ${ARGN}) + + set_target_properties(${TARGET_NAME} PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + ) + + if(TOGGLE_PRE_COMPILED_HEADER) + target_precompile_headers(${TARGET_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/src/framework/pch.h) + endif() + + target_link_libraries(${TARGET_NAME} + PRIVATE + otclient_core + GTest::gtest + GTest::gtest_main + ) + + target_compile_definitions(${TARGET_NAME} + PRIVATE + CLIENT + FRAMEWORK_GRAPHICS + FRAMEWORK_NET + FRAMEWORK_SOUND + FRAMEWORK_XML + ) + + if(MSVC) + target_compile_options(${TARGET_NAME} PRIVATE /utf-8) + if(BUILD_STATIC_LIBRARY) + set_property(TARGET ${TARGET_NAME} PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + else() + set_property(TARGET ${TARGET_NAME} PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") + endif() + endif() + + gtest_discover_tests(${TARGET_NAME}) +endfunction() + +add_subdirectory(map) diff --git a/tests/map/CMakeLists.txt b/tests/map/CMakeLists.txt new file mode 100644 index 000000000..3e12efd2d --- /dev/null +++ b/tests/map/CMakeLists.txt @@ -0,0 +1,5 @@ +set(MAP_TEST_SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/map_spectators_test.cpp +) + +otclient_add_gtest(otclient_map_spectator_tests ${MAP_TEST_SOURCES}) diff --git a/tests/map/map_spectators_test.cpp b/tests/map/map_spectators_test.cpp new file mode 100644 index 000000000..ec4e85446 --- /dev/null +++ b/tests/map/map_spectators_test.cpp @@ -0,0 +1,515 @@ +#include + +#define private public +#define protected public +#include "client/map.h" + +#include "client/creature.h" +#include "client/gameconfig.h" +#include "client/tile.h" +#include "client/thingtype.h" + +#undef protected +#undef private + +#include +#include +#include + +namespace { + +class DummyCreature final : public Creature +{ +public: + DummyCreature() + { + setRemovedSilently(false); + } + + void onPositionChange(const Position&, const Position& oldPos) override + { + setOldPositionSilently(oldPos); + } + + void onAppear() override + { + setRemovedSilently(false); + } + + void onDisappear() override + { + setRemovedSilently(true); + setOldPositionSilently({}); + } + + ThingType* getThingType() const override + { + static ThingType type; + + static const bool initialized = [] { + type.m_null = false; + type.m_category = ThingCategoryCreature; + type.m_size = Size(1, 1); + type.m_realSize = 32; + type.m_layers = 1; + type.m_animationPhases = 1; + type.m_opacity = 1.f; + return true; + }(); + + (void)initialized; + return &type; + } + + void terminateWalk() override + { + m_walking = false; + m_walkedPixels = 0; + m_walkOffset = {}; + } +}; + +class DummyItem final : public Thing +{ +public: + DummyItem() + { + m_clientId = 1; + } + + bool isItem() const override { return true; } + + ThingType* getThingType() const override + { + static ThingType type; + + static const bool initialized = [] { + type.m_null = false; + type.m_category = ThingCategoryItem; + type.m_size = Size(1, 1); + type.m_realSize = 32; + type.m_layers = 1; + type.m_animationPhases = 1; + type.m_opacity = 1.f; + return true; + }(); + + (void)initialized; + return &type; + } +}; + +class FrameworkEnvironment : public testing::Environment +{ +public: + void SetUp() override + { + m_previousLogLevel = g_logger.getLevel(); + g_logger.setLevel(Fw::LogFatal); + g_resources.init("."); + g_resources.addSearchPath("."); + g_textures.init(); + } + + void TearDown() override + { + g_textures.terminate(); + g_resources.terminate(); + g_logger.setLevel(m_previousLogLevel); + } + +private: + Fw::LogLevel m_previousLogLevel{ Fw::LogFatal }; +}; + +[[maybe_unused]] testing::Environment* const g_frameworkEnv = testing::AddGlobalTestEnvironment(new FrameworkEnvironment); + +CreaturePtr makeCreature(const uint32_t id, const Position& position) +{ + auto creature = std::make_shared(); + creature->setId(id); + creature->setPosition(position); + return creature; +} + +ThingPtr makeItem(const Position& position) +{ + auto item = std::make_shared(); + item->setPosition(position); + return item; +} + +std::vector expectedSpectatorsFromTile(Tile& tile) +{ + std::vector creatures; + for (const auto& thing : tile.getThings()) { + if (thing->isCreature()) { + creatures.emplace_back(thing->static_self_cast()); + } + } + return { creatures.rbegin(), creatures.rend() }; +} + +std::pair expectedCreatureSpan(const Tile& tile) +{ + int16_t first = -1; + int16_t last = -1; + + for (int16_t i = 0; i < static_cast(tile.m_things.size()); ++i) { + if (!tile.m_things[i]->isCreature()) { + continue; + } + + if (first == -1) { + first = i; + } + + last = i; + } + + return { first, last }; +} + +} // namespace + +TEST(TileSpectators, AppendSpectatorsMatchesThingOrder) +{ + const Position position(100, 100, 7); + Tile tile(position); + + EXPECT_FALSE(tile.hasCreatures()); + EXPECT_TRUE(tile.getCreatures().empty()); + + auto first = makeCreature(1, position); + auto second = makeCreature(2, position); + auto third = makeCreature(3, position); + + tile.addThing(first, -1); + tile.addThing(second, -1); + tile.addThing(third, -1); + + const auto expected = expectedSpectatorsFromTile(tile); + + std::vector actual; + tile.appendSpectators(actual); + + ASSERT_EQ(expected.size(), actual.size()); + EXPECT_EQ(expected, actual); + + const auto forward = tile.getCreatures(); + EXPECT_EQ(forward.size(), expected.size()); + + for (size_t i = 0; i < forward.size(); ++i) { + EXPECT_EQ(forward[i], expected[expected.size() - 1 - i]); + } +} + +TEST(TileSpectators, CreatureRangeTracksMixedInsertions) +{ + const Position position(200, 200, 7); + Tile tile(position); + + tile.addThing(makeItem(position), -1); + + tile.addThing(makeItem(position), 0); + + auto bottom = makeCreature(1, position); + tile.addThing(bottom, -1); + + tile.addThing(makeItem(position), -1); + + auto middle = makeCreature(2, position); + tile.addThing(middle, -1); + + tile.addThing(makeItem(position), -1); + + auto top = makeCreature(3, position); + tile.addThing(top, -1); + + tile.addThing(makeItem(position), -1); + + const auto expected = expectedSpectatorsFromTile(tile); + + std::vector actual; + tile.appendSpectators(actual); + + EXPECT_EQ(expected, actual); + + const auto [expectedFirst, expectedLast] = expectedCreatureSpan(tile); + EXPECT_EQ(expectedFirst, tile.m_firstCreatureIndex); + EXPECT_EQ(expectedLast, tile.m_lastCreatureIndex); +} + +TEST(TileSpectators, RemovingCreaturesUpdatesSpan) +{ + const Position position(210, 210, 7); + Tile tile(position); + + auto first = makeCreature(1, position); + auto second = makeCreature(2, position); + + tile.addThing(makeItem(position), -1); + tile.addThing(first, -1); + tile.addThing(makeItem(position), -1); + tile.addThing(second, -1); + + ASSERT_TRUE(tile.hasCreatures()); + + const auto expectedBeforeRemoval = expectedSpectatorsFromTile(tile); + ASSERT_FALSE(expectedBeforeRemoval.empty()); + + std::vector actualBeforeRemoval; + tile.appendSpectators(actualBeforeRemoval); + EXPECT_EQ(expectedBeforeRemoval, actualBeforeRemoval); + + const auto forwardBeforeRemoval = tile.getCreatures(); + ASSERT_EQ(expectedBeforeRemoval.size(), forwardBeforeRemoval.size()); + EXPECT_EQ(expectedBeforeRemoval.back(), forwardBeforeRemoval.front()); + + const auto [expectedFirst, expectedLast] = expectedCreatureSpan(tile); + EXPECT_EQ(expectedFirst, tile.m_firstCreatureIndex); + EXPECT_EQ(expectedLast, tile.m_lastCreatureIndex); + + ASSERT_TRUE(tile.removeThing(first)); + EXPECT_TRUE(tile.hasCreatures()); + + const auto expectedAfterFirstRemoval = expectedSpectatorsFromTile(tile); + ASSERT_FALSE(expectedAfterFirstRemoval.empty()); + + std::vector actualAfterFirstRemoval; + tile.appendSpectators(actualAfterFirstRemoval); + EXPECT_EQ(expectedAfterFirstRemoval, actualAfterFirstRemoval); + + const auto forwardAfterFirstRemoval = tile.getCreatures(); + ASSERT_EQ(expectedAfterFirstRemoval.size(), forwardAfterFirstRemoval.size()); + EXPECT_EQ(expectedAfterFirstRemoval.back(), forwardAfterFirstRemoval.front()); + + const auto [expectedFirstAfterRemoval, expectedLastAfterRemoval] = expectedCreatureSpan(tile); + EXPECT_EQ(expectedFirstAfterRemoval, tile.m_firstCreatureIndex); + EXPECT_EQ(expectedLastAfterRemoval, tile.m_lastCreatureIndex); + + ASSERT_TRUE(tile.removeThing(second)); + EXPECT_FALSE(tile.hasCreatures()); + EXPECT_TRUE(tile.getCreatures().empty()); + + const auto [expectedFirstAfterSecondRemoval, expectedLastAfterSecondRemoval] = expectedCreatureSpan(tile); + EXPECT_EQ(expectedFirstAfterSecondRemoval, tile.m_firstCreatureIndex); + EXPECT_EQ(expectedLastAfterSecondRemoval, tile.m_lastCreatureIndex); +} + +TEST(MapSpectators, AggregatesCreaturesFromTiles) +{ + const Position center(105, 205, 7); + + Map map; + map.m_floors.resize(g_gameConfig.getMapMaxZ() + 1); + map.m_centralPosition = center; + + const auto& tile = map.createTile(center); + + auto first = makeCreature(10, center); + auto second = makeCreature(20, center); + + tile->addThing(first, -1); + tile->addThing(second, -1); + + map.m_knownCreatures.try_emplace(first->getId(), first); + map.m_knownCreatures.try_emplace(second->getId(), second); + + const auto expected = expectedSpectatorsFromTile(*tile); + + const auto inRange = map.getSpectatorsInRangeEx(center, false, 0, 0, 0, 0); + EXPECT_EQ(expected, inRange); + + const auto byPattern = map.getSpectatorsByPattern(center, "1", Otc::North); + EXPECT_EQ(expected, byPattern); +} + +std::vector emulateLegacySpectatorCollection(Map& map, const Position& center, const bool multiFloor, const int32_t minXRange, const int32_t maxXRange, const int32_t minYRange, const int32_t maxYRange) +{ + std::vector result; + + uint8_t minZRange = 0; + uint8_t maxZRange = 0; + + if (multiFloor) { + minZRange = center.z - map.getFirstAwareFloor(); + maxZRange = map.getLastAwareFloor() - center.z; + } + + for (int iz = -minZRange; iz <= maxZRange; ++iz) { + for (int iy = -minYRange; iy <= maxYRange; ++iy) { + for (int ix = -minXRange; ix <= maxXRange; ++ix) { + const Position position = center.translated(ix, iy, iz); + if (const auto& tile = map.getTile(position)) { + auto tileCreatures = tile->getCreatures(); + result.insert(result.end(), tileCreatures.rbegin(), tileCreatures.rend()); + } + } + } + } + + return result; +} + +TEST(MapSpectators, AggregationMatchesLegacyTraversal) +{ + const Position center(250, 350, 6); + + Map map; + map.m_floors.resize(g_gameConfig.getMapMaxZ() + 1); + map.m_centralPosition = center; + + const auto centerTile = map.createTile(center); + const auto eastTile = map.createTile(center.translated(2, 0)); + const auto northTile = map.createTile(center.translated(0, -1)); + const auto aboveTile = map.createTile(center.translated(0, 0, 1)); + + auto first = makeCreature(30, center); + auto second = makeCreature(40, center.translated(2, 0)); + auto third = makeCreature(50, center.translated(0, -1)); + auto fourth = makeCreature(60, center.translated(0, 0, 1)); + + centerTile->addThing(first, -1); + eastTile->addThing(second, -1); + northTile->addThing(third, -1); + aboveTile->addThing(fourth, -1); + + map.m_knownCreatures.try_emplace(first->getId(), first); + map.m_knownCreatures.try_emplace(second->getId(), second); + map.m_knownCreatures.try_emplace(third->getId(), third); + map.m_knownCreatures.try_emplace(fourth->getId(), fourth); + + const bool multiFloor = true; + const int range = 3; + + const auto expected = emulateLegacySpectatorCollection(map, center, multiFloor, range, range, range, range); + const auto actual = map.getSpectatorsInRangeEx(center, multiFloor, range, range, range, range); + + EXPECT_EQ(expected, actual); +} + +TEST(MapSpectators, RangeFiltering) +{ + const Position center(101, 101, 8); + + Map map; + map.m_floors.resize(g_gameConfig.getMapMaxZ() + 1); + map.m_centralPosition = center; + + const auto centerTile = map.createTile(center); + const auto adjTile = map.createTile(center.translated(-1, -1)); + const auto farTile = map.createTile(center.translated(2, 2)); + + auto c1 = makeCreature(1, center); + auto c2 = makeCreature(2, adjTile->getPosition()); + auto c3 = makeCreature(3, farTile->getPosition()); + + centerTile->addThing(c1, -1); + adjTile->addThing(c2, -1); + farTile->addThing(c3, -1); + + map.m_knownCreatures.try_emplace(c1->getId(), c1); + map.m_knownCreatures.try_emplace(c2->getId(), c2); + map.m_knownCreatures.try_emplace(c3->getId(), c3); + + { + const auto nearSpectators = map.getSpectatorsInRangeEx(center, false, 1, 1, 1, 1); + const std::vector expected{ c2, c1 }; + EXPECT_EQ(expected, nearSpectators); + } + + { + const auto farSpectators = map.getSpectatorsInRangeEx(center, false, 2, 2, 2, 2); + const std::vector expected{ c2, c1, c3 }; + EXPECT_EQ(expected, farSpectators); + } +} + +TEST(MapSpectators, CreatureOrderingIsDeterministic) +{ + const Position center(180, 280, 7); + + Map map; + map.m_floors.resize(g_gameConfig.getMapMaxZ() + 1); + map.m_centralPosition = center; + + const auto westTile = map.createTile(center.translated(-1, 0)); + const auto centerTile = map.createTile(center); + const auto eastTile = map.createTile(center.translated(1, 0)); + + auto west = makeCreature(1, westTile->getPosition()); + auto middle = makeCreature(2, centerTile->getPosition()); + auto east = makeCreature(3, eastTile->getPosition()); + + westTile->addThing(west, -1); + centerTile->addThing(middle, -1); + eastTile->addThing(east, -1); + + map.m_knownCreatures.try_emplace(west->getId(), west); + map.m_knownCreatures.try_emplace(middle->getId(), middle); + map.m_knownCreatures.try_emplace(east->getId(), east); + + const auto expected = std::vector{ west, middle, east }; + + const auto spectators = map.getSpectatorsInRangeEx(center, false, 1, 1, 0, 0); + ASSERT_EQ(expected.size(), spectators.size()); + EXPECT_EQ(expected, spectators); + + const auto repeated = map.getSpectatorsInRangeEx(center, false, 1, 1, 0, 0); + EXPECT_EQ(spectators, repeated); +} + +TEST(MapSpectators, MultiFloorRangeIncludesVerticalNeighbors) +{ + const Position center(190, 290, 8); + + Map map; + map.m_floors.resize(g_gameConfig.getMapMaxZ() + 1); + map.m_centralPosition = center; + + const auto belowTile = map.createTile(center.translated(0, 0, -1)); + const auto centerTile = map.createTile(center); + const auto aboveTile = map.createTile(center.translated(0, 0, 1)); + + auto below = makeCreature(11, belowTile->getPosition()); + auto middle = makeCreature(12, centerTile->getPosition()); + auto above = makeCreature(13, aboveTile->getPosition()); + + belowTile->addThing(below, -1); + centerTile->addThing(middle, -1); + aboveTile->addThing(above, -1); + + map.m_knownCreatures.try_emplace(below->getId(), below); + map.m_knownCreatures.try_emplace(middle->getId(), middle); + map.m_knownCreatures.try_emplace(above->getId(), above); + + const auto spectators = map.getSpectatorsInRangeEx(center, true, 0, 0, 0, 0); + const auto expected = std::vector{ below, middle, above }; + + ASSERT_EQ(expected.size(), spectators.size()); + EXPECT_EQ(expected, spectators); +} + +TEST(MapSpectators, UniqueCreatures) +{ + const Position center(220, 320, 7); + + Map map; + map.m_floors.resize(g_gameConfig.getMapMaxZ() + 1); + map.m_centralPosition = center; + + const auto firstTile = map.createTile(center); + const auto secondTile = map.createTile(center.translated(1, 0)); + + auto shared = makeCreature(42, firstTile->getPosition()); + + firstTile->addThing(shared, -1); + secondTile->addThing(shared, -1); + + map.m_knownCreatures.try_emplace(shared->getId(), shared); + + const auto spectators = map.getSpectatorsInRangeEx(center, false, 1, 1, 0, 0); + ASSERT_EQ(1u, spectators.size()); + EXPECT_EQ(shared, spectators.front()); +} diff --git a/vcpkg.json b/vcpkg.json index 2c3f8ad00..2ac02388d 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -22,6 +22,7 @@ "zlib", "bshoshany-thread-pool", "fmt", + "gtest", { "name": "luajit", "platform": "!android & !wasm32"