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