diff --git a/SDK b/SDK index 3dc9cf854..2ab854da8 160000 --- a/SDK +++ b/SDK @@ -1 +1 @@ -Subproject commit 3dc9cf854c2a002a683baf50d5845f396c634e2c +Subproject commit 2ab854da82117ddae5c24089440699cae9e61f6b 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..08eb5fef5 --- /dev/null +++ b/Server/Components/NPCs/NPC/npc.cpp @@ -0,0 +1,365 @@ +/* + * 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" + +namespace utils +{ +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; +} +} + +NPC::NPC(NPCComponent* component, IPlayer* playerPtr) + : moveType_(NPCMoveType_None) + , estimatedArrivalTimeNS_(0) + , moveSpeed_(0.0f) + , targetPosition_({ 0.0f, 0.0f, 0.0f }) + , velocity_({ 0.0f, 0.0f, 0.0f }) + , moving_(false) + , needsVelocityUpdate_(false) +{ + // 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 = { 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_.WeaponAdditionalKey = 0; + 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 player_->getPosition(); +} + +void NPC::setPosition(Vector3 pos) +{ + footSync_.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(*this, NetCode::RPC::PlayerRequestClass::PacketID, requestClassBS); + + npcComponent_->emulateRPCIn(*this, NetCode::RPC::PlayerRequestSpawn::PacketID, emptyBS); + npcComponent_->emulateRPCIn(*this, NetCode::RPC::PlayerSpawn::PacketID, emptyBS); +} + +bool NPC::move(Vector3 pos, NPCMoveType moveType) +{ + if (moveType == NPCMoveType_None) + { + return false; + } + + // 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 speed = 0.0f; + if (moveType == NPCMoveType_Sprint) + { + speed = NPC_MOVE_SPEED_SPRINT; + footSync_.Keys |= Key::SPRINT; + } + else if (moveType == NPCMoveType_Jog) + { + speed = NPC_MOVE_SPEED_JOG; + } + else + { + speed = NPC_MOVE_SPEED_WALK; + footSync_.Keys |= Key::WALK; + } + + footSync_.UpDown = 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 = utils::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 * (speed / 100.0f); + + if (!(std::fabs(glm::length(velocity_)) < DBL_EPSILON)) + { + estimatedArrivalTimeNS_ = Time::now().time_since_epoch().count() + (static_cast(distance / glm::length(velocity_)) * (/* (npcComponent_->getFootSyncRate() * 10000) +*/ 1000000)); + } + else + { + estimatedArrivalTimeNS_ = 0; + } + + // Set internal variables + moveSpeed_ = speed; + 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; + estimatedArrivalTimeNS_ = 0; + + footSync_.Keys &= Key::SPRINT; + footSync_.Keys &= Key::WALK; + footSync_.UpDown = 0; +} + +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; +} + +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; + + 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(*this, footSync_.PacketID, bs); +} + +void NPC::advance(TimePoint now) +{ + auto position = getPosition(); + + if (estimatedArrivalTimeNS_ <= Time::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.time_since_epoch()) - duration_cast(lastMove_.time_since_epoch()); + Vector3 travelled = velocity_ * static_cast(difference.count()); + + position += travelled; + footSync_.Velocity = velocity_; + footSync_.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) +{ + // Only process the NPC if it is spawned + if (player_ && (player_->getState() == PlayerState_OnFoot || player_->getState() == PlayerState_Driver || player_->getState() == PlayerState_Passenger || player_->getState() == PlayerState_Spawned)) + { + if (needsVelocityUpdate_) + { + setPosition(getPosition() + velocity_); + setVelocity({ 0.0f, 0.0f, 0.0f }, false); + } + + if (moving_) + { + advance(now); + } + + if ((now - lastUpdate_).count() > npcComponent_->getFootSyncRate()) + { + sendFootSync(); + lastUpdate_ = now; + } + } +} diff --git a/Server/Components/NPCs/NPC/npc.hpp b/Server/Components/NPCs/NPC/npc.hpp new file mode 100644 index 000000000..2afc7c656 --- /dev/null +++ b/Server/Components/NPCs/NPC/npc.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 + +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) override; + + void stopMove() 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; + + void sendFootSync(); + + 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; + } + +private: + // The NPC's player pointer. + IPlayer* player_; + TimePoint lastUpdate_; + + // Movements + NPCMoveType moveType_; + TimePoint lastMove_; + long long estimatedArrivalTimeNS_; + TimePoint moveStart_; + float moveSpeed_; + Vector3 targetPosition_; + Vector3 velocity_; + bool moving_; + bool needsVelocityUpdate_; + // Packets + NetCode::Packet::PlayerFootSync footSync_; + + 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..1af0f8b60 --- /dev/null +++ b/Server/Components/NPCs/Network/npcs_network.hpp @@ -0,0 +1,101 @@ +/* + * 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 +#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..425125e22 --- /dev/null +++ b/Server/Components/NPCs/npcs_impl.cpp @@ -0,0 +1,206 @@ +/* + * 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); +} + +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); + } +} + +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.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(INPC& npc, int rpcId, NetworkBitStream& bs) +{ + auto player = npc.getPlayer(); + + 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(INPC& npc, int type, NetworkBitStream& bs) +{ + auto player = npc.getPlayer(); + + const bool res = npcNetwork.inEventDispatcher.stopAtFalse([player, type, &bs](NetworkInEventHandler* handler) + { + bs.resetReadPointer(); + return handler->onReceivePacket(*player, type, bs); + }); + + if (res) + { + npcNetwork.packetInEventDispatcher.stopAtFalse(type, [player, &bs](SingleNetworkInEventHandler* handler) + { + bs.resetReadPointer(); + 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..86a7b4f87 --- /dev/null +++ b/Server/Components/NPCs/npcs_impl.hpp @@ -0,0 +1,93 @@ +/* + * 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 +#include +#include +#include +#include +#include "./Network/npcs_network.hpp" +#include "./NPC/npc.hpp" + +using namespace Impl; + +class NPCComponent final : public INPCComponent, public CoreEventHandler +{ +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; + + INPC* create(StringView name) override; + + void destroy(INPC& npc) override; + + void emulateRPCIn(INPC& npc, int rpcId, NetworkBitStream& bs); + + void emulatePacketIn(INPC& npc, int type, NetworkBitStream& bs); + + ICore* getCore() + { + return core; + } + + DefaultEventDispatcher& getEventDispatcher_internal() + { + return eventDispatcher; + } + + int getFootSyncRate() const + { + return *footSyncRate; + } + +private: + ICore* core = nullptr; + NPCNetwork npcNetwork; + DefaultEventDispatcher eventDispatcher; + MarkedDynamicPoolStorage storage; + int* footSyncRate = nullptr; +}; 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..d932fd0db --- /dev/null +++ b/Server/Components/Pawn/Scripting/NPC/Events.hpp @@ -0,0 +1,30 @@ +/* + * 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()); + } +}; diff --git a/Server/Components/Pawn/Scripting/NPC/Natives.cpp b/Server/Components/Pawn/Scripting/NPC/Natives.cpp new file mode 100644 index 000000000..91ca746a8 --- /dev/null +++ b/Server/Components/Pawn/Scripting/NPC/Natives.cpp @@ -0,0 +1,188 @@ +/* + * 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)) +{ + return npc.move(targetPos, NPCMoveType(moveType)); +} + +SCRIPT_API(NPC_StopMove, bool(INPC& npc)) +{ + npc.stopMove(); + return true; +} + +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_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; +} 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