diff --git a/SDK b/SDK index 3ee7bc4ab..8e06ac050 160000 --- a/SDK +++ b/SDK @@ -1 +1 @@ -Subproject commit 3ee7bc4ab20c22359c34c08c38f93815b44bffd5 +Subproject commit 8e06ac05053ba8bc278669ddc9d2a62218ebd2fd diff --git a/Server/Components/CAPI/Impl/NPCs/APIs.cpp b/Server/Components/CAPI/Impl/NPCs/APIs.cpp index 53c9c0baf..622b58cfe 100644 --- a/Server/Components/CAPI/Impl/NPCs/APIs.cpp +++ b/Server/Components/CAPI/Impl/NPCs/APIs.cpp @@ -193,6 +193,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 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 npc_->hideInTabListForPlayer(*player_); +} + OMP_CAPI(NPC_GetAll, int(int* npcsArr, int maxNPCs)) { COMPONENT_CHECK_RET(npcs, 0); diff --git a/Server/Components/CAPI/Impl/NPCs/Events.hpp b/Server/Components/CAPI/Impl/NPCs/Events.hpp index c7a227450..fcd469870 100644 --- a/Server/Components/CAPI/Impl/NPCs/Events.hpp +++ b/Server/Components/CAPI/Impl/NPCs/Events.hpp @@ -58,6 +58,21 @@ 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 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); @@ -68,6 +83,27 @@ 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); + } + + 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); + } + 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 0aab1c4ad..7d33e33d5 100644 --- a/Server/Components/NPCs/NPC/npc.cpp +++ b/Server/Components/NPCs/NPC/npc.cpp @@ -17,6 +17,23 @@ #include "../Node/node.hpp" #include +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 = [] +{ + StaticArray list; + list.fill(1.0f); + return list; +}(); +} + NPC::NPC(NPCComponent* component, IPlayer* playerPtr) : footSyncSkipUpdate_(0) , driverSyncSkipUpdate_(0) @@ -26,6 +43,9 @@ NPC::NPC(NPCComponent* component, IPlayer* playerPtr) , keys_(0) , upAndDown_(0) , leftAndRight_(0) + , moveMode_(FCNPCMoveMode_Auto) + , minHeightPosCall_(0.0f) + , lastHeightPosCall_(0.0f) , health_(100.0f) , armour_(0.0f) , animationId_(0) @@ -103,11 +123,10 @@ 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; + customWeaponInfoList_ = DefaultWeaponInfoList; // Keep a handle of NPC copmonent instance internally npcComponent_ = component; @@ -122,6 +141,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 @@ -190,6 +210,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 @@ -199,7 +242,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) { @@ -216,7 +259,7 @@ void NPC::setVehiclePosition(const Vector3& position, bool immediateUpdate) { if (vehicle_ && vehicleSeat_ != SEAT_NONE) { - position_ = position; + setPositionValue(position); if (immediateUpdate) { if (vehicleSeat_ == 0) // driver @@ -280,6 +323,41 @@ 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: + 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; + lastHeightPosCall_ = position_.z; +} + +float NPC::getMinHeightPosCall() const +{ + return minHeightPosCall_; +} + int NPC::getVirtualWorld() const { return player_->getVirtualWorld(); @@ -589,6 +667,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_) @@ -614,7 +762,7 @@ unsigned int NPC::getInterior() const Vector3 NPC::getVelocity() const { - return player_->getPosition(); + return velocity_; } void NPC::setVelocity(Vector3 velocity, bool update) @@ -699,7 +847,6 @@ void NPC::setAmmo(int ammo) ammoInClip_ = ammo_; } updateWeaponState(); - setAmmoInClip(ammo); } int NPC::getAmmo() const @@ -930,7 +1077,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) @@ -1977,6 +2124,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 = DefaultWeaponInfoList[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 = DefaultWeaponInfoList[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_) @@ -2589,7 +2848,7 @@ void NPC::advance(TimePoint now) { auto direction = toTarget / distanceToTarget; auto travelled = direction * velocityLength * deltaTimeMS; - position_ = position + travelled; + setPositionValue(position + travelled); } } @@ -2830,15 +3089,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); + } } } @@ -2973,11 +3240,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/NPCs/NPC/npc.hpp b/Server/Components/NPCs/NPC/npc.hpp index e29345b6c..af33ba5c4 100644 --- a/Server/Components/NPCs/NPC/npc.hpp +++ b/Server/Components/NPCs/NPC/npc.hpp @@ -258,6 +258,32 @@ class NPC : public INPC, public PoolIDProvider, public NoCopy IPlayer* getPlayerMovingTo() override; + SDK_EXPORT int getClosestEntityInBetween(const Vector3& point, float range, EntityCheckType betweenCheckFlags, const Vector3& offsetFrom, int& entityType, int& playerObjectOwnerId, Vector3& hitMap) override; + + SDK_EXPORT void setPlaybackPath(StringView path) override; + + SDK_EXPORT StringView getPlaybackPath() const override; + + SDK_EXPORT void setWeaponInfo(uint8_t weapon, int reloadTime, int shootTime, int clipSize, float accuracy) override; + + SDK_EXPORT bool getWeaponInfo(uint8_t weapon, int& reloadTime, int& shootTime, int& clipSize, float& accuracy) const override; + + SDK_EXPORT static bool setWeaponDefaultInfo(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); + + SDK_EXPORT bool showInTabListForPlayer(IPlayer& forPlayer) override; + + SDK_EXPORT bool hideInTabListForPlayer(IPlayer& forPlayer) override; + + SDK_EXPORT bool setMoveMode(int mode) override; + + SDK_EXPORT int getMoveMode() const override; + + SDK_EXPORT void setMinHeightPosCall(float height) override; + + SDK_EXPORT float getMinHeightPosCall() const override; + void setVehiclePosition(const Vector3& position, bool immediateUpdate) override; void setVehicleRotation(const GTAQuat& rotation, bool immediateUpdate) override; @@ -288,6 +314,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(); @@ -434,8 +464,11 @@ 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_; + float lastHeightPosCall_; float health_; float armour_; uint16_t animationId_; diff --git a/Server/Components/NPCs/npcs_impl.cpp b/Server/Components/NPCs/npcs_impl.cpp index 97c775cd9..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,8 @@ 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); if (components) @@ -45,6 +48,8 @@ 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); if (vehicles) @@ -190,6 +195,83 @@ 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())); + 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) @@ -491,6 +573,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 76b15dc2a..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 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,12 @@ 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; + void onPoolEntryDestroyed(IPlayer& player) override; void onPoolEntryDestroyed(IVehicle& vehicle) override; @@ -149,6 +155,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); @@ -182,6 +192,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 +328,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 a86a179e1..99046323e 100644 --- a/Server/Components/Pawn/Scripting/NPC/Events.hpp +++ b/Server/Components/Pawn/Scripting/NPC/Events.hpp @@ -59,6 +59,21 @@ 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 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); @@ -69,6 +84,28 @@ 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()); + } + + 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); + } + bool onNPCShotMissed(INPC& npc, const PlayerBulletData& bulletData) override { cell ret = PawnManager::Get()->CallInSidesWhile1( diff --git a/Server/Components/Pawn/Scripting/NPC/Natives.cpp b/Server/Components/Pawn/Scripting/NPC/Natives.cpp index a148d96ff..10631a945 100644 --- a/Server/Components/Pawn/Scripting/NPC/Natives.cpp +++ b/Server/Components/Pawn/Scripting/NPC/Natives.cpp @@ -86,6 +86,35 @@ 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_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); +} + +SCRIPT_API(NPC_GetMoveMode, int(INPC& npc)) +{ + return npc.getMoveMode(); +} + +SCRIPT_API(NPC_SetMinHeightPosCall, bool(INPC& npc, float height)) +{ + npc.setMinHeightPosCall(height); + return true; +} + +SCRIPT_API(NPC_GetMinHeightPosCall, float(INPC& npc)) +{ + return npc.getMinHeightPosCall(); +} + SCRIPT_API(NPC_SetRot, bool(INPC& npc, Vector3 rotation)) { npc.setRotation(rotation, true); @@ -104,6 +133,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 +175,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); @@ -198,6 +262,16 @@ SCRIPT_API(NPC_IsAnyStreamedIn, bool(INPC& npc)) return streamedIn.size() > 1; } +SCRIPT_API(NPC_ShowInTabListForPlayer, bool(INPC& npc, IPlayer& forPlayer)) +{ + return npc.showInTabListForPlayer(forPlayer); +} + +SCRIPT_API(NPC_HideInTabListForPlayer, bool(INPC& npc, IPlayer& forPlayer)) +{ + return npc.hideInTabListForPlayer(forPlayer); +} + SCRIPT_API(NPC_GetAll, int(DynamicArray& outputNPCs)) { int index = -1; @@ -242,6 +316,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 +334,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 +368,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 +398,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 +476,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 +511,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 = npc.getClosestEntityInBetween(point, range, EntityCheckType(checkInBetweenFlags), offsetFrom, entityType, objectOwnerId, hitPoint); + return true; +} + SCRIPT_API(NPC_StopAim, bool(INPC& npc)) { npc.stopAim(); @@ -472,6 +588,29 @@ 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)) +{ + 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 npc.getWeaponInfo(static_cast(weapon), reloadTime, shootTime, clipSize, accuracy); +} + +SCRIPT_API(NPC_SetWeaponDefaultInfo, bool(int weapon, int reloadTime, int shootTime, int clipSize, float 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)) +{ + 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)) { npc.enterVehicle(vehicle, seatId, NPCMoveType(moveType)); @@ -790,6 +929,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; + } + + npc.setPlaybackPath(path); + return true; +} + +SCRIPT_API(NPC_GetPlaybackPath, bool(INPC& npc, OutputOnlyString& path)) +{ + path = npc.getPlaybackPath(); + return true; +} + SCRIPT_API(NPC_LoadRecord, int(const std::string& filePath)) { auto component = PawnManager::Get()->npcs; @@ -1005,6 +1161,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 +1272,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();