From 3eebaf78ecf4ae4ad337f2b3bf6935097d11c5d5 Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Thu, 12 Mar 2026 14:30:47 +0100 Subject: [PATCH 01/10] more NPC pawn natives --- Server/Components/NPCs/NPC/npc.cpp | 125 +++++++++++++++- Server/Components/NPCs/NPC/npc.hpp | 14 ++ .../Components/Pawn/Scripting/NPC/Natives.cpp | 141 ++++++++++++++++++ 3 files changed, 278 insertions(+), 2 deletions(-) diff --git a/Server/Components/NPCs/NPC/npc.cpp b/Server/Components/NPCs/NPC/npc.cpp index 0aab1c4ad..35b5f55e4 100644 --- a/Server/Components/NPCs/NPC/npc.cpp +++ b/Server/Components/NPCs/NPC/npc.cpp @@ -17,6 +17,16 @@ #include "../Node/node.hpp" #include +namespace +{ +StaticArray DefaultWeaponAccuracyList = [] +{ + StaticArray list; + list.fill(1.0f); + return list; +}(); +} + NPC::NPC(NPCComponent* component, IPlayer* playerPtr) : footSyncSkipUpdate_(0) , driverSyncSkipUpdate_(0) @@ -103,8 +113,7 @@ NPC::NPC(NPCComponent* component, IPlayer* playerPtr) , nodeSetAngle_(true) , nodeLastPosition_(Vector3(0.0f, 0.0f, 0.0f)) { - // Fill weapon accuracy with 1.0f, let server devs change it with the desired values - weaponAccuracy_.fill(1.0f); + weaponAccuracy_ = DefaultWeaponAccuracyList; // Custom weapon info customWeaponInfoList_ = WeaponInfoList; @@ -1977,6 +1986,118 @@ IPlayer* NPC::getPlayerMovingTo() return followingPlayer_; } +int NPC::getClosestEntityInBetween(const Vector3& point, float range, EntityCheckType betweenCheckFlags, const Vector3& offsetFrom, int& entityType, int& playerObjectOwnerId, Vector3& hitMap) +{ + entityType = int(EntityCheckType::None); + playerObjectOwnerId = INVALID_PLAYER_ID; + hitMap = point; + + if (!npcComponent_) + { + return INVALID_PLAYER_ID; + } + + EntityCheckType resolvedEntityType = EntityCheckType::None; + Vector3 hitOrigin = getPosition() + offsetFrom; + int closestEntityId = ::getClosestEntityInBetween(npcComponent_, hitOrigin, point, range, betweenCheckFlags, getID(), INVALID_PLAYER_ID, resolvedEntityType, playerObjectOwnerId, hitMap); + entityType = int(resolvedEntityType); + return closestEntityId; +} + +void NPC::setPlaybackPath(StringView path) +{ + playbackPath_ = String(path); +} + +StringView NPC::getPlaybackPath() const +{ + return playbackPath_; +} + +void NPC::setWeaponInfo(uint8_t weapon, int reloadTime, int shootTime, int clipSize, float accuracy) +{ + if (reloadTime != -1) + { + setWeaponReloadTime(weapon, reloadTime); + } + if (shootTime != -1) + { + setWeaponShootTime(weapon, shootTime); + } + if (clipSize != -1) + { + setWeaponClipSize(weapon, clipSize); + } + setWeaponAccuracy(weapon, accuracy); +} + +bool NPC::getWeaponInfo(uint8_t weapon, int& reloadTime, int& shootTime, int& clipSize, float& accuracy) const +{ + auto data = WeaponSlotData(weapon); + if (weapon >= customWeaponInfoList_.size() || data.slot() == INVALID_WEAPON_SLOT) + { + return false; + } + + const WeaponInfo& info = customWeaponInfoList_[weapon]; + reloadTime = info.reloadTime; + shootTime = info.shootTime; + clipSize = info.clipSize; + accuracy = weaponAccuracy_[weapon]; + return true; +} + +bool NPC::setWeaponDefaultInfo(int weapon, int reloadTime, int shootTime, int clipSize, float accuracy) +{ + if (weapon < 0 || weapon >= MAX_WEAPON_ID) + { + return false; + } + + auto data = WeaponSlotData(weapon); + if (data.slot() == INVALID_WEAPON_SLOT) + { + return false; + } + + auto& defaultInfo = WeaponInfoList[weapon]; + if (reloadTime != -1) + { + defaultInfo.reloadTime = reloadTime; + } + if (shootTime != -1) + { + defaultInfo.shootTime = shootTime; + } + if (clipSize != -1) + { + defaultInfo.clipSize = clipSize; + } + DefaultWeaponAccuracyList[weapon] = accuracy; + return true; +} + +bool NPC::getWeaponDefaultInfo(int weapon, int& reloadTime, int& shootTime, int& clipSize, float& accuracy) +{ + if (weapon < 0 || weapon >= MAX_WEAPON_ID) + { + return false; + } + + auto data = WeaponSlotData(weapon); + if (data.slot() == INVALID_WEAPON_SLOT) + { + return false; + } + + const auto& defaultInfo = WeaponInfoList[weapon]; + reloadTime = defaultInfo.reloadTime; + shootTime = defaultInfo.shootTime; + clipSize = defaultInfo.clipSize; + accuracy = DefaultWeaponAccuracyList[weapon]; + return true; +} + void NPC::kill(IPlayer* killer, uint8_t weapon) { if (dead_) diff --git a/Server/Components/NPCs/NPC/npc.hpp b/Server/Components/NPCs/NPC/npc.hpp index e29345b6c..6b1635141 100644 --- a/Server/Components/NPCs/NPC/npc.hpp +++ b/Server/Components/NPCs/NPC/npc.hpp @@ -258,6 +258,20 @@ class NPC : public INPC, public PoolIDProvider, public NoCopy IPlayer* getPlayerMovingTo() override; + int getClosestEntityInBetween(const Vector3& point, float range, EntityCheckType betweenCheckFlags, const Vector3& offsetFrom, int& entityType, int& playerObjectOwnerId, Vector3& hitMap); + + void setPlaybackPath(StringView path); + + StringView getPlaybackPath() const; + + void setWeaponInfo(uint8_t weapon, int reloadTime, int shootTime, int clipSize, float accuracy); + + bool getWeaponInfo(uint8_t weapon, int& reloadTime, int& shootTime, int& clipSize, float& accuracy) const; + + static bool setWeaponDefaultInfo(int weapon, int reloadTime, int shootTime, int clipSize, float accuracy); + + static bool getWeaponDefaultInfo(int weapon, int& reloadTime, int& shootTime, int& clipSize, float& accuracy); + void setVehiclePosition(const Vector3& position, bool immediateUpdate) override; void setVehicleRotation(const GTAQuat& rotation, bool immediateUpdate) override; diff --git a/Server/Components/Pawn/Scripting/NPC/Natives.cpp b/Server/Components/Pawn/Scripting/NPC/Natives.cpp index a148d96ff..707236010 100644 --- a/Server/Components/Pawn/Scripting/NPC/Natives.cpp +++ b/Server/Components/Pawn/Scripting/NPC/Natives.cpp @@ -8,6 +8,7 @@ #include "../Types.hpp" #include "sdk.hpp" +#include "../../../NPCs/NPC/npc.hpp" #include #define _USE_MATH_DEFINES #include @@ -27,6 +28,11 @@ inline float getAngleOfLine(float x, float y) return angle; } +inline NPC& getNPCImpl(INPC& npc) +{ + return static_cast(npc); +} + SCRIPT_API(NPC_Create, int(const String& name)) { auto component = PawnManager::Get()->npcs; @@ -86,6 +92,12 @@ SCRIPT_API(NPC_GetPos, bool(INPC& npc, Vector3& position)) return true; } +SCRIPT_API(NPC_GivePos, bool(INPC& npc, Vector3 position)) +{ + npc.setPosition(npc.getPosition() + position, true); + return true; +} + SCRIPT_API(NPC_SetRot, bool(INPC& npc, Vector3 rotation)) { npc.setRotation(rotation, true); @@ -104,6 +116,33 @@ SCRIPT_API(NPC_GetRot, bool(INPC& npc, Vector3& rotation)) return true; } +SCRIPT_API(NPC_SetQuaternion, bool(INPC& npc, float w, float x, float y, float z)) +{ + npc.setRotation(GTAQuat(w, x, y, z), true); + return true; +} + +SCRIPT_API(NPC_GiveQuaternion, bool(INPC& npc, float w, float x, float y, float z)) +{ + GTAQuat rotation = npc.getRotation(); + rotation.q.w += w; + rotation.q.x += x; + rotation.q.y += y; + rotation.q.z += z; + npc.setRotation(rotation, true); + return true; +} + +SCRIPT_API(NPC_GetQuaternion, bool(INPC& npc, float& w, float& x, float& y, float& z)) +{ + glm::quat rotation = npc.getRotation().q; + w = rotation.w; + x = rotation.x; + y = rotation.y; + z = rotation.z; + return true; +} + SCRIPT_API(NPC_SetFacingAngle, bool(INPC& npc, float angle)) { auto rotation = npc.getRotation().ToEuler(); @@ -119,6 +158,14 @@ SCRIPT_API(NPC_GetFacingAngle, bool(INPC& npc, float& angle)) return true; } +SCRIPT_API(NPC_GiveFacingAngle, float(INPC& npc, float angle)) +{ + auto rotation = npc.getRotation().ToEuler(); + rotation.z += angle; + npc.setRotation(rotation, true); + return rotation.z; +} + SCRIPT_API(NPC_SetVirtualWorld, bool(INPC& npc, int virtualWorld)) { npc.setVirtualWorld(virtualWorld); @@ -242,6 +289,13 @@ SCRIPT_API(NPC_SetHealth, bool(INPC& npc, float health)) return true; } +SCRIPT_API(NPC_GiveHealth, float(INPC& npc, float health)) +{ + float newHealth = npc.getHealth() + health; + npc.setHealth(newHealth); + return newHealth; +} + SCRIPT_API(NPC_GetHealth, float(INPC& npc)) { return npc.getHealth(); @@ -253,6 +307,13 @@ SCRIPT_API(NPC_SetArmour, bool(INPC& npc, float armour)) return true; } +SCRIPT_API(NPC_GiveArmour, float(INPC& npc, float armour)) +{ + float newArmour = npc.getArmour() + armour; + npc.setArmour(newArmour); + return newArmour; +} + SCRIPT_API(NPC_GetArmour, float(INPC& npc)) { return npc.getArmour(); @@ -280,6 +341,13 @@ SCRIPT_API(NPC_SetAmmo, bool(INPC& npc, int ammo)) return true; } +SCRIPT_API(NPC_GiveAmmo, int(INPC& npc, int ammo)) +{ + int newAmmo = npc.getAmmo() + ammo; + npc.setAmmo(newAmmo); + return newAmmo; +} + SCRIPT_API(NPC_GetAmmo, int(INPC& npc)) { return npc.getAmmo(); @@ -303,6 +371,13 @@ SCRIPT_API(NPC_SetWeaponSkillLevel, bool(INPC& npc, uint8_t skill, int level)) return true; } +SCRIPT_API(NPC_GiveWeaponSkillLevel, int(INPC& npc, uint8_t skill, int level)) +{ + int newLevel = npc.getWeaponSkillLevel(PlayerWeaponSkill(skill)) + level; + npc.setWeaponSkillLevel(PlayerWeaponSkill(skill), newLevel); + return newLevel; +} + SCRIPT_API(NPC_GetWeaponSkillLevel, int(INPC& npc, int skill)) { return npc.getWeaponSkillLevel(PlayerWeaponSkill(skill)); @@ -374,6 +449,13 @@ SCRIPT_API(NPC_SetAmmoInClip, bool(INPC& npc, int ammo)) return true; } +SCRIPT_API(NPC_GiveAmmoInClip, int(INPC& npc, int ammo)) +{ + int newAmmo = npc.getAmmoInClip() + ammo; + npc.setAmmoInClip(newAmmo); + return newAmmo; +} + SCRIPT_API(NPC_GetAmmoInClip, int(INPC& npc)) { return npc.getAmmoInClip(); @@ -402,6 +484,13 @@ SCRIPT_API(NPC_AimAtPlayer, bool(INPC& npc, IPlayer& atPlayer, bool shoot, int s return true; } +SCRIPT_API(NPC_GetClosestEntityInBetween, bool(INPC& npc, Vector3 point, float range, int checkInBetweenMode, uint8_t checkInBetweenFlags, Vector3 offsetFrom, int& entityId, int& entityType, int& objectOwnerId, Vector3& hitPoint)) +{ + static_cast(checkInBetweenMode); + entityId = getNPCImpl(npc).getClosestEntityInBetween(point, range, EntityCheckType(checkInBetweenFlags), offsetFrom, entityType, objectOwnerId, hitPoint); + return true; +} + SCRIPT_API(NPC_StopAim, bool(INPC& npc)) { npc.stopAim(); @@ -472,6 +561,27 @@ SCRIPT_API(NPC_GetWeaponActualClipSize, int(INPC& npc, int weapon)) return npc.getWeaponActualClipSize(weapon); } +SCRIPT_API(NPC_SetWeaponInfo, bool(INPC& npc, int weapon, int reloadTime, int shootTime, int clipSize, float accuracy)) +{ + getNPCImpl(npc).setWeaponInfo(static_cast(weapon), reloadTime, shootTime, clipSize, accuracy); + return true; +} + +SCRIPT_API(NPC_GetWeaponInfo, bool(INPC& npc, int weapon, int& reloadTime, int& shootTime, int& clipSize, float& accuracy)) +{ + return getNPCImpl(npc).getWeaponInfo(static_cast(weapon), reloadTime, shootTime, clipSize, accuracy); +} + +SCRIPT_API(NPC_SetWeaponDefaultInfo, bool(int weapon, int reloadTime, int shootTime, int clipSize, float accuracy)) +{ + return NPC::setWeaponDefaultInfo(weapon, reloadTime, shootTime, clipSize, accuracy); +} + +SCRIPT_API(NPC_GetWeaponDefaultInfo, bool(int weapon, int& reloadTime, int& shootTime, int& clipSize, float& accuracy)) +{ + return NPC::getWeaponDefaultInfo(weapon, reloadTime, shootTime, clipSize, accuracy); +} + SCRIPT_API(NPC_EnterVehicle, bool(INPC& npc, IVehicle& vehicle, int seatId, int moveType)) { npc.enterVehicle(vehicle, seatId, NPCMoveType(moveType)); @@ -790,6 +900,23 @@ SCRIPT_API(NPC_IsPlaybackPaused, bool(INPC& npc)) return npc.isPlaybackPaused(); } +SCRIPT_API(NPC_SetPlaybackPath, bool(INPC& npc, const std::string& path)) +{ + if (path.empty()) + { + return false; + } + + getNPCImpl(npc).setPlaybackPath(path); + return true; +} + +SCRIPT_API(NPC_GetPlaybackPath, bool(INPC& npc, OutputOnlyString& path)) +{ + path = getNPCImpl(npc).getPlaybackPath(); + return true; +} + SCRIPT_API(NPC_LoadRecord, int(const std::string& filePath)) { auto component = PawnManager::Get()->npcs; @@ -1005,6 +1132,14 @@ SCRIPT_API(NPC_GetSurfingOffsets, bool(INPC& npc, Vector3& offset)) return true; } +SCRIPT_API(NPC_GiveSurfingOffsets, bool(INPC& npc, Vector3 offset)) +{ + auto data = npc.getSurfingData(); + data.offset += offset; + npc.setSurfingData(data); + return true; +} + SCRIPT_API(NPC_SetSurfingVehicle, bool(INPC& npc, IVehicle& vehicle)) { auto data = npc.getSurfingData(); @@ -1108,6 +1243,12 @@ SCRIPT_API(NPC_SetVelocity, bool(INPC& npc, Vector3 velocity)) return true; } +SCRIPT_API(NPC_GiveVelocity, bool(INPC& npc, Vector3 velocity, bool updatePos)) +{ + npc.setVelocity(npc.getVelocity() + velocity, updatePos); + return true; +} + SCRIPT_API(NPC_GetVelocity, bool(INPC& npc, Vector3& velocity)) { velocity = npc.getVelocity(); From 42945a6c26c0beb1e293d3c50b03c82092c6bb2d Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Thu, 12 Mar 2026 14:30:47 +0100 Subject: [PATCH 02/10] add more NPC callbacks --- SDK | 2 +- Server/Components/CAPI/Impl/NPCs/Events.hpp | 20 +++++++++++++++++++ Server/Components/NPCs/NPC/npc.cpp | 20 ++++++++++++++++--- .../Components/Pawn/Scripting/NPC/Events.hpp | 20 +++++++++++++++++++ 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/SDK b/SDK index 3ee7bc4ab..0a0231208 160000 --- a/SDK +++ b/SDK @@ -1 +1 @@ -Subproject commit 3ee7bc4ab20c22359c34c08c38f93815b44bffd5 +Subproject commit 0a023120824e7f04b9811a0ec9a13593869500b1 diff --git a/Server/Components/CAPI/Impl/NPCs/Events.hpp b/Server/Components/CAPI/Impl/NPCs/Events.hpp index c7a227450..92ada176c 100644 --- a/Server/Components/CAPI/Impl/NPCs/Events.hpp +++ b/Server/Components/CAPI/Impl/NPCs/Events.hpp @@ -58,6 +58,11 @@ struct NPCEvents : public NPCEventHandler, public Singleton> ComponentManager::Get()->CallEvent("onNPCRespawn", EventReturnHandler::None, &npc); } + void onNPCUpdate(INPC& npc) override + { + ComponentManager::Get()->CallEvent("onNPCUpdate", EventReturnHandler::None, &npc); + } + void onNPCPlaybackStart(INPC& npc, int recordId) override { ComponentManager::Get()->CallEvent("onNPCPlaybackStart", EventReturnHandler::None, &npc, recordId); @@ -68,6 +73,21 @@ struct NPCEvents : public NPCEventHandler, public Singleton> ComponentManager::Get()->CallEvent("onNPCPlaybackEnd", EventReturnHandler::None, &npc, recordId); } + void onNPCVehicleEntryComplete(INPC& npc, IVehicle& vehicle, int seatId) override + { + ComponentManager::Get()->CallEvent("onNPCVehicleEntryComplete", EventReturnHandler::None, &npc, &vehicle, seatId); + } + + void onNPCVehicleExitComplete(INPC& npc, IVehicle& vehicle) override + { + ComponentManager::Get()->CallEvent("onNPCVehicleExitComplete", EventReturnHandler::None, &npc, &vehicle); + } + + void onNPCChangeHeightPos(INPC& npc, float newZ, float oldZ) override + { + ComponentManager::Get()->CallEvent("onNPCChangeHeightPos", EventReturnHandler::None, &npc, newZ, oldZ); + } + bool onNPCShotMissed(INPC& npc, const PlayerBulletData& bulletData) override { return ComponentManager::Get()->CallEvent("onNPCShotMissed", EventReturnHandler::StopAtFalse, &npc, diff --git a/Server/Components/NPCs/NPC/npc.cpp b/Server/Components/NPCs/NPC/npc.cpp index 35b5f55e4..c962abfe0 100644 --- a/Server/Components/NPCs/NPC/npc.cpp +++ b/Server/Components/NPCs/NPC/npc.cpp @@ -2951,15 +2951,23 @@ void NPC::tick(Microseconds elapsed, TimePoint now) { if (duration_cast(now - vehicleEnterExitUpdateTime_).count() > (jackingVehicle_ ? 5800 : 2500)) { - if (vehicleToEnter_) + IVehicle* enteredVehicle = vehicleToEnter_; + int enteredSeat = vehicleSeatToEnter_; + bool entryCompleted = false; + if (enteredVehicle) { - putInVehicle(*vehicleToEnter_, vehicleSeatToEnter_); + entryCompleted = putInVehicle(*enteredVehicle, enteredSeat); } enteringVehicle_ = false; jackingVehicle_ = false; vehicleToEnter_ = nullptr; vehicleSeatToEnter_ = SEAT_NONE; + + if (entryCompleted) + { + npcComponent_->getEventDispatcher_internal().dispatch(&NPCEventHandler::onNPCVehicleEntryComplete, *this, *enteredVehicle, enteredSeat); + } } } @@ -3094,11 +3102,17 @@ void NPC::tick(Microseconds elapsed, TimePoint now) if (exitingVehicle_ && duration_cast(now - vehicleEnterExitUpdateTime_).count() > (1500)) { - removeFromVehicle(); + IVehicle* exitedVehicle = vehicle_; + bool exitCompleted = exitedVehicle && removeFromVehicle(); exitingVehicle_ = false; + if (exitCompleted) + { + npcComponent_->getEventDispatcher_internal().dispatch(&NPCEventHandler::onNPCVehicleExitComplete, *this, *exitedVehicle); + } } } + npcComponent_->getEventDispatcher_internal().dispatch(&NPCEventHandler::onNPCUpdate, *this); lastUpdate_ = now; } } diff --git a/Server/Components/Pawn/Scripting/NPC/Events.hpp b/Server/Components/Pawn/Scripting/NPC/Events.hpp index a86a179e1..c691eba61 100644 --- a/Server/Components/Pawn/Scripting/NPC/Events.hpp +++ b/Server/Components/Pawn/Scripting/NPC/Events.hpp @@ -59,6 +59,11 @@ struct NPCEvents : public NPCEventHandler, public Singleton PawnManager::Get()->CallAllInEntryFirst("OnNPCRespawn", DefaultReturnValue_True, npc.getID()); } + void onNPCUpdate(INPC& npc) override + { + PawnManager::Get()->CallAllInEntryFirst("OnNPCUpdate", DefaultReturnValue_True, npc.getID()); + } + void onNPCPlaybackStart(INPC& npc, int recordId) override { PawnManager::Get()->CallAllInEntryFirst("OnNPCPlaybackStart", DefaultReturnValue_True, npc.getID(), recordId); @@ -69,6 +74,21 @@ struct NPCEvents : public NPCEventHandler, public Singleton PawnManager::Get()->CallAllInEntryFirst("OnNPCPlaybackEnd", DefaultReturnValue_True, npc.getID(), recordId); } + void onNPCVehicleEntryComplete(INPC& npc, IVehicle& vehicle, int seatId) override + { + PawnManager::Get()->CallAllInEntryFirst("OnNPCVehicleEntryComplete", DefaultReturnValue_True, npc.getID(), vehicle.getID(), seatId); + } + + void onNPCVehicleExitComplete(INPC& npc, IVehicle& vehicle) override + { + PawnManager::Get()->CallAllInEntryFirst("OnNPCVehicleExitComplete", DefaultReturnValue_True, npc.getID(), vehicle.getID()); + } + + void onNPCChangeHeightPos(INPC& npc, float newZ, float oldZ) override + { + PawnManager::Get()->CallAllInEntryFirst("OnNPCChangeHeightPos", DefaultReturnValue_True, npc.getID(), newZ, oldZ); + } + bool onNPCShotMissed(INPC& npc, const PlayerBulletData& bulletData) override { cell ret = PawnManager::Get()->CallInSidesWhile1( From c8cae24d4219e5dcf83d3b2a8d3f8715d16b6844 Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Thu, 12 Mar 2026 14:30:47 +0100 Subject: [PATCH 03/10] add height pos callback and threshold --- Server/Components/NPCs/NPC/npc.cpp | 43 +++++++++++++++++-- Server/Components/NPCs/NPC/npc.hpp | 10 +++++ .../Components/Pawn/Scripting/NPC/Natives.cpp | 11 +++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/Server/Components/NPCs/NPC/npc.cpp b/Server/Components/NPCs/NPC/npc.cpp index c962abfe0..88fcc1dd7 100644 --- a/Server/Components/NPCs/NPC/npc.cpp +++ b/Server/Components/NPCs/NPC/npc.cpp @@ -36,6 +36,8 @@ NPC::NPC(NPCComponent* component, IPlayer* playerPtr) , keys_(0) , upAndDown_(0) , leftAndRight_(0) + , minHeightPosCall_(0.0f) + , lastHeightPosCall_(0.0f) , health_(100.0f) , armour_(0.0f) , animationId_(0) @@ -131,6 +133,7 @@ NPC::NPC(NPCComponent* component, IPlayer* playerPtr) // Initial entity values Vector3 initialPosition = position_ = { 0.0f, 0.0f, 3.5f }; + lastHeightPosCall_ = initialPosition.z; GTAQuat initialRotation = rotation_ = { 0.960891485f, 0.0f, 0.0f, 0.276925147f }; // Initial values for foot sync values @@ -199,6 +202,29 @@ Vector3 NPC::getPosition() const return position_; } +void NPC::setPositionValue(const Vector3& position) +{ + position_ = position; + processHeightPosChange(position.z); +} + +void NPC::processHeightPosChange(float newZ) +{ + if (minHeightPosCall_ <= 0.0f) + { + return; + } + + float oldZ = lastHeightPosCall_; + if (fabs(newZ - oldZ) < minHeightPosCall_) + { + return; + } + + lastHeightPosCall_ = newZ; + npcComponent_->getEventDispatcher_internal().dispatch(&NPCEventHandler::onNPCChangeHeightPos, *this, newZ, oldZ); +} + void NPC::setPosition(const Vector3& pos, bool immediateUpdate) { // Explicitly remove from vehicle if we are in one @@ -208,7 +234,7 @@ void NPC::setPosition(const Vector3& pos, bool immediateUpdate) } // Setting position right after removing from vehicle because removeFromVehicle also sets position - position_ = pos; + setPositionValue(pos); if (immediateUpdate) { @@ -225,7 +251,7 @@ void NPC::setVehiclePosition(const Vector3& position, bool immediateUpdate) { if (vehicle_ && vehicleSeat_ != SEAT_NONE) { - position_ = position; + setPositionValue(position); if (immediateUpdate) { if (vehicleSeat_ == 0) // driver @@ -289,6 +315,17 @@ void NPC::setVehicleRotation(const GTAQuat& rotation, bool immediateUpdate) } } +void NPC::setMinHeightPosCall(float height) +{ + minHeightPosCall_ = height > 0.0f ? height : 0.0f; + lastHeightPosCall_ = position_.z; +} + +float NPC::getMinHeightPosCall() const +{ + return minHeightPosCall_; +} + int NPC::getVirtualWorld() const { return player_->getVirtualWorld(); @@ -2710,7 +2747,7 @@ void NPC::advance(TimePoint now) { auto direction = toTarget / distanceToTarget; auto travelled = direction * velocityLength * deltaTimeMS; - position_ = position + travelled; + setPositionValue(position + travelled); } } diff --git a/Server/Components/NPCs/NPC/npc.hpp b/Server/Components/NPCs/NPC/npc.hpp index 6b1635141..aaefbbfbd 100644 --- a/Server/Components/NPCs/NPC/npc.hpp +++ b/Server/Components/NPCs/NPC/npc.hpp @@ -272,6 +272,10 @@ class NPC : public INPC, public PoolIDProvider, public NoCopy static bool getWeaponDefaultInfo(int weapon, int& reloadTime, int& shootTime, int& clipSize, float& accuracy); + void setMinHeightPosCall(float height); + + float getMinHeightPosCall() const; + void setVehiclePosition(const Vector3& position, bool immediateUpdate) override; void setVehicleRotation(const GTAQuat& rotation, bool immediateUpdate) override; @@ -302,6 +306,10 @@ class NPC : public INPC, public PoolIDProvider, public NoCopy void setAnimation(uint16_t animationId, uint16_t flags); + void setPositionValue(const Vector3& position); + + void processHeightPosChange(float newZ); + void processPlayback(TimePoint now); void updateWeaponState(); @@ -450,6 +458,8 @@ class NPC : public INPC, public PoolIDProvider, public NoCopy uint16_t leftAndRight_; Vector3 position_; GTAQuat rotation_; + float minHeightPosCall_; + float lastHeightPosCall_; float health_; float armour_; uint16_t animationId_; diff --git a/Server/Components/Pawn/Scripting/NPC/Natives.cpp b/Server/Components/Pawn/Scripting/NPC/Natives.cpp index 707236010..50f182f1a 100644 --- a/Server/Components/Pawn/Scripting/NPC/Natives.cpp +++ b/Server/Components/Pawn/Scripting/NPC/Natives.cpp @@ -98,6 +98,17 @@ SCRIPT_API(NPC_GivePos, bool(INPC& npc, Vector3 position)) return true; } +SCRIPT_API(NPC_SetMinHeightPosCall, bool(INPC& npc, float height)) +{ + getNPCImpl(npc).setMinHeightPosCall(height); + return true; +} + +SCRIPT_API(NPC_GetMinHeightPosCall, float(INPC& npc)) +{ + return getNPCImpl(npc).getMinHeightPosCall(); +} + SCRIPT_API(NPC_SetRot, bool(INPC& npc, Vector3 rotation)) { npc.setRotation(rotation, true); From 761a456e17521d74be48393aa2429a8fc993f176 Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Thu, 12 Mar 2026 14:30:47 +0100 Subject: [PATCH 04/10] fix build --- Server/Components/NPCs/NPC/npc.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Server/Components/NPCs/NPC/npc.cpp b/Server/Components/NPCs/NPC/npc.cpp index 88fcc1dd7..fa83e1904 100644 --- a/Server/Components/NPCs/NPC/npc.cpp +++ b/Server/Components/NPCs/NPC/npc.cpp @@ -19,6 +19,8 @@ namespace { +StaticArray DefaultWeaponInfoList = WeaponInfoList; + StaticArray DefaultWeaponAccuracyList = [] { StaticArray list; @@ -118,7 +120,7 @@ NPC::NPC(NPCComponent* component, IPlayer* playerPtr) weaponAccuracy_ = DefaultWeaponAccuracyList; // Custom weapon info - customWeaponInfoList_ = WeaponInfoList; + customWeaponInfoList_ = DefaultWeaponInfoList; // Keep a handle of NPC copmonent instance internally npcComponent_ = component; @@ -976,7 +978,7 @@ void NPC::shoot(int hitId, PlayerBulletHitType hitType, uint8_t weapon, const Ve bool playerIsNPC = false; // Pass original hit ID to correctly handle missed or out of range shots! - int closestEntityId = getClosestEntityInBetween(npcComponent_, bulletData.origin, bulletData.hitPos, std::min(range, targetDistance), betweenCheckFlags, poolID, hitId, closestEntityType, playerObjectOwnerId, hitMapPos); + int closestEntityId = ::getClosestEntityInBetween(npcComponent_, bulletData.origin, bulletData.hitPos, std::min(range, targetDistance), betweenCheckFlags, poolID, hitId, closestEntityType, playerObjectOwnerId, hitMapPos); // Just invalid anything, but INVALID_PLAYER_ID holds the value we want. if (closestEntityId != INVALID_PLAYER_ID) @@ -2097,7 +2099,7 @@ bool NPC::setWeaponDefaultInfo(int weapon, int reloadTime, int shootTime, int cl return false; } - auto& defaultInfo = WeaponInfoList[weapon]; + auto& defaultInfo = DefaultWeaponInfoList[weapon]; if (reloadTime != -1) { defaultInfo.reloadTime = reloadTime; @@ -2127,7 +2129,7 @@ bool NPC::getWeaponDefaultInfo(int weapon, int& reloadTime, int& shootTime, int& return false; } - const auto& defaultInfo = WeaponInfoList[weapon]; + const auto& defaultInfo = DefaultWeaponInfoList[weapon]; reloadTime = defaultInfo.reloadTime; shootTime = defaultInfo.shootTime; clipSize = defaultInfo.clipSize; From 37a745cd45d83c97f93ceab2c9f2c131d398db22 Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Thu, 12 Mar 2026 14:30:47 +0100 Subject: [PATCH 05/10] add npc move mode --- Server/Components/NPCs/NPC/npc.cpp | 26 +++++++++++++++++++ Server/Components/NPCs/NPC/npc.hpp | 5 ++++ .../Components/Pawn/Scripting/NPC/Natives.cpp | 10 +++++++ 3 files changed, 41 insertions(+) diff --git a/Server/Components/NPCs/NPC/npc.cpp b/Server/Components/NPCs/NPC/npc.cpp index fa83e1904..f1f2f3aa2 100644 --- a/Server/Components/NPCs/NPC/npc.cpp +++ b/Server/Components/NPCs/NPC/npc.cpp @@ -19,6 +19,11 @@ namespace { +constexpr int FCNPCMoveMode_Auto = -1; +constexpr int FCNPCMoveMode_None = 0; +constexpr int FCNPCMoveMode_MapAndreas = 1; +constexpr int FCNPCMoveMode_ColAndreas = 2; + StaticArray DefaultWeaponInfoList = WeaponInfoList; StaticArray DefaultWeaponAccuracyList = [] @@ -38,6 +43,7 @@ NPC::NPC(NPCComponent* component, IPlayer* playerPtr) , keys_(0) , upAndDown_(0) , leftAndRight_(0) + , moveMode_(FCNPCMoveMode_Auto) , minHeightPosCall_(0.0f) , lastHeightPosCall_(0.0f) , health_(100.0f) @@ -317,6 +323,26 @@ void NPC::setVehicleRotation(const GTAQuat& rotation, bool immediateUpdate) } } +bool NPC::setMoveMode(int mode) +{ + switch (mode) + { + case FCNPCMoveMode_Auto: + case FCNPCMoveMode_None: + case FCNPCMoveMode_MapAndreas: + case FCNPCMoveMode_ColAndreas: + moveMode_ = mode; + return true; + } + + return false; +} + +int NPC::getMoveMode() const +{ + return moveMode_; +} + void NPC::setMinHeightPosCall(float height) { minHeightPosCall_ = height > 0.0f ? height : 0.0f; diff --git a/Server/Components/NPCs/NPC/npc.hpp b/Server/Components/NPCs/NPC/npc.hpp index aaefbbfbd..a5e9303e2 100644 --- a/Server/Components/NPCs/NPC/npc.hpp +++ b/Server/Components/NPCs/NPC/npc.hpp @@ -272,6 +272,10 @@ class NPC : public INPC, public PoolIDProvider, public NoCopy static bool getWeaponDefaultInfo(int weapon, int& reloadTime, int& shootTime, int& clipSize, float& accuracy); + bool setMoveMode(int mode); + + int getMoveMode() const; + void setMinHeightPosCall(float height); float getMinHeightPosCall() const; @@ -456,6 +460,7 @@ class NPC : public INPC, public PoolIDProvider, public NoCopy uint16_t keys_; uint16_t upAndDown_; uint16_t leftAndRight_; + int moveMode_; Vector3 position_; GTAQuat rotation_; float minHeightPosCall_; diff --git a/Server/Components/Pawn/Scripting/NPC/Natives.cpp b/Server/Components/Pawn/Scripting/NPC/Natives.cpp index 50f182f1a..b0cf600b8 100644 --- a/Server/Components/Pawn/Scripting/NPC/Natives.cpp +++ b/Server/Components/Pawn/Scripting/NPC/Natives.cpp @@ -98,6 +98,16 @@ SCRIPT_API(NPC_GivePos, bool(INPC& npc, Vector3 position)) return true; } +SCRIPT_API(NPC_SetMoveMode, bool(INPC& npc, int mode)) +{ + return getNPCImpl(npc).setMoveMode(mode); +} + +SCRIPT_API(NPC_GetMoveMode, int(INPC& npc)) +{ + return getNPCImpl(npc).getMoveMode(); +} + SCRIPT_API(NPC_SetMinHeightPosCall, bool(INPC& npc, float height)) { getNPCImpl(npc).setMinHeightPosCall(height); From fede64c6ed75cf94088e7676a808e65b10858d80 Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Thu, 12 Mar 2026 14:31:06 +0100 Subject: [PATCH 06/10] add stream in and out callbacks --- SDK | 2 +- Server/Components/CAPI/Impl/NPCs/Events.hpp | 10 ++++++ Server/Components/NPCs/npcs_impl.cpp | 28 ++++++++++++++++ Server/Components/NPCs/npcs_impl.hpp | 33 ++++++++++++++++++- .../Components/Pawn/Scripting/NPC/Events.hpp | 10 ++++++ 5 files changed, 81 insertions(+), 2 deletions(-) diff --git a/SDK b/SDK index 0a0231208..70c51eef1 160000 --- a/SDK +++ b/SDK @@ -1 +1 @@ -Subproject commit 0a023120824e7f04b9811a0ec9a13593869500b1 +Subproject commit 70c51eef1952bdec389cf2b5a822ce670e60a078 diff --git a/Server/Components/CAPI/Impl/NPCs/Events.hpp b/Server/Components/CAPI/Impl/NPCs/Events.hpp index 92ada176c..4fcee7103 100644 --- a/Server/Components/CAPI/Impl/NPCs/Events.hpp +++ b/Server/Components/CAPI/Impl/NPCs/Events.hpp @@ -63,6 +63,16 @@ struct NPCEvents : public NPCEventHandler, public Singleton> ComponentManager::Get()->CallEvent("onNPCUpdate", EventReturnHandler::None, &npc); } + void onNPCStreamIn(INPC& npc, IPlayer& forPlayer) override + { + ComponentManager::Get()->CallEvent("onNPCStreamIn", EventReturnHandler::None, &npc, &forPlayer); + } + + void onNPCStreamOut(INPC& npc, IPlayer& forPlayer) override + { + ComponentManager::Get()->CallEvent("onNPCStreamOut", EventReturnHandler::None, &npc, &forPlayer); + } + void onNPCPlaybackStart(INPC& npc, int recordId) override { ComponentManager::Get()->CallEvent("onNPCPlaybackStart", EventReturnHandler::None, &npc, recordId); diff --git a/Server/Components/NPCs/npcs_impl.cpp b/Server/Components/NPCs/npcs_impl.cpp index 97c775cd9..1aa20b396 100644 --- a/Server/Components/NPCs/npcs_impl.cpp +++ b/Server/Components/NPCs/npcs_impl.cpp @@ -19,6 +19,7 @@ void NPCComponent::onInit(IComponentList* components) npcNetwork.init(core, this); core->getEventDispatcher().addEventHandler(this); core->getPlayers().getPlayerDamageDispatcher().addEventHandler(this); + core->getPlayers().getPlayerStreamDispatcher().addEventHandler(this); core->getPlayers().getPoolEventDispatcher().addEventHandler(this); if (components) @@ -45,6 +46,7 @@ void NPCComponent::free() core->getEventDispatcher().removeEventHandler(this); core->getPlayers().getPlayerDamageDispatcher().removeEventHandler(this); + core->getPlayers().getPlayerStreamDispatcher().removeEventHandler(this); core->getPlayers().getPoolEventDispatcher().removeEventHandler(this); if (vehicles) @@ -190,6 +192,32 @@ void NPCComponent::onPlayerTakeDamage(IPlayer& player, IPlayer* from, float amou } } +void NPCComponent::onPlayerStreamIn(IPlayer& player, IPlayer& forPlayer) +{ + auto npc = static_cast(get(player.getID())); + if (npc && npc->getPlayer()->getID() == player.getID()) + { + if (consumeSuppressedNPCStreamInEvent(npc->getID(), forPlayer.getID())) + { + return; + } + eventDispatcher.dispatch(&NPCEventHandler::onNPCStreamIn, *npc, forPlayer); + } +} + +void NPCComponent::onPlayerStreamOut(IPlayer& player, IPlayer& forPlayer) +{ + auto npc = static_cast(get(player.getID())); + if (npc && npc->getPlayer()->getID() == player.getID()) + { + if (consumeSuppressedNPCStreamOutEvent(npc->getID(), forPlayer.getID())) + { + return; + } + eventDispatcher.dispatch(&NPCEventHandler::onNPCStreamOut, *npc, forPlayer); + } +} + void NPCComponent::onPoolEntryDestroyed(IPlayer& player) { for (auto& _npc : storage) diff --git a/Server/Components/NPCs/npcs_impl.hpp b/Server/Components/NPCs/npcs_impl.hpp index 76b15dc2a..e5a60c928 100644 --- a/Server/Components/NPCs/npcs_impl.hpp +++ b/Server/Components/NPCs/npcs_impl.hpp @@ -21,7 +21,7 @@ using namespace Impl; -class NPCComponent final : public INPCComponent, public CoreEventHandler, public PlayerDamageEventHandler, public PoolEventHandler, public PoolEventHandler, VehicleEventHandler +class NPCComponent final : public INPCComponent, public CoreEventHandler, public PlayerDamageEventHandler, public PlayerStreamEventHandler, public PoolEventHandler, public PoolEventHandler, VehicleEventHandler { public: StringView componentName() const override @@ -82,6 +82,10 @@ class NPCComponent final : public INPCComponent, public CoreEventHandler, public void onPlayerTakeDamage(IPlayer& player, IPlayer* from, float amount, unsigned weapon, BodyPart part) override; + void onPlayerStreamIn(IPlayer& player, IPlayer& forPlayer) override; + + void onPlayerStreamOut(IPlayer& player, IPlayer& forPlayer) override; + void onPoolEntryDestroyed(IPlayer& player) override; void onPoolEntryDestroyed(IVehicle& vehicle) override; @@ -182,6 +186,26 @@ class NPCComponent final : public INPCComponent, public CoreEventHandler, public return eventDispatcher; } + void suppressNPCStreamInEvent(int npcId, int playerId) + { + suppressedNPCStreamInEvents_.insert(makeNPCStreamEventKey(npcId, playerId)); + } + + void suppressNPCStreamOutEvent(int npcId, int playerId) + { + suppressedNPCStreamOutEvents_.insert(makeNPCStreamEventKey(npcId, playerId)); + } + + bool consumeSuppressedNPCStreamInEvent(int npcId, int playerId) + { + return suppressedNPCStreamInEvents_.erase(makeNPCStreamEventKey(npcId, playerId)) != 0; + } + + bool consumeSuppressedNPCStreamOutEvent(int npcId, int playerId) + { + return suppressedNPCStreamOutEvents_.erase(makeNPCStreamEventKey(npcId, playerId)) != 0; + } + int getFootSyncRate() const { return *footSyncRate; @@ -298,11 +322,18 @@ class NPCComponent final : public INPCComponent, public CoreEventHandler, public } private: + static uint32_t makeNPCStreamEventKey(int npcId, int playerId) + { + return (uint32_t(uint16_t(npcId)) << 16) | uint16_t(playerId); + } + ICore* core = nullptr; NPCNetwork npcNetwork; DefaultEventDispatcher eventDispatcher; MarkedDynamicPoolStorage storage; bool shouldCallCustomEvents = true; + FlatHashSet suppressedNPCStreamInEvents_; + FlatHashSet suppressedNPCStreamOutEvents_; // Update rates int* generalNPCUpdateRateMS = nullptr; diff --git a/Server/Components/Pawn/Scripting/NPC/Events.hpp b/Server/Components/Pawn/Scripting/NPC/Events.hpp index c691eba61..17a775a08 100644 --- a/Server/Components/Pawn/Scripting/NPC/Events.hpp +++ b/Server/Components/Pawn/Scripting/NPC/Events.hpp @@ -64,6 +64,16 @@ struct NPCEvents : public NPCEventHandler, public Singleton PawnManager::Get()->CallAllInEntryFirst("OnNPCUpdate", DefaultReturnValue_True, npc.getID()); } + void onNPCStreamIn(INPC& npc, IPlayer& forPlayer) override + { + PawnManager::Get()->CallAllInEntryFirst("OnNPCStreamIn", DefaultReturnValue_True, npc.getID(), forPlayer.getID()); + } + + void onNPCStreamOut(INPC& npc, IPlayer& forPlayer) override + { + PawnManager::Get()->CallAllInEntryFirst("OnNPCStreamOut", DefaultReturnValue_True, npc.getID(), forPlayer.getID()); + } + void onNPCPlaybackStart(INPC& npc, int recordId) override { PawnManager::Get()->CallAllInEntryFirst("OnNPCPlaybackStart", DefaultReturnValue_True, npc.getID(), recordId); From f7a4228d40c95da0b5572afd1162d5f41b5bf1d9 Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Thu, 12 Mar 2026 14:31:06 +0100 Subject: [PATCH 07/10] add tab list visibility functions --- Server/Components/CAPI/Impl/NPCs/APIs.cpp | 15 ++++ Server/Components/NPCs/NPC/npc.cpp | 70 +++++++++++++++++++ Server/Components/NPCs/NPC/npc.hpp | 4 ++ .../Components/Pawn/Scripting/NPC/Natives.cpp | 10 +++ 4 files changed, 99 insertions(+) diff --git a/Server/Components/CAPI/Impl/NPCs/APIs.cpp b/Server/Components/CAPI/Impl/NPCs/APIs.cpp index 53c9c0baf..a1e6e59b1 100644 --- a/Server/Components/CAPI/Impl/NPCs/APIs.cpp +++ b/Server/Components/CAPI/Impl/NPCs/APIs.cpp @@ -8,6 +8,7 @@ #include "../ComponentManager.hpp" #include +#include "../../../NPCs/NPC/npc.hpp" OMP_CAPI(NPC_Create, objectPtr(StringCharPtr name, int* id)) { @@ -193,6 +194,20 @@ OMP_CAPI(NPC_IsAnyStreamedIn, bool(objectPtr npc)) return streamedIn.size() > 1; } +OMP_CAPI(NPC_ShowInTabListForPlayer, bool(objectPtr npc, objectPtr player)) +{ + POOL_ENTITY_RET(players, IPlayer, player, player_, false); + POOL_ENTITY_RET(npcs, INPC, npc, npc_, false); + return static_cast(*npc_).showInTabListForPlayer(*player_); +} + +OMP_CAPI(NPC_HideInTabListForPlayer, bool(objectPtr npc, objectPtr player)) +{ + POOL_ENTITY_RET(players, IPlayer, player, player_, false); + POOL_ENTITY_RET(npcs, INPC, npc, npc_, false); + return static_cast(*npc_).hideInTabListForPlayer(*player_); +} + OMP_CAPI(NPC_GetAll, int(int* npcsArr, int maxNPCs)) { COMPONENT_CHECK_RET(npcs, 0); diff --git a/Server/Components/NPCs/NPC/npc.cpp b/Server/Components/NPCs/NPC/npc.cpp index f1f2f3aa2..fbe94cb9e 100644 --- a/Server/Components/NPCs/NPC/npc.cpp +++ b/Server/Components/NPCs/NPC/npc.cpp @@ -663,6 +663,76 @@ const FlatPtrHashSet& NPC::streamedForPlayers() const return player_->streamedForPlayers(); } +bool NPC::showInTabListForPlayer(IPlayer& forPlayer) +{ + if (!player_ || forPlayer.getID() == getID()) + { + return false; + } + + const bool wasStreamedIn = player_->isStreamedInForPlayer(forPlayer); + if (wasStreamedIn) + { + npcComponent_->suppressNPCStreamOutEvent(getID(), forPlayer.getID()); + player_->streamOutForPlayer(forPlayer); + } + + NetCode::RPC::PlayerQuit playerQuitPacket; + playerQuitPacket.PlayerID = getID(); + playerQuitPacket.Reason = PeerDisconnectReason_Quit; + PacketHelper::send(playerQuitPacket, forPlayer); + + NetCode::RPC::PlayerJoin playerJoinPacket; + playerJoinPacket.PlayerID = getID(); + playerJoinPacket.Col = player_->getColour(); + playerJoinPacket.IsNPC = false; + playerJoinPacket.Name = player_->getName(); + PacketHelper::send(playerJoinPacket, forPlayer); + + if (wasStreamedIn) + { + npcComponent_->suppressNPCStreamInEvent(getID(), forPlayer.getID()); + player_->streamInForPlayer(forPlayer); + } + + return true; +} + +bool NPC::hideInTabListForPlayer(IPlayer& forPlayer) +{ + if (!player_ || forPlayer.getID() == getID()) + { + return false; + } + + const bool wasStreamedIn = player_->isStreamedInForPlayer(forPlayer); + if (wasStreamedIn) + { + npcComponent_->suppressNPCStreamOutEvent(getID(), forPlayer.getID()); + player_->streamOutForPlayer(forPlayer); + } + + NetCode::RPC::PlayerQuit playerQuitPacket; + playerQuitPacket.PlayerID = getID(); + playerQuitPacket.Reason = PeerDisconnectReason_Quit; + PacketHelper::send(playerQuitPacket, forPlayer); + + NetCode::RPC::PlayerJoin playerJoinPacket; + playerJoinPacket.PlayerID = getID(); + playerJoinPacket.Col = player_->getColour(); + playerJoinPacket.IsNPC = true; + playerJoinPacket.Name = player_->getName(); + PacketHelper::send(playerJoinPacket, forPlayer); + + if (wasStreamedIn) + { + npcComponent_->suppressNPCStreamInEvent(getID(), forPlayer.getID()); + player_->streamInForPlayer(forPlayer); + } + + return true; +} + void NPC::setInterior(unsigned int interior) { if (player_) diff --git a/Server/Components/NPCs/NPC/npc.hpp b/Server/Components/NPCs/NPC/npc.hpp index a5e9303e2..598b52c0a 100644 --- a/Server/Components/NPCs/NPC/npc.hpp +++ b/Server/Components/NPCs/NPC/npc.hpp @@ -272,6 +272,10 @@ class NPC : public INPC, public PoolIDProvider, public NoCopy static bool getWeaponDefaultInfo(int weapon, int& reloadTime, int& shootTime, int& clipSize, float& accuracy); + bool showInTabListForPlayer(IPlayer& forPlayer); + + bool hideInTabListForPlayer(IPlayer& forPlayer); + bool setMoveMode(int mode); int getMoveMode() const; diff --git a/Server/Components/Pawn/Scripting/NPC/Natives.cpp b/Server/Components/Pawn/Scripting/NPC/Natives.cpp index b0cf600b8..04ccecf00 100644 --- a/Server/Components/Pawn/Scripting/NPC/Natives.cpp +++ b/Server/Components/Pawn/Scripting/NPC/Natives.cpp @@ -266,6 +266,16 @@ SCRIPT_API(NPC_IsAnyStreamedIn, bool(INPC& npc)) return streamedIn.size() > 1; } +SCRIPT_API(NPC_ShowInTabListForPlayer, bool(INPC& npc, IPlayer& forPlayer)) +{ + return getNPCImpl(npc).showInTabListForPlayer(forPlayer); +} + +SCRIPT_API(NPC_HideInTabListForPlayer, bool(INPC& npc, IPlayer& forPlayer)) +{ + return getNPCImpl(npc).hideInTabListForPlayer(forPlayer); +} + SCRIPT_API(NPC_GetAll, int(DynamicArray& outputNPCs)) { int index = -1; From fc85655a5947f0325407323b474ad9895b51a5ef Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Thu, 12 Mar 2026 14:31:29 +0100 Subject: [PATCH 08/10] fix npc component interfaces --- SDK | 2 +- Server/Components/CAPI/Impl/NPCs/APIs.cpp | 5 ++- Server/Components/NPCs/NPC/npc.cpp | 3 +- Server/Components/NPCs/NPC/npc.hpp | 26 +++++++------- Server/Components/NPCs/npcs_impl.cpp | 10 ++++++ Server/Components/NPCs/npcs_impl.hpp | 4 +++ .../Components/Pawn/Scripting/NPC/Natives.cpp | 34 ++++++++----------- 7 files changed, 46 insertions(+), 38 deletions(-) diff --git a/SDK b/SDK index 70c51eef1..b9434af8f 160000 --- a/SDK +++ b/SDK @@ -1 +1 @@ -Subproject commit 70c51eef1952bdec389cf2b5a822ce670e60a078 +Subproject commit b9434af8f3bbe23ee30f36a6159a1b4406954d64 diff --git a/Server/Components/CAPI/Impl/NPCs/APIs.cpp b/Server/Components/CAPI/Impl/NPCs/APIs.cpp index a1e6e59b1..622b58cfe 100644 --- a/Server/Components/CAPI/Impl/NPCs/APIs.cpp +++ b/Server/Components/CAPI/Impl/NPCs/APIs.cpp @@ -8,7 +8,6 @@ #include "../ComponentManager.hpp" #include -#include "../../../NPCs/NPC/npc.hpp" OMP_CAPI(NPC_Create, objectPtr(StringCharPtr name, int* id)) { @@ -198,14 +197,14 @@ OMP_CAPI(NPC_ShowInTabListForPlayer, bool(objectPtr npc, objectPtr player)) { POOL_ENTITY_RET(players, IPlayer, player, player_, false); POOL_ENTITY_RET(npcs, INPC, npc, npc_, false); - return static_cast(*npc_).showInTabListForPlayer(*player_); + return npc_->showInTabListForPlayer(*player_); } OMP_CAPI(NPC_HideInTabListForPlayer, bool(objectPtr npc, objectPtr player)) { POOL_ENTITY_RET(players, IPlayer, player, player_, false); POOL_ENTITY_RET(npcs, INPC, npc, npc_, false); - return static_cast(*npc_).hideInTabListForPlayer(*player_); + return npc_->hideInTabListForPlayer(*player_); } OMP_CAPI(NPC_GetAll, int(int* npcsArr, int maxNPCs)) diff --git a/Server/Components/NPCs/NPC/npc.cpp b/Server/Components/NPCs/NPC/npc.cpp index fbe94cb9e..45abc03ec 100644 --- a/Server/Components/NPCs/NPC/npc.cpp +++ b/Server/Components/NPCs/NPC/npc.cpp @@ -758,7 +758,7 @@ unsigned int NPC::getInterior() const Vector3 NPC::getVelocity() const { - return player_->getPosition(); + return velocity_; } void NPC::setVelocity(Vector3 velocity, bool update) @@ -843,7 +843,6 @@ void NPC::setAmmo(int ammo) ammoInClip_ = ammo_; } updateWeaponState(); - setAmmoInClip(ammo); } int NPC::getAmmo() const diff --git a/Server/Components/NPCs/NPC/npc.hpp b/Server/Components/NPCs/NPC/npc.hpp index 598b52c0a..af33ba5c4 100644 --- a/Server/Components/NPCs/NPC/npc.hpp +++ b/Server/Components/NPCs/NPC/npc.hpp @@ -258,31 +258,31 @@ class NPC : public INPC, public PoolIDProvider, public NoCopy IPlayer* getPlayerMovingTo() override; - int getClosestEntityInBetween(const Vector3& point, float range, EntityCheckType betweenCheckFlags, const Vector3& offsetFrom, int& entityType, int& playerObjectOwnerId, Vector3& hitMap); + SDK_EXPORT int getClosestEntityInBetween(const Vector3& point, float range, EntityCheckType betweenCheckFlags, const Vector3& offsetFrom, int& entityType, int& playerObjectOwnerId, Vector3& hitMap) override; - void setPlaybackPath(StringView path); + SDK_EXPORT void setPlaybackPath(StringView path) override; - StringView getPlaybackPath() const; + SDK_EXPORT StringView getPlaybackPath() const override; - void setWeaponInfo(uint8_t weapon, int reloadTime, int shootTime, int clipSize, float accuracy); + SDK_EXPORT void setWeaponInfo(uint8_t weapon, int reloadTime, int shootTime, int clipSize, float accuracy) override; - bool getWeaponInfo(uint8_t weapon, int& reloadTime, int& shootTime, int& clipSize, float& accuracy) const; + SDK_EXPORT bool getWeaponInfo(uint8_t weapon, int& reloadTime, int& shootTime, int& clipSize, float& accuracy) const override; - static bool setWeaponDefaultInfo(int weapon, int reloadTime, int shootTime, int clipSize, float accuracy); + SDK_EXPORT static bool setWeaponDefaultInfo(int weapon, int reloadTime, int shootTime, int clipSize, float accuracy); - static bool getWeaponDefaultInfo(int weapon, int& reloadTime, int& shootTime, int& clipSize, float& accuracy); + SDK_EXPORT static bool getWeaponDefaultInfo(int weapon, int& reloadTime, int& shootTime, int& clipSize, float& accuracy); - bool showInTabListForPlayer(IPlayer& forPlayer); + SDK_EXPORT bool showInTabListForPlayer(IPlayer& forPlayer) override; - bool hideInTabListForPlayer(IPlayer& forPlayer); + SDK_EXPORT bool hideInTabListForPlayer(IPlayer& forPlayer) override; - bool setMoveMode(int mode); + SDK_EXPORT bool setMoveMode(int mode) override; - int getMoveMode() const; + SDK_EXPORT int getMoveMode() const override; - void setMinHeightPosCall(float height); + SDK_EXPORT void setMinHeightPosCall(float height) override; - float getMinHeightPosCall() const; + SDK_EXPORT float getMinHeightPosCall() const override; void setVehiclePosition(const Vector3& position, bool immediateUpdate) override; diff --git a/Server/Components/NPCs/npcs_impl.cpp b/Server/Components/NPCs/npcs_impl.cpp index 1aa20b396..f643aa5a2 100644 --- a/Server/Components/NPCs/npcs_impl.cpp +++ b/Server/Components/NPCs/npcs_impl.cpp @@ -519,6 +519,16 @@ bool NPCComponent::getNodeInfo(int nodeId, uint32_t& vehicleNodes, uint32_t& ped return false; } +bool NPCComponent::setWeaponDefaultInfo(int weapon, int reloadTime, int shootTime, int clipSize, float accuracy) +{ + return NPC::setWeaponDefaultInfo(weapon, reloadTime, shootTime, clipSize, accuracy); +} + +bool NPCComponent::getWeaponDefaultInfo(int weapon, int& reloadTime, int& shootTime, int& clipSize, float& accuracy) +{ + return NPC::getWeaponDefaultInfo(weapon, reloadTime, shootTime, clipSize, accuracy); +} + bool NPCComponent::emulatePlayerGiveDamageToNPCEvent(IPlayer& player, INPC& npc, float amount, unsigned weapon, BodyPart part, bool callOriginalEvents) { bool eventResult = eventDispatcher.stopAtFalse([&](NPCEventHandler* handler) diff --git a/Server/Components/NPCs/npcs_impl.hpp b/Server/Components/NPCs/npcs_impl.hpp index e5a60c928..af69c7f62 100644 --- a/Server/Components/NPCs/npcs_impl.hpp +++ b/Server/Components/NPCs/npcs_impl.hpp @@ -153,6 +153,10 @@ class NPCComponent final : public INPCComponent, public CoreEventHandler, public /// Get node information (vehicle nodes, pedestrian nodes, navigation nodes) bool getNodeInfo(int nodeId, uint32_t& vehicleNodes, uint32_t& pedNodes, uint32_t& naviNodes) override; + bool setWeaponDefaultInfo(int weapon, int reloadTime, int shootTime, int clipSize, float accuracy) override; + + bool getWeaponDefaultInfo(int weapon, int& reloadTime, int& shootTime, int& clipSize, float& accuracy) override; + bool emulatePlayerGiveDamageToNPCEvent(IPlayer& player, INPC& npc, float amount, unsigned weapon, BodyPart part, bool callOriginalEvents); bool emulatePlayerTakeDamageFromNPCEvent(IPlayer& player, INPC& npc, float amount, unsigned weapon, BodyPart part, bool callOriginalEvents); diff --git a/Server/Components/Pawn/Scripting/NPC/Natives.cpp b/Server/Components/Pawn/Scripting/NPC/Natives.cpp index 04ccecf00..20f1764e8 100644 --- a/Server/Components/Pawn/Scripting/NPC/Natives.cpp +++ b/Server/Components/Pawn/Scripting/NPC/Natives.cpp @@ -8,7 +8,6 @@ #include "../Types.hpp" #include "sdk.hpp" -#include "../../../NPCs/NPC/npc.hpp" #include #define _USE_MATH_DEFINES #include @@ -28,11 +27,6 @@ inline float getAngleOfLine(float x, float y) return angle; } -inline NPC& getNPCImpl(INPC& npc) -{ - return static_cast(npc); -} - SCRIPT_API(NPC_Create, int(const String& name)) { auto component = PawnManager::Get()->npcs; @@ -100,23 +94,23 @@ SCRIPT_API(NPC_GivePos, bool(INPC& npc, Vector3 position)) SCRIPT_API(NPC_SetMoveMode, bool(INPC& npc, int mode)) { - return getNPCImpl(npc).setMoveMode(mode); + return npc.setMoveMode(mode); } SCRIPT_API(NPC_GetMoveMode, int(INPC& npc)) { - return getNPCImpl(npc).getMoveMode(); + return npc.getMoveMode(); } SCRIPT_API(NPC_SetMinHeightPosCall, bool(INPC& npc, float height)) { - getNPCImpl(npc).setMinHeightPosCall(height); + npc.setMinHeightPosCall(height); return true; } SCRIPT_API(NPC_GetMinHeightPosCall, float(INPC& npc)) { - return getNPCImpl(npc).getMinHeightPosCall(); + return npc.getMinHeightPosCall(); } SCRIPT_API(NPC_SetRot, bool(INPC& npc, Vector3 rotation)) @@ -268,12 +262,12 @@ SCRIPT_API(NPC_IsAnyStreamedIn, bool(INPC& npc)) SCRIPT_API(NPC_ShowInTabListForPlayer, bool(INPC& npc, IPlayer& forPlayer)) { - return getNPCImpl(npc).showInTabListForPlayer(forPlayer); + return npc.showInTabListForPlayer(forPlayer); } SCRIPT_API(NPC_HideInTabListForPlayer, bool(INPC& npc, IPlayer& forPlayer)) { - return getNPCImpl(npc).hideInTabListForPlayer(forPlayer); + return npc.hideInTabListForPlayer(forPlayer); } SCRIPT_API(NPC_GetAll, int(DynamicArray& outputNPCs)) @@ -518,7 +512,7 @@ SCRIPT_API(NPC_AimAtPlayer, bool(INPC& npc, IPlayer& atPlayer, bool shoot, int s SCRIPT_API(NPC_GetClosestEntityInBetween, bool(INPC& npc, Vector3 point, float range, int checkInBetweenMode, uint8_t checkInBetweenFlags, Vector3 offsetFrom, int& entityId, int& entityType, int& objectOwnerId, Vector3& hitPoint)) { static_cast(checkInBetweenMode); - entityId = getNPCImpl(npc).getClosestEntityInBetween(point, range, EntityCheckType(checkInBetweenFlags), offsetFrom, entityType, objectOwnerId, hitPoint); + entityId = npc.getClosestEntityInBetween(point, range, EntityCheckType(checkInBetweenFlags), offsetFrom, entityType, objectOwnerId, hitPoint); return true; } @@ -594,23 +588,25 @@ SCRIPT_API(NPC_GetWeaponActualClipSize, int(INPC& npc, int weapon)) SCRIPT_API(NPC_SetWeaponInfo, bool(INPC& npc, int weapon, int reloadTime, int shootTime, int clipSize, float accuracy)) { - getNPCImpl(npc).setWeaponInfo(static_cast(weapon), reloadTime, shootTime, clipSize, accuracy); + npc.setWeaponInfo(static_cast(weapon), reloadTime, shootTime, clipSize, accuracy); return true; } SCRIPT_API(NPC_GetWeaponInfo, bool(INPC& npc, int weapon, int& reloadTime, int& shootTime, int& clipSize, float& accuracy)) { - return getNPCImpl(npc).getWeaponInfo(static_cast(weapon), reloadTime, shootTime, clipSize, accuracy); + return npc.getWeaponInfo(static_cast(weapon), reloadTime, shootTime, clipSize, accuracy); } SCRIPT_API(NPC_SetWeaponDefaultInfo, bool(int weapon, int reloadTime, int shootTime, int clipSize, float accuracy)) { - return NPC::setWeaponDefaultInfo(weapon, reloadTime, shootTime, clipSize, accuracy); + auto component = PawnManager::Get()->npcs; + return component ? component->setWeaponDefaultInfo(weapon, reloadTime, shootTime, clipSize, accuracy) : false; } SCRIPT_API(NPC_GetWeaponDefaultInfo, bool(int weapon, int& reloadTime, int& shootTime, int& clipSize, float& accuracy)) { - return NPC::getWeaponDefaultInfo(weapon, reloadTime, shootTime, clipSize, accuracy); + auto component = PawnManager::Get()->npcs; + return component ? component->getWeaponDefaultInfo(weapon, reloadTime, shootTime, clipSize, accuracy) : false; } SCRIPT_API(NPC_EnterVehicle, bool(INPC& npc, IVehicle& vehicle, int seatId, int moveType)) @@ -938,13 +934,13 @@ SCRIPT_API(NPC_SetPlaybackPath, bool(INPC& npc, const std::string& path)) return false; } - getNPCImpl(npc).setPlaybackPath(path); + npc.setPlaybackPath(path); return true; } SCRIPT_API(NPC_GetPlaybackPath, bool(INPC& npc, OutputOnlyString& path)) { - path = getNPCImpl(npc).getPlaybackPath(); + path = npc.getPlaybackPath(); return true; } From a6b40f42a5b49697820b658c8a66c1d2ecfcefc6 Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Thu, 12 Mar 2026 14:31:38 +0100 Subject: [PATCH 09/10] add vehicle damage callback --- SDK | 2 +- Server/Components/CAPI/Impl/NPCs/Events.hpp | 6 +++ Server/Components/NPCs/npcs_impl.cpp | 54 +++++++++++++++++++ Server/Components/NPCs/npcs_impl.hpp | 4 +- .../Components/Pawn/Scripting/NPC/Events.hpp | 7 +++ 5 files changed, 71 insertions(+), 2 deletions(-) diff --git a/SDK b/SDK index b9434af8f..8e06ac050 160000 --- a/SDK +++ b/SDK @@ -1 +1 @@ -Subproject commit b9434af8f3bbe23ee30f36a6159a1b4406954d64 +Subproject commit 8e06ac05053ba8bc278669ddc9d2a62218ebd2fd diff --git a/Server/Components/CAPI/Impl/NPCs/Events.hpp b/Server/Components/CAPI/Impl/NPCs/Events.hpp index 4fcee7103..fcd469870 100644 --- a/Server/Components/CAPI/Impl/NPCs/Events.hpp +++ b/Server/Components/CAPI/Impl/NPCs/Events.hpp @@ -93,6 +93,12 @@ struct NPCEvents : public NPCEventHandler, public Singleton> ComponentManager::Get()->CallEvent("onNPCVehicleExitComplete", EventReturnHandler::None, &npc, &vehicle); } + bool onNPCVehicleTakeDamage(INPC& npc, IPlayer& issuer, IVehicle& vehicle, float damage, uint8_t weapon, const Vector3& hitPos) override + { + return ComponentManager::Get()->CallEvent("onNPCVehicleTakeDamage", EventReturnHandler::StopAtFalse, &npc, &issuer, &vehicle, + damage, int(weapon), hitPos.x, hitPos.y, hitPos.z); + } + void onNPCChangeHeightPos(INPC& npc, float newZ, float oldZ) override { ComponentManager::Get()->CallEvent("onNPCChangeHeightPos", EventReturnHandler::None, &npc, newZ, oldZ); diff --git a/Server/Components/NPCs/npcs_impl.cpp b/Server/Components/NPCs/npcs_impl.cpp index f643aa5a2..ea3b76e09 100644 --- a/Server/Components/NPCs/npcs_impl.cpp +++ b/Server/Components/NPCs/npcs_impl.cpp @@ -8,6 +8,7 @@ #include "./npcs_impl.hpp" #include +#include "./utils.hpp" void NPCComponent::onLoad(ICore* c) { @@ -19,6 +20,7 @@ void NPCComponent::onInit(IComponentList* components) npcNetwork.init(core, this); core->getEventDispatcher().addEventHandler(this); core->getPlayers().getPlayerDamageDispatcher().addEventHandler(this); + core->getPlayers().getPlayerShotDispatcher().addEventHandler(this); core->getPlayers().getPlayerStreamDispatcher().addEventHandler(this); core->getPlayers().getPoolEventDispatcher().addEventHandler(this); @@ -46,6 +48,7 @@ void NPCComponent::free() core->getEventDispatcher().removeEventHandler(this); core->getPlayers().getPlayerDamageDispatcher().removeEventHandler(this); + core->getPlayers().getPlayerShotDispatcher().removeEventHandler(this); core->getPlayers().getPlayerStreamDispatcher().removeEventHandler(this); core->getPlayers().getPoolEventDispatcher().removeEventHandler(this); @@ -192,6 +195,57 @@ void NPCComponent::onPlayerTakeDamage(IPlayer& player, IPlayer* from, float amou } } +bool NPCComponent::onPlayerShotVehicle(IPlayer& player, IVehicle& target, const PlayerBulletData& bulletData) +{ + if (!shouldCallCustomEvents) + { + return true; + } + + IPlayer* driver = target.getDriver(); + if (!driver || !driver->isBot()) + { + return true; + } + + auto npc = static_cast(get(driver->getID())); + if (!npc || npc->getPlayer()->getID() != driver->getID()) + { + return true; + } + + const uint8_t weapon = bulletData.weapon; + const float damage = weapon < MAX_WEAPON_ID ? WeaponDamages[weapon] : 0.0f; + + shouldCallCustomEvents = false; + const bool eventResult = eventDispatcher.stopAtFalse( + [&](NPCEventHandler* handler) + { + return handler->onNPCVehicleTakeDamage(*npc, player, target, damage, weapon, bulletData.hitPos); + }); + shouldCallCustomEvents = true; + + if (!eventResult) + { + return false; + } + + float health = npc->getVehicleHealth(); + if (health <= 0.0f) + { + health = target.getHealth(); + } + + health -= damage; + if (health < 0.0f) + { + health = 0.0f; + } + + npc->setVehicleHealth(health); + return true; +} + void NPCComponent::onPlayerStreamIn(IPlayer& player, IPlayer& forPlayer) { auto npc = static_cast(get(player.getID())); diff --git a/Server/Components/NPCs/npcs_impl.hpp b/Server/Components/NPCs/npcs_impl.hpp index af69c7f62..ef19a25f6 100644 --- a/Server/Components/NPCs/npcs_impl.hpp +++ b/Server/Components/NPCs/npcs_impl.hpp @@ -21,7 +21,7 @@ using namespace Impl; -class NPCComponent final : public INPCComponent, public CoreEventHandler, public PlayerDamageEventHandler, public PlayerStreamEventHandler, public PoolEventHandler, public PoolEventHandler, VehicleEventHandler +class NPCComponent final : public INPCComponent, public CoreEventHandler, public PlayerDamageEventHandler, public PlayerShotEventHandler, public PlayerStreamEventHandler, public PoolEventHandler, public PoolEventHandler, VehicleEventHandler { public: StringView componentName() const override @@ -82,6 +82,8 @@ class NPCComponent final : public INPCComponent, public CoreEventHandler, public void onPlayerTakeDamage(IPlayer& player, IPlayer* from, float amount, unsigned weapon, BodyPart part) override; + bool onPlayerShotVehicle(IPlayer& player, IVehicle& target, const PlayerBulletData& bulletData) override; + void onPlayerStreamIn(IPlayer& player, IPlayer& forPlayer) override; void onPlayerStreamOut(IPlayer& player, IPlayer& forPlayer) override; diff --git a/Server/Components/Pawn/Scripting/NPC/Events.hpp b/Server/Components/Pawn/Scripting/NPC/Events.hpp index 17a775a08..99046323e 100644 --- a/Server/Components/Pawn/Scripting/NPC/Events.hpp +++ b/Server/Components/Pawn/Scripting/NPC/Events.hpp @@ -94,6 +94,13 @@ struct NPCEvents : public NPCEventHandler, public Singleton PawnManager::Get()->CallAllInEntryFirst("OnNPCVehicleExitComplete", DefaultReturnValue_True, npc.getID(), vehicle.getID()); } + bool onNPCVehicleTakeDamage(INPC& npc, IPlayer& issuer, IVehicle& vehicle, float damage, uint8_t weapon, const Vector3& hitPos) override + { + auto result = !!PawnManager::Get()->CallAllInEntryFirst("OnNPCVehicleTakeDamage", DefaultReturnValue_True, + npc.getID(), issuer.getID(), vehicle.getID(), damage, weapon, hitPos.x, hitPos.y, hitPos.z); + return result; + } + void onNPCChangeHeightPos(INPC& npc, float newZ, float oldZ) override { PawnManager::Get()->CallAllInEntryFirst("OnNPCChangeHeightPos", DefaultReturnValue_True, npc.getID(), newZ, oldZ); From dfa27357ab3df97ceccbdfd75a9d6b77ad8adf4b Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Thu, 12 Mar 2026 14:31:38 +0100 Subject: [PATCH 10/10] comment mapandreas and colandreas dependency --- Server/Components/NPCs/NPC/npc.cpp | 4 ++++ Server/Components/Pawn/Scripting/NPC/Natives.cpp | 2 ++ 2 files changed, 6 insertions(+) diff --git a/Server/Components/NPCs/NPC/npc.cpp b/Server/Components/NPCs/NPC/npc.cpp index 45abc03ec..7d33e33d5 100644 --- a/Server/Components/NPCs/NPC/npc.cpp +++ b/Server/Components/NPCs/NPC/npc.cpp @@ -325,6 +325,10 @@ void NPC::setVehicleRotation(const GTAQuat& rotation, bool immediateUpdate) bool NPC::setMoveMode(int mode) { + // Preserve FCNPC-compatible values, but do not pretend that open.mp has a + // real MapAndreas/ColAndreas movement backend switch yet. Values 1/2 are + // currently stored as compatibility state only; the external backend + // dependency remains unresolved. switch (mode) { case FCNPCMoveMode_Auto: diff --git a/Server/Components/Pawn/Scripting/NPC/Natives.cpp b/Server/Components/Pawn/Scripting/NPC/Natives.cpp index 20f1764e8..10631a945 100644 --- a/Server/Components/Pawn/Scripting/NPC/Natives.cpp +++ b/Server/Components/Pawn/Scripting/NPC/Natives.cpp @@ -94,6 +94,8 @@ SCRIPT_API(NPC_GivePos, bool(INPC& npc, Vector3 position)) SCRIPT_API(NPC_SetMoveMode, bool(INPC& npc, int mode)) { + // FCNPC compatibility surface only. Modes 1/2 currently do not activate a + // real MapAndreas/ColAndreas backend in open.mp. return npc.setMoveMode(mode); }