diff --git a/code/components/citizen-scripting-core/include/ColshapeManager.h b/code/components/citizen-scripting-core/include/ColshapeManager.h new file mode 100644 index 0000000000..6541cafbf5 --- /dev/null +++ b/code/components/citizen-scripting-core/include/ColshapeManager.h @@ -0,0 +1,368 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ComponentExport.h" + +namespace fx::colshape +{ +enum class ColShapeType +{ + Circle, + Cuboid, + Cylinder, + Rectangle, + Sphere, + Polygon, +}; + + +struct Vec3 +{ + float x = 0.f, y = 0.f, z = 0.f; +}; + +struct Vec2 +{ + float x = 0.f, y = 0.f; +}; + +struct EntitySample +{ + int handle; + int type; + float x, y, z; +}; + +struct ColShape +{ + int id = -1; + + // bumped every time this id is reused, so tracked (id, generation) pairs from a + // previous shape don't alias a new shape that happens to reuse the freed id + uint32_t generation = 0; + + ColShapeType type; + + // resource that created this shape, so it can be dropped when that resource stops + std::string owner; + + Vec3 pos1; + float radius = 0.f; + float radiusSq = 0.f; + float height = 0.f; + float dimW = 0.f; + float dimD = 0.f; + float heading = 0.f; + float cosHeading = 1.f; + float sinHeading = 0.f; + float minZ = 0.f, maxZ = 0.f; + std::vector points; + + float minX = 0.f, maxX = 0.f, minY = 0.f, maxY = 0.f; + + // bit per NetObjEntityType; a set bit excludes that type from detection + uint64_t disabledEntityTypes = 0; + + explicit ColShape(ColShapeType t) + : type(t) + { + } + + void SetEntityType(int type, bool value) + { + if (type < 0 || type >= 64) + { + return; + } + + if (value) + { + disabledEntityTypes &= ~(1ull << type); + } + else + { + disabledEntityTypes |= (1ull << type); + } + } + + bool AcceptsEntityType(int type) const + { + if (type < 0 || type >= 64) + { + return true; + } + + return (disabledEntityTypes & (1ull << type)) == 0; + } + + void ComputeBounds() + { + radiusSq = radius * radius; + + // abs() so a negative radius/dimension can't invert the AABB + switch (type) + { + case ColShapeType::Circle: + case ColShapeType::Cylinder: + case ColShapeType::Sphere: + { + float r = std::fabs(radius); + minX = pos1.x - r; + maxX = pos1.x + r; + minY = pos1.y - r; + maxY = pos1.y + r; + break; + } + case ColShapeType::Cuboid: + { + float hw = std::fabs(dimW) * 0.5f; + float hd = std::fabs(dimD) * 0.5f; + minX = pos1.x - hw; + maxX = pos1.x + hw; + minY = pos1.y - hd; + maxY = pos1.y + hd; + break; + } + case ColShapeType::Rectangle: + { + cosHeading = std::cos(heading); + sinHeading = std::sin(heading); + + // exact rotated-rect AABB (tighter than a circumcircle bound) + float hw = std::fabs(dimW) * 0.5f; + float hd = std::fabs(dimD) * 0.5f; + float ex = std::fabs(cosHeading) * hw + std::fabs(sinHeading) * hd; + float ey = std::fabs(sinHeading) * hw + std::fabs(cosHeading) * hd; + minX = pos1.x - ex; + maxX = pos1.x + ex; + minY = pos1.y - ey; + maxY = pos1.y + ey; + break; + } + case ColShapeType::Polygon: + { + if (points.empty()) + { + minX = maxX = pos1.x; + minY = maxY = pos1.y; + break; + } + + minX = maxX = points[0].x; + minY = maxY = points[0].y; + for (const auto& p : points) + { + minX = std::min(minX, p.x); + maxX = std::max(maxX, p.x); + minY = std::min(minY, p.y); + maxY = std::max(maxY, p.y); + } + break; + } + } + } + + bool ContainsPoint(float px, float py, float pz) const + { + switch (type) + { + case ColShapeType::Circle: + { + float dx = px - pos1.x, dy = py - pos1.y; + return dx * dx + dy * dy <= radiusSq; + } + case ColShapeType::Sphere: + { + float dx = px - pos1.x, dy = py - pos1.y, dz = pz - pos1.z; + return dx * dx + dy * dy + dz * dz <= radiusSq; + } + case ColShapeType::Cylinder: + { + float dz = pz - pos1.z; + if (dz < -height * 0.5f || dz > height * 0.5f) + { + return false; + } + + float dx = px - pos1.x, dy = py - pos1.y; + return dx * dx + dy * dy <= radiusSq; + } + case ColShapeType::Cuboid: + { + return px >= pos1.x - dimW * 0.5f && px <= pos1.x + dimW * 0.5f + && py >= pos1.y - dimD * 0.5f && py <= pos1.y + dimD * 0.5f + && pz >= pos1.z - height * 0.5f && pz <= pos1.z + height * 0.5f; + } + case ColShapeType::Rectangle: + { + float dx = px - pos1.x, dy = py - pos1.y; + float lx = dx * cosHeading + dy * sinHeading; + float ly = -dx * sinHeading + dy * cosHeading; + return lx >= -dimW * 0.5f && lx <= dimW * 0.5f + && ly >= -dimD * 0.5f && ly <= dimD * 0.5f; + } + case ColShapeType::Polygon: + { + if (pz < minZ || pz > maxZ) + { + return false; + } + + return PointInPoly(px, py); + } + } + return false; + } + + bool PointInPoly(float px, float py) const + { + // ray-cast even-odd rule + bool in = false; + size_t n = points.size(); + for (size_t i = 0, j = n - 1; i < n; j = i++) + { + const auto& a = points[i]; + const auto& b = points[j]; + if (((a.y > py) != (b.y > py)) && + (px < (b.x - a.x) * (py - a.y) / (b.y - a.y) + a.x)) + { + in = !in; + } + } + return in; + } +}; + +class ColshapeManager +{ +public: + // generation lets the feed drop an event whose shape id was freed and reused + // between detection and emission + using EventFn = std::function; + + COMPONENT_EXPORT(CITIZEN_SCRIPTING_CORE) static ColshapeManager& Get(); + + int Add(ColShape&& shape); + bool Delete(int id); + + // the returned pointer is only valid while no writer runs. callers on the script + // thread (the native handlers) are safe because Add/Delete/SetEntityType also run + // there; the detection worker calls this under m_mutex (see Update). do not call + // from any other thread without holding m_mutex. + COMPONENT_EXPORT(CITIZEN_SCRIPTING_CORE) ColShape* Find(int id); + COMPONENT_EXPORT(CITIZEN_SCRIPTING_CORE) const ColShape* Find(int id) const; + + // true if id currently maps to a live shape with this exact generation + COMPONENT_EXPORT(CITIZEN_SCRIPTING_CORE) bool IsLive(int id, uint32_t generation) const; + + void SetEntityType(int id, int type, bool value); + void DeleteByOwner(const std::string& owner); + + COMPONENT_EXPORT(CITIZEN_SCRIPTING_CORE) void Update(const std::vector& entities, const EventFn& emit); + +private: + struct BvhNode + { + float minX, minY, maxX, maxY; + + int32_t right = 0; + int32_t start = 0; + int32_t count = 0; + }; + + // self-contained (nodes own their bounds, leaves store shape ids) so it can be + // built on a background thread and swapped in without touching manager state + struct Bvh + { + std::vector nodes; + std::vector ids; + }; + + struct BvhInput + { + int id; + float minX, minY, maxX, maxY; + }; + + static std::shared_ptr BuildBvh(std::vector& input); + static int BuildBvhNode(Bvh& bvh, std::vector& input, int begin, int end); + + template + static void QueryBvh(const Bvh& bvh, float px, float py, const TFn& fn); + + void KickBvhBuild(); + + // an id plus the generation the shape had when it was recorded; the pair detects + // an id that has since been freed and reused for a different shape + struct ShapeRef + { + int id; + uint32_t generation; + + bool operator<(const ShapeRef& o) const { return id < o.id; } + }; + + // sorted by id so the per-tick merge-diff is one pass and enter/exit order is stable + struct EntityInsideState + { + uint64_t epoch = 0; + std::vector shapes; + }; + + int m_nextId = 0; + uint64_t m_epoch = 0; + + // freed ids waiting to be reused (each reuse bumps that id's generation), so the + // id space is recycled instead of exhausting at 65535 under create/delete churn + std::vector m_freeIds; + std::vector m_generations; + + // chunked so growth never relocates existing shapes (Find pointers stay stable); + // ids resolve through a flat id -> slot table (-1 = deleted) + static constexpr size_t kShapeChunkShift = 13; + static constexpr size_t kShapeChunkSize = size_t(1) << kShapeChunkShift; + + std::vector> m_shapes; + size_t m_shapeCount = 0; + std::vector m_idToSlot; + + ColShape& ShapeAt(size_t slot) + { + return m_shapes[slot >> kShapeChunkShift][slot & (kShapeChunkSize - 1)]; + } + + const ColShape& ShapeAt(size_t slot) const + { + return m_shapes[slot >> kShapeChunkShift][slot & (kShapeChunkSize - 1)]; + } + + // (id, bounds) in lockstep with m_shapes, so the BVH snapshot is a vector copy + std::vector m_bounds; + + // shapes newer than the current tree; scanned alongside it so they're detectable + // before the next rebuild + std::vector m_recent; + + // mismatched generations => the tree is stale and a rebuild is due + std::shared_ptr m_bvh; + std::atomic m_bvhGeneration{ 0 }; + std::atomic m_bvhBuiltGeneration{ 0 }; + std::atomic m_bvhBuilding{ false }; + std::atomic m_lastBvhKickMs{ 0 }; + std::atomic m_lastMutationMs{ 0 }; + std::mutex m_bvhSwapMutex; + + std::unordered_map m_entityInside; + mutable std::shared_mutex m_mutex; +}; +} diff --git a/code/components/citizen-scripting-core/src/ColshapeScriptFunctions.cpp b/code/components/citizen-scripting-core/src/ColshapeScriptFunctions.cpp new file mode 100644 index 0000000000..3e4650143a --- /dev/null +++ b/code/components/citizen-scripting-core/src/ColshapeScriptFunctions.cpp @@ -0,0 +1,723 @@ +/* + * Colshape natives (shared client + server). The geometry/registry is pure and shared; + * enter/exit tracking is driven by a per-side entity feed that calls + * ColshapeManager::Update() on a tick and emits onColshapeEnter / onColshapeExit. + */ + +#include "StdInc.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "ColshapeManager.h" + +namespace fx::colshape +{ +static double NowMs() +{ + using namespace std::chrono; + return duration(steady_clock::now().time_since_epoch()).count(); +} + +static std::string CurrentResourceName() +{ + fx::OMPtr runtime; + if (FX_SUCCEEDED(fx::GetCurrentScriptRuntime(&runtime))) + { + if (auto* resource = reinterpret_cast(runtime->GetParentObject())) + { + return resource->GetName(); + } + } + return {}; +} + +// codegen passes an `object` arg as a msgpack (ptr, len) pair: a flat [x1,y1,x2,y2,...] +static void ParsePolygonPoints(fx::ScriptContext& context, int argIdx, std::vector& out) +{ + auto data = context.GetArgument(argIdx); + auto length = context.GetArgument(argIdx + 1); + if (!data || !length) + { + return; + } + + try + { + auto unpacked = msgpack::unpack(data, length); + auto obj = unpacked.get(); + if (obj.type != msgpack::type::ARRAY) + { + return; + } + + auto& arr = obj.via.array; + for (uint32_t i = 0; i + 1 < arr.size; i += 2) + { + out.push_back({ arr.ptr[i].as(), arr.ptr[i + 1].as() }); + } + } + catch (const std::exception&) + { + } +} + +ColshapeManager& ColshapeManager::Get() +{ + static ColshapeManager inst; + return inst; +} + +int ColshapeManager::Add(ColShape&& shape) +{ + // NaN/Inf would break the BVH's nth_element ordering, so reject malformed shapes + if (!std::isfinite(shape.pos1.x) || !std::isfinite(shape.pos1.y) || !std::isfinite(shape.pos1.z) || + !std::isfinite(shape.radius) || !std::isfinite(shape.height) || + !std::isfinite(shape.dimW) || !std::isfinite(shape.dimD) || + !std::isfinite(shape.heading) || !std::isfinite(shape.minZ) || !std::isfinite(shape.maxZ)) + { + return -1; + } + + for (const auto& p : shape.points) + { + if (!std::isfinite(p.x) || !std::isfinite(p.y)) + { + return -1; + } + } + + // negative extents pass the BVH's abs() bounds but make the signed narrow-phase + // test unsatisfiable, so reject them rather than register an undetectable shape + if (shape.radius < 0.f || shape.height < 0.f || shape.dimW < 0.f || shape.dimD < 0.f) + { + return -1; + } + + std::unique_lock lock(m_mutex); + + int id; + if (!m_freeIds.empty()) + { + // reuse a freed id and bump its generation so old (id, generation) refs miss + id = m_freeIds.back(); + m_freeIds.pop_back(); + m_generations[id]++; + } + else + { + // enhanced caps ids at 16 bits; we recycle freed ids, so this only trips if + // 65536 shapes are live at once + if (m_nextId > 0xFFFF) + { + return -1; + } + id = m_nextId++; + m_generations.push_back(0); + m_idToSlot.push_back(-1); + } + + shape.id = id; + shape.generation = m_generations[id]; + shape.owner = CurrentResourceName(); + shape.ComputeBounds(); + + m_idToSlot[id] = static_cast(m_shapeCount); + m_bounds.push_back({ id, shape.minX, shape.minY, shape.maxX, shape.maxY }); + m_recent.push_back(m_bounds.back()); + + if (m_shapes.empty() || m_shapes.back().size() == kShapeChunkSize) + { + m_shapes.emplace_back(); + m_shapes.back().reserve(kShapeChunkSize); + } + m_shapes.back().push_back(std::move(shape)); + m_shapeCount++; + + m_lastMutationMs.store(static_cast(NowMs()), std::memory_order_relaxed); + m_bvhGeneration.fetch_add(1, std::memory_order_relaxed); + return id; +} + +bool ColshapeManager::Delete(int id) +{ + std::unique_lock lock(m_mutex); + + if (id < 0 || static_cast(id) >= m_idToSlot.size()) + { + return false; + } + + int32_t slot = m_idToSlot[id]; + if (slot < 0) + { + return false; + } + + size_t last = m_shapeCount - 1; + if (static_cast(slot) != last) + { + int movedId = ShapeAt(last).id; + ShapeAt(slot) = std::move(ShapeAt(last)); + m_bounds[slot] = m_bounds[last]; + m_idToSlot[movedId] = slot; + } + + m_shapes.back().pop_back(); + if (m_shapes.back().empty()) + { + m_shapes.pop_back(); + } + m_shapeCount--; + + m_bounds.pop_back(); + m_idToSlot[id] = -1; + m_freeIds.push_back(id); + + // stale entries left in m_recent/the tree are fine - queries re-validate via Find() + m_lastMutationMs.store(static_cast(NowMs()), std::memory_order_relaxed); + m_bvhGeneration.fetch_add(1, std::memory_order_relaxed); + return true; +} + +const ColShape* ColshapeManager::Find(int id) const +{ + if (id < 0 || static_cast(id) >= m_idToSlot.size()) + { + return nullptr; + } + + int32_t slot = m_idToSlot[id]; + return (slot >= 0) ? &ShapeAt(slot) : nullptr; +} + +ColShape* ColshapeManager::Find(int id) +{ + return const_cast(static_cast(this)->Find(id)); +} + +bool ColshapeManager::IsLive(int id, uint32_t generation) const +{ + std::shared_lock lock(m_mutex); + const ColShape* s = Find(id); + return s != nullptr && s->generation == generation; +} + +void ColshapeManager::SetEntityType(int id, int type, bool value) +{ + std::unique_lock lock(m_mutex); + + if (auto* shape = Find(id)) + { + shape->SetEntityType(type, value); + } +} + +void ColshapeManager::DeleteByOwner(const std::string& owner) +{ + std::vector ids; + { + std::shared_lock lock(m_mutex); + for (size_t i = 0; i < m_shapeCount; i++) + { + if (ShapeAt(i).owner == owner) + { + ids.push_back(ShapeAt(i).id); + } + } + } + + for (int id : ids) + { + Delete(id); + } +} + +// static + reads only the passed-in snapshot, so it is safe on a background thread +std::shared_ptr ColshapeManager::BuildBvh(std::vector& input) +{ + auto bvh = std::make_shared(); + if (input.empty()) + { + return bvh; + } + + // leaf size 4 => at most N-1 nodes + bvh->nodes.reserve(input.size()); + bvh->ids.reserve(input.size()); + BuildBvhNode(*bvh, input, 0, static_cast(input.size())); + return bvh; +} + +int ColshapeManager::BuildBvhNode(Bvh& bvh, std::vector& input, int begin, int end) +{ + int nodeIdx = static_cast(bvh.nodes.size()); + bvh.nodes.push_back({}); + + float minX = FLT_MAX, minY = FLT_MAX, maxX = -FLT_MAX, maxY = -FLT_MAX; + for (int i = begin; i < end; i++) + { + minX = std::min(minX, input[i].minX); + minY = std::min(minY, input[i].minY); + maxX = std::max(maxX, input[i].maxX); + maxY = std::max(maxY, input[i].maxY); + } + + bvh.nodes[nodeIdx].minX = minX; + bvh.nodes[nodeIdx].minY = minY; + bvh.nodes[nodeIdx].maxX = maxX; + bvh.nodes[nodeIdx].maxY = maxY; + + int n = end - begin; + if (n <= 4) + { + bvh.nodes[nodeIdx].start = static_cast(bvh.ids.size()); + bvh.nodes[nodeIdx].count = n; + for (int i = begin; i < end; i++) + { + bvh.ids.push_back(input[i].id); + } + return nodeIdx; + } + + bool splitX = (maxX - minX) >= (maxY - minY); + int mid = begin + n / 2; + std::nth_element(input.data() + begin, input.data() + mid, input.data() + end, [splitX](const BvhInput& a, const BvhInput& b) + { + if (splitX) + { + return (a.minX + a.maxX) < (b.minX + b.maxX); + } + return (a.minY + a.maxY) < (b.minY + b.maxY); + }); + + BuildBvhNode(bvh, input, begin, mid); + int right = BuildBvhNode(bvh, input, mid, end); + bvh.nodes[nodeIdx].right = right; + return nodeIdx; +} + +template +void ColshapeManager::QueryBvh(const Bvh& bvh, float px, float py, const TFn& fn) +{ + if (bvh.nodes.empty()) + { + return; + } + + int stack[128]; + int sp = 0; + stack[sp++] = 0; + + while (sp > 0) + { + int nodeIdx = stack[--sp]; + const BvhNode& node = bvh.nodes[nodeIdx]; + + if (px < node.minX || px > node.maxX || py < node.minY || py > node.maxY) + { + continue; + } + + if (node.count > 0) + { + for (int i = 0; i < node.count; i++) + { + fn(bvh.ids[node.start + i]); + } + } + else + { + stack[sp++] = nodeIdx + 1; + stack[sp++] = node.right; + } + } +} + +// builds+swaps the tree on a background thread; the current tree serves queries meanwhile +void ColshapeManager::KickBvhBuild() +{ + if (m_bvhBuiltGeneration.load() == m_bvhGeneration.load(std::memory_order_relaxed)) + { + return; + } + + // let mutations settle before rebuilding so a burst can't trigger back-to-back + // builds; the staleness cap still indexes a steady trickle + int64_t nowMs = static_cast(NowMs()); + bool quiet = (nowMs - m_lastMutationMs.load(std::memory_order_relaxed)) >= 150; + bool tooStale = (nowMs - m_lastBvhKickMs.load()) >= 2000; + if (!quiet && !tooStale) + { + return; + } + + bool expected = false; + if (!m_bvhBuilding.compare_exchange_strong(expected, true)) + { + return; + } + + m_lastBvhKickMs.store(nowMs); + + uint64_t buildingGen = m_bvhGeneration.load(std::memory_order_relaxed); + + auto input = std::make_shared>(m_bounds); + + // the tree covers exactly the m_recent entries that existed now; new shapes only + // ever append, so the covered ones are this leading count (ids are recycled, so a + // covered-max-id test would wrongly prune a recycled low id created mid-build) + size_t coveredRecent = m_recent.size(); + + std::thread([this, input, buildingGen, coveredRecent]() + { + auto built = BuildBvh(*input); + + { + std::lock_guard swap(m_bvhSwapMutex); + m_bvh = built; + } + m_bvhBuiltGeneration.store(buildingGen); + + // drop the m_recent prefix the new tree now covers (separate lock scope, so + // m_mutex and the swap mutex are never held together) + { + std::unique_lock lock(m_mutex); + size_t covered = std::min(coveredRecent, m_recent.size()); + m_recent.erase(m_recent.begin(), m_recent.begin() + covered); + } + + m_bvhBuilding.store(false); + }).detach(); +} + +void ColshapeManager::Update(const std::vector& entities, const EventFn& emit) +{ + std::shared_lock lock(m_mutex); + + if (entities.empty() && m_entityInside.empty()) + { + return; + } + + KickBvhBuild(); + + std::shared_ptr bvh; + { + std::lock_guard swap(m_bvhSwapMutex); + bvh = m_bvh; + } + + const uint64_t epoch = ++m_epoch; + + // beyond this, scanning the tail per entity per tick starves the create path, so + // new shapes wait for the next rebuild instead + constexpr size_t kMaxRecentScan = 8192; + const bool scanRecent = m_recent.size() <= kMaxRecentScan; + + std::vector current; + for (const auto& e : entities) + { + current.clear(); + + auto test = [&](int shapeId) + { + const ColShape* s = Find(shapeId); + if (s && s->AcceptsEntityType(e.type) && s->ContainsPoint(e.x, e.y, e.z)) + { + current.push_back({ shapeId, s->generation }); + } + }; + + if (bvh) + { + QueryBvh(*bvh, e.x, e.y, test); + } + + if (scanRecent) + { + for (const auto& r : m_recent) + { + if (e.x >= r.minX && e.x <= r.maxX && e.y >= r.minY && e.y <= r.maxY) + { + test(r.id); + } + } + } + + std::sort(current.begin(), current.end()); + current.erase(std::unique(current.begin(), current.end(), + [](const ShapeRef& a, const ShapeRef& b) { return a.id == b.id; }), current.end()); + + auto stateIt = m_entityInside.find(e.handle); + if (stateIt == m_entityInside.end()) + { + if (current.empty()) + { + continue; + } + stateIt = m_entityInside.emplace(e.handle, EntityInsideState{}).first; + } + + auto& state = stateIt->second; + state.epoch = epoch; + + // merge-diff vs last tick by id: only-in-new -> enter, only-in-old -> exit. a + // same id with a bumped generation is a delete+recreate, so exit then enter. + const auto& prev = state.shapes; + size_t ci = 0, pi = 0; + while (ci < current.size() || pi < prev.size()) + { + if (pi == prev.size() || (ci < current.size() && current[ci].id < prev[pi].id)) + { + emit("onColshapeEnter", e.handle, current[ci].id, current[ci].generation); + ci++; + } + else if (ci == current.size() || prev[pi].id < current[ci].id) + { + emit("onColshapeExit", e.handle, prev[pi].id, prev[pi].generation); + pi++; + } + else + { + if (current[ci].generation != prev[pi].generation) + { + emit("onColshapeExit", e.handle, prev[pi].id, prev[pi].generation); + emit("onColshapeEnter", e.handle, current[ci].id, current[ci].generation); + } + ci++; + pi++; + } + } + + state.shapes.swap(current); + if (state.shapes.empty()) + { + m_entityInside.erase(stateIt); + } + } + + // entities missing this tick despawned/disconnected -> exit them from every shape + for (auto it = m_entityInside.begin(); it != m_entityInside.end(); ) + { + if (it->second.epoch != epoch) + { + for (const ShapeRef& s : it->second.shapes) + { + emit("onColshapeExit", it->first, s.id, s.generation); + } + it = m_entityInside.erase(it); + } + else + { + ++it; + } + } +} +} + +static InitFunction initFunction([]() +{ + using namespace fx::colshape; + + auto& mgr = ColshapeManager::Get(); + + // shapes die with the resource that created them + fx::Resource::OnInitializeInstance.Connect([](fx::Resource* resource) + { + std::string resourceName = resource->GetName(); + resource->OnStop.Connect([resourceName]() + { + ColshapeManager::Get().DeleteByOwner(resourceName); + }); + }); + + // ---- creators ---- + fx::ScriptEngine::RegisterNativeHandler("CREATE_COLSHAPE_SPHERE", [](fx::ScriptContext& context) + { + ColShape s(ColShapeType::Sphere); + s.pos1 = { context.GetArgument(0), context.GetArgument(1), context.GetArgument(2) }; + s.radius = context.GetArgument(3); + context.SetResult(ColshapeManager::Get().Add(std::move(s))); + }); + + fx::ScriptEngine::RegisterNativeHandler("CREATE_COLSHAPE_CIRCLE", [](fx::ScriptContext& context) + { + ColShape s(ColShapeType::Circle); + s.pos1 = { context.GetArgument(0), context.GetArgument(1), 0.0f }; + s.radius = context.GetArgument(2); + context.SetResult(ColshapeManager::Get().Add(std::move(s))); + }); + + fx::ScriptEngine::RegisterNativeHandler("CREATE_COLSHAPE_CYLINDER", [](fx::ScriptContext& context) + { + ColShape s(ColShapeType::Cylinder); + s.pos1 = { context.GetArgument(0), context.GetArgument(1), context.GetArgument(2) }; + s.radius = context.GetArgument(3); + s.height = context.GetArgument(4); + context.SetResult(ColshapeManager::Get().Add(std::move(s))); + }); + + fx::ScriptEngine::RegisterNativeHandler("CREATE_COLSHAPE_CUBOID", [](fx::ScriptContext& context) + { + ColShape s(ColShapeType::Cuboid); + s.pos1 = { context.GetArgument(0), context.GetArgument(1), context.GetArgument(2) }; + s.dimW = context.GetArgument(3); + s.dimD = context.GetArgument(4); + s.height = context.GetArgument(5); + context.SetResult(ColshapeManager::Get().Add(std::move(s))); + }); + + fx::ScriptEngine::RegisterNativeHandler("CREATE_COLSHAPE_RECTANGLE", [](fx::ScriptContext& context) + { + ColShape s(ColShapeType::Rectangle); + s.pos1 = { context.GetArgument(0), context.GetArgument(1), context.GetArgument(2) }; + s.dimW = context.GetArgument(3); + s.dimD = context.GetArgument(4); + s.heading = context.GetArgument(5); + context.SetResult(ColshapeManager::Get().Add(std::move(s))); + }); + + fx::ScriptEngine::RegisterNativeHandler("CREATE_COLSHAPE_POLYGON", [](fx::ScriptContext& context) + { + ColShape s(ColShapeType::Polygon); + s.minZ = context.GetArgument(0); + s.maxZ = context.GetArgument(1); + ParsePolygonPoints(context, 2, s.points); + + if (s.points.size() < 3) + { + context.SetResult(-1); + return; + } + + context.SetResult(ColshapeManager::Get().Add(std::move(s))); + }); + + // ---- manage ---- + fx::ScriptEngine::RegisterNativeHandler("DELETE_COLSHAPE", [](fx::ScriptContext& context) + { + context.SetResult(ColshapeManager::Get().Delete(context.GetArgument(0))); + }); + + fx::ScriptEngine::RegisterNativeHandler("DOES_COLSHAPE_EXIST", [](fx::ScriptContext& context) + { + context.SetResult(ColshapeManager::Get().Find(context.GetArgument(0)) != nullptr); + }); + + auto registerIsType = [](const char* name, ColShapeType type) + { + fx::ScriptEngine::RegisterNativeHandler(name, [type](fx::ScriptContext& context) + { + auto* s = ColshapeManager::Get().Find(context.GetArgument(0)); + context.SetResult(s != nullptr && s->type == type); + }); + }; + registerIsType("IS_COLSHAPE_SPHERE", ColShapeType::Sphere); + registerIsType("IS_COLSHAPE_CIRCLE", ColShapeType::Circle); + registerIsType("IS_COLSHAPE_CYLINDER", ColShapeType::Cylinder); + registerIsType("IS_COLSHAPE_CUBOID", ColShapeType::Cuboid); + registerIsType("IS_COLSHAPE_RECTANGLE", ColShapeType::Rectangle); + registerIsType("IS_COLSHAPE_POLYGON", ColShapeType::Polygon); + + fx::ScriptEngine::RegisterNativeHandler("IS_POINT_INSIDE_COLSHAPE", [](fx::ScriptContext& context) + { + auto* s = ColshapeManager::Get().Find(context.GetArgument(0)); + bool inside = s != nullptr && s->ContainsPoint(context.GetArgument(1), context.GetArgument(2), context.GetArgument(3)); + context.SetResult(inside); + }); + + fx::ScriptEngine::RegisterNativeHandler("SET_COLSHAPE_ENTITY_TYPE", [](fx::ScriptContext& context) + { + ColshapeManager::Get().SetEntityType(context.GetArgument(0), context.GetArgument(1), context.GetArgument(2)); + }); + + fx::ScriptEngine::RegisterNativeHandler("IS_COLSHAPE_ENTITY_TYPE_SET", [](fx::ScriptContext& context) + { + auto* s = ColshapeManager::Get().Find(context.GetArgument(0)); + context.SetResult(s != nullptr && s->AcceptsEntityType(context.GetArgument(1))); + }); + + // getters return ok + float* out-params (Lua: ok, out1, ..), zeroed on failure + fx::ScriptEngine::RegisterNativeHandler("GET_COLSHAPE_SPHERE_DATA", [](fx::ScriptContext& context) + { + auto* s = ColshapeManager::Get().Find(context.GetArgument(0)); + bool ok = s != nullptr && s->type == ColShapeType::Sphere; + *context.GetArgument(1) = ok ? s->pos1.x : 0.0f; + *context.GetArgument(2) = ok ? s->pos1.y : 0.0f; + *context.GetArgument(3) = ok ? s->pos1.z : 0.0f; + *context.GetArgument(4) = ok ? s->radius : 0.0f; + context.SetResult(ok); + }); + + fx::ScriptEngine::RegisterNativeHandler("GET_COLSHAPE_CIRCLE_DATA", [](fx::ScriptContext& context) + { + auto* s = ColshapeManager::Get().Find(context.GetArgument(0)); + bool ok = s != nullptr && s->type == ColShapeType::Circle; + *context.GetArgument(1) = ok ? s->pos1.x : 0.0f; + *context.GetArgument(2) = ok ? s->pos1.y : 0.0f; + *context.GetArgument(3) = ok ? s->radius : 0.0f; + context.SetResult(ok); + }); + + fx::ScriptEngine::RegisterNativeHandler("GET_COLSHAPE_CYLINDER_DATA", [](fx::ScriptContext& context) + { + auto* s = ColshapeManager::Get().Find(context.GetArgument(0)); + bool ok = s != nullptr && s->type == ColShapeType::Cylinder; + *context.GetArgument(1) = ok ? s->pos1.x : 0.0f; + *context.GetArgument(2) = ok ? s->pos1.y : 0.0f; + *context.GetArgument(3) = ok ? s->pos1.z : 0.0f; + *context.GetArgument(4) = ok ? s->radius : 0.0f; + *context.GetArgument(5) = ok ? s->height : 0.0f; + context.SetResult(ok); + }); + + fx::ScriptEngine::RegisterNativeHandler("GET_COLSHAPE_CUBOID_DATA", [](fx::ScriptContext& context) + { + auto* s = ColshapeManager::Get().Find(context.GetArgument(0)); + bool ok = s != nullptr && s->type == ColShapeType::Cuboid; + *context.GetArgument(1) = ok ? s->pos1.x : 0.0f; + *context.GetArgument(2) = ok ? s->pos1.y : 0.0f; + *context.GetArgument(3) = ok ? s->pos1.z : 0.0f; + *context.GetArgument(4) = ok ? s->dimW : 0.0f; + *context.GetArgument(5) = ok ? s->dimD : 0.0f; + *context.GetArgument(6) = ok ? s->height : 0.0f; + context.SetResult(ok); + }); + + fx::ScriptEngine::RegisterNativeHandler("GET_COLSHAPE_RECTANGLE_DATA", [](fx::ScriptContext& context) + { + auto* s = ColshapeManager::Get().Find(context.GetArgument(0)); + bool ok = s != nullptr && s->type == ColShapeType::Rectangle; + *context.GetArgument(1) = ok ? s->pos1.x : 0.0f; + *context.GetArgument(2) = ok ? s->pos1.y : 0.0f; + *context.GetArgument(3) = ok ? s->pos1.z : 0.0f; + *context.GetArgument(4) = ok ? s->dimW : 0.0f; + *context.GetArgument(5) = ok ? s->dimD : 0.0f; + *context.GetArgument(6) = ok ? s->heading : 0.0f; + context.SetResult(ok); + }); + + fx::ScriptEngine::RegisterNativeHandler("GET_COLSHAPE_POLYGON_DATA", [](fx::ScriptContext& context) + { + auto* s = ColshapeManager::Get().Find(context.GetArgument(0)); + bool ok = s != nullptr && s->type == ColShapeType::Polygon; + *context.GetArgument(1) = ok ? s->minZ : 0.0f; + *context.GetArgument(2) = ok ? s->maxZ : 0.0f; + context.SetResult(ok); + }); +}); diff --git a/code/components/citizen-server-impl/include/state/ServerGameStatePublic.h b/code/components/citizen-server-impl/include/state/ServerGameStatePublic.h index 014bde26f7..1836d590c1 100644 --- a/code/components/citizen-server-impl/include/state/ServerGameStatePublic.h +++ b/code/components/citizen-server-impl/include/state/ServerGameStatePublic.h @@ -52,6 +52,12 @@ class Entity virtual uint32_t GetModel() = 0; virtual std::string GetType() = 0; + + // the GET_ENTITY_TYPE classification: 1 = ped, 2 = vehicle, 3 = object, 0 = none + virtual int GetTypeIndex() = 0; + + // the script-facing entity handle (as returned by entity natives) + virtual uint32_t GetScriptGuid() = 0; }; } diff --git a/code/components/citizen-server-impl/src/ColshapeEntityFeed.cpp b/code/components/citizen-server-impl/src/ColshapeEntityFeed.cpp new file mode 100644 index 0000000000..d277a6fb34 --- /dev/null +++ b/code/components/citizen-server-impl/src/ColshapeEntityFeed.cpp @@ -0,0 +1,139 @@ +#include "StdInc.h" + +#include +#include + +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include + +// the tick gathers samples and emits events; a worker runs the query in between +struct PendingEvent +{ + const char* name; + int entity; + int shape; + uint32_t generation; +}; + +static std::mutex g_jobMutex; +static std::condition_variable g_jobCv; +static std::vector g_pendingSamples; +static bool g_hasJob = false; + +static std::mutex g_resultMutex; +static std::vector g_results; + +static void ColshapeWorker() +{ + for (;;) + { + std::vector samples; + + { + std::unique_lock lock(g_jobMutex); + g_jobCv.wait(lock, []() + { + return g_hasJob; + }); + + samples = std::move(g_pendingSamples); + g_pendingSamples.clear(); + g_hasJob = false; + } + + std::vector found; + fx::colshape::ColshapeManager::Get().Update(samples, [&found](const char* event, int entity, int shape, uint32_t generation) + { + found.push_back({ event, entity, shape, generation }); + }); + + if (!found.empty()) + { + std::lock_guard lock(g_resultMutex); + g_results.insert(g_results.end(), found.begin(), found.end()); + } + } +} + +static InitFunction initFunction([]() +{ + // order 100 so this runs after GameServer attaches its component (default order 0), + // else GetComponent() below asserts + fx::ServerInstanceBase::OnServerCreate.Connect([](fx::ServerInstanceBase* instance) + { + static std::chrono::milliseconds lastTick{ 0 }; + static std::thread worker(ColshapeWorker); + worker.detach(); + + instance->GetComponent()->OnTick.Connect([instance]() + { + auto eventManager = instance->GetComponent()->GetComponent(); + if (eventManager.GetRef()) + { + std::vector results; + { + std::lock_guard lock(g_resultMutex); + results = std::move(g_results); + g_results.clear(); + } + + for (const auto& r : results) + { + // drop events whose shape id was freed (or freed+reused) since detection + if (!fx::colshape::ColshapeManager::Get().IsLive(r.shape, r.generation)) + { + continue; + } + + eventManager->QueueEvent2(r.name, {}, r.entity, r.shape); + } + } + + auto now = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()); + if (now - lastTick < std::chrono::milliseconds{ 50 }) + { + return; + } + + lastTick = now; + + auto gameState = instance->GetComponent(); + if (!gameState.GetRef()) + { + return; + } + + std::vector samples; + samples.reserve(256); + gameState->ForAllEntities([&samples](fx::sync::Entity* entity) + { + int type = entity->GetTypeIndex(); + if (type < 0) + { + return; + } + + auto pos = entity->GetPosition(); + samples.push_back({ static_cast(entity->GetScriptGuid()), type, pos.x, pos.y, pos.z }); + }); + + { + std::lock_guard lock(g_jobMutex); + g_pendingSamples = std::move(samples); + g_hasJob = true; + } + g_jobCv.notify_one(); + }); + }, 100); +}); diff --git a/code/components/citizen-server-impl/src/state/ServerGameState.cpp b/code/components/citizen-server-impl/src/state/ServerGameState.cpp index 3052fc9c35..fb1ae259d9 100644 --- a/code/components/citizen-server-impl/src/state/ServerGameState.cpp +++ b/code/components/citizen-server-impl/src/state/ServerGameState.cpp @@ -452,8 +452,8 @@ static const char* TypeToString(fx::sync::NetObjEntityType type) struct EntityImpl : sync::Entity { - EntityImpl(const sync::SyncEntityPtr& ent) - : ent(ent) + EntityImpl(const sync::SyncEntityPtr& ent, ServerGameState* sgs) + : ent(ent), sgs(sgs) { } @@ -481,8 +481,14 @@ struct EntityImpl : sync::Entity return {}; } + virtual uint32_t GetScriptGuid() override + { + return sgs ? sgs->MakeScriptHandle(ent) : ent->handle; + } + private: sync::SyncEntityPtr ent; + ServerGameState* sgs; // Inherited via Entity virtual uint32_t GetId() override @@ -515,6 +521,12 @@ struct EntityImpl : sync::Entity { return TypeToString(ent->type); } + virtual int GetTypeIndex() override + { + // the raw sync entity type (NetObjEntityType) - automobile, bike, ped, + // player etc. - which is what the colshape entity-type filter uses + return static_cast(ent->type); + } }; void ServerGameState::ForAllEntities(const std::function& cb) @@ -523,7 +535,7 @@ void ServerGameState::ForAllEntities(const std::function& c for (auto& entity : m_entityList) { - EntityImpl ent(entity); + EntityImpl ent(entity, this); cb(&ent); } } diff --git a/code/components/extra-natives-five/component.json b/code/components/extra-natives-five/component.json index a0b576016d..b20405b9fe 100644 --- a/code/components/extra-natives-five/component.json +++ b/code/components/extra-natives-five/component.json @@ -15,7 +15,8 @@ "net", "voip:mumble", "vendor:directxtex", - "vendor:im3d" + "vendor:im3d", + "gta:net:five" ], "provides": [] } diff --git a/code/components/extra-natives-five/src/ColshapeEntityFeed.cpp b/code/components/extra-natives-five/src/ColshapeEntityFeed.cpp new file mode 100644 index 0000000000..f1755fb4a7 --- /dev/null +++ b/code/components/extra-natives-five/src/ColshapeEntityFeed.cpp @@ -0,0 +1,183 @@ +#include "StdInc.h" + +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +// defined in PoolTraversalNatives.cpp (owns the vehicle-pool pointer scan) +void ForAllVehicles(const std::function& fn); + +// networked entities carry their NetObjEntityType; local-only ones fall back per pool +static void AddSample(fwEntity* entity, fx::sync::NetObjEntityType fallback, std::vector& out) +{ + if (!entity) + { + return; + } + + int handle = rage::fwScriptGuid::GetGuidFromBase(entity); + if (handle == 0) + { + return; + } + + int type = static_cast(fallback); + if (auto netObj = static_cast(entity->GetNetObject())) + { + type = netObj->GetObjectType(); + } + + auto pos = entity->GetPosition(); + out.push_back({ handle, type, pos.x, pos.y, pos.z }); +} + +template +static void GatherPool(const char* poolName, fx::sync::NetObjEntityType fallback, std::vector& out) +{ + auto pool = rage::GetPool(poolName); + if (!pool) + { + return; + } + + for (int i = 0; i < pool->GetSize(); i++) + { + AddSample(pool->GetAt(i), fallback, out); + } +} + +static void GatherVehicles(std::vector& out) +{ + ForAllVehicles([&out](fwEntity* entity) + { + AddSample(entity, fx::sync::NetObjEntityType::Automobile, out); + }); +} + +// the game frame gathers samples and emits events; a worker runs the query in between +struct PendingEvent +{ + const char* name; + int entity; + int shape; + uint32_t generation; +}; + +static std::mutex g_jobMutex; +static std::condition_variable g_jobCv; +static std::vector g_pendingSamples; +static bool g_hasJob = false; +static bool g_workerStop = false; + +static std::mutex g_resultMutex; +static std::vector g_results; + +static void ColshapeWorker() +{ + for (;;) + { + std::vector samples; + + { + std::unique_lock lock(g_jobMutex); + g_jobCv.wait(lock, []() + { + return g_hasJob || g_workerStop; + }); + + if (g_workerStop) + { + return; + } + + samples = std::move(g_pendingSamples); + g_pendingSamples.clear(); + g_hasJob = false; + } + + std::vector found; + fx::colshape::ColshapeManager::Get().Update(samples, [&found](const char* event, int entity, int shape, uint32_t generation) + { + found.push_back({ event, entity, shape, generation }); + }); + + if (!found.empty()) + { + std::lock_guard lock(g_resultMutex); + g_results.insert(g_results.end(), found.begin(), found.end()); + } + } +} + +static InitFunction initFunction([]() +{ + static std::chrono::milliseconds lastTick{ 0 }; + static std::thread worker(ColshapeWorker); + worker.detach(); + + OnGameFrame.Connect([]() + { + auto resourceManager = fx::ResourceManager::GetCurrent(); + if (resourceManager) + { + auto eventManager = resourceManager->GetComponent(); + if (eventManager.GetRef()) + { + std::vector results; + { + std::lock_guard lock(g_resultMutex); + results = std::move(g_results); + g_results.clear(); + } + + for (const auto& r : results) + { + // drop events whose shape id was freed (or freed+reused) since detection + if (!fx::colshape::ColshapeManager::Get().IsLive(r.shape, r.generation)) + { + continue; + } + + eventManager->QueueEvent2(r.name, {}, r.entity, r.shape); + } + } + } + + auto now = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()); + if (now - lastTick < std::chrono::milliseconds{ 50 }) + { + return; + } + + lastTick = now; + + std::vector samples; + samples.reserve(512); + + GatherPool("Peds", fx::sync::NetObjEntityType::Ped, samples); + GatherVehicles(samples); + GatherPool("Object", fx::sync::NetObjEntityType::Object, samples); + + { + std::lock_guard lock(g_jobMutex); + g_pendingSamples = std::move(samples); + g_hasJob = true; + } + g_jobCv.notify_one(); + }); +}); diff --git a/code/components/extra-natives-five/src/PoolTraversalNatives.cpp b/code/components/extra-natives-five/src/PoolTraversalNatives.cpp index f09d17315f..73fcaee1de 100644 --- a/code/components/extra-natives-five/src/PoolTraversalNatives.cpp +++ b/code/components/extra-natives-five/src/PoolTraversalNatives.cpp @@ -308,4 +308,21 @@ static HookFunction hookFunction([]() { g_vehiclePool = hook::get_address(hook::get_pattern("48 8B 05 ? ? ? ? F3 0F 59 F6 48 8B 08", 3)); }); + +void ForAllVehicles(const std::function& fn) +{ + if (!g_vehiclePool || !*g_vehiclePool || !**g_vehiclePool) + { + return; + } + + auto pool = VehiclePoolTraits::GetPool(); + for (int i = 0; i < pool->GetSize(); i++) + { + if (auto entry = pool->GetAt(i)) + { + fn(entry); + } + } +} #endif diff --git a/code/components/extra-natives-rdr3/component.json b/code/components/extra-natives-rdr3/component.json index c19f70abf1..bcd48cd033 100644 --- a/code/components/extra-natives-rdr3/component.json +++ b/code/components/extra-natives-rdr3/component.json @@ -14,7 +14,8 @@ "nui:core", "citizen:resources:client", "net", - "voip:mumble" + "voip:mumble", + "gta:net:rdr3" ], "provides": [] } diff --git a/code/components/extra-natives-rdr3/src/ColshapeEntityFeed.cpp b/code/components/extra-natives-rdr3/src/ColshapeEntityFeed.cpp new file mode 100644 index 0000000000..77143ab0ca --- /dev/null +++ b/code/components/extra-natives-rdr3/src/ColshapeEntityFeed.cpp @@ -0,0 +1,167 @@ +#include "StdInc.h" + +#include + +#include +#include + +#include +#include +#include +#include + +#include + + +#include +#include +#include +#include +#include + +// networked entities carry their NetObjEntityType; local-only ones fall back per pool +static void GatherPool(const char* poolName, fx::sync::NetObjEntityType fallback, std::vector& out) +{ + auto pool = rage::GetPool(poolName); + if (!pool) + { + return; + } + + for (int i = 0; i < pool->GetSize(); i++) + { + fwEntity* entity = pool->GetAt(i); + if (!entity) + { + continue; + } + + int handle = rage::fwScriptGuid::GetGuidFromBase(entity); + if (handle == 0) + { + continue; + } + + int type = static_cast(fallback); + if (auto netObj = static_cast(entity->GetNetObject())) + { + type = netObj->GetObjectType(); + } + + auto pos = entity->GetPosition(); + out.push_back({ handle, type, pos.x, pos.y, pos.z }); + } +} + +// the game frame gathers samples and emits events; a worker runs the query in between +struct PendingEvent +{ + const char* name; + int entity; + int shape; + uint32_t generation; +}; + +static std::mutex g_jobMutex; +static std::condition_variable g_jobCv; +static std::vector g_pendingSamples; +static bool g_hasJob = false; +static bool g_workerStop = false; + +static std::mutex g_resultMutex; +static std::vector g_results; + +static void ColshapeWorker() +{ + for (;;) + { + std::vector samples; + + { + std::unique_lock lock(g_jobMutex); + g_jobCv.wait(lock, []() + { + return g_hasJob || g_workerStop; + }); + + if (g_workerStop) + { + return; + } + + samples = std::move(g_pendingSamples); + g_pendingSamples.clear(); + g_hasJob = false; + } + + std::vector found; + fx::colshape::ColshapeManager::Get().Update(samples, [&found](const char* event, int entity, int shape, uint32_t generation) + { + found.push_back({ event, entity, shape, generation }); + }); + + if (!found.empty()) + { + std::lock_guard lock(g_resultMutex); + g_results.insert(g_results.end(), found.begin(), found.end()); + } + } +} + +static InitFunction initFunction([]() +{ + static std::chrono::milliseconds lastTick{ 0 }; + static std::thread worker(ColshapeWorker); + worker.detach(); + + OnGameFrame.Connect([]() + { + auto resourceManager = fx::ResourceManager::GetCurrent(); + if (resourceManager) + { + auto eventManager = resourceManager->GetComponent(); + if (eventManager.GetRef()) + { + std::vector results; + { + std::lock_guard lock(g_resultMutex); + results = std::move(g_results); + g_results.clear(); + } + + for (const auto& r : results) + { + // drop events whose shape id was freed (or freed+reused) since detection + if (!fx::colshape::ColshapeManager::Get().IsLive(r.shape, r.generation)) + { + continue; + } + + eventManager->QueueEvent2(r.name, {}, r.entity, r.shape); + } + } + } + + auto now = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()); + if (now - lastTick < std::chrono::milliseconds{ 50 }) + { + return; + } + + lastTick = now; + + std::vector samples; + samples.reserve(512); + + GatherPool("Peds", fx::sync::NetObjEntityType::Ped, samples); + GatherPool("CVehicle", fx::sync::NetObjEntityType::Automobile, samples); + GatherPool("Object", fx::sync::NetObjEntityType::Object, samples); + + { + std::lock_guard lock(g_jobMutex); + g_pendingSamples = std::move(samples); + g_hasJob = true; + } + g_jobCv.notify_one(); + }); +}); diff --git a/code/components/gta-streaming-rdr3/include/EntitySystem.h b/code/components/gta-streaming-rdr3/include/EntitySystem.h index d3c99905c4..c959dc8e85 100644 --- a/code/components/gta-streaming-rdr3/include/EntitySystem.h +++ b/code/components/gta-streaming-rdr3/include/EntitySystem.h @@ -80,6 +80,8 @@ class STREAMING_EXPORT fwScriptGuid { public: static fwEntity* GetBaseFromGuid(int handle); + + static int GetGuidFromBase(fwEntity* base); }; using fwEntity = ::fwEntity; diff --git a/code/components/gta-streaming-rdr3/src/EntitySystem.cpp b/code/components/gta-streaming-rdr3/src/EntitySystem.cpp index 9ba841ceba..353a07b0fd 100644 --- a/code/components/gta-streaming-rdr3/src/EntitySystem.cpp +++ b/code/components/gta-streaming-rdr3/src/EntitySystem.cpp @@ -13,6 +13,16 @@ fwEntity* rage::fwScriptGuid::GetBaseFromGuid(int handle) return getScriptEntity(handle); } +static hook::cdecl_stub getScriptGuidForEntity([]() +{ + return hook::get_pattern("32 DB E8 ? ? ? ? 48 85 C0 75 ? 8A 05", -35); +}); + +int rage::fwScriptGuid::GetGuidFromBase(fwEntity* base) +{ + return int(getScriptGuidForEntity(base)); +} + static hook::cdecl_stub getArchetype([]() { return hook::get_call(hook::pattern("8B 4E 08 C1 EB 05 80 E3 01 E8").count(1).get(0).get(9)); diff --git a/ext/native-decls/CreateColshapeCircle.md b/ext/native-decls/CreateColshapeCircle.md new file mode 100644 index 0000000000..334c9e0dc1 --- /dev/null +++ b/ext/native-decls/CreateColshapeCircle.md @@ -0,0 +1,19 @@ +--- +ns: CFX +apiset: shared +--- +## CREATE_COLSHAPE_CIRCLE + +```c +int CREATE_COLSHAPE_CIRCLE(float x, float y, float radius); +``` + +Creates a 2D circular collision shape at the specified position. + +## Parameters +* **x**: Center X coordinate. +* **y**: Center Y coordinate. +* **radius**: Circle radius. + +## Return value +The collision shape ID, or -1 on failure. diff --git a/ext/native-decls/CreateColshapeCuboid.md b/ext/native-decls/CreateColshapeCuboid.md new file mode 100644 index 0000000000..9e83fa94da --- /dev/null +++ b/ext/native-decls/CreateColshapeCuboid.md @@ -0,0 +1,22 @@ +--- +ns: CFX +apiset: shared +--- +## CREATE_COLSHAPE_CUBOID + +```c +int CREATE_COLSHAPE_CUBOID(float x, float y, float z, float width, float depth, float height); +``` + +Creates an axis-aligned box collision shape at the specified 3D position. + +## Parameters +* **x**: Center X coordinate. +* **y**: Center Y coordinate. +* **z**: Center Z coordinate. +* **width**: Total width. +* **depth**: Total depth. +* **height**: Total height. + +## Return value +The collision shape ID, or -1 on failure. diff --git a/ext/native-decls/CreateColshapeCylinder.md b/ext/native-decls/CreateColshapeCylinder.md new file mode 100644 index 0000000000..43b4834b09 --- /dev/null +++ b/ext/native-decls/CreateColshapeCylinder.md @@ -0,0 +1,21 @@ +--- +ns: CFX +apiset: shared +--- +## CREATE_COLSHAPE_CYLINDER + +```c +int CREATE_COLSHAPE_CYLINDER(float x, float y, float z, float radius, float height); +``` + +Creates a vertical cylinder collision shape at the specified 3D position. + +## Parameters +* **x**: Center X coordinate. +* **y**: Center Y coordinate. +* **z**: Center Z coordinate. +* **radius**: Cylinder radius. +* **height**: Total height. + +## Return value +The collision shape ID, or -1 on failure. diff --git a/ext/native-decls/CreateColshapePolygon.md b/ext/native-decls/CreateColshapePolygon.md new file mode 100644 index 0000000000..930185d65c --- /dev/null +++ b/ext/native-decls/CreateColshapePolygon.md @@ -0,0 +1,35 @@ +--- +ns: CFX +apiset: shared +--- +## CREATE_COLSHAPE_POLYGON + +```c +int CREATE_COLSHAPE_POLYGON(float minZ, float maxZ, object points); +``` + +Creates a polygon collision shape defined by a set of 2D points extruded between `minZ` and `maxZ`. Requires at least 3 points. + +When an entity enters or leaves a collision shape, the `onColshapeEnter` and `onColshapeExit` events are triggered. Both events are called with the entity handle that triggered them and the collision shape ID. + +## Parameters +* **minZ**: Minimum Z height of the polygon. +* **maxZ**: Maximum Z height of the polygon. +* **points**: An array of interleaved x, y point pairs (e.g., `{x1, y1, x2, y2, x3, y3, ...}`). Must contain at least 3 pairs. + +## Return value +The collision shape ID, or -1 on failure. + +## Examples +```lua +local colShape = CreateColshapePolygon(28.0, 32.0, { 293.089, 180.466, 303.089, 180.466, 298.089, 190.466 }) +print('created colshape with id ' .. tostring(colShape)) + +AddEventHandler('onColshapeEnter', function(entity, shape) + print('entity ' .. tostring(entity) .. ' entered colshape ' .. tostring(shape)) +end) + +AddEventHandler('onColshapeExit', function(entity, shape) + print('entity ' .. tostring(entity) .. ' left colshape ' .. tostring(shape)) +end) +``` diff --git a/ext/native-decls/CreateColshapeRectangle.md b/ext/native-decls/CreateColshapeRectangle.md new file mode 100644 index 0000000000..798b4142c8 --- /dev/null +++ b/ext/native-decls/CreateColshapeRectangle.md @@ -0,0 +1,24 @@ +--- +ns: CFX +apiset: shared +--- +## CREATE_COLSHAPE_RECTANGLE + +```c +int CREATE_COLSHAPE_RECTANGLE(float x, float y, float z, float width, float depth, float heading); +``` + +Creates a rotated rectangle collision shape at the specified 3D position. + +When an entity enters or leaves a collision shape, the `onColshapeEnter` and `onColshapeExit` events are triggered. Both events are called with the entity handle that triggered them and the collision shape ID. + +## Parameters +* **x**: Center X coordinate. +* **y**: Center Y coordinate. +* **z**: Center Z coordinate. +* **width**: Total width. +* **depth**: Total depth. +* **heading**: Rotation heading in radians. + +## Return value +The collision shape ID, or -1 on failure. diff --git a/ext/native-decls/CreateColshapeSphere.md b/ext/native-decls/CreateColshapeSphere.md new file mode 100644 index 0000000000..511c7821f8 --- /dev/null +++ b/ext/native-decls/CreateColshapeSphere.md @@ -0,0 +1,22 @@ +--- +ns: CFX +apiset: shared +--- +## CREATE_COLSHAPE_SPHERE + +```c +int CREATE_COLSHAPE_SPHERE(float x, float y, float z, float radius); +``` + +Creates a spherical collision shape at the specified 3D position. + +When an entity enters or leaves a collision shape, the `onColshapeEnter` and `onColshapeExit` events are triggered. Both events are called with the entity handle that triggered them and the collision shape ID. + +## Parameters +* **x**: Center X coordinate. +* **y**: Center Y coordinate. +* **z**: Center Z coordinate. +* **radius**: Sphere radius. + +## Return value +The collision shape ID, or -1 on failure. diff --git a/ext/native-decls/DeleteColshape.md b/ext/native-decls/DeleteColshape.md new file mode 100644 index 0000000000..c6e1db8493 --- /dev/null +++ b/ext/native-decls/DeleteColshape.md @@ -0,0 +1,14 @@ +--- +ns: CFX +apiset: shared +--- +## DELETE_COLSHAPE + +```c +void DELETE_COLSHAPE(int colShapeId); +``` + +Deletes the collision shape with the given ID. + +## Parameters +* **colShapeId**: The collision shape ID. diff --git a/ext/native-decls/DoesColshapeExist.md b/ext/native-decls/DoesColshapeExist.md new file mode 100644 index 0000000000..313565abac --- /dev/null +++ b/ext/native-decls/DoesColshapeExist.md @@ -0,0 +1,17 @@ +--- +ns: CFX +apiset: shared +--- +## DOES_COLSHAPE_EXIST + +```c +BOOL DOES_COLSHAPE_EXIST(int colShapeId); +``` + +Returns whether a collision shape with the given ID exists. + +## Parameters +* **colShapeId**: The collision shape ID. + +## Return value +Returns true if the collision shape exists, false otherwise. diff --git a/ext/native-decls/GetColshapeCircleData.md b/ext/native-decls/GetColshapeCircleData.md new file mode 100644 index 0000000000..5a2276ad13 --- /dev/null +++ b/ext/native-decls/GetColshapeCircleData.md @@ -0,0 +1,21 @@ +--- +ns: CFX +apiset: shared +--- +## GET_COLSHAPE_CIRCLE_DATA + +```c +BOOL GET_COLSHAPE_CIRCLE_DATA(int colShapeId, float* x, float* y, float* radius); +``` + +Gets the data of a circle collision shape. + +## Parameters +* **colShapeId**: The collision shape ID. +* **x**: Center X coordinate output. +* **y**: Center Y coordinate output. +* **radius**: Circle radius output. + + +## Return value +Returns true if the data was retrieved successfully. Returns false if the ID is invalid or the shape is of a different type. diff --git a/ext/native-decls/GetColshapeCuboidData.md b/ext/native-decls/GetColshapeCuboidData.md new file mode 100644 index 0000000000..df50afe61d --- /dev/null +++ b/ext/native-decls/GetColshapeCuboidData.md @@ -0,0 +1,24 @@ +--- +ns: CFX +apiset: shared +--- +## GET_COLSHAPE_CUBOID_DATA + +```c +BOOL GET_COLSHAPE_CUBOID_DATA(int colShapeId, float* x, float* y, float* z, float* width, float* depth, float* height); +``` + +Gets the data of a cuboid collision shape. + +## Parameters +* **colShapeId**: The collision shape ID. +* **x**: Center X coordinate output. +* **y**: Center Y coordinate output. +* **z**: Center Z coordinate output. +* **width**: Width output. +* **depth**: Depth output. +* **height**: Height output. + + +## Return value +Returns true if the data was retrieved successfully. Returns false if the ID is invalid or the shape is of a different type. diff --git a/ext/native-decls/GetColshapeCylinderData.md b/ext/native-decls/GetColshapeCylinderData.md new file mode 100644 index 0000000000..f4698f8cf5 --- /dev/null +++ b/ext/native-decls/GetColshapeCylinderData.md @@ -0,0 +1,23 @@ +--- +ns: CFX +apiset: shared +--- +## GET_COLSHAPE_CYLINDER_DATA + +```c +BOOL GET_COLSHAPE_CYLINDER_DATA(int colShapeId, float* x, float* y, float* z, float* radius, float* height); +``` + +Gets the data of a cylinder collision shape. + +## Parameters +* **colShapeId**: The collision shape ID. +* **x**: Center X coordinate output. +* **y**: Center Y coordinate output. +* **z**: Center Z coordinate output. +* **radius**: Cylinder radius output. +* **height**: Height output. + + +## Return value +Returns true if the data was retrieved successfully. Returns false if the ID is invalid or the shape is of a different type. diff --git a/ext/native-decls/GetColshapePolygonData.md b/ext/native-decls/GetColshapePolygonData.md new file mode 100644 index 0000000000..223641b1be --- /dev/null +++ b/ext/native-decls/GetColshapePolygonData.md @@ -0,0 +1,20 @@ +--- +ns: CFX +apiset: shared +--- +## GET_COLSHAPE_POLYGON_DATA + +```c +BOOL GET_COLSHAPE_POLYGON_DATA(int colShapeId, float* minZ, float* maxZ); +``` + +Gets the vertical bounds of a polygon collision shape. + +## Parameters +* **colShapeId**: The collision shape ID. +* **minZ**: Lower Z bound output. +* **maxZ**: Upper Z bound output. + + +## Return value +Returns true if the data was retrieved successfully. Returns false if the ID is invalid or the shape is of a different type. diff --git a/ext/native-decls/GetColshapeRectangleData.md b/ext/native-decls/GetColshapeRectangleData.md new file mode 100644 index 0000000000..c51787d044 --- /dev/null +++ b/ext/native-decls/GetColshapeRectangleData.md @@ -0,0 +1,24 @@ +--- +ns: CFX +apiset: shared +--- +## GET_COLSHAPE_RECTANGLE_DATA + +```c +BOOL GET_COLSHAPE_RECTANGLE_DATA(int colShapeId, float* x, float* y, float* z, float* width, float* depth, float* heading); +``` + +Gets the data of a rectangle collision shape. + +## Parameters +* **colShapeId**: The collision shape ID. +* **x**: Center X coordinate output. +* **y**: Center Y coordinate output. +* **z**: Center Z coordinate output. +* **width**: Width output. +* **depth**: Depth output. +* **heading**: Heading output in radians. + + +## Return value +Returns true if the data was retrieved successfully. Returns false if the ID is invalid or the shape is of a different type. diff --git a/ext/native-decls/GetColshapeSphereData.md b/ext/native-decls/GetColshapeSphereData.md new file mode 100644 index 0000000000..d2d8fa556b --- /dev/null +++ b/ext/native-decls/GetColshapeSphereData.md @@ -0,0 +1,22 @@ +--- +ns: CFX +apiset: shared +--- +## GET_COLSHAPE_SPHERE_DATA + +```c +BOOL GET_COLSHAPE_SPHERE_DATA(int colShapeId, float* x, float* y, float* z, float* radius); +``` + +Gets the data of a sphere collision shape. + +## Parameters +* **colShapeId**: The collision shape ID. +* **x**: Center X coordinate output. +* **y**: Center Y coordinate output. +* **z**: Center Z coordinate output. +* **radius**: Sphere radius output. + + +## Return value +Returns true if the data was retrieved successfully. Returns false if the ID is invalid or the shape is of a different type. diff --git a/ext/native-decls/IsColshapeCircle.md b/ext/native-decls/IsColshapeCircle.md new file mode 100644 index 0000000000..a626d362ce --- /dev/null +++ b/ext/native-decls/IsColshapeCircle.md @@ -0,0 +1,17 @@ +--- +ns: CFX +apiset: shared +--- +## IS_COLSHAPE_CIRCLE + +```c +BOOL IS_COLSHAPE_CIRCLE(int colShapeId); +``` + +Returns whether the collision shape with the given ID is a circle. + +## Parameters +* **colShapeId**: The collision shape ID. + +## Return value +Returns true if the collision shape exists and is a circle, false otherwise. diff --git a/ext/native-decls/IsColshapeCuboid.md b/ext/native-decls/IsColshapeCuboid.md new file mode 100644 index 0000000000..7f93aaeca1 --- /dev/null +++ b/ext/native-decls/IsColshapeCuboid.md @@ -0,0 +1,17 @@ +--- +ns: CFX +apiset: shared +--- +## IS_COLSHAPE_CUBOID + +```c +BOOL IS_COLSHAPE_CUBOID(int colShapeId); +``` + +Returns whether the collision shape with the given ID is a cuboid. + +## Parameters +* **colShapeId**: The collision shape ID. + +## Return value +Returns true if the collision shape exists and is a cuboid, false otherwise. diff --git a/ext/native-decls/IsColshapeCylinder.md b/ext/native-decls/IsColshapeCylinder.md new file mode 100644 index 0000000000..2a2e493bbc --- /dev/null +++ b/ext/native-decls/IsColshapeCylinder.md @@ -0,0 +1,17 @@ +--- +ns: CFX +apiset: shared +--- +## IS_COLSHAPE_CYLINDER + +```c +BOOL IS_COLSHAPE_CYLINDER(int colShapeId); +``` + +Returns whether the collision shape with the given ID is a cylinder. + +## Parameters +* **colShapeId**: The collision shape ID. + +## Return value +Returns true if the collision shape exists and is a cylinder, false otherwise. diff --git a/ext/native-decls/IsColshapeEntityTypeSet.md b/ext/native-decls/IsColshapeEntityTypeSet.md new file mode 100644 index 0000000000..96e0e71e6b --- /dev/null +++ b/ext/native-decls/IsColshapeEntityTypeSet.md @@ -0,0 +1,18 @@ +--- +ns: CFX +apiset: shared +--- +## IS_COLSHAPE_ENTITY_TYPE_SET + +```c +BOOL IS_COLSHAPE_ENTITY_TYPE_SET(int colShapeId, int entityType); +``` + +Returns whether the collision shape is set to detect the given entity type. Every type is enabled by default; a type reads as disabled only after `SET_COLSHAPE_ENTITY_TYPE(colShapeId, entityType, false)`. + +## Parameters +* **colShapeId**: The collision shape ID. +* **entityType**: The sync (network) entity type index (e.g. GTA5: automobile `0`, object `5`, ped `6`, player `11`). See `SET_COLSHAPE_ENTITY_TYPE` for the full per-game tables. + +## Return value +Returns true if the entity type is detected by the collision shape, false otherwise. diff --git a/ext/native-decls/IsColshapePolygon.md b/ext/native-decls/IsColshapePolygon.md new file mode 100644 index 0000000000..865e392859 --- /dev/null +++ b/ext/native-decls/IsColshapePolygon.md @@ -0,0 +1,17 @@ +--- +ns: CFX +apiset: shared +--- +## IS_COLSHAPE_POLYGON + +```c +BOOL IS_COLSHAPE_POLYGON(int colShapeId); +``` + +Returns whether the collision shape with the given ID is a polygon. + +## Parameters +* **colShapeId**: The collision shape ID. + +## Return value +Returns true if the collision shape exists and is a polygon, false otherwise. diff --git a/ext/native-decls/IsColshapeRectangle.md b/ext/native-decls/IsColshapeRectangle.md new file mode 100644 index 0000000000..964d200aba --- /dev/null +++ b/ext/native-decls/IsColshapeRectangle.md @@ -0,0 +1,17 @@ +--- +ns: CFX +apiset: shared +--- +## IS_COLSHAPE_RECTANGLE + +```c +BOOL IS_COLSHAPE_RECTANGLE(int colShapeId); +``` + +Returns whether the collision shape with the given ID is a rectangle. + +## Parameters +* **colShapeId**: The collision shape ID. + +## Return value +Returns true if the collision shape exists and is a rectangle, false otherwise. diff --git a/ext/native-decls/IsColshapeSphere.md b/ext/native-decls/IsColshapeSphere.md new file mode 100644 index 0000000000..36ff182f35 --- /dev/null +++ b/ext/native-decls/IsColshapeSphere.md @@ -0,0 +1,17 @@ +--- +ns: CFX +apiset: shared +--- +## IS_COLSHAPE_SPHERE + +```c +BOOL IS_COLSHAPE_SPHERE(int colShapeId); +``` + +Returns whether the collision shape with the given ID is a sphere. + +## Parameters +* **colShapeId**: The collision shape ID. + +## Return value +Returns true if the collision shape exists and is a sphere, false otherwise. diff --git a/ext/native-decls/IsPointInsideColshape.md b/ext/native-decls/IsPointInsideColshape.md new file mode 100644 index 0000000000..a255ee121b --- /dev/null +++ b/ext/native-decls/IsPointInsideColshape.md @@ -0,0 +1,20 @@ +--- +ns: CFX +apiset: shared +--- +## IS_POINT_INSIDE_COLSHAPE + +```c +BOOL IS_POINT_INSIDE_COLSHAPE(int colShapeId, float x, float y, float z); +``` + +Returns whether the given 3D point is inside the collision shape. + +## Parameters +* **colShapeId**: The collision shape ID. +* **x**: Point X coordinate. +* **y**: Point Y coordinate. +* **z**: Point Z coordinate. + +## Return value +Returns true if the point is inside the collision shape, false otherwise. diff --git a/ext/native-decls/SetColshapeEntityType.md b/ext/native-decls/SetColshapeEntityType.md new file mode 100644 index 0000000000..c100125f8e --- /dev/null +++ b/ext/native-decls/SetColshapeEntityType.md @@ -0,0 +1,59 @@ +--- +ns: CFX +apiset: shared +--- +## SET_COLSHAPE_ENTITY_TYPE + +```c +void SET_COLSHAPE_ENTITY_TYPE(int colShapeId, int entityType, BOOL value); +``` + +Sets whether the collision shape should detect a specific entity type. + +By default every entity type is enabled, so a fresh collision shape detects all entities. Setting a type to `false` excludes only that type from detection; every other type stays as it was. To detect a single type (e.g. automobiles only) disable every other type. + +The entity type is the sync (network) entity type index (tables below), not the `GET_ENTITY_TYPE` classification. + +### GTA5 sync entity types +| Index | Type | +| --- | --- | +| 0 | automobile | +| 1 | bike | +| 2 | boat | +| 3 | door | +| 4 | heli | +| 5 | object | +| 6 | ped | +| 7 | pickup | +| 8 | pickup placement | +| 9 | plane | +| 10 | submarine | +| 11 | player | +| 12 | trailer | +| 13 | train | + +### RedM sync entity types +| Index | Type | +| --- | --- | +| 0 | animal | +| 1 | automobile | +| 2 | bike | +| 3 | boat | +| 4 | door | +| 5 | heli | +| 6 | object | +| 7 | ped | +| 8 | pickup | +| 9 | pickup placement | +| 10 | plane | +| 11 | submarine | +| 12 | player | +| 13 | trailer | +| 14 | train | +| 15 | draft vehicle | +| 21 | horse | + +## Parameters +* **colShapeId**: The collision shape ID. +* **entityType**: The sync entity type index (see the tables above). +* **value**: `true` to include this entity type in detection, `false` to exclude it.