diff --git a/SDK b/SDK index 18addb6c7..c266ae1a3 160000 --- a/SDK +++ b/SDK @@ -1 +1 @@ -Subproject commit 18addb6c7175439d17bc88166abfe70fe01538b2 +Subproject commit c266ae1a3318bdb2edb60b4e18d1cd04440ac8d9 diff --git a/Server/Components/CMakeLists.txt b/Server/Components/CMakeLists.txt index b949f3c48..76b6021db 100644 --- a/Server/Components/CMakeLists.txt +++ b/Server/Components/CMakeLists.txt @@ -20,6 +20,7 @@ add_subdirectory(TextLabels) add_subdirectory(Timers) add_subdirectory(Variables) add_subdirectory(Vehicles) +add_subdirectory(NPCs) # Pawn if(BUILD_PAWN_COMPONENT) diff --git a/Server/Components/NPCs/CMakeLists.txt b/Server/Components/NPCs/CMakeLists.txt new file mode 100644 index 000000000..09bd36528 --- /dev/null +++ b/Server/Components/NPCs/CMakeLists.txt @@ -0,0 +1,2 @@ +get_filename_component(ProjectId ${CMAKE_CURRENT_SOURCE_DIR} NAME) +add_server_component(${ProjectId}) diff --git a/Server/Components/NPCs/NPC/npc.cpp b/Server/Components/NPCs/NPC/npc.cpp new file mode 100644 index 000000000..8a6620702 --- /dev/null +++ b/Server/Components/NPCs/NPC/npc.cpp @@ -0,0 +1,1534 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. + * + * The original code is copyright (c) 2022, open.mp team and contributors. + */ + +#include "npc.hpp" +#include +#define _USE_MATH_DEFINES +#include +#include "../npcs_impl.hpp" +#include "../utils.hpp" +#include + +NPC::NPC(NPCComponent* component, IPlayer* playerPtr) + : skin_(0) + , dead_(false) + , keys_(0) + , upAndDown_(0) + , leftAndRight_(0) + , meleeAttacking_(false) + , meleeAttackDelay_(0) + , meleeSecondaryAttack_(false) + , moveType_(NPCMoveType_None) + , estimatedArrivalTimeMS_(0) + , moveSpeed_(0.0f) + , targetPosition_({ 0.0f, 0.0f, 0.0f }) + , velocity_({ 0.0f, 0.0f, 0.0f }) + , moving_(false) + , needsVelocityUpdate_(false) + , weapon_(0) + , ammo_(0) + , ammoInClip_(0) + , infiniteAmmo_(false) + , hasReloading_(true) + , reloading_(false) + , shooting_(false) + , shootDelay_(0) + , weaponState_(PlayerWeaponState_Unknown) + , aiming_(false) + , aimAt_({ 0.0f, 0.0f, 0.0f }) + , aimOffsetFrom_({ 0.0f, 0.0f, 0.0f }) + , aimOffset_({ 0.0f, 0.0f, 0.0f }) + , updateAimAngle_(false) + , betweenCheckFlags_(EntityCheckType::None) + , hitId_(0) + , hitType_(PlayerBulletHitType_None) + , lastDamager_(nullptr) + , lastDamagerWeapon_(PlayerWeapon_End) + , vehicleToEnter_(nullptr) + , vehicleSeatToEnter_(SEAT_NONE) + , enteringVehicle_(false) + , jackingVehicle_(false) +{ + // Fill weapon accuracy with 1.0f, let server devs change it with the desired values + weaponAccuracy.fill(1.0f); + + // Keep a handle of NPC copmonent instance internally + npcComponent_ = component; + // We created a player instance for it, let's keep a handle of it internally + player_ = playerPtr; + + // Initial entity values + Vector3 initialPosition = position_ = { 0.0f, 0.0f, 3.5f }; + GTAQuat initialRotation = { 0.960891485f, 0.0f, 0.0f, 0.276925147f }; + + // Initial values for foot sync values + footSync_.LeftRight = 0; + footSync_.UpDown = 0; + footSync_.Keys = 0; + footSync_.Position = initialPosition; + footSync_.Velocity = velocity_; + footSync_.Rotation = initialRotation; + footSync_.AdditionalKey = 0; + footSync_.Weapon = weapon_; + footSync_.HealthArmour = { 100.0f, 0.0f }; + footSync_.SpecialAction = 0; + footSync_.AnimationID = 0; + footSync_.AnimationFlags = 0; + footSync_.SurfingData.type = PlayerSurfingData::Type::None; + footSync_.SurfingData.ID = 0; + footSync_.SurfingData.offset = { 0.0f, 0.0f, 0.0f }; +} + +Vector3 NPC::getPosition() const +{ + return position_; +} + +void NPC::setPosition(Vector3 pos) +{ + position_ = pos; + + // Let it update for all players and internally in open.mp + sendFootSync(); + + if (moving_) + { + move(targetPosition_, moveType_); + } +} + +GTAQuat NPC::getRotation() const +{ + return player_->getRotation(); +} + +void NPC::setRotation(GTAQuat rot) +{ + footSync_.Rotation = rot; + + // Let it update for all players and internally in open.mp + sendFootSync(); + + if (moving_) + { + move(targetPosition_, moveType_); + } +} + +int NPC::getVirtualWorld() const +{ + return player_->getVirtualWorld(); +} + +void NPC::setVirtualWorld(int vw) +{ + player_->setVirtualWorld(vw); +} + +void NPC::spawn() +{ + NetworkBitStream requestClassBS; + NetworkBitStream emptyBS; + + requestClassBS.writeUINT16(0); + npcComponent_->emulateRPCIn(*player_, NetCode::RPC::PlayerRequestClass::PacketID, requestClassBS); + + npcComponent_->emulateRPCIn(*player_, NetCode::RPC::PlayerRequestSpawn::PacketID, emptyBS); + npcComponent_->emulateRPCIn(*player_, NetCode::RPC::PlayerSpawn::PacketID, emptyBS); + + // Make sure we resend this again, at spawn + player_->setSkin(player_->getSkin()); + + // Set the player stats + setHealth(100.0f); + setArmour(0.0f); + setWeapon(PlayerWeapon_Fist); + setAmmo(0); + + dead_ = false; + + lastDamager_ = nullptr; + lastDamagerWeapon_ = PlayerWeapon_End; + + npcComponent_->getEventDispatcher_internal().dispatch(&NPCEventHandler::onNPCSpawn, *this); +} + +bool NPC::move(Vector3 pos, NPCMoveType moveType, float moveSpeed) +{ + if (moveType == NPCMoveType_None) + { + return false; + } + + if (moveType == NPCMoveType_Sprint && aiming_) + { + stopAim(); + } + + // Set up everything to start moving in next tick + auto position = getPosition(); + float distance = glm::distance(position, pos); + + // Determine which speed to use based on moving type + float moveSpeed_ = 0.0f; + moveType_ = moveType; + + if (isEqualFloat(moveSpeed, NPC_MOVE_SPEED_AUTO)) + { + if (moveType_ == NPCMoveType_Sprint) + { + moveSpeed_ = NPC_MOVE_SPEED_SPRINT; + } + else if (moveType_ == NPCMoveType_Jog) + { + moveSpeed_ = NPC_MOVE_SPEED_JOG; + } + else + { + moveSpeed_ = NPC_MOVE_SPEED_WALK; + } + } + else + { + DynamicArray speedValues = { NPC_MOVE_SPEED_WALK, NPC_MOVE_SPEED_JOG, NPC_MOVE_SPEED_SPRINT }; + float nearestSpeed = getNearestFloatValue(moveSpeed_, speedValues); + + if (isEqualFloat(nearestSpeed, NPC_MOVE_SPEED_SPRINT)) + { + moveType_ = NPCMoveType_Sprint; + } + else if (isEqualFloat(nearestSpeed, NPC_MOVE_SPEED_JOG)) + { + moveType_ = NPCMoveType_Jog; + } + else if (isEqualFloat(nearestSpeed, NPC_MOVE_SPEED_WALK)) + { + moveType_ = NPCMoveType_Walk; + } + } + + if (moveType == NPCMoveType_Sprint) + { + applyKey(Key::SPRINT); + } + else if (moveType == NPCMoveType_Walk) + { + applyKey(Key::WALK); + } + + upAndDown_ = static_cast(Key::ANALOG_UP); + + // Calculate front vector and player's facing angle: + Vector3 front; + if (!(std::fabs(distance) < DBL_EPSILON)) + { + front = (pos - position) / distance; + } + + auto rotation = getRotation().ToEuler(); + rotation.z = getAngleOfLine(front.x, front.y); + footSync_.Rotation = rotation; // Do this directly, if you use NPC::setRotation it's going to cause recursion + + // Calculate velocity to use on tick + velocity_ = front * (moveSpeed_ / 100.0f); + + if (!(std::fabs(glm::length(velocity_)) < DBL_EPSILON)) + { + estimatedArrivalTimeMS_ = duration_cast(Time::now().time_since_epoch()).count() + (static_cast(distance / glm::length(velocity_)) * (/* (npcComponent_->getFootSyncRate() * 10000) +*/ 1000)); + } + else + { + estimatedArrivalTimeMS_ = 0; + } + + // Set internal variables + targetPosition_ = pos; + moving_ = true; + moveType_ = moveType; + lastMove_ = Time::now(); + return true; +} + +void NPC::stopMove() +{ + moving_ = false; + moveSpeed_ = 0.0f; + targetPosition_ = { 0.0f, 0.0f, 0.0f }; + velocity_ = { 0.0f, 0.0f, 0.0f }; + moveType_ = NPCMoveType_None; + estimatedArrivalTimeMS_ = 0; + + upAndDown_ &= ~Key::UP; + removeKey(Key::SPRINT); + removeKey(Key::WALK); + footSync_.UpDown = 0; +} + +bool NPC::isMoving() const +{ + return moving_; +} + +void NPC::setSkin(int model) +{ + player_->setSkin(model); +} + +bool NPC::isStreamedInForPlayer(const IPlayer& other) const +{ + if (player_) + { + return player_->isStreamedInForPlayer(other); + } + + return false; +} + +const FlatPtrHashSet& NPC::streamedForPlayers() const +{ + return player_->streamedForPlayers(); +} + +void NPC::setInterior(unsigned int interior) +{ + if (player_) + { + player_->setInterior(interior); + } +} + +unsigned int NPC::getInterior() const +{ + if (player_) + { + return player_->getInterior(); + } + + return 0; +} + +Vector3 NPC::getVelocity() const +{ + return player_->getPosition(); +} + +void NPC::setVelocity(Vector3 velocity, bool update) +{ + if (moving_ && !update) + { + velocity_ = velocity; + footSync_.Velocity = velocity; + } + + needsVelocityUpdate_ = update; +} + +void NPC::setHealth(float health) +{ + if (health < 0.0f) + { + footSync_.HealthArmour.x = 0.0f; + } + else + { + footSync_.HealthArmour.x = health; + } +} + +float NPC::getHealth() const +{ + return footSync_.HealthArmour.x; +} + +void NPC::setArmour(float armour) +{ + if (armour < 0.0f) + { + footSync_.HealthArmour.y = 0.0f; + } + else + { + footSync_.HealthArmour.y = armour; + } +} + +float NPC::getArmour() const +{ + return footSync_.HealthArmour.y; +} + +bool NPC::isDead() const +{ + return dead_; +} + +void NPC::setWeapon(uint8_t weapon) +{ + auto slot = WeaponSlotData(weapon).slot(); + if (slot != INVALID_WEAPON_SLOT) + { + weapon_ = weapon; + } + updateWeaponState(); +} + +uint8_t NPC::getWeapon() const +{ + return weapon_; +} + +void NPC::setAmmo(int ammo) +{ + ammo_ = ammo; + + if (ammo_ < ammoInClip_) + { + ammoInClip_ = ammo_; + } + updateWeaponState(); + setAmmoInClip(ammo); +} + +int NPC::getAmmo() const +{ + return ammo_; +} + +void NPC::setWeaponSkillLevel(PlayerWeaponSkill weaponSkill, int level) +{ + if (weaponSkill >= 11 || weaponSkill < 0) + { + auto weaponData = WeaponInfo::get(weapon_); + if (weaponData.type != PlayerWeaponType_None) + { + auto currentWeaponClipSize = weaponData.clipSize; + if (weaponSkill == getWeaponSkillID(weapon_) && isWeaponDoubleHanded(weapon_, getWeaponSkillLevel(getWeaponSkillID(weapon_))) && level < 999 && ammoInClip_ > currentWeaponClipSize) + { + if (ammo_ < ammoInClip_) + { + ammoInClip_ = ammo_; + } + + if (ammoInClip_ > currentWeaponClipSize) + { + ammoInClip_ = currentWeaponClipSize; + } + } + + player_->setSkillLevel(weaponSkill, level); + } + } +} + +int NPC::getWeaponSkillLevel(PlayerWeaponSkill weaponSkill) const +{ + auto skills = player_->getSkillLevels(); + if (weaponSkill >= PlayerWeaponSkill(11) || weaponSkill < PlayerWeaponSkill(0)) + { + return 0; + } + return skills[weaponSkill]; +} + +void NPC::setKeys(uint16_t upAndDown, uint16_t leftAndRight, uint16_t keys) +{ + upAndDown_ = upAndDown; + leftAndRight_ = leftAndRight; + keys_ = keys; +} + +void NPC::getKeys(uint16_t& upAndDown, uint16_t& leftAndRight, uint16_t& keys) const +{ + upAndDown = upAndDown_; + leftAndRight = leftAndRight_; + keys = keys_; +} + +PlayerWeaponState NPC::getWeaponState() const +{ + return weaponState_; +} + +void NPC::setAmmoInClip(int ammo) +{ + auto clipSize = getWeaponActualClipSize(weapon_, ammo_, getWeaponSkillLevel(getWeaponSkillID(weapon_)), infiniteAmmo_); + + ammoInClip_ = ammo < clipSize ? ammo : clipSize; +} + +int NPC::getAmmoInClip() const +{ + return ammoInClip_; +} + +void NPC::meleeAttack(int time, bool secondaryMeleeAttack) +{ + if (meleeAttacking_) + { + return; + } + + if (player_->getState() != PlayerState_OnFoot || dead_) + { + return; + } + + auto weaponData = WeaponInfo::get(weapon_); + if (weaponData.type != PlayerWeaponType_Melee) + { + return; + } + + if (time == -1) + { + meleeAttackDelay_ = Milliseconds(weaponData.shootTime); + } + else + { + meleeAttackDelay_ = Milliseconds(time); + } + + if (meleeAttackDelay_ <= Milliseconds(npcComponent_->getFootSyncRate())) + { + meleeAttackDelay_ = Milliseconds(npcComponent_->getFootSyncRate() + 5); + } + + shootUpdateTime_ = lastUpdate_; + meleeAttacking_ = true; + meleeSecondaryAttack_ = secondaryMeleeAttack; + + // Apply appropiate keys for melee attack + if (meleeSecondaryAttack_) + { + applyKey(Key::AIM); + applyKey(Key::SECONDARY_ATTACK); + } + else + { + applyKey(Key::FIRE); + } +} + +void NPC::stopMeleeAttack() +{ + if (!meleeAttacking_) + { + return; + } + + if (meleeSecondaryAttack_) + { + removeKey(Key::AIM); + removeKey(Key::SECONDARY_ATTACK); + } + else + { + removeKey(Key::FIRE); + } + + meleeAttacking_ = false; + meleeSecondaryAttack_ = false; +} + +bool NPC::isMeleeAttacking() const +{ + return meleeAttacking_; +} + +void NPC::enableReloading(bool toggle) +{ + hasReloading_ = toggle; +} + +bool NPC::isReloadEnabled() const +{ + return hasReloading_; +} + +bool NPC::isReloading() const +{ + return reloading_; +} + +void NPC::enableInfiniteAmmo(bool enable) +{ + infiniteAmmo_ = enable; +} + +bool NPC::isInfiniteAmmoEnabled() const +{ + return infiniteAmmo_; +} + +void NPC::setFightingStyle(PlayerFightingStyle style) +{ + if (player_) + { + player_->setFightingStyle(style); + } +} + +PlayerFightingStyle NPC::getFightingStyle() const +{ + if (player_) + { + return player_->getFightingStyle(); + } + return PlayerFightingStyle_Normal; +} + +void NPC::shoot(int hitId, PlayerBulletHitType hitType, uint8_t weapon, const Vector3& endPoint, const Vector3& offset, bool isHit, EntityCheckType betweenCheckFlags) +{ + auto weaponData = WeaponInfo::get(weapon); + if (weaponData.type != PlayerWeaponType_Bullet) + { + return; + } + + auto originPoint = getPosition(); + originPoint += offset; + + PlayerBulletData bulletData; + bulletData.weapon = weapon; + bulletData.origin = originPoint; + bulletData.hitPos = bulletData.offset = endPoint; + bulletData.hitID = hitId; + bulletData.hitType = hitType; + + if (!isHit) + { + bulletData.hitID = INVALID_PLAYER_ID; // Using INVALID_PLAYER_ID but it's for all invalid entity IDs + bulletData.hitType = PlayerBulletHitType_None; + } + + float targetDistance = glm::distance(bulletData.origin, bulletData.hitPos); + if (targetDistance > weaponData.range) + { + bulletData.hitID = INVALID_PLAYER_ID; // Using INVALID_PLAYER_ID but it's for all invalid entity IDs + bulletData.hitType = PlayerBulletHitType_None; + } + + // If something is in between the origin and the target (we currently don't handle checking beyond the target, even when missing with leftover range) + EntityCheckType closestEntityType = EntityCheckType::None; + int playerObjectOwnerId = INVALID_PLAYER_ID; + Vector3 hitMapPos = bulletData.hitPos; + float range = weaponData.range; + Pair results = { bulletData.hitPos, bulletData.hitPos }; + bool eventResult = true; + + // Pass original hit ID to correctly handle missed or out of range shots! + void* closestEntity = getClosestEntityInBetween(npcComponent_, bulletData.origin, bulletData.hitPos, std::min(range, targetDistance), betweenCheckFlags, poolID, hitId, closestEntityType, playerObjectOwnerId, hitMapPos, results); + + bulletData.hitPos = results.first; + bulletData.offset = results.second; + + switch (EntityCheckType(closestEntityType)) + { + case EntityCheckType::Player: + { + if (closestEntity) + { + bulletData.hitType = PlayerBulletHitType_Player; + auto player = static_cast(closestEntity); + bulletData.hitID = player->getID(); + eventResult = npcComponent_->getEventDispatcher_internal().stopAtFalse([&](NPCEventHandler* handler) + { + return handler->onNPCShotPlayer(*this, *player, bulletData); + }); + } + break; + } + case EntityCheckType::NPC: + { + if (closestEntity) + { + bulletData.hitType = PlayerBulletHitType_Player; + auto npc = static_cast(closestEntity); + bulletData.hitID = npc->getID(); + eventResult = npcComponent_->getEventDispatcher_internal().stopAtFalse([&](NPCEventHandler* handler) + { + return handler->onNPCShotNPC(*this, *npc, bulletData); + }); + } + break; + } + case EntityCheckType::Actor: + { + bulletData.hitType = PlayerBulletHitType_None; + bulletData.hitID = INVALID_PLAYER_ID; + eventResult = npcComponent_->getEventDispatcher_internal().stopAtFalse([&](NPCEventHandler* handler) + { + return handler->onNPCShotMissed(*this, bulletData); + }); + break; + } + case EntityCheckType::Vehicle: + { + if (closestEntity) + { + bulletData.hitType = PlayerBulletHitType_Vehicle; + auto vehicle = static_cast(closestEntity); + bulletData.hitID = vehicle->getID(); + eventResult = npcComponent_->getEventDispatcher_internal().stopAtFalse([&](NPCEventHandler* handler) + { + return handler->onNPCShotVehicle(*this, *vehicle, bulletData); + }); + } + break; + } + case EntityCheckType::Object: + { + if (closestEntity) + { + bulletData.hitType = PlayerBulletHitType_Object; + auto object = static_cast(closestEntity); + bulletData.hitID = object->getID(); + eventResult = npcComponent_->getEventDispatcher_internal().stopAtFalse([&](NPCEventHandler* handler) + { + return handler->onNPCShotObject(*this, *object, bulletData); + }); + } + break; + } + case EntityCheckType::ProjectOrig: + case EntityCheckType::ProjectTarg: + { + if (closestEntity) + { + bulletData.hitType = PlayerBulletHitType_PlayerObject; + auto playerObject = static_cast(closestEntity); + bulletData.hitID = playerObject->getID(); + eventResult = npcComponent_->getEventDispatcher_internal().stopAtFalse([&](NPCEventHandler* handler) + { + return handler->onNPCShotPlayerObject(*this, *playerObject, bulletData); + }); + } + break; + } + case EntityCheckType::Map: + default: + { + bulletData.hitType = PlayerBulletHitType_None; + bulletData.hitID = INVALID_PLAYER_ID; + eventResult = npcComponent_->getEventDispatcher_internal().stopAtFalse([&](NPCEventHandler* handler) + { + return handler->onNPCShotMissed(*this, bulletData); + }); + break; + } + } + + if (eventResult) + { + NetworkBitStream bs; + bs.writeUINT8(NetCode::Packet::PlayerBulletSync::PacketID); + bs.writeUINT8(uint8_t(bulletData.hitType)); + bs.writeUINT16(bulletData.hitID); + bs.writeVEC3(bulletData.origin); + bs.writeVEC3(bulletData.hitPos); + bs.writeVEC3(bulletData.offset); + bs.writeUINT8(bulletData.weapon); + npcComponent_->emulatePacketIn(*player_, NetCode::Packet::PlayerBulletSync::PacketID, bs); + + if (bulletData.hitType == PlayerBulletHitType_Player) + { + auto npc = static_cast(npcComponent_->get(bulletData.hitID)); + if (npc) + { + if (!dead_) + { + bool eventResult = npcComponent_->emulatePlayerGiveDamageToNPCEvent(*player_, *npc, WeaponDamages[bulletData.weapon], weapon, BodyPart_Torso, true); + npc->processDamage(player_, WeaponDamages[bulletData.weapon], bulletData.weapon, BodyPart_Torso, eventResult); + + npcComponent_->emulatePlayerTakeDamageFromNPCEvent(*npc->getPlayer(), *this, WeaponDamages[bulletData.weapon], weapon, BodyPart_Torso, true); + } + } + } + } +} + +bool NPC::isShooting() const +{ + return aiming_ && shooting_; +} + +void NPC::aimAt(const Vector3& point, bool shoot, int shootDelay, bool setAngle, const Vector3& offsetFrom, EntityCheckType betweenCheckFlags) +{ + if (moving_ && moveType_ == NPCMoveType_Sprint) + { + return; + } + + // Set the aiming flag + if (!aiming_) + { + // Get the shooting start tick + shootUpdateTime_ = lastUpdate_; + reloading_ = false; + } + + // Update aiming data + aimOffsetFrom_ = offsetFrom; + updateAimData(point, setAngle); + + // Set keys + if (!aiming_) + { + aiming_ = true; + applyKey(Key::AIM); + } + + // Set the shoot delay + auto updateRate = npcComponent_->getGeneralNPCUpdateRate(); + if (shootDelay <= updateRate) + { + shootDelay_ = Milliseconds(updateRate + 5); + } + else + { + shootDelay_ = Milliseconds(shootDelay); + } + + // Set the shooting flag + shooting_ = shoot; + + // Set the inBetween mode and flags + betweenCheckFlags_ = betweenCheckFlags; +} + +void NPC::aimAtPlayer(IPlayer& atPlayer, bool shoot, int shootDelay, bool setAngle, const Vector3& offset, const Vector3& offsetFrom, EntityCheckType betweenCheckFlags) +{ + aimAt(atPlayer.getPosition() + offset, shoot, shootDelay, setAngle, offsetFrom, betweenCheckFlags); + hitId_ = atPlayer.getID(); + hitType_ = PlayerBulletHitType_Player; + aimOffset_ = offset; +} + +void NPC::stopAim() +{ + // Make sure the player is aiming + if (!aiming_) + { + return; + } + + if (reloading_) + { + ammoInClip_ = getWeaponActualClipSize(weapon_, ammo_, getWeaponSkillLevel(getWeaponSkillID(weapon_)), infiniteAmmo_); + } + + // Reset aiming flags + aiming_ = false; + reloading_ = false; + shooting_ = false; + hitId_ = INVALID_PLAYER_ID; + hitType_ = PlayerBulletHitType_None; + updateAimAngle_ = false; + betweenCheckFlags_ = EntityCheckType::None; + + // Reset keys + removeKey(Key::AIM); + removeKey(Key::FIRE); +} + +bool NPC::isAiming() const +{ + return aiming_; +} + +bool NPC::isAimingAtPlayer(IPlayer& player) const +{ + return aiming_ && hitType_ == PlayerBulletHitType_Player && hitId_ == player.getID(); +} + +void NPC::setWeaponAccuracy(uint8_t weapon, float accuracy) +{ + auto data = WeaponSlotData(weapon); + if (data.slot() != INVALID_WEAPON_SLOT && weapon < weaponAccuracy.size()) + { + weaponAccuracy[weapon] = accuracy; + } +} + +float NPC::getWeaponAccuracy(uint8_t weapon) const +{ + float ret = 0.0f; + auto data = WeaponSlotData(weapon); + if (data.slot() != INVALID_WEAPON_SLOT && weapon < weaponAccuracy.size()) + { + ret = weaponAccuracy[weapon]; + } + + return ret; +} + +void NPC::enterVehicle(IVehicle& vehicle, uint8_t seatId, NPCMoveType moveType) +{ + if (player_->getState() != PlayerState_OnFoot) + { + return; + } + + if (int(moveType) > int(NPCMoveType_Sprint) || int(moveType) < int(NPCMoveType_Walk)) + { + moveType = NPCMoveType_Jog; + } + + int passengerSeats = Impl::getVehiclePassengerSeats(vehicle.getModel()); + if (passengerSeats == 0xFF || seatId < 1 || seatId > passengerSeats) + { + return; + } + + auto destination = getVehicleSeatPos(vehicle, seatId); + float distance = glm::distance(getPosition(), destination); + if (distance > MAX_DISTANCE_TO_ENTER_VEHICLE) + { + return; + } + + // Save the entering stats + vehicleToEnter_ = &vehicle; + vehicleSeatToEnter_ = seatId; + + // Check distance + if (distance < MIN_VEHICLE_GO_TO_DISTANCE) + { + // Wait until the entry animation is finished + vehicleEnterExitUpdateTime_ = lastUpdate_; + enteringVehicle_ = true; + + // Check whether the player is jacking the vehicle or not + if (seatId == 0) + { + IPlayer* driver = vehicle.getDriver(); + if (driver && driver->getID() != player_->getID()) + { + jackingVehicle_ = true; + } + } + else + { + const FlatHashSet& passengers = vehicle.getPassengers(); + for (auto passenger : passengers) + { + if (passenger && passenger->getID() != player_->getID()) + { + IPlayerVehicleData* data = queryExtension(passenger); + if (data && data->getSeat() == seatId) + { + jackingVehicle_ = true; + } + } + } + } + + // Call the SAMP enter vehicle function + NetworkBitStream bs; + bs.writeUINT16(vehicle.getID()); + bs.writeUINT8(vehicleSeatToEnter_); + npcComponent_->emulateRPCIn(*player_, NetCode::RPC::OnPlayerEnterVehicle::PacketID, bs); + } + else + { + // Go to the vehicle + move(destination, moveType); + } +} + +void NPC::exitVehicle() +{ + if (player_->getState() != PlayerState_Driver && player_->getState() != PlayerState_Passenger) + { + return; + } + + IPlayerVehicleData* vehicleData = queryExtension(player_); + if (!vehicleData || vehicleData->getVehicle() == nullptr) + { + return; + } + + NetworkBitStream bs; + bs.writeUINT16(vehicleData->getVehicle()->getID()); + npcComponent_->emulateRPCIn(*player_, NetCode::RPC::OnPlayerExitVehicle::PacketID, bs); + + vehicleEnterExitUpdateTime_ = lastUpdate_; +} + +void NPC::setWeaponState(PlayerWeaponState state) +{ + if (state == PlayerWeaponState_Unknown) + { + return; + } + + PlayerWeaponState oldState = weaponState_; + weaponState_ = state; + + switch (state) + { + case PlayerWeaponState_LastBullet: + if (ammo_ > 0) + { + ammoInClip_ = 1; + } + break; + case PlayerWeaponState_MoreBullets: + if (ammo_ > 1 && ammoInClip_ <= 1) + { + ammoInClip_ = getWeaponActualClipSize(weapon_, ammo_, getWeaponSkillLevel(getWeaponSkillID(weapon_)), infiniteAmmo_); + } + break; + case PlayerWeaponState_NoBullets: + ammoInClip_ = 0; + break; + case PlayerWeaponState_Reloading: + if (!reloading_) + { + reloadingUpdateTime_ = lastUpdate_; + reloading_ = true; + shooting_ = false; + } + break; + default: + break; + } + + if (oldState != state) + { + npcComponent_->getEventDispatcher_internal().dispatch(&NPCEventHandler::onNPCWeaponStateChange, *this, state, oldState); + } +} + +void NPC::updateWeaponState() +{ + switch (weapon_) + { + case 0: + case PlayerWeapon_BrassKnuckle: + case PlayerWeapon_GolfClub: + case PlayerWeapon_NiteStick: + case PlayerWeapon_Knife: + case PlayerWeapon_Bat: + case PlayerWeapon_Shovel: + case PlayerWeapon_PoolStick: + case PlayerWeapon_Katana: + case PlayerWeapon_Chainsaw: + case PlayerWeapon_Dildo: + case PlayerWeapon_Dildo2: + case PlayerWeapon_Vibrator: + case PlayerWeapon_Vibrator2: + case PlayerWeapon_Flower: + case PlayerWeapon_Cane: + case PlayerWeapon_Bomb: + case PlayerWeapon_Camera: + case PlayerWeapon_Night_Vis_Goggles: + case PlayerWeapon_Thermal_Goggles: + case PlayerWeapon_Parachute: + setWeaponState(PlayerWeaponState_NoBullets); + break; + + case PlayerWeapon_Grenade: + case PlayerWeapon_Teargas: + case PlayerWeapon_Moltov: + case PlayerWeapon_Rifle: + case PlayerWeapon_Sniper: + case PlayerWeapon_RocketLauncher: + case PlayerWeapon_HeatSeeker: + case PlayerWeapon_Satchel: + setWeaponState(PlayerWeaponState_LastBullet); + break; + + case PlayerWeapon_Colt45: + case PlayerWeapon_Silenced: + case PlayerWeapon_Deagle: + case PlayerWeapon_Sawedoff: + case PlayerWeapon_Shotgspa: + case PlayerWeapon_UZI: + case PlayerWeapon_MP5: + case PlayerWeapon_AK47: + case PlayerWeapon_M4: + case PlayerWeapon_TEC9: + case PlayerWeapon_FlameThrower: + case PlayerWeapon_Minigun: + case PlayerWeapon_SprayCan: + case PlayerWeapon_FireExtinguisher: + if (reloading_) + { + setWeaponState(PlayerWeaponState_Reloading); + } + else if (ammoInClip_ == 1) + { + setWeaponState(PlayerWeaponState_LastBullet); + } + else if (ammo_ == 0 && !infiniteAmmo_) + { + setWeaponState(PlayerWeaponState_NoBullets); + } + else if (ammoInClip_ > 1) + { + setWeaponState(PlayerWeaponState_MoreBullets); + } + break; + + case PlayerWeapon_Shotgun: + if (reloading_) + { + setWeaponState(PlayerWeaponState_Reloading); + } + else if (ammo_ == 0 && !infiniteAmmo_) + { + setWeaponState(PlayerWeaponState_NoBullets); + } + else if (ammoInClip_ == 1) + { + setWeaponState(PlayerWeaponState_LastBullet); + } + break; + + default: + setWeaponState(PlayerWeaponState_NoBullets); + break; + } +} + +void NPC::kill(IPlayer* killer, uint8_t weapon) +{ + if (dead_) + { + return; + } + + stopMove(); + resetKeys(); + dead_ = true; + + // Emulate death rpc + NetworkBitStream bs; + + bs.writeUINT8(weapon); + bs.writeUINT16(killer ? killer->getID() : INVALID_PLAYER_ID); + npcComponent_->emulateRPCIn(*player_, NetCode::RPC::OnPlayerDeath::PacketID, bs); + + npcComponent_->getEventDispatcher_internal().dispatch(&NPCEventHandler::onNPCDeath, *this, killer, weapon); +} + +void NPC::processDamage(IPlayer* damager, float damage, uint8_t weapon, BodyPart bodyPart, bool handleHealthAndArmour) +{ + if (!damager) + { + return; + } + + // Check the returned value + if (handleHealthAndArmour) + { + // Check the armour + if (getArmour() > 0.0f) + { + // Save the old armour + float armour = getArmour(); + // Decrease the armor + setArmour(armour - damage); + // If the damage is bigger than the armour then decrease the health aswell + if (armour - damage < 0.0f) + { + setHealth(getHealth() - (damage - armour)); + } + } + else + { + setHealth(getHealth() - damage); + } + } + + // Save the last damager + lastDamager_ = damager; + lastDamagerWeapon_ = weapon; +} + +void NPC::updateAim() +{ + if (aiming_) + { + PlayerWeaponType weaponType = WeaponInfo::get(weapon_).type; + + // Set the camera mode + if (weaponType == PlayerWeaponType_Melee) + { + aimSync_.CamMode = 4; + } + else if (weapon_ == PlayerWeapon_Sniper) + { + aimSync_.CamMode = 7; + } + else if (weapon_ == PlayerWeapon_Camera) + { + aimSync_.CamMode = 46; + } + else if (weapon_ == PlayerWeapon_RocketLauncher) + { + aimSync_.CamMode = 8; + } + else if (weapon_ == PlayerWeapon_HeatSeeker) + { + aimSync_.CamMode = 51; + } + else + { + aimSync_.CamMode = 53; + } + } + else + { + // Set the camera mode and weapon state + aimSync_.CamMode = 0; + // Convert the player angle to radians + + float angle = glm::radians(player_->getRotation().ToEuler().z); + // Calculate the camera target + Vector3 vecTarget(aimSync_.CamPos.x - glm::sin(angle) * 0.2f, + aimSync_.CamPos.z + glm::cos(angle) * 0.2f, + aimSync_.CamPos.z); + + // Calculate the camera front vector + aimSync_.CamFrontVector = vecTarget - aimSync_.CamPos; + } + + // Update the weapon state + updateWeaponState(); + // Set the aim sync flag + // m_pPlayer->bHasAimSync = true; +} + +void NPC::updateAimData(const Vector3& point, bool setAngle) +{ + // Adjust the player position + auto camPosition = getPosition() + aimOffsetFrom_; + + // Get the aiming distance + auto camFronVector = point - camPosition; + + // Get the distance to the destination point + float distance = glm::distance(camPosition, point); + + // Calculate the aiming Z angle + float xyLength = glm::length(glm::vec2(camFronVector.x, camFronVector.y)); // XY-plane distance + float totalLength = glm::length(camFronVector); // 3D distance + + float aimZ = xyLength / totalLength; + if (aimZ > 1.0f) + { + aimZ = 1.0f; + } + else if (aimZ < -1.0f) + { + aimZ = -1.0f; + } + + if (camFronVector.z < 0.0f) + { + aimZ = glm::acos(aimZ); + } + else + { + aimZ = -glm::acos(aimZ); + } + + // Get the destination angle + auto unitVec = camFronVector / distance; + + if (setAngle) + { + auto rotation = getRotation().ToEuler(); + auto angle = glm::degrees(glm::atan(unitVec.y, unitVec.x)) + 270.0f; + if (angle >= 360.0f) + { + angle -= 360.0f; + } + else if (angle < 0.0f) + { + angle += 360.0f; + } + + rotation.z = angle; + setRotation(rotation); + } + + // Set the aim sync data + aimSync_.AimZ = aimZ; + aimSync_.CamFrontVector = unitVec; + aimSync_.CamPos = camPosition; + + // set the flags + aimAt_ = point; + updateAimAngle_ = setAngle; +} + +void NPC::sendFootSync() +{ + // Only send foot sync if player is spawned + if (!(player_->getState() == PlayerState_OnFoot || player_->getState() == PlayerState_Driver || player_->getState() == PlayerState_Passenger || player_->getState() == PlayerState_Spawned)) + { + return; + } + + NetworkBitStream bs; + + auto& quat = footSync_.Rotation.q; + uint16_t upAndDown; + uint16_t leftAndDown; + uint16_t keys; + + getKeys(upAndDown, leftAndDown, keys); + + footSync_.Position = position_; + footSync_.LeftRight = leftAndDown; + footSync_.UpDown = upAndDown; + footSync_.Keys = keys; + footSync_.Weapon = weapon_; + + bs.writeUINT8(footSync_.PacketID); + bs.writeUINT16(footSync_.LeftRight); + bs.writeUINT16(footSync_.UpDown); + bs.writeUINT16(footSync_.Keys); + bs.writeVEC3(footSync_.Position); + bs.writeVEC4(Vector4(quat.w, quat.x, quat.y, quat.z)); + bs.writeUINT8(uint8_t(footSync_.HealthArmour.x)); + bs.writeUINT8(uint8_t(footSync_.HealthArmour.y)); + bs.writeUINT8(footSync_.WeaponAdditionalKey); + bs.writeUINT8(footSync_.SpecialAction); + bs.writeVEC3(footSync_.Velocity); + bs.writeVEC3(footSync_.SurfingData.offset); + bs.writeUINT16(footSync_.SurfingData.ID); + bs.writeUINT16(footSync_.AnimationID); + bs.writeUINT16(footSync_.AnimationFlags); + + npcComponent_->emulatePacketIn(*player_, footSync_.PacketID, bs); +} + +void NPC::sendAimSync() +{ + // Only send aim sync if player is on foot + if (player_->getState() != PlayerState_OnFoot) + { + return; + } + + NetworkBitStream bs; + + bs.writeUINT8(aimSync_.CamMode); + bs.writeVEC3(aimSync_.CamFrontVector); + bs.writeVEC3(aimSync_.CamPos); + bs.writeFLOAT(aimSync_.AimZ); + bs.writeUINT8(aimSync_.ZoomWepState); + bs.writeUINT8(aimSync_.AspectRatio); + + npcComponent_->emulatePacketIn(*player_, aimSync_.PacketID, bs); +} + +void NPC::advance(TimePoint now) +{ + auto position = getPosition(); + + if (estimatedArrivalTimeMS_ <= duration_cast(now.time_since_epoch()).count() || glm::distance(position, targetPosition_) <= 0.1f) + { + auto pos = targetPosition_; + stopMove(); + setPosition(pos); + npcComponent_->getEventDispatcher_internal().dispatch(&NPCEventHandler::onNPCFinishMove, *this); + } + else + { + Milliseconds difference = duration_cast(now - lastMove_); + Vector3 travelled = velocity_ * static_cast(difference.count()); + + position += travelled; + footSync_.Velocity = velocity_; + position_ = position; // Do this directly, if you use NPC::setPosition it's going to cause recursion + } + + lastMove_ = Time::now(); +} + +void NPC::tick(Microseconds elapsed, TimePoint now) +{ + if (player_) + { + auto state = player_->getState(); + + // Only process if it's needed based on update rate + if (duration_cast(now - lastUpdate_).count() > npcComponent_->getGeneralNPCUpdateRate()) + { + // Only process the NPC if it is spawned + if (player_->getState() == PlayerState_OnFoot || player_->getState() == PlayerState_Driver || player_->getState() == PlayerState_Passenger || player_->getState() == PlayerState_Spawned) + { + if (getHealth() <= 0.0f && state != PlayerState_Wasted && state != PlayerState_Spawned) + { + // check on vehicle + if (state == PlayerState_Driver || state == PlayerState_Passenger) + { + // TODO: Handle NPC driver/passenger death + } + + // Kill the player + kill(lastDamager_, lastDamagerWeapon_); + } + + if (needsVelocityUpdate_) + { + setPosition(getPosition() + velocity_); + setVelocity({ 0.0f, 0.0f, 0.0f }, false); + } + + if (moving_) + { + advance(now); + } + + if (state == PlayerState_OnFoot) + { + if (aiming_) + { + auto player = npcComponent_->getCore()->getPlayers().get(hitId_); + if (player && hitType_ == PlayerBulletHitType_Player) + { + if (isAimingAtPlayer(*player)) + { + auto point = player->getPosition() + aimOffset_; + if (aimAt_ != point) + { + updateAimData(point, updateAimAngle_); + } + } + } + else + { + stopAim(); + } + } + + if (reloading_) + { + int weaponSkill = getWeaponSkillLevel(getWeaponSkillID(weapon_)); + uint32_t reloadTime = getWeaponActualReloadTime(weapon_, weaponSkill); + bool reloadFinished = reloadTime != -1 && duration_cast(lastUpdate_ - reloadingUpdateTime_) >= Milliseconds(reloadTime); + + if (reloadFinished) + { + shootUpdateTime_ = lastUpdate_; + reloading_ = false; + shooting_ = true; + ammoInClip_ = getWeaponActualClipSize(weapon_, ammo_, weaponSkill, infiniteAmmo_); + } + else + { + removeKey(Key::FIRE); + applyKey(Key::AIM); + } + } + else if (shooting_) + { + if (ammo_ == 0 && !infiniteAmmo_) + { + shooting_ = false; + removeKey(Key::FIRE); + applyKey(Key::AIM); + } + else + { + int shootTime = getWeaponActualShootTime(weapon_); + if (shootTime != -1 && Milliseconds(shootTime) < shootDelay_) + { + shootTime = shootDelay_.count(); + } + + Milliseconds lastShootTime = duration_cast(lastUpdate_ - shootUpdateTime_); + if (lastShootTime >= shootDelay_) + { + removeKey(Key::FIRE); + applyKey(Key::AIM); + } + + if (shootTime != -1 && Milliseconds(shootTime) <= lastShootTime) + { + if (ammoInClip_ != 0) + { + auto weaponData = WeaponInfo::get(weapon_); + if (weaponData.type == PlayerWeaponType_Bullet) + { + bool isHit = rand() % 100 < static_cast(weaponAccuracy[weapon_] * 100.0f); + shoot(hitId_, hitType_, weapon_, aimAt_, aimOffsetFrom_, isHit, betweenCheckFlags_); + } + + applyKey(Key::AIM); + applyKey(Key::FIRE); + + if (!infiniteAmmo_) + { + ammo_--; + } + + ammoInClip_--; + + bool needsReloading = hasReloading_ && getWeaponActualClipSize(weapon_, ammo_, getWeaponSkillLevel(getWeaponSkillID(weapon_)), infiniteAmmo_) > 0 && (ammo_ != 0 || infiniteAmmo_) && ammoInClip_ == 0; + if (needsReloading) + { + reloadingUpdateTime_ = lastUpdate_; + reloading_ = true; + shooting_ = false; + } + + shootUpdateTime_ = lastUpdate_; + } + } + } + } + else if (meleeAttacking_) + { + if (duration_cast(lastUpdate_ - shootUpdateTime_) >= meleeAttackDelay_) + { + if (meleeSecondaryAttack_) + { + applyKey(Key::AIM); + applyKey(Key::SECONDARY_ATTACK); + } + else + { + applyKey(Key::FIRE); + } + shootUpdateTime_ = lastUpdate_; + } + else if (lastUpdate_ > shootUpdateTime_) + { + if (meleeSecondaryAttack_) + { + removeKey(Key::SECONDARY_ATTACK); + } + else + { + removeKey(Key::FIRE); + } + } + } + } + + lastUpdate_ = now; + } + } + + if (duration_cast(now - lastFootSyncUpdate_).count() > npcComponent_->getFootSyncRate()) + { + sendFootSync(); + sendAimSync(); + updateAim(); + lastFootSyncUpdate_ = now; + } + } +} diff --git a/Server/Components/NPCs/NPC/npc.hpp b/Server/Components/NPCs/NPC/npc.hpp new file mode 100644 index 000000000..44a2456f8 --- /dev/null +++ b/Server/Components/NPCs/NPC/npc.hpp @@ -0,0 +1,256 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. + * + * The original code is copyright (c) 2022, open.mp team and contributors. + */ + +#pragma once + +#include +#include +#include + +class NPCComponent; + +class NPC : public INPC, public PoolIDProvider, public NoCopy +{ +public: + NPC(NPCComponent* npcComponent, IPlayer* playerPtr); + + Vector3 getPosition() const override; + + void setPosition(Vector3 position) override; + + GTAQuat getRotation() const override; + + void setRotation(GTAQuat rotation) override; + + int getVirtualWorld() const override; + + void setVirtualWorld(int vw) override; + + void spawn() override; + + bool move(Vector3 position, NPCMoveType moveType, float moveSpeed = NPC_MOVE_SPEED_AUTO) override; + + void stopMove() override; + + bool isMoving() const override; + + void setSkin(int model) override; + + bool isStreamedInForPlayer(const IPlayer& other) const override; + + const FlatPtrHashSet& streamedForPlayers() const override; + + void setInterior(unsigned int interior) override; + + unsigned int getInterior() const override; + + Vector3 getVelocity() const override; + + void setVelocity(Vector3 position, bool update = false) override; + + void setHealth(float health) override; + + float getHealth() const override; + + void setArmour(float armour) override; + + float getArmour() const override; + + bool isDead() const override; + + void setWeapon(uint8_t weapon) override; + + uint8_t getWeapon() const override; + + void setAmmo(int ammo) override; + + int getAmmo() const override; + + void setWeaponSkillLevel(PlayerWeaponSkill weaponSkill, int level) override; + + int getWeaponSkillLevel(PlayerWeaponSkill weaponSkill) const override; + + void setKeys(uint16_t upAndDown, uint16_t leftAndRight, uint16_t keys) override; + + void getKeys(uint16_t& upAndDown, uint16_t& leftAndRight, uint16_t& keys) const override; + + void meleeAttack(int time, bool secondaryMeleeAttack = false) override; + + void stopMeleeAttack() override; + + bool isMeleeAttacking() const override; + + void setFightingStyle(PlayerFightingStyle style) override; + + PlayerFightingStyle getFightingStyle() const override; + + void enableReloading(bool toggle) override; + + bool isReloadEnabled() const override; + + bool isReloading() const override; + + void enableInfiniteAmmo(bool toggle) override; + + bool isInfiniteAmmoEnabled() const override; + + PlayerWeaponState getWeaponState() const override; + + void setAmmoInClip(int ammo) override; + + int getAmmoInClip() const override; + + void shoot(int hitId, PlayerBulletHitType hitType, uint8_t weapon, const Vector3& endPoint, const Vector3& offset, bool isHit, EntityCheckType betweenCheckFlags) override; + + bool isShooting() const override; + + void aimAt(const Vector3& point, bool shoot, int shootDelay, bool setAngle, const Vector3& offsetFrom, EntityCheckType betweenCheckFlags) override; + + void aimAtPlayer(IPlayer& atPlayer, bool shoot, int shootDelay, bool setAngle, const Vector3& offset, const Vector3& offsetFrom, EntityCheckType betweenCheckFlags) override; + + void stopAim() override; + + bool isAiming() const override; + + bool isAimingAtPlayer(IPlayer& player) const override; + + void setWeaponAccuracy(uint8_t weapon, float accuracy) override; + + float getWeaponAccuracy(uint8_t weapon) const override; + + void enterVehicle(IVehicle& vehicle, uint8_t seatId, NPCMoveType moveType) override; + + void exitVehicle() override; + + void setWeaponState(PlayerWeaponState state); + + void updateWeaponState(); + + void kill(IPlayer* killer, uint8_t weapon); + + void processDamage(IPlayer* damager, float damage, uint8_t weapon, BodyPart bodyPart, bool handleHealthAndArmour); + + void updateAim(); + + void updateAimData(const Vector3& point, bool setAngle); + + void sendFootSync(); + + void sendAimSync(); + + void tick(Microseconds elapsed, TimePoint now); + + void advance(TimePoint now); + + int getID() const override + { + return poolID; + } + + IPlayer* getPlayer() override + { + return player_; + } + + void setPlayer(IPlayer* player) + { + player_ = player; + } + + void applyKey(Key key) + { + keys_ |= key; + } + + void removeKey(Key key) + { + keys_ &= ~key; + } + + void resetKeys() + { + keys_ = 0; + upAndDown_ = 0; + leftAndRight_ = 0; + } + +private: + // The NPC's player pointer. + IPlayer* player_; + TimePoint lastUpdate_; + TimePoint lastFootSyncUpdate_; + + // General data + int skin_; + bool dead_; + uint16_t keys_; + uint16_t upAndDown_; + uint16_t leftAndRight_; + Vector3 position_; + + // Attack data + bool meleeAttacking_; + Milliseconds meleeAttackDelay_; + bool meleeSecondaryAttack_; + + // Movements + NPCMoveType moveType_; + TimePoint lastMove_; + long long estimatedArrivalTimeMS_; + TimePoint moveStart_; + float moveSpeed_; + Vector3 targetPosition_; + Vector3 velocity_; + bool moving_; + bool needsVelocityUpdate_; + + // Weapon data + uint8_t weapon_; + int ammo_; + int ammoInClip_; + bool infiniteAmmo_; + bool hasReloading_; + bool reloading_; + TimePoint reloadingUpdateTime_; + bool shooting_; + TimePoint shootUpdateTime_; + Milliseconds shootDelay_; + PlayerWeaponState weaponState_; + std::array weaponAccuracy; + + // Aim data + bool aiming_; + Vector3 aimAt_; + Vector3 aimOffsetFrom_; + Vector3 aimOffset_; + bool updateAimAngle_; + + // Weapon raycast/shot checks data + EntityCheckType betweenCheckFlags_; + int hitId_; + PlayerBulletHitType hitType_; + + // Damager data + IPlayer* lastDamager_; + uint8_t lastDamagerWeapon_; + + // Vehicle data + IVehicle* vehicleToEnter_; + int vehicleSeatToEnter_; + bool enteringVehicle_; + bool jackingVehicle_; + TimePoint vehicleEnterExitUpdateTime_; + + // Packets + NetCode::Packet::PlayerFootSync footSync_; + NetCode::Packet::PlayerVehicleSync driverSync_; + NetCode::Packet::PlayerPassengerSync passengerSync_; + NetCode::Packet::PlayerAimSync aimSync_; + + NPCComponent* npcComponent_; +}; diff --git a/Server/Components/NPCs/Network/npcs_network.hpp b/Server/Components/NPCs/Network/npcs_network.hpp new file mode 100644 index 000000000..8d5e1cccb --- /dev/null +++ b/Server/Components/NPCs/Network/npcs_network.hpp @@ -0,0 +1,102 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. + * + * The original code is copyright (c) 2022, open.mp team and contributors. + */ + +#pragma once +#include +#include +#include + +using namespace Impl; + +class NPCNetwork : public Impl::Network +{ +private: + ICore* core; + INPCComponent* npcComponent; + DynamicArray markedToBeKicked; + +public: + void init(ICore* c, INPCComponent* comp) + { + core = c; + npcComponent = comp; + } + + DynamicArray& getMarkedForKickNPCs() + { + return markedToBeKicked; + } + + ENetworkType getNetworkType() const override + { + return ENetworkType(3); + } + + bool sendPacket(IPlayer& peer, Span data, int channel, bool dispatchEvents = true) override + { + // core->logLn(LogLevel::Error, "[npc network] sendPacket(\"%.*s\", data, %i, %i)\n", peer.getName().length(), peer.getName().data(), channel, dispatchEvents); + return true; + } + + bool broadcastPacket(Span data, int channel, const IPlayer* exceptPeer = nullptr, bool dispatchEvents = true) override + { + // core->logLn(LogLevel::Error, "[npc network] broadcastPacket(data, %i, \"%.*s\", %i)\n", channel, exceptPeer == nullptr ? 0 : exceptPeer->getName().length(), exceptPeer == nullptr ? "" : exceptPeer->getName().data(), dispatchEvents); + return true; + } + + bool sendRPC(IPlayer& peer, int id, Span data, int channel, bool dispatchEvents = true) override + { + // core->logLn(LogLevel::Error, "[npc network] sendRpc(\"%.*s\", %i, data, %i, %i)\n", peer.getName().length(), peer.getName().data(), id, channel, dispatchEvents); + return true; + } + + bool broadcastRPC(int id, Span data, int channel, const IPlayer* exceptPeer = nullptr, bool dispatchEvents = true) override + { + // core->logLn(LogLevel::Error, "[npc network] broadcastRPC(%i, data, %i, \"%.*s\", %i)\n", id, channel, exceptPeer == nullptr ? 0 : exceptPeer->getName().length(), exceptPeer == nullptr ? "" : exceptPeer->getName().data(), dispatchEvents); + return true; + } + + NetworkStats getStatistics(IPlayer* player = nullptr) override + { + return NetworkStats(); + } + + unsigned getPing(const IPlayer& peer) override + { + return 0; + } + + void disconnect(const IPlayer& peer) override + { + auto id = peer.getID(); + auto npc = npcComponent->get(id); + if (npc) + { + markedToBeKicked.push_back(npc->getID()); + } + } + + void ban(const BanEntry& entry, Milliseconds expire = Milliseconds(0)) override + { + } + + void unban(const BanEntry& entry) override + { + } + + void update() override + { + } + + NPCNetwork() + : Network(256, 256) + { + } + + ~NPCNetwork() { } +}; diff --git a/Server/Components/NPCs/npcs_impl.cpp b/Server/Components/NPCs/npcs_impl.cpp new file mode 100644 index 000000000..b0124d243 --- /dev/null +++ b/Server/Components/NPCs/npcs_impl.cpp @@ -0,0 +1,292 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. + * + * The original code is copyright (c) 2022, open.mp team and contributors. + */ + +#include "./npcs_impl.hpp" +#include + +void NPCComponent::onLoad(ICore* c) +{ + core = c; + footSyncRate = c->getConfig().getInt("network.on_foot_sync_rate"); +} + +void NPCComponent::onInit(IComponentList* components) +{ + npcNetwork.init(core, this); + core->getEventDispatcher().addEventHandler(this); + core->getPlayers().getPlayerDamageDispatcher().addEventHandler(this); + + if (components) + { + vehicles = components->queryComponent(); + objects = components->queryComponent(); + actors = components->queryComponent(); + } +} + +void NPCComponent::free() +{ + core->getEventDispatcher().removeEventHandler(this); + delete this; +} + +INetwork* NPCComponent::getNetwork() +{ + return &npcNetwork; +} + +IEventDispatcher& NPCComponent::getEventDispatcher() +{ + return eventDispatcher; +} + +IEventDispatcher>& NPCComponent::getPoolEventDispatcher() +{ + return storage.getEventDispatcher(); +} + +const FlatPtrHashSet& NPCComponent::entries() +{ + return storage._entries(); +} + +Pair NPCComponent::bounds() const +{ + return std::make_pair(storage.Lower, storage.Upper); +} + +INPC* NPCComponent::get(int index) +{ + if (index == -1) + { + return nullptr; + } + return storage.get(index); +} + +void NPCComponent::release(int index) +{ + auto ptr = storage.get(index); + if (ptr) + { + // Call disconnect events for both NPC and player. This way NPC's player instance is going to be handled and cleared properly. + ScopedPoolReleaseLock lock(*this, ptr->getID()); + if (lock.entry) + { + eventDispatcher.dispatch(&NPCEventHandler::onNPCDestroy, *lock.entry); + npcNetwork.networkEventDispatcher.dispatch(&NetworkEventHandler::onPeerDisconnect, *lock.entry->getPlayer(), PeerDisconnectReason_Quit); + } + + storage.release(index, false); + } +} + +void NPCComponent::lock(int index) +{ + storage.lock(index); +} + +bool NPCComponent::unlock(int index) +{ + return storage.unlock(index); +} + +void NPCComponent::onTick(Microseconds elapsed, TimePoint now) +{ + // Go through NPCs ready to be destroyed/kicked + auto& markedForKick = npcNetwork.getMarkedForKickNPCs(); + for (auto& npc : markedForKick) + { + release(npc); + } + + // Clean this pool because it is now processed + markedForKick.clear(); + + for (auto& npc : storage) + { + static_cast(npc)->tick(elapsed, now); + } +} + +void NPCComponent::onPlayerGiveDamage(IPlayer& player, IPlayer& to, float amount, unsigned weapon, BodyPart part) +{ + if (shouldCallCustomEvents) + { + auto npc = static_cast(get(to.getID())); + if (npc && npc->getPlayer()->getID() == to.getID()) + { + bool eventResult = emulatePlayerGiveDamageToNPCEvent(player, *npc, amount, weapon, part, false); + npc->processDamage(&player, amount, weapon, part, eventResult); + } + shouldCallCustomEvents = false; + } +} + +void NPCComponent::onPlayerTakeDamage(IPlayer& player, IPlayer* from, float amount, unsigned weapon, BodyPart part) +{ + if (shouldCallCustomEvents) + { + if (from) + { + auto npc = static_cast(get(from->getID())); + if (npc && npc->getPlayer()->getID() == from->getID()) + { + emulatePlayerTakeDamageFromNPCEvent(player, *npc, amount, weapon, part, false); + } + } + } +} + +bool NPCComponent::emulatePlayerGiveDamageToNPCEvent(IPlayer& player, INPC& npc, float amount, unsigned weapon, BodyPart part, bool callOriginalEvents) +{ + bool eventResult = eventDispatcher.stopAtFalse([&](NPCEventHandler* handler) + { + return handler->onNPCTakeDamage(npc, player, amount, weapon, part); + }); + + if (eventResult && callOriginalEvents) + { + shouldCallCustomEvents = false; + + // Emulate receiving damage rpc + NetworkBitStream bs; + bs.writeBIT(false); // Taking + bs.writeUINT16(npc.getID()); + bs.writeFLOAT(amount); + bs.writeUINT32(weapon); + bs.writeUINT32(int(part)); + emulateRPCIn(player, NetCode::RPC::OnPlayerGiveTakeDamage::PacketID, bs); + + shouldCallCustomEvents = true; + } + + return eventResult; +} + +bool NPCComponent::emulatePlayerTakeDamageFromNPCEvent(IPlayer& player, INPC& npc, float amount, unsigned weapon, BodyPart part, bool callOriginalEvents) +{ + bool eventResult = eventDispatcher.stopAtFalse([&](NPCEventHandler* handler) + { + return handler->onNPCGiveDamage(npc, player, amount, weapon, part); + }); + + if (eventResult && callOriginalEvents) + { + shouldCallCustomEvents = false; + + // Emulate receiving damage rpc + NetworkBitStream bs; + bs.writeBIT(true); // Taking + bs.writeUINT16(npc.getID()); + bs.writeFLOAT(amount); + bs.writeUINT32(weapon); + bs.writeUINT32(int(part)); + emulateRPCIn(player, NetCode::RPC::OnPlayerGiveTakeDamage::PacketID, bs); + + shouldCallCustomEvents = true; + } + + return eventResult; +} + +INPC* NPCComponent::create(StringView name) +{ + // Reserve a random ephemeral port for our NPC client + // Ephemeral ports: https://en.wikipedia.org/wiki/Ephemeral_port + uint16_t port = 0; + std::random_device rd; + std::mt19937 gen(rd()); + port = std::uniform_int_distribution(49152, 65535)(gen); + + PeerNetworkData data; + data.network = getNetwork(); + data.networkID.address.v4 = 16777343; // Set ipv4 to 127.0.0.1 + data.networkID.address.ipv6 = false; + data.networkID.port = port; // Set our randomly generated port + + PeerRequestParams request; + request.bot = true; // Mark as an NPC + request.name = name; + + Pair newConnectionResult { NewConnectionResult_Ignore, nullptr }; + newConnectionResult = core->getPlayers().requestPlayer(data, request); + + if (newConnectionResult.first == NewConnectionResult_NoPlayerSlot) + { + core->logLn(LogLevel::Error, "[NPC] NPC creation failed. Server is either full or max_bots in config is not enough!"); + return nullptr; + } + else if (newConnectionResult.first == NewConnectionResult_BadName) + { + core->logLn(LogLevel::Error, "[NPC] NPC has a bad name!"); + return nullptr; + } + + // Hint newly initialized player's ID as our pool ID in NPC pool. This way they're going to have identical IDs + auto npcId = storage.claimHint(newConnectionResult.second->getID(), this, newConnectionResult.second); + + auto npc = storage.get(npcId); + if (npc) + { + // Call connect events for both NPC and player, this way it can get initialized properly in player pool too + ScopedPoolReleaseLock lock(*this, npc->getID()); + if (lock.entry) + { + eventDispatcher.dispatch(&NPCEventHandler::onNPCCreate, *lock.entry); + npcNetwork.networkEventDispatcher.dispatch(&NetworkEventHandler::onPeerConnect, *lock.entry->getPlayer()); + } + } + + return npc; +} + +void NPCComponent::destroy(INPC& npc) +{ + npcNetwork.disconnect(*npc.getPlayer()); +} + +void NPCComponent::emulateRPCIn(IPlayer& player, int rpcId, NetworkBitStream& bs) +{ + const bool res = npcNetwork.inEventDispatcher.stopAtFalse([&player, rpcId, &bs](NetworkInEventHandler* handler) + { + return handler->onReceiveRPC(player, rpcId, bs); + }); + + if (res) + { + npcNetwork.rpcInEventDispatcher.stopAtFalse(rpcId, [&player, &bs](SingleNetworkInEventHandler* handler) + { + bs.resetReadPointer(); + return handler->onReceive(player, bs); + }); + } +} + +void NPCComponent::emulatePacketIn(IPlayer& player, int type, NetworkBitStream& bs) +{ + const bool res = npcNetwork.inEventDispatcher.stopAtFalse([&player, type, &bs](NetworkInEventHandler* handler) + { + bs.SetReadOffset(8); // Ignore packet ID + return handler->onReceivePacket(player, type, bs); + }); + + if (res) + { + npcNetwork.packetInEventDispatcher.stopAtFalse(type, [&player, &bs](SingleNetworkInEventHandler* handler) + { + bs.SetReadOffset(8); // Ignore packet ID + return handler->onReceive(player, bs); + }); + } +} + +COMPONENT_ENTRY_POINT() +{ + return new NPCComponent(); +} diff --git a/Server/Components/NPCs/npcs_impl.hpp b/Server/Components/NPCs/npcs_impl.hpp new file mode 100644 index 000000000..e4bf33afd --- /dev/null +++ b/Server/Components/NPCs/npcs_impl.hpp @@ -0,0 +1,151 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. + * + * The original code is copyright (c) 2022, open.mp team and contributors. + */ +#pragma once +#include +#include + +#include +#include +#include +#include +#include "./Network/npcs_network.hpp" +#include "./NPC/npc.hpp" + +using namespace Impl; + +class NPCComponent final : public INPCComponent, public CoreEventHandler, public PlayerDamageEventHandler +{ +public: + StringView componentName() const override + { + return "Controllable NPCs"; + } + + SemanticVersion componentVersion() const override + { + return SemanticVersion(0, 0, 1, 0); + } + + void onLoad(ICore* c) override; + + void onInit(IComponentList* components) override; + + void onReady() override { } + + void free() override; + + void reset() override + { + } + + INetwork* getNetwork() override; + + IEventDispatcher& getEventDispatcher() override; + + IEventDispatcher>& getPoolEventDispatcher() override; + + const FlatPtrHashSet& entries() override; + + Pair bounds() const override; + + INPC* get(int index) override; + + void release(int index) override; + + void lock(int index) override; + + bool unlock(int index) override; + + void onTick(Microseconds elapsed, TimePoint now) override; + + void onPlayerGiveDamage(IPlayer& player, IPlayer& to, float amount, unsigned weapon, BodyPart part) override; + + void onPlayerTakeDamage(IPlayer& player, IPlayer* from, float amount, unsigned weapon, BodyPart part) override; + + INPC* create(StringView name) override; + + void destroy(INPC& npc) 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); + + void emulateRPCIn(IPlayer& player, int rpcId, NetworkBitStream& bs); + + void emulatePacketIn(IPlayer& player, int type, NetworkBitStream& bs); + + ICore* getCore() + { + return core; + } + + IVehiclesComponent* getVehiclesPool() + { + return vehicles; + } + + IObjectsComponent* getObjectsPool() + { + return objects; + } + + IActorsComponent* getActorsPool() + { + return actors; + } + + DefaultEventDispatcher& getEventDispatcher_internal() + { + return eventDispatcher; + } + + int getFootSyncRate() const + { + return *footSyncRate; + } + + int getGeneralNPCUpdateRate() const + { + return *generalNPCUpdateRateMS; + } + + void provideConfiguration(ILogger& logger, IEarlyConfig& config, bool defaults) override + { + int defaultGeneralNPCUpdateRateMS = 50; + if (defaults) + { + config.setInt("npc.globalUpdateRate", defaultGeneralNPCUpdateRateMS); + } + else + { + // Set default values if options are not set. + if (config.getType("npc.globalUpdateRate") == ConfigOptionType_None) + { + config.setInt("npc.globalUpdateRate", defaultGeneralNPCUpdateRateMS); + } + } + + generalNPCUpdateRateMS = config.getInt("npc.globalUpdateRate"); + } + +private: + ICore* core = nullptr; + NPCNetwork npcNetwork; + DefaultEventDispatcher eventDispatcher; + MarkedDynamicPoolStorage storage; + bool shouldCallCustomEvents = true; + + // Update rates + int* generalNPCUpdateRateMS = nullptr; + int* footSyncRate = nullptr; + + // Components + IVehiclesComponent* vehicles = nullptr; + IObjectsComponent* objects = nullptr; + IActorsComponent* actors = nullptr; +}; diff --git a/Server/Components/NPCs/utils.hpp b/Server/Components/NPCs/utils.hpp new file mode 100644 index 000000000..35f42c466 --- /dev/null +++ b/Server/Components/NPCs/utils.hpp @@ -0,0 +1,625 @@ +#pragma once +#include +#include "npcs_impl.hpp" +#include + +#define MAX_HIT_RADIUS 0.4f +#define MAX_HIT_RADIUS_VEHICLE 1.0f +#define MAX_DISTANCE_TO_ENTER_VEHICLE 30.0f +#define MIN_VEHICLE_GO_TO_DISTANCE 1.0f + +static const float WeaponDamages[MAX_WEAPON_ID] = { + 5.0f, // fists (0) + 5.0f, // WEAPON_BRASSKNUCKLE (1) + 5.0f, // WEAPON_GOLFCLUB (2) + 5.0f, // WEAPON_NITESTICK (3) + 5.0f, // WEAPON_KNIFE (4) + 5.0f, // WEAPON_BAT (5) + 5.0f, // WEAPON_SHOVEL (6) + 5.0f, // WEAPON_POOLSTICK (7) + 5.0f, // WEAPON_KATANA (8) + 5.0f, // WEAPON_CHAINSAW (9) + 5.0f, // WEAPON_DILDO (10) + 5.0f, // WEAPON_DILDO2 (11) + 5.0f, // WEAPON_VIBRATOR (12) + 5.0f, // WEAPON_VIBRATOR2 (13) + 5.0f, // WEAPON_FLOWER (14) + 5.0f, // WEAPON_CANE (15) + 5.0f, // WEAPON_GRENADE (16) + 5.0f, // WEAPON_TEARGAS (17) + 5.0f, // WEAPON_MOLTOV (18) + 0.0f, // nothing (19) + 0.0f, // nothing (20) + 0.0f, // nothing (21) + 8.25f, // WEAPON_COLT45 (22) + 13.2f, // WEAPON_SILENCED (23) + 46.2f, // WEAPON_DEAGLE (24) + 30.0f, // WEAPON_SHOTGUN (25) + 30.0f, // WEAPON_SAWEDOFF (26) + 30.0f, // WEAPON_SHOTGSPA (27) + 6.6f, // WEAPON_UZI (28) + 8.25f, // WEAPON_MP5 (29) + 9.9f, // WEAPON_AK47 (30) + 9.9f, // WEAPON_M4 (31) + 6.6f, // WEAPON_TEC9 (32) + 24.8f, // WEAPON_RIFLE (33) + 41.3f, // WEAPON_SNIPER (34) + 5.0f, // WEAPON_ROCKETLAUNCHER (35) + 5.0f, // WEAPON_HEATSEEKER (36) + 5.0f, // WEAPON_FLAMETHROWER (37) + 46.2f, // WEAPON_MINIGUN (38) + 5.0f, // WEAPON_SATCHEL (39) + 5.0f, // WEAPON_BOMB (40) + 5.0f, // WEAPON_SPRAYCAN (41) + 5.0f, // WEAPON_FIREEXTINGUISHER (42) + 0.0f, // WEAPON_CAMERA (43) + 0.0f, // WEAPON_NIGHTVISION (44) + 0.0f, // WEAPON_INFRARED (45) + 0.0f, // WEAPON_PARACHUTE (46) +}; + +inline bool canWeaponBeDoubleHanded(uint8_t weapon) +{ + switch (weapon) + { + case PlayerWeapon_Colt45: + case PlayerWeapon_Sawedoff: + case PlayerWeapon_UZI: + case PlayerWeapon_TEC9: + return true; + } + + return false; +} + +inline bool isWeaponDoubleHanded(uint8_t weapon, int skillLevel) +{ + return canWeaponBeDoubleHanded(weapon) && skillLevel > 999; +} + +inline PlayerWeaponSkill getWeaponSkillID(uint8_t weapon) +{ + static PlayerWeaponSkill skills[] = { + PlayerWeaponSkill_Pistol, // 22 + PlayerWeaponSkill_SilencedPistol, // 23 + PlayerWeaponSkill_DesertEagle, // 24 + PlayerWeaponSkill_Shotgun, // 25 + PlayerWeaponSkill_SawnOff, // 26 + PlayerWeaponSkill_SPAS12, // 27 + PlayerWeaponSkill_Uzi, // 28 + PlayerWeaponSkill_MP5, // 29 + PlayerWeaponSkill_AK47, // 30 + PlayerWeaponSkill_M4, // 31 + PlayerWeaponSkill_Uzi, // 32 + PlayerWeaponSkill_Sniper, // 33 + PlayerWeaponSkill_Sniper // 34 + }; + + if (!WeaponSlotData(weapon).shootable()) + { + return PlayerWeaponSkill_Invalid; + } + + return skills[weapon - 22]; +} + +inline int getWeaponActualClipSize(uint8_t weapon, int currentAmmo, int weaponSkillLevel, bool isInfiniteAmmoEnabled) +{ + auto data = WeaponInfo::get(weapon); + if (data.type != PlayerWeaponType_None) + { + int size = data.clipSize; + if (isWeaponDoubleHanded(weapon, weaponSkillLevel)) + { + size *= 2; + } + + if (currentAmmo < size && !isInfiniteAmmoEnabled) + { + size = currentAmmo; + } + + return size; + } + return 0; +} + +inline int getWeaponActualReloadTime(uint8_t weapon, int skillLevel) +{ + auto data = WeaponInfo::get(weapon); + if (data.type != PlayerWeaponType_None) + { + int time = data.reloadTime; + if (isWeaponDoubleHanded(weapon, skillLevel)) + { + time += 700; + } + + return time; + } + return 0; +} + +inline int getWeaponActualShootTime(uint8_t weapon) +{ + auto data = WeaponInfo::get(weapon); + if (data.type != PlayerWeaponType_None) + { + return data.shootTime; + } + return 0; +} + +inline Vector3 getNearestPointToRay(const Vector3& startPosition, const Vector3& endPosition, const Vector3& point) +{ + Vector3 vecDirection = (endPosition - startPosition) / glm::distance(startPosition, endPosition); + return vecDirection * glm::distance(startPosition, point) + startPosition; +} + +inline float getDistanceFromRayToPoint(const Vector3& startPosition, const Vector3& endPosition, const Vector3& point) +{ + return glm::distance(getNearestPointToRay(startPosition, endPosition, point), point); +} + +inline IPlayer* getClosestPlayerInBetween(IPlayerPool* players, const Vector3& hitOrigin, const Vector3& hitTarget, float range, float& distance, int playerId, int targetId, std::pair& results) +{ + IPlayer* closestPlayer = nullptr; + + // Loop through all the players + for (auto player : players->entries()) + { + // Validate the player + if (!player) + { + continue; + } + + auto currPlayerId = player->getID(); + if (playerId == currPlayerId || targetId == currPlayerId || player->isBot()) + { + continue; + } + + auto pos = player->getPosition(); + + // Is the player on the ray + if (getDistanceFromRayToPoint(hitOrigin, hitTarget, pos) > MAX_HIT_RADIUS) + { + continue; + } + + // Is the player in the damage range + float playerDistance = glm::distance(hitOrigin, pos); + if (playerDistance > range) + { + continue; + } + + // Is the player closer than another player + if (!closestPlayer || playerDistance < distance) + { + distance = playerDistance; + closestPlayer = player; + + results.first = getNearestPointToRay(hitOrigin, hitTarget, pos); + results.second = hitTarget - pos; + } + } + return closestPlayer; +} + +inline INPC* getClosestNpcInBetween(NPCComponent* npcs, const Vector3& hitOrigin, const Vector3& hitTarget, float range, float& distance, int playerId, int targetId, std::pair& results) +{ + INPC* closestNpc = nullptr; + + // Loop through all the NPCs + for (auto npc : npcs->entries()) + { + if (!npc) + { + continue; + } + + auto npcId = npc->getID(); + if (playerId == npcId || targetId == npcId) + { + continue; + } + + auto pos = npc->getPosition(); + + // Is the NPC on the ray + if (getDistanceFromRayToPoint(hitOrigin, hitTarget, pos) > MAX_HIT_RADIUS) + { + continue; + } + + // Is the NPC in the damage range + float npcDistance = glm::distance(hitOrigin, pos); + if (npcDistance > range) + { + continue; + } + + // Is the NPC closer than another NPC + if (!closestNpc || npcDistance < distance) + { + distance = npcDistance; + closestNpc = npc; + + results.first = getNearestPointToRay(hitOrigin, hitTarget, pos); + results.second = hitTarget - pos; + } + } + + return closestNpc; +} + +inline IActor* getClosestActorInBetween(IActorsComponent* actors, const Vector3& hitOrigin, const Vector3& hitTarget, float range, float& distance, std::pair& results) +{ + IActor* closestActor = nullptr; + + // Loop through all the actors + for (auto actor : *actors) + { + if (!actor) + { + continue; + } + + // Is the actor on the ray + if (getDistanceFromRayToPoint(hitOrigin, hitTarget, actor->getPosition()) > MAX_HIT_RADIUS) + { + continue; + } + + auto pos = actor->getPosition(); + + // Is the actor in the damage range + float actorDistance = glm::distance(hitOrigin, pos); + if (actorDistance > range) + { + continue; + } + + // Is the actor closer than another actor + if (!closestActor || actorDistance < distance) + { + distance = actorDistance; + closestActor = actor; + + results.first = getNearestPointToRay(hitOrigin, hitTarget, pos); + results.second = hitTarget; + } + } + return closestActor; +} + +inline IVehicle* getClosestVehicleInBetween(IVehiclesComponent* vehicles, const Vector3& hitOrigin, const Vector3& hitTarget, float range, float& distance, std::pair& results) +{ + IVehicle* closestVehicle = nullptr; + + // Loop through all the vehicles + for (auto vehicle : *vehicles) + { + if (!vehicle) + { + continue; + } + + auto pos = vehicle->getPosition(); + + // Is the vehicle on the ray + if (getDistanceFromRayToPoint(hitOrigin, hitTarget, pos) > MAX_HIT_RADIUS_VEHICLE) + { + // Don't use MAX_HIT_RADIUS + continue; + } + + // Is the vehicle in the damage range + float vehicleDistance = glm::distance(hitOrigin, pos); + if (vehicleDistance > range) + { + continue; + } + + // Is the vehicle closer than another vehicle + if (!closestVehicle || vehicleDistance < distance) + { + distance = vehicleDistance; + closestVehicle = vehicle; + + results.first = getNearestPointToRay(hitOrigin, hitTarget, pos); + results.second = hitTarget - pos; + } + } + return closestVehicle; +} + +inline IObject* getClosestObjectInBetween(IObjectsComponent* objects, const Vector3& hitOrigin, const Vector3& hitTarget, float range, float& distance, std::pair& results) +{ + IObject* closestObject = nullptr; + + // Loop through all the objects + for (auto object : *objects) + { + if (!object) + { + continue; + } + + auto pos = object->getPosition(); + + // Is the object on the ray + if (getDistanceFromRayToPoint(hitOrigin, hitTarget, pos) > MAX_HIT_RADIUS) + { + continue; + } + + // Is the object in the damage range + float objectDistance = glm::distance(hitOrigin, pos); + if (objectDistance > range) + { + continue; + } + + // Is the object closer than another object + if (!closestObject || objectDistance < distance) + { + distance = objectDistance; + closestObject = object; + + results.first = getNearestPointToRay(hitOrigin, hitTarget, pos); + results.second = hitTarget - pos; + } + } + return closestObject; +} + +inline IPlayerObject* getClosestPlayerObjectInBetween(IPlayerPool* players, const Vector3& hitOrigin, const Vector3& hitTarget, float range, float& distance, int ownerId, std::pair& results) +{ + IPlayerObject* closestPlayerObject = nullptr; + + // Validate the owner + auto player = players->get(ownerId); + if (!player) + { + return closestPlayerObject; + } + + // Loop through all the player objects of the owner + auto playerObjects = queryExtension(player); + if (playerObjects) + { + for (auto object : *playerObjects) + { + if (!object) + { + continue; + } + + auto pos = object->getPosition(); + + // Is the player object on the ray + if (getDistanceFromRayToPoint(hitOrigin, hitTarget, pos) > MAX_HIT_RADIUS) + { + continue; + } + + // Is the player object in the damage range + float playerObjectDistance = glm::distance(hitOrigin, pos); + if (playerObjectDistance > range) + { + continue; + } + + // Is the player object closer than another player object + if (!closestPlayerObject || playerObjectDistance < distance) + { + distance = playerObjectDistance; + closestPlayerObject = object; + + results.first = getNearestPointToRay(hitOrigin, hitTarget, pos); + results.second = hitTarget - pos; + } + } + } + return closestPlayerObject; +} + +inline void* getClosestEntityInBetween(NPCComponent* npcs, const Vector3& hitOrigin, const Vector3& hitTarget, float range, EntityCheckType betweenCheckFlags, int playerId, int targetId, EntityCheckType& entityType, int& playerObjectOwnerId, Vector3& hitMap, std::pair& results) +{ + void* closestEntity = nullptr; + float closestEntityDistance = 0.0f; + entityType = EntityCheckType::None; + + IPlayer* closestPlayer = nullptr; + if (int(betweenCheckFlags) & int(EntityCheckType::Player)) + { + float closestPlayerDistance = 0.0f; + closestPlayer = getClosestPlayerInBetween(&npcs->getCore()->getPlayers(), hitOrigin, hitTarget, range, closestPlayerDistance, playerId, targetId, results); + if (closestPlayer != nullptr && (closestEntity == nullptr || closestPlayerDistance < closestEntityDistance)) + { + entityType = EntityCheckType::Player; + closestEntityDistance = closestPlayerDistance; + closestEntity = static_cast(closestPlayer); + } + } + + INPC* closestNPC = nullptr; + if (int(betweenCheckFlags) & int(EntityCheckType::NPC)) + { + float closestNPCDistance = 0.0f; + closestNPC = getClosestNpcInBetween(npcs, hitOrigin, hitTarget, range, closestNPCDistance, playerId, targetId, results); + if (closestNPC != nullptr && (closestEntity == nullptr || closestNPCDistance < closestEntityDistance)) + { + entityType = EntityCheckType::NPC; + closestEntityDistance = closestNPCDistance; + closestEntity = static_cast(closestNPC); + } + } + + if (int(betweenCheckFlags) & int(EntityCheckType::Actor)) + { + float closestActorDistance = 0.0f; + IActor* closestActor = getClosestActorInBetween(npcs->getActorsPool(), hitOrigin, hitTarget, range, closestActorDistance, results); + if (closestActor != nullptr && (closestEntity == nullptr || closestActorDistance < closestEntityDistance)) + { + entityType = EntityCheckType::Actor; + closestEntityDistance = closestActorDistance; + closestEntity = static_cast(closestActor); + } + } + + if (int(betweenCheckFlags) & int(EntityCheckType::Vehicle)) + { + float closestVehicleDistance = 0.0f; + IVehicle* closestVehicle = getClosestVehicleInBetween(npcs->getVehiclesPool(), hitOrigin, hitTarget, range, closestVehicleDistance, results); + if (closestVehicle != nullptr && (closestEntity == nullptr || closestVehicleDistance < closestEntityDistance)) + { + entityType = EntityCheckType::Vehicle; + closestEntityDistance = closestVehicleDistance; + closestEntity = static_cast(closestVehicle); + } + } + + if (int(betweenCheckFlags) & int(EntityCheckType::Object)) + { + float closestObjectDistance = 0.0f; + IObject* closestObject = getClosestObjectInBetween(npcs->getObjectsPool(), hitOrigin, hitTarget, range, closestObjectDistance, results); + if (closestObject != nullptr && (closestEntity == nullptr || closestObjectDistance < closestEntityDistance)) + { + entityType = EntityCheckType::Object; + closestEntityDistance = closestObjectDistance; + closestEntity = static_cast(closestObject); + } + } + + if (int(betweenCheckFlags) & int(EntityCheckType::ProjectOrig)) + { + float closestPlayerObjectDistance = 0.0f; + IPlayerObject* closestPlayerObject = getClosestPlayerObjectInBetween(&npcs->getCore()->getPlayers(), hitOrigin, hitTarget, range, closestPlayerObjectDistance, playerId, results); + if (closestPlayerObject != nullptr && (closestEntity == nullptr || closestPlayerObjectDistance < closestEntityDistance)) + { + entityType = EntityCheckType::ProjectOrig; + playerObjectOwnerId = playerId; + closestEntityDistance = closestPlayerObjectDistance; + closestEntity = static_cast(closestPlayerObject); + } + } + + if (int(betweenCheckFlags) & int(EntityCheckType::ProjectTarg)) + { + // One flag for 3 checks + // Check if a player object of the target is in between the origin and the target + if (targetId != INVALID_PLAYER_ID) + { + float closestPlayerObjectDistance = 0.0; + IPlayerObject* closestPlayerObject = getClosestPlayerObjectInBetween(&npcs->getCore()->getPlayers(), hitOrigin, hitTarget, range, closestPlayerObjectDistance, targetId, results); + if (closestPlayerObject != nullptr && (closestEntity == nullptr || closestPlayerObjectDistance < closestEntityDistance)) + { + entityType = EntityCheckType::ProjectTarg; + playerObjectOwnerId = targetId; + closestEntityDistance = closestPlayerObjectDistance; + closestEntity = static_cast(closestPlayerObject); + } + } + + // Check if a player object of the closestPlayer is in between the origin and the target when the closestPlayer is currently the closest entity + if (closestPlayer != nullptr && closestEntity == closestPlayer) + { + float closestPlayerObjectDistance = 0.0; + IPlayerObject* closestPlayerObject = getClosestPlayerObjectInBetween(&npcs->getCore()->getPlayers(), hitOrigin, hitTarget, range, closestPlayerObjectDistance, closestPlayer->getID(), results); + if (closestPlayerObject != nullptr && (closestEntity == nullptr || closestPlayerObjectDistance < closestEntityDistance)) + { + entityType = EntityCheckType::ProjectTarg; + playerObjectOwnerId = closestPlayer->getID(); + closestEntityDistance = closestPlayerObjectDistance; + closestEntity = static_cast(closestPlayerObject); + } + } + + // Check if a player object of the closestNPC is in between the origin and the target when the closestNPC is currently the closest entity + if (closestNPC != nullptr && closestEntity == closestNPC) + { + float closestPlayerObjectDistance = 0.0; + IPlayerObject* closestPlayerObject = getClosestPlayerObjectInBetween(&npcs->getCore()->getPlayers(), hitOrigin, hitTarget, range, closestPlayerObjectDistance, closestNPC->getID(), results); + if (closestPlayerObject != nullptr && (closestEntity == nullptr || closestPlayerObjectDistance < closestEntityDistance)) + { + entityType = EntityCheckType::ProjectTarg; + playerObjectOwnerId = closestNPC->getID(); + // closestEntityDistance = closestPlayerObjectDistance; + closestEntity = static_cast(closestPlayerObject); + } + } + } + + return closestEntity; +} + +inline float getNearestFloatValue(float value, const DynamicArray& floatArray) +{ + float nearest = floatArray[0]; + + for (auto f : floatArray) + { + if (std::abs(f - value) < std::abs(nearest - value)) + { + nearest = f; + } + } + + return nearest; +} + +inline bool isEqualFloat(float a, float b) +{ + return glm::epsilonEqual(a, b, glm::epsilon()); +} + +inline float getAngleOfLine(float x, float y) +{ + float angle = atan2(y, x) * (180.0f / M_PI) + 270.0f; + if (angle >= 360.0f) + { + angle -= 360.0f; + } + else if (angle < 0.0f) + { + angle += 360.0f; + } + return angle; +} + +inline Vector3 getVehicleSeatPos(IVehicle& vehicle, int seatId) +{ + // Get the seat position + Vector3 seatPosFromModelInfo; + + if (seatId == 0 || seatId == 1) + { + Impl::getVehicleModelInfo(vehicle.getModel(), VehicleModelInfo_FrontSeat, seatPosFromModelInfo); + } + else + { + Impl::getVehicleModelInfo(vehicle.getModel(), VehicleModelInfo_RearSeat, seatPosFromModelInfo); + } + + // Adjust the seat vector + Vector3 seatPosNoAngle(seatPosFromModelInfo.x + 1.3f, seatPosFromModelInfo.y - 0.6f, seatPosFromModelInfo.z); + + if (seatId == 0 || seatId == 2) + { + seatPosNoAngle.x = -seatPosNoAngle.x; + } + + // Get vehicle angle + float angle = vehicle.getZAngle(); + float _angle = angle * 0.01570796326794897f; + + Vector3 seatPos(seatPosNoAngle.x * cos(_angle) - seatPosNoAngle.y * sin(_angle), + seatPosNoAngle.x * sin(_angle) + seatPosNoAngle.y * cos(_angle), + seatPosNoAngle.z); + + return seatPos + vehicle.getPosition(); +} diff --git a/Server/Components/Pawn/Manager/Manager.hpp b/Server/Components/Pawn/Manager/Manager.hpp index 0f1b606b5..5218db894 100644 --- a/Server/Components/Pawn/Manager/Manager.hpp +++ b/Server/Components/Pawn/Manager/Manager.hpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include diff --git a/Server/Components/Pawn/Scripting/Impl.cpp b/Server/Components/Pawn/Scripting/Impl.cpp index 4ccde9462..4c569b757 100644 --- a/Server/Components/Pawn/Scripting/Impl.cpp +++ b/Server/Components/Pawn/Scripting/Impl.cpp @@ -21,6 +21,7 @@ #include "Vehicle/Events.hpp" #include "GangZone/Events.hpp" #include "CustomModels/Events.hpp" +#include "NPC/Events.hpp" Scripting::~Scripting() { @@ -88,6 +89,10 @@ Scripting::~Scripting() { mgr->models->getEventDispatcher().removeEventHandler(CustomModelsEvents::Get()); } + if (mgr->npcs) + { + mgr->npcs->getEventDispatcher().removeEventHandler(NPCEvents::Get()); + } } void Scripting::addEvents() const @@ -156,4 +161,8 @@ void Scripting::addEvents() const { mgr->models->getEventDispatcher().addEventHandler(CustomModelsEvents::Get()); } + if (mgr->npcs) + { + mgr->npcs->getEventDispatcher().addEventHandler(NPCEvents::Get()); + } } diff --git a/Server/Components/Pawn/Scripting/NPC/Events.hpp b/Server/Components/Pawn/Scripting/NPC/Events.hpp new file mode 100644 index 000000000..144be8d6d --- /dev/null +++ b/Server/Components/Pawn/Scripting/NPC/Events.hpp @@ -0,0 +1,57 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. + * + * The original code is copyright (c) 2022, open.mp team and contributors. + */ + +#pragma once +#include "../../Manager/Manager.hpp" +#include "../../Singleton.hpp" +#include "sdk.hpp" + +struct NPCEvents : public NPCEventHandler, public Singleton +{ + void onNPCFinishMove(INPC& npc) override + { + PawnManager::Get()->CallAllInEntryFirst("OnNPCFinishMove", DefaultReturnValue_True, npc.getID()); + } + + void onNPCCreate(INPC& npc) override + { + PawnManager::Get()->CallAllInEntryFirst("OnNPCCreate", DefaultReturnValue_True, npc.getID()); + } + + void onNPCDestroy(INPC& npc) override + { + PawnManager::Get()->CallAllInEntryFirst("OnNPCDestroy", DefaultReturnValue_True, npc.getID()); + } + + void onNPCWeaponStateChange(INPC& npc, PlayerWeaponState newState, PlayerWeaponState oldState) override + { + PawnManager::Get()->CallAllInEntryFirst("OnNPCWeaponStateChange", DefaultReturnValue_True, npc.getID(), int(newState), int(oldState)); + } + + bool onNPCTakeDamage(INPC& npc, IPlayer& damager, float damage, uint8_t weapon, BodyPart bodyPart) override + { + auto result = !!PawnManager::Get()->CallAllInEntryFirst("OnNPCTakeDamage", DefaultReturnValue_True, npc.getID(), damager.getID(), damage, weapon, int(bodyPart)); + return result; + } + + bool onNPCGiveDamage(INPC& npc, IPlayer& damager, float damage, uint8_t weapon, BodyPart bodyPart) override + { + auto result = !!PawnManager::Get()->CallAllInEntryFirst("OnNPCGiveDamage", DefaultReturnValue_True, npc.getID(), damager.getID(), damage, weapon, int(bodyPart)); + return result; + } + + void onNPCDeath(INPC& npc, IPlayer* killer, int reason) override + { + PawnManager::Get()->CallAllInEntryFirst("OnNPCDeath", DefaultReturnValue_True, npc.getID(), killer ? killer->getID() : INVALID_PLAYER_ID, reason); + } + + void onNPCSpawn(INPC& npc) override + { + PawnManager::Get()->CallAllInEntryFirst("OnNPCSpawn", DefaultReturnValue_True, npc.getID()); + } +}; diff --git a/Server/Components/Pawn/Scripting/NPC/Natives.cpp b/Server/Components/Pawn/Scripting/NPC/Natives.cpp new file mode 100644 index 000000000..d3189c69a --- /dev/null +++ b/Server/Components/Pawn/Scripting/NPC/Natives.cpp @@ -0,0 +1,376 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. + * + * The original code is copyright (c) 2022, open.mp team and contributors. + */ + +#include "../Types.hpp" +#include "sdk.hpp" +#include +#include "../../format.hpp" + +SCRIPT_API(NPC_Create, int(const String& name)) +{ + auto component = PawnManager::Get()->npcs; + if (component) + { + auto npc = component->create(name.c_str()); + if (npc) + { + return npc->getID(); + } + } + return INVALID_PLAYER_ID; +} + +SCRIPT_API(NPC_Destroy, bool(INPC& npc)) +{ + PawnManager::Get()->npcs->destroy(npc); + return true; +} + +SCRIPT_API(NPC_IsValid, bool(INPC* npc)) +{ + return npc != nullptr; +} + +SCRIPT_API(NPC_Spawn, bool(INPC& npc)) +{ + npc.spawn(); + return true; +} + +SCRIPT_API(NPC_SetPos, bool(INPC& npc, Vector3 position)) +{ + npc.setPosition(position); + return true; +} + +SCRIPT_API(NPC_GetPos, bool(INPC& npc, Vector3& position)) +{ + position = npc.getPosition(); + return true; +} + +SCRIPT_API(NPC_SetRot, bool(INPC& npc, Vector3 rotation)) +{ + npc.setRotation(rotation); + return true; +} + +SCRIPT_API(NPC_GetRot, bool(INPC& npc, Vector3& rotation)) +{ + rotation = npc.getRotation().ToEuler(); + return true; +} + +SCRIPT_API(NPC_SetFacingAngle, bool(INPC& npc, float angle)) +{ + auto rotation = npc.getRotation().ToEuler(); + rotation.z = angle; + npc.setRotation(rotation); + return true; +} + +SCRIPT_API(NPC_GetFacingAngle, bool(INPC& npc, float& angle)) +{ + auto rotation = npc.getRotation().ToEuler(); + angle = rotation.z; + return true; +} + +SCRIPT_API(NPC_SetVirtualWorld, bool(INPC& npc, int virtualWorld)) +{ + npc.setVirtualWorld(virtualWorld); + return true; +} + +SCRIPT_API(NPC_GetVirtualWorld, int(INPC& npc)) +{ + return npc.getVirtualWorld(); +} + +SCRIPT_API(NPC_Move, bool(INPC& npc, Vector3 targetPos, int moveType, float moveSpeed)) +{ + return npc.move(targetPos, NPCMoveType(moveType), moveSpeed); +} + +SCRIPT_API(NPC_StopMove, bool(INPC& npc)) +{ + npc.stopMove(); + return true; +} + +SCRIPT_API(NPC_IsMoving, bool(INPC& npc)) +{ + return npc.isMoving(); +} + +SCRIPT_API(NPC_SetSkin, bool(INPC& npc, int model)) +{ + npc.setSkin(model); + return true; +} + +SCRIPT_API(NPC_IsStreamedIn, bool(INPC& npc, IPlayer& player)) +{ + return npc.isStreamedInForPlayer(player); +} + +SCRIPT_API(NPC_IsAnyStreamedIn, bool(INPC& npc)) +{ + auto streamedIn = npc.streamedForPlayers(); + return streamedIn.size() > 0; +} + +SCRIPT_API(NPC_GetAll, int(DynamicArray& outputNPCs)) +{ + int index = -1; + auto npcs = PawnManager::Get()->npcs; + if (npcs) + { + if (outputNPCs.size() < npcs->count()) + { + PawnManager::Get()->core->printLn( + "There are %zu NPCs in your server but array size used in `NPC_GetAll` is %zu; Use a bigger size in your script.", + npcs->count(), + outputNPCs.size()); + } + + for (INPC* npc : *npcs) + { + index++; + if (index >= outputNPCs.size()) + { + break; + } + outputNPCs[index] = npc->getID(); + } + } + return index + 1; +} + +SCRIPT_API(NPC_SetInterior, bool(INPC& npc, int interior)) +{ + npc.setInterior(interior); + return true; +} + +SCRIPT_API(NPC_GetInterior, int(INPC& npc)) +{ + return npc.getInterior(); +} + +SCRIPT_API(NPC_SetHealth, bool(INPC& npc, float health)) +{ + npc.setHealth(health); + return true; +} + +SCRIPT_API(NPC_GetHealth, float(INPC& npc)) +{ + return npc.getHealth(); +} + +SCRIPT_API(NPC_SetArmour, bool(INPC& npc, float armour)) +{ + npc.setArmour(armour); + return true; +} + +SCRIPT_API(NPC_GetArmour, float(INPC& npc)) +{ + return npc.getArmour(); +} + +SCRIPT_API(NPC_IsDead, bool(INPC& npc)) +{ + return npc.isDead(); +} + +SCRIPT_API(NPC_ApplyAnimation, bool(INPC& npc, const std::string& animlib, const std::string& animname, float delta, bool loop, bool lockX, bool lockY, bool freeze, uint32_t time, int sync)) +{ + const AnimationData animationData(delta, loop, lockX, lockY, freeze, time, animlib, animname); + npc.getPlayer()->applyAnimation(animationData, PlayerAnimationSyncType_SyncOthers); + return true; +} + +SCRIPT_API(NPC_SetWeapon, bool(INPC& npc, uint8_t weapon)) +{ + npc.setWeapon(weapon); + return true; +} + +SCRIPT_API(NPC_GetWeapon, uint8_t(INPC& npc)) +{ + return npc.getWeapon(); +} + +SCRIPT_API(NPC_SetAmmo, bool(INPC& npc, int ammo)) +{ + npc.setAmmo(ammo); + return true; +} + +SCRIPT_API(NPC_GetAmmo, int(INPC& npc)) +{ + return npc.getAmmo(); +} + +SCRIPT_API(NPC_SetKeys, bool(INPC& npc, uint16_t upAndDown, uint16_t leftAndDown, uint16_t keys)) +{ + npc.setKeys(upAndDown, leftAndDown, keys); + return true; +} + +SCRIPT_API(NPC_GetKeys, bool(INPC& npc, uint16_t& upAndDown, uint16_t& leftAndDown, uint16_t& keys)) +{ + npc.getKeys(upAndDown, leftAndDown, keys); + return true; +} + +SCRIPT_API(NPC_SetWeaponSkillLevel, bool(INPC& npc, uint8_t skill, int level)) +{ + npc.setWeaponSkillLevel(PlayerWeaponSkill(skill), level); + return true; +} + +SCRIPT_API(NPC_GetWeaponSkillLevel, int(INPC& npc, int skill)) +{ + return npc.getWeaponSkillLevel(PlayerWeaponSkill(skill)); +} + +SCRIPT_API(NPC_MeleeAttack, bool(INPC& npc, int time, bool secondaryAttack)) +{ + npc.meleeAttack(time, secondaryAttack); + return true; +} + +SCRIPT_API(NPC_StopMeleeAttack, bool(INPC& npc)) +{ + npc.stopMeleeAttack(); + return true; +} + +SCRIPT_API(NPC_IsMeleeAttacking, bool(INPC& npc)) +{ + return npc.isMeleeAttacking(); +} + +SCRIPT_API(NPC_SetFightingStyle, bool(INPC& npc, int style)) +{ + npc.setFightingStyle(PlayerFightingStyle(style)); + return true; +} + +SCRIPT_API(NPC_GetFightingStyle, int(INPC& npc)) +{ + return int(npc.getFightingStyle()); +} + +SCRIPT_API(NPC_EnableReloading, bool(INPC& npc, bool enable)) +{ + npc.enableReloading(enable); + return true; +} + +SCRIPT_API(NPC_IsReloadEnabled, bool(INPC& npc)) +{ + return npc.isReloadEnabled(); +} + +SCRIPT_API(NPC_IsReloading, bool(INPC& npc)) +{ + return npc.isReloading(); +} + +SCRIPT_API(NPC_EnableInfiniteAmmo, bool(INPC& npc, bool enable)) +{ + npc.enableInfiniteAmmo(enable); + return true; +} + +SCRIPT_API(NPC_IsInfiniteAmmoEnabled, bool(INPC& npc)) +{ + return npc.isInfiniteAmmoEnabled(); +} + +SCRIPT_API(NPC_GetWeaponState, int(INPC& npc)) +{ + return int(npc.getWeaponState()); +} + +SCRIPT_API(NPC_SetAmmoInClip, bool(INPC& npc, int ammo)) +{ + npc.setAmmoInClip(ammo); + return true; +} + +SCRIPT_API(NPC_GetAmmoInClip, int(INPC& npc)) +{ + return npc.getAmmoInClip(); +} + +SCRIPT_API(NPC_Shoot, bool(INPC& npc, uint8_t weapon, int hitId, int hitType, Vector3 endPoint, Vector3 offset, bool isHit, uint8_t checkInBetweenFlags)) +{ + npc.shoot(hitId, PlayerBulletHitType(hitType), weapon, endPoint, offset, isHit, EntityCheckType(checkInBetweenFlags)); + return true; +} + +SCRIPT_API(NPC_IsShooting, bool(INPC& npc)) +{ + return npc.isShooting(); +} + +SCRIPT_API(NPC_AimAt, bool(INPC& npc, Vector3 point, bool shoot, int shootDelay, bool updateAngle, Vector3 offsetFrom, uint8_t checkInBetweenFlags)) +{ + npc.aimAt(point, shoot, shootDelay, updateAngle, offsetFrom, EntityCheckType(checkInBetweenFlags)); + return true; +} + +SCRIPT_API(NPC_AimAtPlayer, bool(INPC& npc, IPlayer& atPlayer, bool shoot, int shootDelay, bool updateAngle, Vector3 offset, Vector3 offsetFrom, uint8_t checkInBetweenFlags)) +{ + npc.aimAtPlayer(atPlayer, shoot, shootDelay, updateAngle, offset, offsetFrom, EntityCheckType(checkInBetweenFlags)); + return true; +} + +SCRIPT_API(NPC_StopAim, bool(INPC& npc)) +{ + npc.stopAim(); + return true; +} + +SCRIPT_API(NPC_IsAiming, bool(INPC& npc)) +{ + return npc.isAiming(); +} + +SCRIPT_API(NPC_IsAimingAtPlayer, bool(INPC& npc, IPlayer& atPlayer)) +{ + return npc.isAimingAtPlayer(atPlayer); +} + +SCRIPT_API(NPC_SetWeaponAccuracy, bool(INPC& npc, int weapon, float accuracy)) +{ + npc.setWeaponAccuracy(weapon, accuracy); + return true; +} + +SCRIPT_API(NPC_GetWeaponAccuracy, float(INPC& npc, int weapon)) +{ + return npc.getWeaponAccuracy(weapon); +} + +SCRIPT_API(NPC_EnterVehicle, bool(INPC& npc, IVehicle& vehicle, int seatId, int moveType)) +{ + npc.enterVehicle(vehicle, seatId, NPCMoveType(moveType)); + return true; +} + +SCRIPT_API(NPC_ExitVehicle, bool(INPC& npc)) +{ + npc.exitVehicle(); + return true; +} diff --git a/Server/Components/Pawn/main.cpp b/Server/Components/Pawn/main.cpp index a054d1e16..8c99558ac 100644 --- a/Server/Components/Pawn/main.cpp +++ b/Server/Components/Pawn/main.cpp @@ -167,6 +167,7 @@ class PawnComponent final : public IPawnComponent, public CoreEventHandler, publ mgr->vars = components->queryComponent(); mgr->vehicles = components->queryComponent(); mgr->models = components->queryComponent(); + mgr->npcs = components->queryComponent(); scriptingInstance.addEvents(); diff --git a/Server/Components/Recordings/recordings_main.cpp b/Server/Components/Recordings/recordings_main.cpp index 0a884b595..75d70060b 100644 --- a/Server/Components/Recordings/recordings_main.cpp +++ b/Server/Components/Recordings/recordings_main.cpp @@ -85,16 +85,16 @@ class RecordingsComponent final : public IRecordingsComponent, public PlayerConn bool onReceive(IPlayer& peer, NetworkBitStream& bs) override { - NetCode::Packet::PlayerFootSync footSync; - if (!footSync.read(bs)) + PlayerRecordingData* data = queryExtension(peer); + if (!data) { - return false; + return true; } - PlayerRecordingData* data = queryExtension(peer); - if (!data) + NetCode::Packet::PlayerFootSync footSync; + if (!footSync.read(bs)) { - return false; + return true; } // Write on foot recording data @@ -135,16 +135,16 @@ class RecordingsComponent final : public IRecordingsComponent, public PlayerConn bool onReceive(IPlayer& peer, NetworkBitStream& bs) override { - NetCode::Packet::PlayerVehicleSync vehicleSync; - if (!vehicleSync.read(bs)) + PlayerRecordingData* data = queryExtension(peer); + if (!data) { - return false; + return true; } - PlayerRecordingData* data = queryExtension(peer); - if (!data) + NetCode::Packet::PlayerVehicleSync vehicleSync; + if (!vehicleSync.read(bs)) { - return false; + return true; } // Write driver recording data diff --git a/Server/Source/player_impl.hpp b/Server/Source/player_impl.hpp index 1ad442b45..22d772372 100644 --- a/Server/Source/player_impl.hpp +++ b/Server/Source/player_impl.hpp @@ -928,7 +928,7 @@ struct Player final : public IPlayer, public PoolIDProvider, public NoCopy void setSkillLevel(PlayerWeaponSkill skill, int level) override { - if (skill < skillLevels_.size()) + if (skill != PlayerWeaponSkill_Invalid && skill < skillLevels_.size()) { skillLevels_[skill] = level; NetCode::RPC::SetPlayerSkillLevel setPlayerSkillLevelRPC; diff --git a/Shared/NetCode/vehicle.hpp b/Shared/NetCode/vehicle.hpp index 0f0a7cbab..95bfde31c 100644 --- a/Shared/NetCode/vehicle.hpp +++ b/Shared/NetCode/vehicle.hpp @@ -214,7 +214,6 @@ namespace RPC bool read(NetworkBitStream& bs) { return bs.readUINT16(VehicleID); - ; } void write(NetworkBitStream& bs) const