From cc9ca8a6f3103d7f12a16bd2ed28bf7390184259 Mon Sep 17 00:00:00 2001 From: Pursche Date: Sat, 30 Aug 2025 16:35:40 +0200 Subject: [PATCH] Add ProximityTriggers Added support for ProximityTriggers with OnEnter, OnExit and OnStay functions Triggers can be ServerSideOnly, ServerAuthorative (OnEnter command gets sent) or completely clientside Triggers get replicated from the server to the client Added AABB and WorldAABB components Added cheat commands to Add and Remove triggers Added CollisionUtil with the IsInside and Overlaps functions I needed Updated Engine submodule --- .../Updates/Update_2025_08_12_2222.sql | 21 ++ Source/Server-Common/Server-Common.lua | 2 +- .../Server-Common/Database/Definitions.h | 20 ++ .../Database/Util/ProximityTriggerUtils.cpp | 113 +++++++++ .../Database/Util/ProximityTriggerUtils.h | 25 ++ .../Server-Game/ECS/Components/AABB.h | 17 ++ .../ECS/Components/ProximityTrigger.h | 22 ++ .../Server-Game/ECS/Components/Tags.h | 4 + .../Server-Game/Server-Game/ECS/Scheduler.cpp | 3 + .../Server-Game/ECS/Singletons/GameCache.h | 1 + .../ECS/Singletons/ProximityTriggers.h | 22 ++ .../ECS/Systems/CharacterInitialization.cpp | 26 +- .../Server-Game/ECS/Systems/DatabaseSetup.cpp | 3 + .../ECS/Systems/NetworkConnection.cpp | 142 ++++++++++- .../ECS/Systems/ProximityTrigger.cpp | 240 ++++++++++++++++++ .../ECS/Systems/ProximityTrigger.h | 13 + .../Server-Game/ECS/Util/CollisionUtil.cpp | 57 +++++ .../Server-Game/ECS/Util/CollisionUtil.h | 17 ++ .../ECS/Util/MessageBuilderUtil.cpp | 30 ++- .../Server-Game/ECS/Util/MessageBuilderUtil.h | 14 +- .../ECS/Util/ProximityTriggerUtil.cpp | 23 ++ .../ECS/Util/ProximityTriggerUtil.h | 19 ++ Submodules/Engine | 2 +- 23 files changed, 828 insertions(+), 8 deletions(-) create mode 100644 Resources/Database/Updates/Update_2025_08_12_2222.sql create mode 100644 Source/Server-Common/Server-Common/Database/Util/ProximityTriggerUtils.cpp create mode 100644 Source/Server-Common/Server-Common/Database/Util/ProximityTriggerUtils.h create mode 100644 Source/Server-Game/Server-Game/ECS/Components/AABB.h create mode 100644 Source/Server-Game/Server-Game/ECS/Components/ProximityTrigger.h create mode 100644 Source/Server-Game/Server-Game/ECS/Singletons/ProximityTriggers.h create mode 100644 Source/Server-Game/Server-Game/ECS/Systems/ProximityTrigger.cpp create mode 100644 Source/Server-Game/Server-Game/ECS/Systems/ProximityTrigger.h create mode 100644 Source/Server-Game/Server-Game/ECS/Util/CollisionUtil.cpp create mode 100644 Source/Server-Game/Server-Game/ECS/Util/CollisionUtil.h create mode 100644 Source/Server-Game/Server-Game/ECS/Util/ProximityTriggerUtil.cpp create mode 100644 Source/Server-Game/Server-Game/ECS/Util/ProximityTriggerUtil.h diff --git a/Resources/Database/Updates/Update_2025_08_12_2222.sql b/Resources/Database/Updates/Update_2025_08_12_2222.sql new file mode 100644 index 0000000..5dc768a --- /dev/null +++ b/Resources/Database/Updates/Update_2025_08_12_2222.sql @@ -0,0 +1,21 @@ +DROP TABLE IF EXISTS public.proximity_triggers CASCADE; + +CREATE TABLE public.proximity_triggers +( + id serial NOT NULL, + name text NOT NULL DEFAULT '', + flags smallint NOT NULL DEFAULT '0', + map_id smallint NOT NULL DEFAULT '0', + position_x real NOT NULL DEFAULT '0.0', + position_y real NOT NULL DEFAULT '0.0', + position_z real NOT NULL DEFAULT '0.0', + extents_x real NOT NULL DEFAULT '1.0', + extents_y real NOT NULL DEFAULT '1.0', + extents_z real NOT NULL DEFAULT '1.0', + PRIMARY KEY (id) +) + +TABLESPACE pg_default; + +ALTER TABLE IF EXISTS public.proximity_triggers + OWNER to postgres; \ No newline at end of file diff --git a/Source/Server-Common/Server-Common.lua b/Source/Server-Common/Server-Common.lua index bb5fdda..f3e6ca7 100644 --- a/Source/Server-Common/Server-Common.lua +++ b/Source/Server-Common/Server-Common.lua @@ -1,4 +1,4 @@ -local mod = Solution.Util.CreateModuleTable("Server-Common", { "base", "fileformat", "input", "network", "gameplay", "luau-compiler", "luau-vm", "enkits", "refl-cpp", "utfcpp", "base64", "libpqxx" }) +local mod = Solution.Util.CreateModuleTable("Server-Common", { "base", "fileformat", "meta", "input", "network", "gameplay", "luau-compiler", "luau-vm", "enkits", "refl-cpp", "utfcpp", "base64", "libpqxx" }) Solution.Util.CreateStaticLib(mod.Name, Solution.Projects.Current.BinDir, mod.Dependencies, function() local defines = { "_CRT_SECURE_NO_WARNINGS", "_SILENCE_ALL_CXX17_DEPRECATION_WARNINGS", "_SILENCE_ALL_MS_EXT_DEPRECATION_WARNINGS" } diff --git a/Source/Server-Common/Server-Common/Database/Definitions.h b/Source/Server-Common/Server-Common/Database/Definitions.h index 993e759..7689c24 100644 --- a/Source/Server-Common/Server-Common/Database/Definitions.h +++ b/Source/Server-Common/Server-Common/Database/Definitions.h @@ -4,6 +4,8 @@ #include +#include + #include #include @@ -288,6 +290,17 @@ namespace Database u16 slot = 0; }; + struct ProximityTrigger + { + public: + u32 id = 0; + std::string name = ""; + Generated::ProximityTriggerFlagEnum flags = Generated::ProximityTriggerFlagEnum::None; + u16 mapID = 0; + vec3 position = vec3(0.0f, 0.0f, 0.0f); + vec3 extents = vec3(1.0f, 1.0f, 1.0f); + }; + struct PermissionTables { public: @@ -322,4 +335,11 @@ namespace Database robin_hood::unordered_map> charIDToPermissionGroups; robin_hood::unordered_map> charIDToCurrency; }; + + struct ProximityTriggersTables + { + public: + std::vector triggers; + robin_hood::unordered_map triggerIDToEntity; + }; } \ No newline at end of file diff --git a/Source/Server-Common/Server-Common/Database/Util/ProximityTriggerUtils.cpp b/Source/Server-Common/Server-Common/Database/Util/ProximityTriggerUtils.cpp new file mode 100644 index 0000000..32a3d54 --- /dev/null +++ b/Source/Server-Common/Server-Common/Database/Util/ProximityTriggerUtils.cpp @@ -0,0 +1,113 @@ +#include "ProximityTriggerUtils.h" +#include "Server-Common/Database/DBController.h" + +#include +#include + +#include + +namespace Database::Util::ProximityTrigger +{ + namespace Loading + { + void InitProximityTriggersTablesPreparedStatements(std::shared_ptr& dbConnection) + { + NC_LOG_INFO("Loading Prepared Statements Proximity Triggers Tables..."); + + try + { + dbConnection->connection->prepare("ProximityTriggerCreate", "INSERT INTO public.proximity_triggers (name, flags, map_id, position_x, position_y, position_z, extents_x, extents_y, extents_z) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id"); + dbConnection->connection->prepare("ProximityTriggerDelete", "DELETE FROM public.proximity_triggers WHERE id = $1"); + + NC_LOG_INFO("Loaded Prepared Statements Proximity Triggers Tables\n"); + } + catch (const pqxx::sql_error& e) + { + NC_LOG_CRITICAL("{0}", e.what()); + return; + } + } + + void LoadProximityTriggersTables(std::shared_ptr& dbConnection, Database::ProximityTriggersTables& proximityTriggersTables) + { + NC_LOG_INFO("-- Loading Proximity Triggers Tables --"); + + u64 totalRows = 0; + totalRows += LoadProximityTriggers(dbConnection, proximityTriggersTables); + + NC_LOG_INFO("-- Loaded Proximity Triggers Tables ({0} rows) --\n", totalRows); + } + + u64 LoadProximityTriggers(std::shared_ptr& dbConnection, Database::ProximityTriggersTables& proximityTriggersTables) + { + NC_LOG_INFO("Loading Table 'proximity_triggers'"); + + pqxx::nontransaction nonTransaction = dbConnection->NewNonTransaction(); + + auto result = nonTransaction.exec("SELECT COUNT(*) FROM public.proximity_triggers"); + u64 numRows = result[0][0].as(); + + proximityTriggersTables.triggers.clear(); + + if (numRows == 0) + { + NC_LOG_INFO("Skipped Table 'proximity_triggers'"); + return 0; + } + + proximityTriggersTables.triggers.reserve(numRows); + + nonTransaction.for_stream("SELECT * FROM public.proximity_triggers", [&proximityTriggersTables](u32 id, const std::string& name, u16 flags, u16 mapID, f32 positionX, f32 positionY, f32 positionZ, f32 extentsX, f32 extentsY, f32 extentsZ) + { + Database::ProximityTrigger trigger = + { + .id = id, + .name = name, + .flags = static_cast(flags), + .mapID = mapID, + .position = vec3(positionX, positionY, positionZ), + .extents = vec3(extentsX, extentsY, extentsZ) + }; + proximityTriggersTables.triggers.push_back(trigger); + }); + + NC_LOG_INFO("Loaded Table 'proximity_triggers' ({0} Rows)", numRows); + return numRows; + } + } + + bool ProximityTriggerCreate(pqxx::work& transaction, const std::string& name, u16 flags, u16 mapID, const vec3& position, const vec3& extents, u32& triggerID) + { + try + { + auto queryResult = transaction.exec(pqxx::prepped("ProximityTriggerCreate"), pqxx::params{ name, flags, mapID, position.x, position.y, position.z, extents.x, extents.y, extents.z }); + if (queryResult.empty()) + return false; + + triggerID = queryResult[0][0].as(); + return true; + } + catch (const pqxx::sql_error& e) + { + NC_LOG_WARNING("{0}", e.what()); + return false; + } + } + + bool ProximityTriggerDelete(pqxx::work& transaction, u32 triggerID) + { + try + { + auto queryResult = transaction.exec(pqxx::prepped("ProximityTriggerDelete"), pqxx::params{ triggerID }); + if (queryResult.affected_rows() == 0) + return false; + + return true; + } + catch (const pqxx::sql_error& e) + { + NC_LOG_WARNING("{0}", e.what()); + return false; + } + } +} \ No newline at end of file diff --git a/Source/Server-Common/Server-Common/Database/Util/ProximityTriggerUtils.h b/Source/Server-Common/Server-Common/Database/Util/ProximityTriggerUtils.h new file mode 100644 index 0000000..28f5c20 --- /dev/null +++ b/Source/Server-Common/Server-Common/Database/Util/ProximityTriggerUtils.h @@ -0,0 +1,25 @@ +#pragma once +#include "Server-Common/Database/Definitions.h" + +#include + +#include + +namespace Database +{ + struct DBConnection; + + namespace Util::ProximityTrigger + { + namespace Loading + { + void InitProximityTriggersTablesPreparedStatements(std::shared_ptr& dbConnection); + + void LoadProximityTriggersTables(std::shared_ptr& dbConnection, Database::ProximityTriggersTables& proximityTriggersTables); + u64 LoadProximityTriggers(std::shared_ptr& dbConnection, Database::ProximityTriggersTables& proximityTriggersTables); + } + + bool ProximityTriggerCreate(pqxx::work& transaction, const std::string& name, u16 flags, u16 mapID, const vec3& position, const vec3& extents, u32& triggerID); + bool ProximityTriggerDelete(pqxx::work& transaction, u32 triggerID); + } +} \ No newline at end of file diff --git a/Source/Server-Game/Server-Game/ECS/Components/AABB.h b/Source/Server-Game/Server-Game/ECS/Components/AABB.h new file mode 100644 index 0000000..11942e5 --- /dev/null +++ b/Source/Server-Game/Server-Game/ECS/Components/AABB.h @@ -0,0 +1,17 @@ +#pragma once +#include + +namespace ECS::Components +{ + struct AABB + { + vec3 centerPos; + vec3 extents; + }; + + struct WorldAABB + { + vec3 min; + vec3 max; + }; +} \ No newline at end of file diff --git a/Source/Server-Game/Server-Game/ECS/Components/ProximityTrigger.h b/Source/Server-Game/Server-Game/ECS/Components/ProximityTrigger.h new file mode 100644 index 0000000..741c876 --- /dev/null +++ b/Source/Server-Game/Server-Game/ECS/Components/ProximityTrigger.h @@ -0,0 +1,22 @@ +#pragma once +#include + +#include +DECLARE_GENERIC_BITWISE_OPERATORS(Generated::ProximityTriggerFlagEnum); + +#include +#include + +namespace ECS::Components +{ + struct ProximityTrigger + { + public: + u32 triggerID; + std::string name; + Generated::ProximityTriggerFlagEnum flags = Generated::ProximityTriggerFlagEnum::None; + robin_hood::unordered_set playersInside; // Entities currently inside the trigger + robin_hood::unordered_set playersEntered; // Entities that just entered + robin_hood::unordered_set playersExited; // Entities that just entered + }; +} \ No newline at end of file diff --git a/Source/Server-Game/Server-Game/ECS/Components/Tags.h b/Source/Server-Game/Server-Game/ECS/Components/Tags.h index 868b17f..a7c0067 100644 --- a/Source/Server-Game/Server-Game/ECS/Components/Tags.h +++ b/Source/Server-Game/Server-Game/ECS/Components/Tags.h @@ -7,4 +7,8 @@ namespace ECS::Tags struct CharacterNeedsDeinitialization {}; struct CharacterNeedsContainerUpdate {}; struct CharacterNeedsDisplayUpdate {}; + + struct ProximityTriggerNeedsInitialization {}; + struct ProximityTriggerIsServerSideOnly {}; + struct ProximityTriggerIsClientSide {}; } \ No newline at end of file diff --git a/Source/Server-Game/Server-Game/ECS/Scheduler.cpp b/Source/Server-Game/Server-Game/ECS/Scheduler.cpp index b8e2aca..a055f21 100644 --- a/Source/Server-Game/Server-Game/ECS/Scheduler.cpp +++ b/Source/Server-Game/Server-Game/ECS/Scheduler.cpp @@ -6,6 +6,7 @@ #include "Server-Game/ECS/Systems/CharacterUpdate.h" #include "Server-Game/ECS/Systems/DatabaseSetup.h" #include "Server-Game/ECS/Systems/NetworkConnection.h" +#include "Server-Game/ECS/Systems/ProximityTrigger.h" #include "Server-Game/ECS/Systems/Replication.h" #include "Server-Game/ECS/Systems/UpdatePower.h" #include "Server-Game/ECS/Systems/UpdateScripts.h" @@ -30,6 +31,7 @@ namespace ECS Systems::CharacterLoginHandler::Init(registry); Systems::CharacterInitialization::Init(registry); Systems::CharacterUpdate::Init(registry); + Systems::ProximityTrigger::Init(registry); Systems::UpdatePower::Init(registry); Systems::UpdateSpell::Init(registry); Systems::UpdateScripts::Init(registry); @@ -43,6 +45,7 @@ namespace ECS Systems::CharacterLoginHandler::Update(registry, deltaTime); Systems::CharacterInitialization::Update(registry, deltaTime); Systems::CharacterUpdate::Update(registry, deltaTime); + Systems::ProximityTrigger::Update(registry, deltaTime); Systems::UpdatePower::Update(registry, deltaTime); Systems::UpdateSpell::Update(registry, deltaTime); Systems::Replication::Update(registry, deltaTime); diff --git a/Source/Server-Game/Server-Game/ECS/Singletons/GameCache.h b/Source/Server-Game/Server-Game/ECS/Singletons/GameCache.h index 4cbf59e..f64f2e0 100644 --- a/Source/Server-Game/Server-Game/ECS/Singletons/GameCache.h +++ b/Source/Server-Game/Server-Game/ECS/Singletons/GameCache.h @@ -25,6 +25,7 @@ namespace ECS Database::CurrencyTables currencyTables; Database::ItemTables itemTables; Database::CharacterTables characterTables; + Database::ProximityTriggersTables proximityTriggerTables; }; } diff --git a/Source/Server-Game/Server-Game/ECS/Singletons/ProximityTriggers.h b/Source/Server-Game/Server-Game/ECS/Singletons/ProximityTriggers.h new file mode 100644 index 0000000..72e2441 --- /dev/null +++ b/Source/Server-Game/Server-Game/ECS/Singletons/ProximityTriggers.h @@ -0,0 +1,22 @@ +#pragma once +#include + +#include +#include +#include + +namespace ECS +{ + namespace Singletons + { + struct ProximityTriggers + { + public: + robin_hood::unordered_map triggerIDToEntity; + robin_hood::unordered_set triggerIDsToDestroy; + + robin_hood::unordered_set activeTriggers; + RTree activeTriggersVisTree; + }; + } +} \ No newline at end of file diff --git a/Source/Server-Game/Server-Game/ECS/Systems/CharacterInitialization.cpp b/Source/Server-Game/Server-Game/ECS/Systems/CharacterInitialization.cpp index cd28529..d433ca0 100644 --- a/Source/Server-Game/Server-Game/ECS/Systems/CharacterInitialization.cpp +++ b/Source/Server-Game/Server-Game/ECS/Systems/CharacterInitialization.cpp @@ -1,11 +1,13 @@ #include "CharacterInitialization.h" +#include "Server-Game/ECS/Components/AABB.h" #include "Server-Game/ECS/Components/CastInfo.h" #include "Server-Game/ECS/Components/CharacterInfo.h" #include "Server-Game/ECS/Components/DisplayInfo.h" #include "Server-Game/ECS/Components/NetInfo.h" #include "Server-Game/ECS/Components/ObjectInfo.h" #include "Server-Game/ECS/Components/PlayerContainers.h" +#include "Server-Game/ECS/Components/ProximityTrigger.h" #include "Server-Game/ECS/Components/Tags.h" #include "Server-Game/ECS/Components/TargetInfo.h" #include "Server-Game/ECS/Components/UnitStatsComponent.h" @@ -134,6 +136,9 @@ namespace ECS::Systems transform.mapID = databaseResult[0][10].as(); transform.position = vec3(databaseResult[0][11].as(), databaseResult[0][12].as(), databaseResult[0][13].as()); + auto& aabb = registry.emplace(entity); + aabb.extents = vec3(3.0f, 5.0f, 2.0f); // TODO: Create proper data for this + f32 orientation = databaseResult[0][14].as(); transform.rotation = quat(vec3(0.0f, orientation, 0.0f)); @@ -231,8 +236,27 @@ namespace ECS::Systems } } } + } + registry.emplace_or_replace(entity); + + for (auto& trigger : gameCache.proximityTriggerTables.triggers) + { + if (!gameCache.proximityTriggerTables.triggerIDToEntity.contains(trigger.id)) + continue; + + entt::entity triggerEntity = gameCache.proximityTriggerTables.triggerIDToEntity[trigger.id]; + if (registry.all_of(triggerEntity)) + continue; + + auto& triggerTransform = registry.get(triggerEntity); + auto& triggerAABB = registry.get(triggerEntity); + auto& proximityTrigger = registry.get(triggerEntity); + + if (triggerTransform.mapID != transform.mapID) + continue; - registry.emplace_or_replace(entity); + if (!Util::MessageBuilder::ProximityTrigger::BuildProximityTriggerCreate(buffer, trigger.id, trigger.name, trigger.flags, triggerTransform.mapID, triggerTransform.position, triggerAABB.extents)) + continue; } if (failed) diff --git a/Source/Server-Game/Server-Game/ECS/Systems/DatabaseSetup.cpp b/Source/Server-Game/Server-Game/ECS/Systems/DatabaseSetup.cpp index 17d8a35..40dd0b7 100644 --- a/Source/Server-Game/Server-Game/ECS/Systems/DatabaseSetup.cpp +++ b/Source/Server-Game/Server-Game/ECS/Systems/DatabaseSetup.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -70,11 +71,13 @@ namespace ECS::Systems Database::Util::Currency::InitCurrencyTablesPreparedStatements(characterConnection); Database::Util::Item::Loading::InitItemTablesPreparedStatements(characterConnection); Database::Util::Character::Loading::InitCharacterTablesPreparedStatements(characterConnection); + Database::Util::ProximityTrigger::Loading::InitProximityTriggersTablesPreparedStatements(characterConnection); Database::Util::Permission::LoadPermissionTables(characterConnection, gameCache.permissionTables); Database::Util::Currency::LoadCurrencyTables(characterConnection, gameCache.currencyTables); Database::Util::Item::Loading::LoadItemTables(characterConnection, gameCache.itemTables); Database::Util::Character::Loading::LoadCharacterTables(characterConnection, gameCache.characterTables, gameCache.itemTables); + Database::Util::ProximityTrigger::Loading::LoadProximityTriggersTables(characterConnection, gameCache.proximityTriggerTables); } } diff --git a/Source/Server-Game/Server-Game/ECS/Systems/NetworkConnection.cpp b/Source/Server-Game/Server-Game/ECS/Systems/NetworkConnection.cpp index 9db3bd3..378d759 100644 --- a/Source/Server-Game/Server-Game/ECS/Systems/NetworkConnection.cpp +++ b/Source/Server-Game/Server-Game/ECS/Systems/NetworkConnection.cpp @@ -1,23 +1,28 @@ #include "NetworkConnection.h" #include "Server-Game/Application/EnttRegistries.h" +#include "Server-Game/ECS/Components/AABB.h" #include "Server-Game/ECS/Components/CastInfo.h" #include "Server-Game/ECS/Components/CharacterInfo.h" #include "Server-Game/ECS/Components/DisplayInfo.h" #include "Server-Game/ECS/Components/NetInfo.h" #include "Server-Game/ECS/Components/ObjectInfo.h" #include "Server-Game/ECS/Components/PlayerContainers.h" +#include "Server-Game/ECS/Components/ProximityTrigger.h" #include "Server-Game/ECS/Components/TargetInfo.h" #include "Server-Game/ECS/Components/Transform.h" #include "Server-Game/ECS/Components/Tags.h" #include "Server-Game/ECS/Components/UnitStatsComponent.h" #include "Server-Game/ECS/Singletons/GameCache.h" #include "Server-Game/ECS/Singletons/NetworkState.h" -#include "Server-Game/ECS/Singletons/WorldState.h" +#include "Server-Game/ECS/Singletons/ProximityTriggers.h" #include "Server-Game/ECS/Singletons/TimeState.h" +#include "Server-Game/ECS/Singletons/WorldState.h" +#include "Server-Game/ECS/Util/CollisionUtil.h" #include "Server-Game/ECS/Util/ContainerUtil.h" #include "Server-Game/ECS/Util/GridUtil.h" #include "Server-Game/ECS/Util/MessageBuilderUtil.h" +#include "Server-Game/ECS/Util/ProximityTriggerUtil.h" #include "Server-Game/ECS/Util/UnitUtil.h" #include "Server-Game/ECS/Util/Cache/CacheUtil.h" #include "Server-Game/ECS/Util/Network/NetworkUtil.h" @@ -30,6 +35,7 @@ #include #include #include +#include #include #include @@ -663,6 +669,93 @@ namespace ECS::Systems return true; } + bool HandleOnCheatTriggerCreate(entt::registry* registry, Network::SocketID socketID, entt::entity entity, Network::Message& message) + { + std::string name; + u16 flags; + u16 mapID; + vec3 position; + vec3 extents; + + if (!message.buffer->GetString(name)) + return false; + + if (!message.buffer->GetU16(flags)) + return false; + + if (!message.buffer->GetU16(mapID)) + return false; + + if (!message.buffer->Get(position)) + return false; + + if (!message.buffer->Get(extents)) + return false; + + auto& ctx = registry->ctx(); + auto& networkState = ctx.get(); + auto& gameCache = ctx.get(); + + auto conn = gameCache.dbController->GetConnection(::Database::DBType::Character); + if (!conn) + return true; + + auto transaction = conn->NewTransaction(); + + u32 triggerID; + if (Database::Util::ProximityTrigger::ProximityTriggerCreate(transaction, name, flags, mapID, position, extents, triggerID)) + { + transaction.commit(); + + entt::entity triggerEntity = registry->create(); + + auto& transform = registry->emplace(triggerEntity); + transform.mapID = mapID; + transform.position = position; + + registry->emplace(triggerEntity, vec3(0, 0, 0), extents); + auto& proximityTrigger = registry->emplace(triggerEntity); + proximityTrigger.triggerID = triggerID; + proximityTrigger.name = name; + proximityTrigger.flags = static_cast(flags); + registry->emplace(triggerEntity); + + gameCache.proximityTriggerTables.triggerIDToEntity[triggerID] = triggerEntity; + } + + return true; + } + + bool HandleOnCheatTriggerDestroy(entt::registry* registry, Network::SocketID socketID, entt::entity entity, Network::Message& message) + { + u32 triggerID; + + if (!message.buffer->GetU32(triggerID)) + return false; + + auto& ctx = registry->ctx(); + auto& networkState = ctx.get(); + auto& gameCache = ctx.get(); + auto& proximityTriggers = ctx.get(); + + if (!gameCache.proximityTriggerTables.triggerIDToEntity.contains(triggerID)) + return true; + + auto conn = gameCache.dbController->GetConnection(::Database::DBType::Character); + if (!conn) + return true; + + auto transaction = conn->NewTransaction(); + if (Database::Util::ProximityTrigger::ProximityTriggerDelete(transaction, triggerID)) + { + transaction.commit(); + + proximityTriggers.triggerIDsToDestroy.insert(triggerID); + } + + return true; + } + bool HandleOnSendCheatCommand(Network::SocketID socketID, Network::Message& message) { entt::registry* registry = ServiceLocator::GetEnttRegistries()->gameRegistry; @@ -771,6 +864,16 @@ namespace ECS::Systems return HandleOnCheatRemoveItem(registry, socketID, entity, message); } + case Network::CheatCommands::TriggerAdd: + { + return HandleOnCheatTriggerCreate(registry, socketID, entity, message); + } + + case Network::CheatCommands::TriggerRemove: + { + return HandleOnCheatTriggerDestroy(registry, socketID, entity, message); + } + default: break; } @@ -1039,6 +1142,41 @@ namespace ECS::Systems return true; } + bool HandleOnTriggerEnter(Network::SocketID socketID, Network::Message& message) + { + u32 triggerID; + + if (!message.buffer->GetU32(triggerID)) + return false; + + entt::registry* registry = ServiceLocator::GetEnttRegistries()->gameRegistry; + auto& ctx = registry->ctx(); + auto& networkState = ctx.get(); + auto& gameCache = ctx.get(); + + entt::entity triggerEntity; + if (!gameCache.proximityTriggerTables.triggerIDToEntity.contains(triggerID)) + return false; + + triggerEntity = gameCache.proximityTriggerTables.triggerIDToEntity[triggerID]; + + entt::entity playerEntity; + if (!Util::Network::GetEntityIDFromSocketID(networkState, socketID, playerEntity)) + return false; + + auto& playerTransform = registry->get(playerEntity); + auto& playerAABB = registry->get(playerEntity); + auto& triggerTransform = registry->get(triggerEntity); + auto& triggerAABB = registry->get(triggerEntity); + + if (!Util::Collision::Overlaps(playerTransform, playerAABB, triggerTransform, triggerAABB)) + return false; // This might be a malicious client sending fake packets, do we want to do something about this? + + Util::ProximityTrigger::AddPlayerToTrigger(*registry, triggerEntity, playerEntity); + + return true; + } + void NetworkConnection::Init(entt::registry& registry) { entt::registry::context& ctx = registry.ctx(); @@ -1063,6 +1201,8 @@ namespace ECS::Systems networkState.gameMessageRouter->SetMessageHandler(Network::GameOpcode::Client_LocalRequestSpellCast, Network::GameMessageHandler(Network::ConnectionStatus::Connected, 0u, -1, &HandleOnRequestSpellCast)); + networkState.gameMessageRouter->SetMessageHandler(Network::GameOpcode::Client_TriggerEnter, Network::GameMessageHandler(Network::ConnectionStatus::Connected, 0u, -1, &HandleOnTriggerEnter)); + // Bind to IP/Port std::string ipAddress = "0.0.0.0"; diff --git a/Source/Server-Game/Server-Game/ECS/Systems/ProximityTrigger.cpp b/Source/Server-Game/Server-Game/ECS/Systems/ProximityTrigger.cpp new file mode 100644 index 0000000..4131f50 --- /dev/null +++ b/Source/Server-Game/Server-Game/ECS/Systems/ProximityTrigger.cpp @@ -0,0 +1,240 @@ +#include "ProximityTrigger.h" + +#include "Server-Game/ECS/Components/AABB.h" +#include "Server-Game/ECS/Components/CastInfo.h" +#include "Server-Game/ECS/Components/ProximityTrigger.h" +#include "Server-Game/ECS/Components/Tags.h" +#include "Server-Game/ECS/Components/Transform.h" +#include "Server-Game/ECS/Singletons/GameCache.h" +#include "Server-Game/ECS/Singletons/NetworkState.h" +#include "Server-Game/ECS/Singletons/ProximityTriggers.h" +#include "Server-Game/ECS/Singletons/WorldState.h" +#include "Server-Game/ECS/Util/CollisionUtil.h" +#include "Server-Game/ECS/Util/GridUtil.h" +#include "Server-Game/ECS/Util/MessageBuilderUtil.h" +#include "Server-Game/ECS/Util/Network/NetworkUtil.h" +#include "Server-Game/Util/ServiceLocator.h" + +#include + +#include + +#include +#include + +namespace ECS::Systems +{ + void ProximityTrigger::Init(entt::registry& registry) + { + auto& ctx = registry.ctx(); + + auto& gameCache = ctx.get(); + auto& proximityTriggers = ctx.emplace(); + + for(const auto& trigger : gameCache.proximityTriggerTables.triggers) + { + entt::entity triggerEntity = registry.create(); + + auto& transform = registry.emplace(triggerEntity); + transform.mapID = trigger.mapID; + transform.position = trigger.position; + + registry.emplace(triggerEntity, vec3(0, 0, 0), trigger.extents); + auto& proximityTrigger = registry.emplace(triggerEntity); + proximityTrigger.triggerID = trigger.id; + proximityTrigger.name = trigger.name; + proximityTrigger.flags = trigger.flags; + registry.emplace(triggerEntity); + + gameCache.proximityTriggerTables.triggerIDToEntity[trigger.id] = triggerEntity; + } + } + + void OnEnter(Components::ProximityTrigger& trigger, entt::entity playerEntity) + { + NC_LOG_INFO("Trigger '{}' entered!", trigger.name); + } + + void OnExit(Components::ProximityTrigger& trigger, entt::entity playerEntity) + { + NC_LOG_INFO("Trigger '{}' exited!", trigger.name); + } + + void OnStay(Components::ProximityTrigger& trigger, entt::entity playerEntity) + { + NC_LOG_INFO("Trigger '{}' stayed!", trigger.name); + } + + void ProximityTrigger::Update(entt::registry& registry, f32 deltaTime) + { + ZoneScopedN("ECS::ProximityTriggers"); + + auto& ctx = registry.ctx(); + + auto& gameCache = ctx.get(); + auto& networkState = ctx.get(); + auto& proximityTriggers = ctx.get(); + auto& worldState = ctx.get(); + + // Initialize any new triggers that need to be created + auto proximityTriggerNeedsinitializationView = registry.view(); + proximityTriggerNeedsinitializationView.each([®istry, &worldState, &networkState, &proximityTriggers](entt::entity entity, Components::Transform& transform, Components::AABB& aabb, Components::ProximityTrigger& trigger) + { + World& world = worldState.GetWorld(transform.mapID); + + bool isServerSideOnly = (trigger.flags & Generated::ProximityTriggerFlagEnum::IsServerSideOnly) != Generated::ProximityTriggerFlagEnum::None; + + if (isServerSideOnly) + { + registry.emplace(entity); + + // Calculate world AABB + Components::WorldAABB worldAABB; + worldAABB.min = transform.position + aabb.centerPos - aabb.extents; + worldAABB.max = transform.position + aabb.centerPos + aabb.extents; + + // Add server side only triggers to the activeTriggers R-tree + proximityTriggers.activeTriggersVisTree.Insert(reinterpret_cast(&worldAABB.min), reinterpret_cast(&worldAABB.max), trigger.triggerID); + proximityTriggers.activeTriggers.insert(trigger.triggerID); + } + else + { + registry.emplace(entity); + + // Replicate to the clients if this is a client side trigger + std::shared_ptr triggerCreateMessage = Bytebuffer::Borrow<64>(); + if (!Util::MessageBuilder::ProximityTrigger::BuildProximityTriggerCreate(triggerCreateMessage, trigger.triggerID, trigger.name, trigger.flags, transform.mapID, transform.position, aabb.extents)) + return; + + world.GetPlayersInRadius(transform.position.x, transform.position.z, 100000.0f, [&](const GameDefine::ObjectGuid& guid) -> bool + { + entt::entity playerEntity = world.GetEntity(guid); + + Network::SocketID socketID; + if (!Util::Network::GetSocketIDFromCharacterID(networkState, guid.GetCounter(), socketID)) + return true; + + networkState.server->SendPacket(socketID, triggerCreateMessage); + return true; + }); + } + }); + registry.clear(); + + // Destroy any triggers that need to be removed + for(auto triggerID : proximityTriggers.triggerIDsToDestroy) + { + if (!gameCache.proximityTriggerTables.triggerIDToEntity.contains(triggerID)) + continue; + + entt::entity triggerEntity = gameCache.proximityTriggerTables.triggerIDToEntity[triggerID]; + + auto& transform = registry.get(triggerEntity); + World& world = worldState.GetWorld(transform.mapID); + + std::shared_ptr triggerDestroyMessage = Bytebuffer::Borrow<32>(); + if (!Util::MessageBuilder::ProximityTrigger::BuildProximityTriggerDelete(triggerDestroyMessage, triggerID)) + continue; + + gameCache.proximityTriggerTables.triggerIDToEntity.erase(triggerID); + proximityTriggers.activeTriggers.erase(triggerID); + + auto& proximityTrigger = registry.get(triggerEntity); + bool isServerSideOnly = (proximityTrigger.flags & Generated::ProximityTriggerFlagEnum::IsServerSideOnly) == Generated::ProximityTriggerFlagEnum::IsServerSideOnly; + + world.GetPlayersInRadius(transform.position.x, transform.position.z, 100000.0f, [&](const GameDefine::ObjectGuid& guid) -> bool + { + entt::entity playerEntity = world.GetEntity(guid); + + if (isServerSideOnly && proximityTrigger.playersInside.contains(playerEntity)) + { + OnExit(proximityTrigger, playerEntity); + } + + Network::SocketID socketID; + if (!Util::Network::GetSocketIDFromCharacterID(networkState, guid.GetCounter(), socketID)) + return true; + + networkState.server->SendPacket(socketID, triggerDestroyMessage); + return true; + }); + + registry.destroy(triggerEntity); + } + proximityTriggers.triggerIDsToDestroy.clear(); + + // Call client side trigger events + // For these we only care about OnEnter + auto clientSideTriggerView = registry.view(); + clientSideTriggerView.each([®istry, &worldState](entt::entity triggerEntity, Components::Transform& triggerTransform, Components::AABB& triggerAABB, Components::ProximityTrigger& trigger) + { + // Loop over players that just entered the trigger and call OnEnter + for(auto playerEntity : trigger.playersEntered) + { + if (!registry.valid(playerEntity)) + continue; + OnEnter(trigger, playerEntity); + trigger.playersInside.insert(playerEntity); + } + trigger.playersEntered.clear(); + }); + + // Call server side trigger events + robin_hood::unordered_set playersToRemove; + playersToRemove.reserve(1024); + + for (u32 triggerID : proximityTriggers.activeTriggers) + { + entt::entity triggerEntity = gameCache.proximityTriggerTables.triggerIDToEntity[triggerID]; + if (!registry.valid(triggerEntity)) + continue; + + auto& triggerTransform = registry.get(triggerEntity); + auto& triggerAABB = registry.get(triggerEntity); + auto& proximityTrigger = registry.get(triggerEntity); + + World& world = worldState.GetWorld(triggerTransform.mapID); + + vec4 minMaxRect = vec4( + triggerTransform.position.x + triggerAABB.centerPos.x - triggerAABB.extents.x, + triggerTransform.position.z + triggerAABB.centerPos.z - triggerAABB.extents.z, + triggerTransform.position.x + triggerAABB.centerPos.x + triggerAABB.extents.x, + triggerTransform.position.z + triggerAABB.centerPos.z + triggerAABB.extents.z + ); + + playersToRemove = proximityTrigger.playersInside; + + world.GetPlayersInRect(minMaxRect, [&](const GameDefine::ObjectGuid& guid) -> bool + { + entt::entity playerEntity = world.GetEntity(guid); + + Network::SocketID socketID; + if (!Util::Network::GetSocketIDFromCharacterID(networkState, guid.GetCounter(), socketID)) + return true; + + if (proximityTrigger.playersInside.contains(playerEntity)) + { + OnStay(proximityTrigger, playerEntity); + playersToRemove.erase(playerEntity); + } + else + { + OnEnter(proximityTrigger, playerEntity); + proximityTrigger.playersInside.insert(playerEntity); + } + + return true; + }); + + // Loop over players that have exited the trigger and call OnExit + for (auto playerEntity : playersToRemove) + { + if (!registry.valid(playerEntity)) + continue; + + OnExit(proximityTrigger, playerEntity); + proximityTrigger.playersInside.erase(playerEntity); + } + } + } +} \ No newline at end of file diff --git a/Source/Server-Game/Server-Game/ECS/Systems/ProximityTrigger.h b/Source/Server-Game/Server-Game/ECS/Systems/ProximityTrigger.h new file mode 100644 index 0000000..6f28a50 --- /dev/null +++ b/Source/Server-Game/Server-Game/ECS/Systems/ProximityTrigger.h @@ -0,0 +1,13 @@ +#pragma once +#include +#include + +namespace ECS::Systems +{ + class ProximityTrigger + { + public: + static void Init(entt::registry& registry); + static void Update(entt::registry& registry, f32 deltaTime); + }; +} \ No newline at end of file diff --git a/Source/Server-Game/Server-Game/ECS/Util/CollisionUtil.cpp b/Source/Server-Game/Server-Game/ECS/Util/CollisionUtil.cpp new file mode 100644 index 0000000..5b66356 --- /dev/null +++ b/Source/Server-Game/Server-Game/ECS/Util/CollisionUtil.cpp @@ -0,0 +1,57 @@ +#include "CollisionUtil.h" + +#include "Server-Game/ECS/Components/AABB.h" +#include "Server-Game/ECS/Components/Transform.h" + +#include +#include +#include + +namespace ECS::Util::Collision +{ + // Convert a local-space AABB under TRS to a world-space (min,max) AABB. + // Uses worldHalf = |R*S| * localHalf + inline void LocalAABBToWorldAABB(const Components::Transform& t, const Components::AABB& local, vec3& outMin, vec3& outMax) + { + const quat q = glm::normalize(t.rotation); + const mat3x3 R = glm::mat3_cast(q); + + // Guard against degenerate scale to avoid infinities on crazy pipelines + const vec3 s = glm::max(t.scale, glm::vec3(1e-6f)); + const mat3x3 S = glm::mat3(glm::vec3(s.x, 0, 0), vec3(0, s.y, 0), vec3(0, 0, s.z)); + const mat3x3 M = R * S; + + const vec3 worldCenter = t.position + M * local.centerPos; + + // |M| as "componentwise abs on columns" + const mat3x3 A = mat3x3(glm::abs(M[0]), glm::abs(M[1]), glm::abs(M[2])); + const vec3 worldHalf = A * local.extents; + + outMin = worldCenter - worldHalf; + outMax = worldCenter + worldHalf; + } + + // Point-in-AABB + bool IsInside(const Components::Transform& pointTransform, const Components::Transform& boxTransform, const Components::AABB& boxLocalAABB) + { + vec3 minW, maxW; + LocalAABBToWorldAABB(boxTransform, boxLocalAABB, minW, maxW); + + const vec3 p = pointTransform.position; + return (p.x >= minW.x && p.x <= maxW.x) && + (p.y >= minW.y && p.y <= maxW.y) && + (p.z >= minW.z && p.z <= maxW.z); + } + + // AABB-vs-AABB overlap (both AABBs defined in their own local spaces) + bool Overlaps(const Components::Transform& aT, const Components::AABB& aLocal, const Components::Transform& bT, const Components::AABB& bLocal) + { + vec3 aMin, aMax, bMin, bMax; + LocalAABBToWorldAABB(aT, aLocal, aMin, aMax); + LocalAABBToWorldAABB(bT, bLocal, bMin, bMax); + + return (aMin.x <= bMax.x && aMax.x >= bMin.x) && + (aMin.y <= bMax.y && aMax.y >= bMin.y) && + (aMin.z <= bMax.z && aMax.z >= bMin.z); + } +} \ No newline at end of file diff --git a/Source/Server-Game/Server-Game/ECS/Util/CollisionUtil.h b/Source/Server-Game/Server-Game/ECS/Util/CollisionUtil.h new file mode 100644 index 0000000..23ebfce --- /dev/null +++ b/Source/Server-Game/Server-Game/ECS/Util/CollisionUtil.h @@ -0,0 +1,17 @@ +#pragma once +#include + +namespace ECS +{ + namespace Components + { + struct Transform; + struct AABB; + } +} + +namespace ECS::Util::Collision +{ + bool IsInside(const Components::Transform& pointTransform, const Components::Transform& boxTransform, const Components::AABB& boxLocalAABB); + bool Overlaps(const Components::Transform& aT, const Components::AABB& aLocal, const Components::Transform& bT, const Components::AABB& bLocal); +} \ No newline at end of file diff --git a/Source/Server-Game/Server-Game/ECS/Util/MessageBuilderUtil.cpp b/Source/Server-Game/Server-Game/ECS/Util/MessageBuilderUtil.cpp index 0909a57..fd10514 100644 --- a/Source/Server-Game/Server-Game/ECS/Util/MessageBuilderUtil.cpp +++ b/Source/Server-Game/Server-Game/ECS/Util/MessageBuilderUtil.cpp @@ -368,6 +368,34 @@ namespace ECS::Util::MessageBuilder } } + namespace ProximityTrigger + { + bool BuildProximityTriggerCreate(std::shared_ptr& buffer, u32 triggerID, const std::string& name, Generated::ProximityTriggerFlagEnum flags, u16 mapID, const vec3& position, const vec3& extents) + { + bool result = CreatePacket(buffer, Network::GameOpcode::Server_TriggerCreate, [&, triggerID, mapID]() + { + buffer->PutU32(triggerID); + buffer->PutString(name); + buffer->Put(flags); + buffer->PutU16(mapID); + buffer->Put(position); + buffer->Put(extents); + }); + + return result; + } + + bool BuildProximityTriggerDelete(std::shared_ptr& buffer, u32 triggerID) + { + bool result = CreatePacket(buffer, Network::GameOpcode::Server_TriggerDestroy, [&buffer, triggerID]() + { + buffer->PutU32(triggerID); + }); + + return result; + } + } + namespace Cheat { bool BuildCheatCommandResultMessage(std::shared_ptr& buffer, u8 result, const std::string& response) @@ -462,4 +490,4 @@ namespace ECS::Util::MessageBuilder return BuildCheatCommandResultMessage(buffer, result, *resultString); } } -} +} \ No newline at end of file diff --git a/Source/Server-Game/Server-Game/ECS/Util/MessageBuilderUtil.h b/Source/Server-Game/Server-Game/ECS/Util/MessageBuilderUtil.h index 5d6163d..62227c3 100644 --- a/Source/Server-Game/Server-Game/ECS/Util/MessageBuilderUtil.h +++ b/Source/Server-Game/Server-Game/ECS/Util/MessageBuilderUtil.h @@ -7,9 +7,11 @@ #include #include -#include "Gameplay/GameDefine.h" -#include "Gameplay/Network/Define.h" -#include "Gameplay/Network/Opcode.h" +#include +#include +#include + +#include #include @@ -84,6 +86,12 @@ namespace ECS bool BuildRessurectedMessage(std::shared_ptr& buffer, GameDefine::ObjectGuid sourceGuid, GameDefine::ObjectGuid targetGuid); } + namespace ProximityTrigger + { + bool BuildProximityTriggerCreate(std::shared_ptr& buffer, u32 triggerID, const std::string& name, Generated::ProximityTriggerFlagEnum flags, u16 mapID, const vec3& position, const vec3& extents); + bool BuildProximityTriggerDelete(std::shared_ptr& buffer, u32 triggerID); + } + namespace Cheat { bool BuildCheatCommandResultMessage(std::shared_ptr& buffer, u8 result, const std::string& response); diff --git a/Source/Server-Game/Server-Game/ECS/Util/ProximityTriggerUtil.cpp b/Source/Server-Game/Server-Game/ECS/Util/ProximityTriggerUtil.cpp new file mode 100644 index 0000000..4f47bb2 --- /dev/null +++ b/Source/Server-Game/Server-Game/ECS/Util/ProximityTriggerUtil.cpp @@ -0,0 +1,23 @@ +#include "ProximityTriggerUtil.h" + +#include "Server-Game/ECS/Components/ProximityTrigger.h" +#include "Server-Game/ECS/Components/Tags.h" + +namespace ECS::Util::ProximityTrigger +{ + bool AddPlayerToTrigger(entt::registry& registry, entt::entity triggerEntity, entt::entity playerEntity) + { + auto& ProximityTrigger = registry.get(triggerEntity); + ProximityTrigger.playersEntered.insert(playerEntity); + + return true; + } + + bool RemovePlayerFromTrigger(entt::registry& registry, entt::entity triggerEntity, entt::entity playerEntity) + { + auto& ProximityTrigger = registry.get(triggerEntity); + ProximityTrigger.playersExited.insert(playerEntity); + + return true; + } +} \ No newline at end of file diff --git a/Source/Server-Game/Server-Game/ECS/Util/ProximityTriggerUtil.h b/Source/Server-Game/Server-Game/ECS/Util/ProximityTriggerUtil.h new file mode 100644 index 0000000..07c56e1 --- /dev/null +++ b/Source/Server-Game/Server-Game/ECS/Util/ProximityTriggerUtil.h @@ -0,0 +1,19 @@ +#pragma once +#include + +#include + +namespace ECS +{ + namespace Components + { + struct Transform; + struct AABB; + } +} + +namespace ECS::Util::ProximityTrigger +{ + bool AddPlayerToTrigger(entt::registry& registry, entt::entity triggerEntity, entt::entity playerEntity); + bool RemovePlayerFromTrigger(entt::registry& registry, entt::entity triggerEntity, entt::entity playerEntity); +} \ No newline at end of file diff --git a/Submodules/Engine b/Submodules/Engine index 07d90e8..45479e2 160000 --- a/Submodules/Engine +++ b/Submodules/Engine @@ -1 +1 @@ -Subproject commit 07d90e806a8dfc0e5e218005d3c3f227039b80f8 +Subproject commit 45479e2b9b18d721b3ba3bc1a5d14b55fad7c5d4