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"