diff --git a/CMakeLists.txt b/CMakeLists.txt index bff3500cea..0cd5c6c433 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 5fbb71dbc3..a383f6f9ab 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 # ***************************************************************************** @@ -210,12 +151,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) @@ -366,9 +307,6 @@ set(SOURCE_FILES client/uiminimap.cpp client/uiprogressrect.cpp client/uisprite.cpp - - main.cpp - androidmain.cpp ) if (TOGGLE_FRAMEWORK_GRAPHICS) @@ -446,9 +384,79 @@ if (WASM) ) endif() +add_library(otclient_core STATIC ${SOURCE_FILES}) +if(NOT MSVC) + target_link_options(otclient_core PUBLIC -flto=auto) +endif() + +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_sources(${PROJECT_NAME} PRIVATE ${SOURCE_FILES}) -target_link_options(${PROJECT_NAME} PUBLIC -flto=auto) +target_compile_definitions(otclient_core PRIVATE ENABLE_DISCORD_RPC=${OTCLIENT_ENABLE_DISCORD_RPC}) # ***************************************************************************** # Includes and librarys @@ -456,13 +464,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}") @@ -482,15 +490,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} @@ -503,8 +520,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} @@ -533,8 +550,8 @@ if(MSVC) fmt::fmt-header-only ) elseif(ANDROID) - target_include_directories(${PROJECT_NAME} - PRIVATE + target_include_directories(otclient_core + PUBLIC $ $ ${LUAJIT_INCLUDE_DIR} @@ -549,8 +566,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} @@ -583,8 +600,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} @@ -597,8 +614,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} @@ -641,7 +658,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 ) @@ -667,8 +684,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} @@ -682,8 +699,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} @@ -722,7 +739,7 @@ else() # Linux ) if(CMAKE_BUILD_TYPE STREQUAL "Debug") - target_compile_options(${PROJECT_NAME} + target_compile_options(otclient_core PRIVATE -Wall -Wextra -Wpedantic ) @@ -730,7 +747,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 89bdc3c780..bbd6e58642 100644 --- a/src/client/creature.h +++ b/src/client/creature.h @@ -153,11 +153,13 @@ class Creature : public Thing 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); @@ -211,6 +213,9 @@ minHeight, 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; @@ -362,12 +367,12 @@ minHeight, 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 e391a95d6b..879fcb2ce4 100644 --- a/src/client/effect.h +++ b/src/client/effect.h @@ -33,7 +33,7 @@ class Effect final : public Thing 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 0f4a87a29d..ad5c671f69 100644 --- a/src/client/item.h +++ b/src/client/item.h @@ -104,7 +104,7 @@ class Item final : public Thing ItemPtr clone(); ItemPtr asItem() { return static_self_cast(); } - bool isItem() override { return true; } + bool isItem() const override { return true; } void updatePatterns(); int calculateAnimationPhase(); @@ -143,7 +143,7 @@ class Item final : public Thing 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 60eeadfa24..2efbb6f3be 100644 --- a/src/client/localplayer.h +++ b/src/client/localplayer.h @@ -124,7 +124,7 @@ class LocalPlayer final : public Player 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 3553121646..55aab8792a 100644 --- a/src/client/luafunctions.cpp +++ b/src/client/luafunctions.cpp @@ -599,7 +599,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); @@ -915,7 +918,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 4db0a7a040..9bfd6fe16d 100644 --- a/src/client/map.cpp +++ b/src/client/map.cpp @@ -36,6 +36,22 @@ #include #include #include +#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" @@ -630,6 +646,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; @@ -638,19 +658,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; } @@ -1467,15 +1499,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 63d5c920d5..2a09cc4107 100644 --- a/src/client/missile.h +++ b/src/client/missile.h @@ -34,7 +34,7 @@ class Missile final : public Thing 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 e890368bac..350469022f 100644 --- a/src/client/player.h +++ b/src/client/player.h @@ -32,5 +32,5 @@ class Player : public Creature ~Player() override = default; PlayerPtr asPlayer() { return static_self_cast(); } - bool isPlayer() override { return true; } + bool isPlayer() const override { return true; } }; diff --git a/src/client/thing.h b/src/client/thing.h index 613c40abd2..129b52f618 100644 --- a/src/client/thing.h +++ b/src/client/thing.h @@ -31,7 +31,7 @@ #include #include - // @bindclass +// @bindclass #pragma pack(push,1) // disable memory alignment class Thing : public AttachableObject { @@ -57,118 +57,456 @@ class Thing : public AttachableObject 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 isNpc() { return false; } - virtual bool isMonster() { return false; } - virtual bool isPlayer() { return false; } - virtual bool isLocalPlayer() { return false; } - - Animator* getAnimator() const { return getThingType()->getAnimator(); } - Animator* getIdleAnimator() const { return getThingType()->getIdleAnimator(); } - - virtual Point getDisplacement() const { return getThingType()->getDisplacement(); } - virtual int getDisplacementX() const { return getThingType()->getDisplacementX(); } - virtual int getDisplacementY() const { return getThingType()->getDisplacementY(); } - virtual int getExactSize(const int layer = 0, const int xPattern = 0, const int yPattern = 0, const int zPattern = 0, const int animationPhase = 0) { return getThingType()->getExactSize(layer, xPattern, yPattern, zPattern, animationPhase); } - - virtual const Light& getLight() const { return getThingType()->getLight(); } - virtual bool hasLight() const { return getThingType()->hasLight(); } - - const MarketData& getMarketData() { return getThingType()->getMarketData(); } - const std::vector& getNpcSaleData() { return getThingType()->getNpcSaleData(); } - int getMeanPrice() { return getThingType()->getMeanPrice(); } - const Size& getSize() const { return getThingType()->getSize(); } - - int getWidth() const { return getThingType()->getWidth(); } - int getHeight() const { return getThingType()->getHeight(); } - int getRealSize() const { return getThingType()->getRealSize(); } - int getLayers() const { return getThingType()->getLayers(); } - int getNumPatternX()const { return getThingType()->getNumPatternX(); } - int getNumPatternY()const { return getThingType()->getNumPatternY(); } - int getNumPatternZ()const { return getThingType()->getNumPatternZ(); } - int getAnimationPhases()const { return getThingType()->getAnimationPhases(); } - int getGroundSpeed() const { return getThingType()->getGroundSpeed(); } - int getMaxTextLength()const { return getThingType()->getMaxTextLength(); } - int getMinimapColor()const { return getThingType()->getMinimapColor(); } - int getLensHelp()const { return getThingType()->getLensHelp(); } - int getElevation() const { return getThingType()->getElevation(); } - - int getClothSlot() { return getThingType()->getClothSlot(); } - - bool blockProjectile() const { return getThingType()->blockProjectile(); } - - virtual bool isContainer() { return getThingType()->isContainer(); } - - bool isCommon() { return !isGround() && !isGroundBorder() && !isOnTop() && !isCreature() && !isOnBottom(); } - - bool isTopGround() { return !isCreature() && getThingType()->isTopGround(); } - bool isTopGroundBorder() { return !isCreature() && getThingType()->isTopGroundBorder(); } - bool isSingleGround() { return !isCreature() && getThingType()->isSingleGround(); } - bool isSingleGroundBorder() { return !isCreature() && getThingType()->isSingleGroundBorder(); } - bool isGround() { return !isCreature() && getThingType()->isGround(); } - bool isGroundBorder() { return !isCreature() && getThingType()->isGroundBorder(); } - bool isOnBottom() { return !isCreature() && getThingType()->isOnBottom(); } - bool isOnTop() { return !isCreature() && getThingType()->isOnTop(); } - - bool isMarketable() { return getThingType()->isMarketable(); } - bool isStackable() { return getThingType()->isStackable(); } - bool isFluidContainer() { return getThingType()->isFluidContainer(); } - bool isForceUse() { return getThingType()->isForceUse(); } - bool isMultiUse() { return getThingType()->isMultiUse(); } - bool isWritable() { return getThingType()->isWritable(); } - bool isChargeable() { return getThingType()->isChargeable(); } - bool isWritableOnce() { return getThingType()->isWritableOnce(); } - bool isSplash() { return getThingType()->isSplash(); } - bool isNotWalkable() { return getThingType()->isNotWalkable(); } - bool isNotMoveable() { return getThingType()->isNotMoveable(); } - bool isMoveable() { return !getThingType()->isNotMoveable(); } - bool isNotPathable() { return getThingType()->isNotPathable(); } - bool isPickupable() { return getThingType()->isPickupable(); } - bool isHangable() { return getThingType()->isHangable(); } - bool isHookSouth() { return getThingType()->isHookSouth(); } - bool isHookEast() { return getThingType()->isHookEast(); } - bool isRotateable() { return getThingType()->isRotateable(); } - bool isDontHide() { return getThingType()->isDontHide(); } - bool isTranslucent() { return getThingType()->isTranslucent(); } - bool isLyingCorpse() { return getThingType()->isLyingCorpse(); } - bool isAnimateAlways() { return getThingType()->isAnimateAlways(); } - bool isFullGround() { return getThingType()->isFullGround(); } - bool isIgnoreLook() { return getThingType()->isIgnoreLook(); } - bool isCloth() { return getThingType()->isCloth(); } - bool isUsable() { return getThingType()->isUsable(); } - bool isWrapable() { return getThingType()->isWrapable(); } - bool isUnwrapable() { return getThingType()->isUnwrapable(); } - bool isTopEffect() { return getThingType()->isTopEffect(); } - bool isPodium() const { return getThingType()->isPodium(); } - bool isOpaque() const { return getThingType()->isOpaque(); } - bool isLoading() const { return getThingType()->isLoading(); } - bool isSingleDimension() const { return getThingType()->isSingleDimension(); } - bool isTall(const bool useRealSize = false) const { return getThingType()->isTall(useRealSize); } - - bool hasMiniMapColor() const { return getThingType()->hasMiniMapColor(); } - bool hasLensHelp() const { return getThingType()->hasLensHelp(); } - bool hasDisplacement() const { return getThingType()->hasDisplacement(); } - bool hasElevation() const { return getThingType()->hasElevation(); } - bool hasAction() const { return getThingType()->hasAction(); } - bool hasWearOut() const { return getThingType()->hasWearOut(); } - bool hasClockExpire() const { return getThingType()->hasClockExpire(); } - bool hasExpire() const { return getThingType()->hasExpire(); } - bool hasExpireStop() const { return getThingType()->hasExpireStop(); } - bool hasAnimationPhases() const { return getThingType()->getAnimationPhases() > 1; } - bool isDecoKit() const { return getThingType()->isDecoKit(); } - - PLAYER_ACTION getDefaultAction() { return getThingType()->getDefaultAction(); } - - uint16_t getClassification() { return getThingType()->getClassification(); } + 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() const { return false; } + virtual bool isMonster() const { return false; } + virtual bool isPlayer() const { return false; } + virtual bool isLocalPlayer() const { return false; } + + Animator* getAnimator() const { + if (const auto t = getThingType(); t) + return t->getAnimator(); + return nullptr; + } + Animator* getIdleAnimator() const { + if (const auto t = getThingType(); t) + return t->getIdleAnimator(); + return nullptr; + } + + virtual Point getDisplacement() const { + if (const auto t = getThingType(); t) + return t->getDisplacement(); + return Point(); + } + virtual int getDisplacementX() const { + if (const auto t = getThingType(); t) + return t->getDisplacementX(); + return 0; + } + virtual int getDisplacementY() const { + if (const auto t = getThingType(); t) + return t->getDisplacementY(); + return 0; + } + virtual int getExactSize(const int layer = 0, const int xPattern = 0, const int yPattern = 0, const int zPattern = 0, const int animationPhase = 0) { + if (const auto t = getThingType(); t) + return t->getExactSize(layer, xPattern, yPattern, zPattern, animationPhase); + return 0; + } + + virtual const Light& getLight() const { + if (const auto t = getThingType(); t) + return t->getLight(); + static const Light kEmptyLight; + return kEmptyLight; + } + virtual bool hasLight() const { + if (const auto t = getThingType(); t) + return t->hasLight(); + return false; + } + + const MarketData& getMarketData() const { + if (const auto t = getThingType(); t) + return t->getMarketData(); + static const MarketData kEmptyMarketData{}; + return kEmptyMarketData; + } + const std::vector& getNpcSaleData() const { + if (const auto t = getThingType(); t) + return t->getNpcSaleData(); + static const std::vector kEmptyNpcData; + return kEmptyNpcData; + } + int getMeanPrice() const { + if (const auto t = getThingType(); t) + return t->getMeanPrice(); + return 0; + } + const Size& getSize() const { + if (const auto t = getThingType(); t) + return t->getSize(); + static const Size kEmptySize; + return kEmptySize; + } + int getWidth() const { + if (const auto t = getThingType(); t) + return t->getWidth(); + return 0; + } + int getHeight() const { + if (const auto t = getThingType(); t) + return t->getHeight(); + return 0; + } + int getRealSize() const { + if (const auto t = getThingType(); t) + return t->getRealSize(); + return 0; + } + int getLayers() const { + if (const auto t = getThingType(); t) + return t->getLayers(); + return 0; + } + int getNumPatternX() const { + if (const auto t = getThingType(); t) + return t->getNumPatternX(); + return 0; + } + int getNumPatternY() const { + if (const auto t = getThingType(); t) + return t->getNumPatternY(); + return 0; + } + int getNumPatternZ() const { + if (const auto t = getThingType(); t) + return t->getNumPatternZ(); + return 0; + } + int getAnimationPhases() const { + if (const auto t = getThingType(); t) + return t->getAnimationPhases(); + return 0; + } + int getGroundSpeed() const { + if (const auto t = getThingType(); t) + return t->getGroundSpeed(); + return 0; + } + int getMaxTextLength() const { + if (const auto t = getThingType(); t) + return t->getMaxTextLength(); + return 0; + } + int getMinimapColor() const { + if (const auto t = getThingType(); t) + return t->getMinimapColor(); + return 0; + } + int getLensHelp() const { + if (const auto t = getThingType(); t) + return t->getLensHelp(); + return 0; + } + int getElevation() const { + if (const auto t = getThingType(); t) + return t->getElevation(); + return 0; + } + + int getClothSlot() const { + if (const auto t = getThingType(); t) + return t->getClothSlot(); + return 0; + } + + bool blockProjectile() const { + if (const auto t = getThingType(); t) + return t->blockProjectile(); + return false; + } + + virtual bool isContainer() const { + if (const auto t = getThingType(); t) + return t->isContainer(); + return false; + } + + bool isCommon() const { return !isGround() && !isGroundBorder() && !isOnTop() && !isCreature() && !isOnBottom(); } + + bool isTopGround() const { + if (const auto t = getThingType(); !isCreature() && t) + return t->isTopGround(); + return false; + } + bool isTopGroundBorder() const { + if (const auto t = getThingType(); !isCreature() && t) + return t->isTopGroundBorder(); + return false; + } + bool isSingleGround() const { + if (const auto t = getThingType(); !isCreature() && t) + return t->isSingleGround(); + return false; + } + bool isSingleGroundBorder() const { + if (const auto t = getThingType(); !isCreature() && t) + return t->isSingleGroundBorder(); + return false; + } + bool isGround() const { + if (const auto t = getThingType(); !isCreature() && t) + return t->isGround(); + return false; + } + bool isGroundBorder() const { + if (const auto t = getThingType(); !isCreature() && t) + return t->isGroundBorder(); + return false; + } + bool isOnBottom() const { + if (const auto t = getThingType(); !isCreature() && t) + return t->isOnBottom(); + return false; + } + bool isOnTop() const { + if (const auto t = getThingType(); !isCreature() && t) + return t->isOnTop(); + return false; + } + + bool isMarketable() const { + if (const auto t = getThingType(); t) + return t->isMarketable(); + return false; + } + bool isStackable() const { + if (const auto t = getThingType(); t) + return t->isStackable(); + return false; + } + bool isFluidContainer() const { + if (const auto t = getThingType(); t) + return t->isFluidContainer(); + return false; + } + bool isForceUse() const { + if (const auto t = getThingType(); t) + return t->isForceUse(); + return false; + } + bool isMultiUse() const { + if (const auto t = getThingType(); t) + return t->isMultiUse(); + return false; + } + bool isWritable() const { + if (const auto t = getThingType(); t) + return t->isWritable(); + return false; + } + bool isChargeable() const { + if (const auto t = getThingType(); t) + return t->isChargeable(); + return false; + } + bool isWritableOnce() const { + if (const auto t = getThingType(); t) + return t->isWritableOnce(); + return false; + } + bool isSplash() const { + if (const auto t = getThingType(); t) + return t->isSplash(); + return false; + } + bool isNotWalkable() const { + if (const auto t = getThingType(); t) + return t->isNotWalkable(); + return false; + } + bool isNotMoveable() const { + if (const auto t = getThingType(); t) + return t->isNotMoveable(); + return false; + } + bool isMoveable() const { + if (const auto t = getThingType(); t) + return !t->isNotMoveable(); + return false; + } + bool isNotPathable() const { + if (const auto t = getThingType(); t) + return t->isNotPathable(); + return false; + } + bool isPickupable() const { + if (const auto t = getThingType(); t) + return t->isPickupable(); + return false; + } + bool isHangable() const { + if (const auto t = getThingType(); t) + return t->isHangable(); + return false; + } + bool isHookSouth() const { + if (const auto t = getThingType(); t) + return t->isHookSouth(); + return false; + } + bool isHookEast() const { + if (const auto t = getThingType(); t) + return t->isHookEast(); + return false; + } + bool isRotateable() const { + if (const auto t = getThingType(); t) + return t->isRotateable(); + return false; + } + bool isDontHide() const { + if (const auto t = getThingType(); t) + return t->isDontHide(); + return false; + } + bool isTranslucent() const { + if (const auto t = getThingType(); t) + return t->isTranslucent(); + return false; + } + bool isLyingCorpse() const { + if (const auto t = getThingType(); t) + return t->isLyingCorpse(); + return false; + } + bool isAnimateAlways() const { + if (const auto t = getThingType(); t) + return t->isAnimateAlways(); + return false; + } + bool isFullGround() const { + if (const auto t = getThingType(); t) + return t->isFullGround(); + return false; + } + bool isIgnoreLook() const { + if (const auto t = getThingType(); t) + return t->isIgnoreLook(); + return false; + } + bool isCloth() const { + if (const auto t = getThingType(); t) + return t->isCloth(); + return false; + } + bool isUsable() const { + if (const auto t = getThingType(); t) + return t->isUsable(); + return false; + } + bool isWrapable() const { + if (const auto t = getThingType(); t) + return t->isWrapable(); + return false; + } + bool isUnwrapable() const { + if (const auto t = getThingType(); t) + return t->isUnwrapable(); + return false; + } + bool isTopEffect() const { + if (const auto t = getThingType(); t) + return t->isTopEffect(); + return false; + } + bool isPodium() const { + if (const auto t = getThingType(); t) + return t->isPodium(); + return false; + } + bool isOpaque() const { + if (const auto t = getThingType(); t) + return t->isOpaque(); + return false; + } + bool isLoading() const { + if (const auto t = getThingType(); t) + return t->isLoading(); + return false; + } + bool isSingleDimension() const { + if (const auto t = getThingType(); t) + return t->isSingleDimension(); + return false; + } + bool isTall(const bool useRealSize = false) const { + if (const auto t = getThingType(); t) + return t->isTall(useRealSize); + return false; + } + + bool hasMiniMapColor() const { + if (const auto t = getThingType(); t) + return t->hasMiniMapColor(); + return false; + } + bool hasLensHelp() const { + if (const auto t = getThingType(); t) + return t->hasLensHelp(); + return false; + } + bool hasDisplacement() const { + if (const auto t = getThingType(); t) + return t->hasDisplacement(); + return false; + } + bool hasElevation() const { + if (const auto t = getThingType(); t) + return t->hasElevation(); + return false; + } + bool hasAction() const { + if (const auto t = getThingType(); t) + return t->hasAction(); + return false; + } + bool hasWearOut() const { + if (const auto t = getThingType(); t) + return t->hasWearOut(); + return false; + } + bool hasClockExpire() const { + if (const auto t = getThingType(); t) + return t->hasClockExpire(); + return false; + } + bool hasExpire() const { + if (const auto t = getThingType(); t) + return t->hasExpire(); + return false; + } + bool hasExpireStop() const { + if (const auto t = getThingType(); t) + return t->hasExpireStop(); + return false; + } + bool hasAnimationPhases() const { + if (const auto t = getThingType(); t) + return t->getAnimationPhases() > 1; + return false; + } + bool isDecoKit() const { + if (const auto t = getThingType(); t) + return t->isDecoKit(); + return false; + } + + PLAYER_ACTION getDefaultAction() const { + if (const auto t = getThingType(); t) + return t->getDefaultAction(); + return static_cast(0); + } + + uint16_t getClassification() const { + if (const auto t = getThingType(); t) + return t->getClassification(); + return 0; + } void canDraw(const bool canDraw) { m_canDraw = canDraw; } bool canDraw(const Color& color = Color::white) 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; } void setShader(std::string_view name); diff --git a/src/client/tile.cpp b/src/client/tile.cpp index d15e525078..31d0be9297 100644 --- a/src/client/tile.cpp +++ b/src/client/tile.cpp @@ -211,6 +211,9 @@ void Tile::clean() m_thingTypeFlag = 0; + m_firstCreatureIndex = -1; + m_lastCreatureIndex = -1; + #ifdef FRAMEWORK_EDITOR m_flags = 0; #endif @@ -315,6 +318,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()) @@ -387,16 +392,74 @@ ThingPtr Tile::getThing(const int stackPos) return nullptr; } -std::vector Tile::getCreatures() +void Tile::updateCreatureRangeForInsert(const int16_t stackPos, const ThingPtr& thing) { - std::vector creatures; - if (hasCreatures()) { - for (const auto& thing : m_things) { - if (thing->isCreature()) - creatures.emplace_back(thing->static_self_cast()); + 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; + appendSpectators(creatures); + std::ranges::reverse(creatures); return creatures; } @@ -425,8 +488,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 14b61c0aea..13214bc06d 100644 --- a/src/client/tile.h +++ b/src/client/tile.h @@ -152,7 +152,10 @@ class Tile final : public AttachableObject bool hasGround() { return (getGround() && getGround()->isSingleGround()) || m_thingTypeFlag & HAS_GROUND_BORDER; }; bool hasTopGround(const bool ignoreBorder = false) { return (getGround() && getGround()->isTopGround()) || (!ignoreBorder && m_thingTypeFlag & HAS_TOP_GROUND_BORDER); } - 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; } @@ -221,11 +224,15 @@ class Tile final : public AttachableObject void drawTop(const Point& dest, int flags, bool forceDraw, uint8_t drawElevation); void drawCreature(const Point& dest, int flags, bool forceDraw, uint8_t drawElevation, const LightViewPtr& 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); } @@ -264,6 +271,9 @@ class Tile final : public AttachableObject 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/core/application.h b/src/framework/core/application.h index e0ce3e79a5..a18b7f4a59 100644 --- a/src/framework/core/application.h +++ b/src/framework/core/application.h @@ -28,6 +28,7 @@ class ApplicationContext { public: ApplicationContext() = default; + virtual ~ApplicationContext() = default; }; //@bindsingleton g_app diff --git a/src/framework/luaengine/luabinder.h b/src/framework/luaengine/luabinder.h index 477aef059c..77c65a6bb6 100644 --- a/src/framework/luaengine/luabinder.h +++ b/src/framework/luaengine/luabinder.h @@ -169,8 +169,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 Ret{}; + } return mf(obj.get(), args...); }; } @@ -179,8 +181,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 +237,47 @@ 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"); + return Ret{}; + } + 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 { return 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 df2c9a0e9c..b8e22f7407 100644 --- a/src/framework/luaengine/luainterface.h +++ b/src/framework/luaengine/luainterface.h @@ -92,19 +92,23 @@ class LuaInterface 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 @@ class LuaInterface 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 0000000000..799c2fc029 --- /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 0000000000..3e12efd2df --- /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 0000000000..8218e7a26a --- /dev/null +++ b/tests/map/map_spectators_test.cpp @@ -0,0 +1,487 @@ +#include + +#define private public +#define protected public +#include "client/map.h" +#undef protected +#undef private + +#include "client/creature.h" +#include "client/gameconfig.h" +#include "client/tile.h" + +#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({}); + } +}; + +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 2c3f8ad000..2ac02388d3 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -22,6 +22,7 @@ "zlib", "bshoshany-thread-pool", "fmt", + "gtest", { "name": "luajit", "platform": "!android & !wasm32"