Merge remote-tracking branch 'origin/main' into dCinema

This commit is contained in:
Wincent
2025-04-18 14:49:18 +00:00
351 changed files with 175610 additions and 46883 deletions

View File

@@ -26,12 +26,279 @@
#include "eMissionTaskType.h"
#include "eReplicaComponentType.h"
#include "eConnectionType.h"
#include "User.h"
#include "StringifiedEnum.h"
namespace {
const std::string DefaultSender = "%[MAIL_SYSTEM_NOTIFICATION]";
}
namespace Mail {
std::map<eMessageID, std::function<std::unique_ptr<MailLUBitStream>()>> g_Handlers = {
{eMessageID::SendRequest, []() {
return std::make_unique<SendRequest>();
}},
{eMessageID::DataRequest, []() {
return std::make_unique<DataRequest>();
}},
{eMessageID::AttachmentCollectRequest, []() {
return std::make_unique<AttachmentCollectRequest>();
}},
{eMessageID::DeleteRequest, []() {
return std::make_unique<DeleteRequest>();
}},
{eMessageID::ReadRequest, []() {
return std::make_unique<ReadRequest>();
}},
{eMessageID::NotificationRequest, []() {
return std::make_unique<NotificationRequest>();
}},
};
void MailLUBitStream::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(messageID);
}
bool MailLUBitStream::Deserialize(RakNet::BitStream& bitstream) {
VALIDATE_READ(bitstream.Read(messageID));
return true;
}
bool SendRequest::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(mailInfo.Deserialize(bitStream));
return true;
}
void SendRequest::Handle() {
SendResponse response;
auto* character = player->GetCharacter();
if (character && !(character->HasPermission(ePermissionMap::RestrictedMailAccess) || character->GetParentUser()->GetIsMuted())) {
mailInfo.recipient = std::regex_replace(mailInfo.recipient, std::regex("[^0-9a-zA-Z]+"), "");
auto receiverID = Database::Get()->GetCharacterInfo(mailInfo.recipient);
if (!receiverID) {
response.status = eSendResponse::RecipientNotFound;
} else if (GeneralUtils::CaseInsensitiveStringCompare(mailInfo.recipient, character->GetName()) || receiverID->id == character->GetID()) {
response.status = eSendResponse::CannotMailSelf;
} else {
uint32_t mailCost = Game::zoneManager->GetWorldConfig()->mailBaseFee;
uint32_t stackSize = 0;
auto inventoryComponent = player->GetComponent<InventoryComponent>();
Item* item = nullptr;
bool hasAttachment = mailInfo.itemID != 0 && mailInfo.itemCount > 0;
if (hasAttachment) {
item = inventoryComponent->FindItemById(mailInfo.itemID);
if (item) {
mailCost += (item->GetInfo().baseValue * Game::zoneManager->GetWorldConfig()->mailPercentAttachmentFee);
mailInfo.itemLOT = item->GetLot();
}
}
if (hasAttachment && !item) {
response.status = eSendResponse::AttachmentNotFound;
} else if (player->GetCharacter()->GetCoins() - mailCost < 0) {
response.status = eSendResponse::NotEnoughCoins;
} else {
bool removeSuccess = true;
// Remove coins and items from the sender
player->GetCharacter()->SetCoins(player->GetCharacter()->GetCoins() - mailCost, eLootSourceType::MAIL);
if (inventoryComponent && hasAttachment && item) {
removeSuccess = inventoryComponent->RemoveItem(mailInfo.itemLOT, mailInfo.itemCount, INVALID, true);
auto* missionComponent = player->GetComponent<MissionComponent>();
if (missionComponent && removeSuccess) missionComponent->Progress(eMissionTaskType::GATHER, mailInfo.itemLOT, LWOOBJID_EMPTY, "", -mailInfo.itemCount);
}
// we passed all the checks, now we can actully send the mail
if (removeSuccess) {
mailInfo.senderId = character->GetID();
mailInfo.senderUsername = character->GetName();
mailInfo.receiverId = receiverID->id;
mailInfo.itemSubkey = LWOOBJID_EMPTY;
//clear out the attachementID
mailInfo.itemID = 0;
Database::Get()->InsertNewMail(mailInfo);
response.status = eSendResponse::Success;
character->SaveXMLToDatabase();
} else {
response.status = eSendResponse::AttachmentNotFound;
}
}
}
} else {
response.status = eSendResponse::SenderAccountIsMuted;
}
response.Send(sysAddr);
}
void SendResponse::Serialize(RakNet::BitStream& bitStream) const {
MailLUBitStream::Serialize(bitStream);
bitStream.Write(status);
}
void NotificationResponse::Serialize(RakNet::BitStream& bitStream) const {
MailLUBitStream::Serialize(bitStream);
bitStream.Write(status);
bitStream.Write<uint64_t>(0); // unused
bitStream.Write<uint64_t>(0); // unused
bitStream.Write(auctionID);
bitStream.Write<uint64_t>(0); // unused
bitStream.Write(mailCount);
bitStream.Write<uint32_t>(0); // packing
}
void DataRequest::Handle() {
const auto* character = player->GetCharacter();
if (!character) return;
auto playerMail = Database::Get()->GetMailForPlayer(character->GetID(), 20);
DataResponse response;
response.playerMail = playerMail;
response.Send(sysAddr);
}
void DataResponse::Serialize(RakNet::BitStream& bitStream) const {
MailLUBitStream::Serialize(bitStream);
bitStream.Write(this->throttled);
bitStream.Write<uint16_t>(this->playerMail.size());
bitStream.Write<uint16_t>(0); // packing
for (const auto& mail : this->playerMail) {
mail.Serialize(bitStream);
}
}
bool AttachmentCollectRequest::Deserialize(RakNet::BitStream& bitStream) {
uint32_t unknown;
VALIDATE_READ(bitStream.Read(unknown));
VALIDATE_READ(bitStream.Read(mailID));
VALIDATE_READ(bitStream.Read(playerID));
return true;
}
void AttachmentCollectRequest::Handle() {
AttachmentCollectResponse response;
response.mailID = mailID;
auto inv = player->GetComponent<InventoryComponent>();
if (mailID > 0 && playerID == player->GetObjectID() && inv) {
auto playerMail = Database::Get()->GetMail(mailID);
if (!playerMail) {
response.status = eAttachmentCollectResponse::MailNotFound;
} else if (!inv->HasSpaceForLoot({ {playerMail->itemLOT, playerMail->itemCount} })) {
response.status = eAttachmentCollectResponse::NoSpaceInInventory;
} else {
inv->AddItem(playerMail->itemLOT, playerMail->itemCount, eLootSourceType::MAIL);
Database::Get()->ClaimMailItem(mailID);
response.status = eAttachmentCollectResponse::Success;
}
}
response.Send(sysAddr);
}
void AttachmentCollectResponse::Serialize(RakNet::BitStream& bitStream) const {
MailLUBitStream::Serialize(bitStream);
bitStream.Write(status);
bitStream.Write(mailID);
}
bool DeleteRequest::Deserialize(RakNet::BitStream& bitStream) {
int32_t unknown;
VALIDATE_READ(bitStream.Read(unknown));
VALIDATE_READ(bitStream.Read(mailID));
VALIDATE_READ(bitStream.Read(playerID));
return true;
}
void DeleteRequest::Handle() {
DeleteResponse response;
response.mailID = mailID;
auto mailData = Database::Get()->GetMail(mailID);
if (mailData && !(mailData->itemLOT != 0 && mailData->itemCount > 0)) {
Database::Get()->DeleteMail(mailID);
response.status = eDeleteResponse::Success;
} else if (mailData && mailData->itemLOT != 0 && mailData->itemCount > 0) {
response.status = eDeleteResponse::HasAttachments;
} else {
response.status = eDeleteResponse::NotFound;
}
response.Send(sysAddr);
}
void DeleteResponse::Serialize(RakNet::BitStream& bitStream) const {
MailLUBitStream::Serialize(bitStream);
bitStream.Write(status);
bitStream.Write(mailID);
}
bool ReadRequest::Deserialize(RakNet::BitStream& bitStream) {
int32_t unknown;
VALIDATE_READ(bitStream.Read(unknown));
VALIDATE_READ(bitStream.Read(mailID));
return true;
}
void ReadRequest::Handle() {
ReadResponse response;
response.mailID = mailID;
if (Database::Get()->GetMail(mailID)) {
response.status = eReadResponse::Success;
Database::Get()->MarkMailRead(mailID);
}
response.Send(sysAddr);
}
void ReadResponse::Serialize(RakNet::BitStream& bitStream) const {
MailLUBitStream::Serialize(bitStream);
bitStream.Write(status);
bitStream.Write(mailID);
}
void NotificationRequest::Handle() {
NotificationResponse response;
auto character = player->GetCharacter();
if (character) {
auto unreadMailCount = Database::Get()->GetUnreadMailCount(character->GetID());
response.status = eNotificationResponse::NewMail;
response.mailCount = unreadMailCount;
}
response.Send(sysAddr);
}
}
// Non Stuct Functions
void Mail::HandleMail(RakNet::BitStream& inStream, const SystemAddress& sysAddr, Entity* player) {
MailLUBitStream data;
if (!data.Deserialize(inStream)) {
LOG_DEBUG("Error Reading Mail header");
return;
}
auto it = g_Handlers.find(data.messageID);
if (it != g_Handlers.end()) {
auto request = it->second();
request->sysAddr = sysAddr;
request->player = player;
if (!request->Deserialize(inStream)) {
LOG_DEBUG("Error Reading Mail Request: %s", StringifiedEnum::ToString(data.messageID).data());
return;
}
request->Handle();
} else {
LOG_DEBUG("Unhandled Mail Request with ID: %i", data.messageID);
}
}
void Mail::SendMail(const Entity* recipient, const std::string& subject, const std::string& body, const LOT attachment,
const uint16_t attachmentCount) {
SendMail(
LWOOBJID_EMPTY,
ServerName,
DefaultSender,
recipient->GetObjectID(),
recipient->GetCharacter()->GetName(),
subject,
@@ -46,7 +313,7 @@ void Mail::SendMail(const LWOOBJID recipient, const std::string& recipientName,
const std::string& body, const LOT attachment, const uint16_t attachmentCount, const SystemAddress& sysAddr) {
SendMail(
LWOOBJID_EMPTY,
ServerName,
DefaultSender,
recipient,
recipientName,
subject,
@@ -75,7 +342,7 @@ void Mail::SendMail(const LWOOBJID sender, const std::string& senderName, const
void Mail::SendMail(const LWOOBJID sender, const std::string& senderName, LWOOBJID recipient,
const std::string& recipientName, const std::string& subject, const std::string& body, const LOT attachment,
const uint16_t attachmentCount, const SystemAddress& sysAddr) {
IMail::MailInfo mailInsert;
MailInfo mailInsert;
mailInsert.senderUsername = senderName;
mailInsert.recipient = recipientName;
mailInsert.subject = subject;
@@ -90,316 +357,7 @@ void Mail::SendMail(const LWOOBJID sender, const std::string& senderName, LWOOBJ
Database::Get()->InsertNewMail(mailInsert);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) return; // TODO: Echo to chat server
SendNotification(sysAddr, 1); //Show the "one new mail" message
}
void Mail::HandleMailStuff(RakNet::BitStream& packet, const SystemAddress& sysAddr, Entity* entity) {
int mailStuffID = 0;
packet.Read(mailStuffID);
auto returnVal = std::async(std::launch::async, [&packet, &sysAddr, entity, mailStuffID]() {
Mail::MailMessageID stuffID = MailMessageID(mailStuffID);
switch (stuffID) {
case MailMessageID::AttachmentCollect:
Mail::HandleAttachmentCollect(packet, sysAddr, entity);
break;
case MailMessageID::DataRequest:
Mail::HandleDataRequest(packet, sysAddr, entity);
break;
case MailMessageID::MailDelete:
Mail::HandleMailDelete(packet, sysAddr);
break;
case MailMessageID::MailRead:
Mail::HandleMailRead(packet, sysAddr);
break;
case MailMessageID::NotificationRequest:
Mail::HandleNotificationRequest(sysAddr, entity->GetObjectID());
break;
case MailMessageID::Send:
Mail::HandleSendMail(packet, sysAddr, entity);
break;
default:
LOG("Unhandled and possibly undefined MailStuffID: %i", int(stuffID));
}
});
}
void Mail::HandleSendMail(RakNet::BitStream& packet, const SystemAddress& sysAddr, Entity* entity) {
//std::string subject = GeneralUtils::WStringToString(ReadFromPacket(packet, 50));
//std::string body = GeneralUtils::WStringToString(ReadFromPacket(packet, 400));
//std::string recipient = GeneralUtils::WStringToString(ReadFromPacket(packet, 32));
// Check if the player has restricted mail access
auto* character = entity->GetCharacter();
if (!character) return;
if (character->HasPermission(ePermissionMap::RestrictedMailAccess)) {
// Send a message to the player
ChatPackets::SendSystemMessage(
sysAddr,
u"This character has restricted mail access."
);
Mail::SendSendResponse(sysAddr, Mail::MailSendResponse::AccountIsMuted);
return;
}
LUWString subjectRead(50);
packet.Read(subjectRead);
LUWString bodyRead(400);
packet.Read(bodyRead);
LUWString recipientRead(32);
packet.Read(recipientRead);
const std::string subject = subjectRead.GetAsString();
const std::string body = bodyRead.GetAsString();
//Cleanse recipient:
const std::string recipient = std::regex_replace(recipientRead.GetAsString(), std::regex("[^0-9a-zA-Z]+"), "");
uint64_t unknown64 = 0;
LWOOBJID attachmentID;
uint16_t attachmentCount;
packet.Read(unknown64);
packet.Read(attachmentID);
packet.Read(attachmentCount); //We don't care about the rest of the packet.
uint32_t itemID = static_cast<uint32_t>(attachmentID);
LOT itemLOT = 0;
//Inventory::InventoryType itemType;
int mailCost = Game::zoneManager->GetWorldConfig()->mailBaseFee;
int stackSize = 0;
auto inv = static_cast<InventoryComponent*>(entity->GetComponent(eReplicaComponentType::INVENTORY));
Item* item = nullptr;
if (itemID > 0 && attachmentCount > 0 && inv) {
item = inv->FindItemById(attachmentID);
if (item) {
mailCost += (item->GetInfo().baseValue * Game::zoneManager->GetWorldConfig()->mailPercentAttachmentFee);
stackSize = item->GetCount();
itemLOT = item->GetLot();
} else {
Mail::SendSendResponse(sysAddr, MailSendResponse::AttachmentNotFound);
return;
}
}
//Check if we can even send this mail (negative coins bug):
if (entity->GetCharacter()->GetCoins() - mailCost < 0) {
Mail::SendSendResponse(sysAddr, MailSendResponse::NotEnoughCoins);
return;
}
//Get the receiver's id:
auto receiverID = Database::Get()->GetCharacterInfo(recipient);
if (!receiverID) {
Mail::SendSendResponse(sysAddr, Mail::MailSendResponse::RecipientNotFound);
return;
}
//Check if we have a valid receiver:
if (GeneralUtils::CaseInsensitiveStringCompare(recipient, character->GetName()) || receiverID->id == character->GetID()) {
Mail::SendSendResponse(sysAddr, Mail::MailSendResponse::CannotMailSelf);
return;
} else {
IMail::MailInfo mailInsert;
mailInsert.senderUsername = character->GetName();
mailInsert.recipient = recipient;
mailInsert.subject = subject;
mailInsert.body = body;
mailInsert.senderId = character->GetID();
mailInsert.receiverId = receiverID->id;
mailInsert.itemCount = attachmentCount;
mailInsert.itemID = itemID;
mailInsert.itemLOT = itemLOT;
mailInsert.itemSubkey = LWOOBJID_EMPTY;
Database::Get()->InsertNewMail(mailInsert);
}
Mail::SendSendResponse(sysAddr, Mail::MailSendResponse::Success);
entity->GetCharacter()->SetCoins(entity->GetCharacter()->GetCoins() - mailCost, eLootSourceType::MAIL);
LOG("Seeing if we need to remove item with ID/count/LOT: %i %i %i", itemID, attachmentCount, itemLOT);
if (inv && itemLOT != 0 && attachmentCount > 0 && item) {
LOG("Trying to remove item with ID/count/LOT: %i %i %i", itemID, attachmentCount, itemLOT);
inv->RemoveItem(itemLOT, attachmentCount, INVALID, true);
auto* missionCompoent = entity->GetComponent<MissionComponent>();
if (missionCompoent != nullptr) {
missionCompoent->Progress(eMissionTaskType::GATHER, itemLOT, LWOOBJID_EMPTY, "", -attachmentCount);
}
}
character->SaveXMLToDatabase();
}
void Mail::HandleDataRequest(RakNet::BitStream& packet, const SystemAddress& sysAddr, Entity* player) {
auto playerMail = Database::Get()->GetMailForPlayer(player->GetCharacter()->GetID(), 20);
RakNet::BitStream bitStream;
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, eClientMessageType::MAIL);
bitStream.Write(int(MailMessageID::MailData));
bitStream.Write(int(0)); // throttled
bitStream.Write<uint16_t>(playerMail.size()); // size
bitStream.Write<uint16_t>(0);
for (const auto& mail : playerMail) {
bitStream.Write(mail.id); //MailID
const LUWString subject(mail.subject, 50);
bitStream.Write(subject); //subject
const LUWString body(mail.body, 400);
bitStream.Write(body); //body
const LUWString sender(mail.senderUsername, 32);
bitStream.Write(sender); //sender
bitStream.Write(uint32_t(0)); // packing
bitStream.Write(uint64_t(0)); // attachedCurrency
bitStream.Write(mail.itemID); //Attachment ID
LOT lot = mail.itemLOT;
if (lot <= 0) bitStream.Write(LOT(-1));
else bitStream.Write(lot);
bitStream.Write(uint32_t(0)); // packing
bitStream.Write(mail.itemSubkey); // Attachment subKey
bitStream.Write<uint16_t>(mail.itemCount); // Attachment count
bitStream.Write(uint8_t(0)); // subject type (used for auction)
bitStream.Write(uint8_t(0)); // packing
bitStream.Write(uint32_t(0)); // packing
bitStream.Write<uint64_t>(mail.timeSent); // expiration date
bitStream.Write<uint64_t>(mail.timeSent);// send date
bitStream.Write<uint8_t>(mail.wasRead); //was read
bitStream.Write(uint8_t(0)); // isLocalized
bitStream.Write(uint16_t(0)); // packing
bitStream.Write(uint32_t(0)); // packing
}
Game::server->Send(bitStream, sysAddr, false);
}
void Mail::HandleAttachmentCollect(RakNet::BitStream& packet, const SystemAddress& sysAddr, Entity* player) {
int unknown;
uint64_t mailID;
LWOOBJID playerID;
packet.Read(unknown);
packet.Read(mailID);
packet.Read(playerID);
if (mailID > 0 && playerID == player->GetObjectID()) {
auto playerMail = Database::Get()->GetMail(mailID);
LOT attachmentLOT = 0;
uint32_t attachmentCount = 0;
if (playerMail) {
attachmentLOT = playerMail->itemLOT;
attachmentCount = playerMail->itemCount;
}
auto inv = player->GetComponent<InventoryComponent>();
if (!inv) return;
inv->AddItem(attachmentLOT, attachmentCount, eLootSourceType::MAIL);
Mail::SendAttachmentRemoveConfirm(sysAddr, mailID);
Database::Get()->ClaimMailItem(mailID);
}
}
void Mail::HandleMailDelete(RakNet::BitStream& packet, const SystemAddress& sysAddr) {
int unknown;
uint64_t mailID;
LWOOBJID playerID;
packet.Read(unknown);
packet.Read(mailID);
packet.Read(playerID);
if (mailID > 0) Mail::SendDeleteConfirm(sysAddr, mailID, playerID);
}
void Mail::HandleMailRead(RakNet::BitStream& packet, const SystemAddress& sysAddr) {
int unknown;
uint64_t mailID;
packet.Read(unknown);
packet.Read(mailID);
if (mailID > 0) Mail::SendReadConfirm(sysAddr, mailID);
}
void Mail::HandleNotificationRequest(const SystemAddress& sysAddr, uint32_t objectID) {
auto unreadMailCount = Database::Get()->GetUnreadMailCount(objectID);
if (unreadMailCount > 0) Mail::SendNotification(sysAddr, unreadMailCount);
}
void Mail::SendSendResponse(const SystemAddress& sysAddr, MailSendResponse response) {
RakNet::BitStream bitStream;
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, eClientMessageType::MAIL);
bitStream.Write(int(MailMessageID::SendResponse));
bitStream.Write(int(response));
Game::server->Send(bitStream, sysAddr, false);
}
void Mail::SendNotification(const SystemAddress& sysAddr, int mailCount) {
RakNet::BitStream bitStream;
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, eClientMessageType::MAIL);
uint64_t messageType = 2;
uint64_t s1 = 0;
uint64_t s2 = 0;
uint64_t s3 = 0;
uint64_t s4 = 0;
bitStream.Write(messageType);
bitStream.Write(s1);
bitStream.Write(s2);
bitStream.Write(s3);
bitStream.Write(s4);
bitStream.Write(mailCount);
bitStream.Write(int(0)); //Unknown
Game::server->Send(bitStream, sysAddr, false);
}
void Mail::SendAttachmentRemoveConfirm(const SystemAddress& sysAddr, uint64_t mailID) {
RakNet::BitStream bitStream;
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, eClientMessageType::MAIL);
bitStream.Write(int(MailMessageID::AttachmentCollectConfirm));
bitStream.Write(int(0)); //unknown
bitStream.Write(mailID);
Game::server->Send(bitStream, sysAddr, false);
}
void Mail::SendDeleteConfirm(const SystemAddress& sysAddr, uint64_t mailID, LWOOBJID playerID) {
RakNet::BitStream bitStream;
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, eClientMessageType::MAIL);
bitStream.Write(int(MailMessageID::MailDeleteConfirm));
bitStream.Write(int(0)); //unknown
bitStream.Write(mailID);
Game::server->Send(bitStream, sysAddr, false);
Database::Get()->DeleteMail(mailID);
}
void Mail::SendReadConfirm(const SystemAddress& sysAddr, uint64_t mailID) {
RakNet::BitStream bitStream;
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, eClientMessageType::MAIL);
bitStream.Write(int(MailMessageID::MailReadConfirm));
bitStream.Write(int(0)); //unknown
bitStream.Write(mailID);
Game::server->Send(bitStream, sysAddr, false);
Database::Get()->MarkMailRead(mailID);
NotificationResponse response;
response.status = eNotificationResponse::NewMail;
response.Send(sysAddr);
}

View File

@@ -1,43 +1,210 @@
#pragma once
#ifndef __MAIL_H__
#define __MAIL_H__
#include <cstdint>
#include "BitStream.h"
#include "RakNetTypes.h"
#include "dCommonVars.h"
#include "BitStreamUtils.h"
#include "MailInfo.h"
class Entity;
namespace Mail {
enum class MailMessageID {
Send = 0x00,
SendResponse = 0x01,
DataRequest = 0x03,
MailData = 0x04,
AttachmentCollect = 0x05,
AttachmentCollectConfirm = 0x06,
MailDelete = 0x07,
MailDeleteConfirm = 0x08,
MailRead = 0x09,
MailReadConfirm = 0x0a,
NotificationRequest = 0x0b
enum class eMessageID : uint32_t {
SendRequest = 0,
SendResponse,
NotificationResponse,
DataRequest,
DataResponse,
AttachmentCollectRequest,
AttachmentCollectResponse,
DeleteRequest,
DeleteResponse,
ReadRequest,
ReadResponse,
NotificationRequest,
AuctionCreate,
AuctionCreationResponse,
AuctionCancel,
AuctionCancelResponse,
AuctionList,
AuctionListResponse,
AuctionBid,
AuctionBidResponse,
UnknownError
};
enum class MailSendResponse {
enum class eSendResponse : uint32_t {
Success = 0,
NotEnoughCoins,
AttachmentNotFound,
ItemCannotBeMailed,
CannotMailSelf,
RecipientNotFound,
DifferentFaction,
Unknown,
RecipientDifferentFaction,
UnHandled7,
ModerationFailure,
AccountIsMuted,
UnknownFailure,
SenderAccountIsMuted,
UnHandled10,
RecipientIsIgnored,
UnknownFailure3,
RecipientIsFTP
UnHandled12,
RecipientIsFTP,
UnknownError
};
const std::string ServerName = "Darkflame Universe";
enum class eDeleteResponse : uint32_t {
Success = 0,
HasAttachments,
NotFound,
Throttled,
UnknownError
};
enum class eAttachmentCollectResponse : uint32_t {
Success = 0,
AttachmentNotFound,
NoSpaceInInventory,
MailNotFound,
Throttled,
UnknownError
};
enum class eNotificationResponse : uint32_t {
NewMail = 0,
UnHandled,
AuctionWon,
AuctionSold,
AuctionOutbided,
AuctionExpired,
AuctionCancelled,
AuctionUpdated,
UnknownError
};
enum class eReadResponse : uint32_t {
Success = 0,
UnknownError
};
enum class eAuctionCreateResponse : uint32_t {
Success = 0,
NotEnoughMoney,
ItemNotFound,
ItemNotSellable,
UnknownError
};
enum class eAuctionCancelResponse : uint32_t {
NotFound = 0,
NotYours,
HasBid,
NoLongerExists,
UnknownError
};
struct MailLUBitStream : public LUBitStream {
eMessageID messageID = eMessageID::UnknownError;
SystemAddress sysAddr = UNASSIGNED_SYSTEM_ADDRESS;
Entity* player = nullptr;
MailLUBitStream() = default;
MailLUBitStream(eMessageID _messageID) : LUBitStream(eConnectionType::CLIENT, MessageType::Client::MAIL), messageID{_messageID} {};
virtual void Serialize(RakNet::BitStream& bitStream) const override;
virtual bool Deserialize(RakNet::BitStream& bitStream) override;
virtual void Handle() override {};
};
struct SendRequest : public MailLUBitStream {
MailInfo mailInfo;
bool Deserialize(RakNet::BitStream& bitStream) override;
void Handle() override;
};
struct SendResponse :public MailLUBitStream {
eSendResponse status = eSendResponse::UnknownError;
void Serialize(RakNet::BitStream& bitStream) const override;
};
struct NotificationResponse : public MailLUBitStream {
eNotificationResponse status = eNotificationResponse::UnknownError;
LWOOBJID auctionID = LWOOBJID_EMPTY;
uint32_t mailCount = 1;
NotificationResponse() : MailLUBitStream(eMessageID::NotificationResponse) {};
void Serialize(RakNet::BitStream& bitStream) const override;
};
struct DataRequest : public MailLUBitStream {
bool Deserialize(RakNet::BitStream& bitStream) override { return true; };
void Handle() override;
};
struct DataResponse : public MailLUBitStream {
uint32_t throttled = 0;
std::vector<MailInfo> playerMail;
DataResponse() : MailLUBitStream(eMessageID::DataResponse) {};
void Serialize(RakNet::BitStream& bitStream) const override;
};
struct AttachmentCollectRequest : public MailLUBitStream {
uint64_t mailID = 0;
LWOOBJID playerID = LWOOBJID_EMPTY;
AttachmentCollectRequest() : MailLUBitStream(eMessageID::AttachmentCollectRequest) {};
bool Deserialize(RakNet::BitStream& bitStream) override;
void Handle() override;
};
struct AttachmentCollectResponse : public MailLUBitStream {
eAttachmentCollectResponse status = eAttachmentCollectResponse::UnknownError;
uint64_t mailID = 0;
AttachmentCollectResponse() : MailLUBitStream(eMessageID::AttachmentCollectResponse) {};
void Serialize(RakNet::BitStream& bitStream) const override;
};
struct DeleteRequest : public MailLUBitStream {
uint64_t mailID = 0;
LWOOBJID playerID = LWOOBJID_EMPTY;
DeleteRequest() : MailLUBitStream(eMessageID::DeleteRequest) {};
bool Deserialize(RakNet::BitStream& bitStream) override;
void Handle() override;
};
struct DeleteResponse : public MailLUBitStream {
eDeleteResponse status = eDeleteResponse::UnknownError;
uint64_t mailID = 0;
DeleteResponse() : MailLUBitStream(eMessageID::DeleteResponse) {};
void Serialize(RakNet::BitStream& bitStream) const override;
};
struct ReadRequest : public MailLUBitStream {
uint64_t mailID = 0;
ReadRequest() : MailLUBitStream(eMessageID::ReadRequest) {};
bool Deserialize(RakNet::BitStream& bitStream) override;
void Handle() override;
};
struct ReadResponse : public MailLUBitStream {
uint64_t mailID = 0;
eReadResponse status = eReadResponse::UnknownError;
ReadResponse() : MailLUBitStream(eMessageID::ReadResponse) {};
void Serialize(RakNet::BitStream& bitStream) const override;
};
struct NotificationRequest : public MailLUBitStream {
NotificationRequest() : MailLUBitStream(eMessageID::NotificationRequest) {};
bool Deserialize(RakNet::BitStream& bitStream) override { return true; };
void Handle() override;
};
void HandleMail(RakNet::BitStream& inStream, const SystemAddress& sysAddr, Entity* player);
void SendMail(
const Entity* recipient,
@@ -78,18 +245,6 @@ namespace Mail {
uint16_t attachmentCount,
const SystemAddress& sysAddr
);
void HandleMailStuff(RakNet::BitStream& packet, const SystemAddress& sysAddr, Entity* entity);
void HandleSendMail(RakNet::BitStream& packet, const SystemAddress& sysAddr, Entity* entity);
void HandleDataRequest(RakNet::BitStream& packet, const SystemAddress& sysAddr, Entity* player);
void HandleAttachmentCollect(RakNet::BitStream& packet, const SystemAddress& sysAddr, Entity* player);
void HandleMailDelete(RakNet::BitStream& packet, const SystemAddress& sysAddr);
void HandleMailRead(RakNet::BitStream& packet, const SystemAddress& sysAddr);
void HandleNotificationRequest(const SystemAddress& sysAddr, uint32_t objectID);
void SendSendResponse(const SystemAddress& sysAddr, MailSendResponse response);
void SendNotification(const SystemAddress& sysAddr, int mailCount);
void SendAttachmentRemoveConfirm(const SystemAddress& sysAddr, uint64_t mailID);
void SendDeleteConfirm(const SystemAddress& sysAddr, uint64_t mailID, LWOOBJID playerID);
void SendReadConfirm(const SystemAddress& sysAddr, uint64_t mailID);
};
#endif // !__MAIL_H__

View File

@@ -137,7 +137,7 @@ bool Precondition::CheckValue(Entity* player, const uint32_t value, bool evaluat
return inventoryComponent->GetLotCount(value) >= count;
case PreconditionType::DoesNotHaveItem:
return inventoryComponent->IsEquipped(value) < count;
return inventoryComponent->IsEquipped(value) && count > 0;
case PreconditionType::HasAchievement:
if (missionComponent == nullptr) return false;
return missionComponent->GetMissionState(value) >= eMissionState::COMPLETE;

View File

@@ -16,7 +16,7 @@
#include "Amf3.h"
#include "Database.h"
#include "eChatMessageType.h"
#include "MessageType/Chat.h"
#include "dServer.h"
namespace {
@@ -153,7 +153,7 @@ void SlashCommandHandler::SendAnnouncement(const std::string& title, const std::
//Notify chat about it
CBITSTREAM;
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, eChatMessageType::GM_ANNOUNCE);
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::GM_ANNOUNCE);
bitStream.Write<uint32_t>(title.size());
for (auto character : title) {
@@ -287,8 +287,8 @@ void SlashCommandHandler::Startup() {
RegisterCommand(SpawnPhysicsVertsCommand);
Command TeleportCommand{
.help = "Teleports you",
.info = "Teleports you. If no Y is given, you are teleported to the height of the terrain or physics object at (x, z)",
.help = "Teleports you to a position or a player to another player.",
.info = "Teleports you. If no Y is given, you are teleported to the height of the terrain or physics object at (x, z). Any of the coordinates can use the syntax of an exact position (10.0), or a relative position (~+10.0). A ~ means use the current value of that axis as the base value. Addition or subtraction is supported (~+10) (~-10). If source player and target player are players that exist in the world, then the source player will be teleported to target player.",
.aliases = { "teleport", "tele", "tp" },
.handle = DEVGMCommands::Teleport,
.requiredLevel = eGameMasterLevel::JUNIOR_DEVELOPER
@@ -996,7 +996,7 @@ void SlashCommandHandler::Startup() {
Command RequestMailCountCommand{
.help = "Gets the players mail count",
.info = "Sends notification with number of unread messages in the player's mailbox",
.aliases = { "requestmailcount" },
.aliases = { "requestmailcount", "checkmail" },
.handle = GMZeroCommands::RequestMailCount,
.requiredLevel = eGameMasterLevel::CIVILIAN
};
@@ -1056,6 +1056,15 @@ void SlashCommandHandler::Startup() {
};
RegisterCommand(InstanceInfoCommand);
Command ServerUptimeCommand{
.help = "Display the time the current world server has been active",
.info = "Display the time the current world server has been active",
.aliases = { "uptime" },
.handle = GMZeroCommands::ServerUptime,
.requiredLevel = eGameMasterLevel::DEVELOPER
};
RegisterCommand(ServerUptimeCommand);
//Commands that are handled by the client
Command faqCommand{

View File

@@ -45,7 +45,7 @@
// Enums
#include "eGameMasterLevel.h"
#include "eMasterMessageType.h"
#include "MessageType/Master.h"
#include "eInventoryType.h"
#include "ePlayerFlag.h"
@@ -503,7 +503,7 @@ namespace DEVGMCommands {
void ShutdownUniverse(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
//Tell the master server that we're going to be shutting down whole "universe":
CBITSTREAM;
BitStreamUtils::WriteHeader(bitStream, eConnectionType::MASTER, eMasterMessageType::SHUTDOWN_UNIVERSE);
BitStreamUtils::WriteHeader(bitStream, eConnectionType::MASTER, MessageType::Master::SHUTDOWN_UNIVERSE);
Game::server->SendToMaster(bitStream);
ChatPackets::SendSystemMessage(sysAddr, u"Sent universe shutdown notification to master.");
@@ -555,25 +555,45 @@ namespace DEVGMCommands {
}
}
std::optional<float> ParseRelativeAxis(const float sourcePos, const std::string& toParse) {
if (toParse.empty()) return std::nullopt;
// relative offset from current position
if (toParse[0] == '~') {
if (toParse.size() == 1) return sourcePos;
if (toParse.size() < 3 || !(toParse[1] != '+' || toParse[1] != '-')) return std::nullopt;
const auto offset = GeneralUtils::TryParse<float>(toParse.substr(2));
if (!offset.has_value()) return std::nullopt;
bool isNegative = toParse[1] == '-';
return isNegative ? sourcePos - offset.value() : sourcePos + offset.value();
}
return GeneralUtils::TryParse<float>(toParse);
}
void Teleport(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
const auto splitArgs = GeneralUtils::SplitString(args, ' ');
const auto& sourcePos = entity->GetPosition();
NiPoint3 pos{};
auto* sourceEntity = entity;
if (splitArgs.size() == 3) {
const auto x = GeneralUtils::TryParse<float>(splitArgs.at(0));
const auto x = ParseRelativeAxis(sourcePos.x, splitArgs[0]);
if (!x) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid x.");
return;
}
const auto y = GeneralUtils::TryParse<float>(splitArgs.at(1));
const auto y = ParseRelativeAxis(sourcePos.y, splitArgs[1]);
if (!y) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid y.");
return;
}
const auto z = GeneralUtils::TryParse<float>(splitArgs.at(2));
const auto z = ParseRelativeAxis(sourcePos.z, splitArgs[2]);
if (!z) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid z.");
return;
@@ -584,32 +604,39 @@ namespace DEVGMCommands {
pos.SetZ(z.value());
LOG("Teleporting objectID: %llu to %f, %f, %f", entity->GetObjectID(), pos.x, pos.y, pos.z);
GameMessages::SendTeleport(entity->GetObjectID(), pos, NiQuaternion(), sysAddr);
} else if (splitArgs.size() == 2) {
const auto x = ParseRelativeAxis(sourcePos.x, splitArgs[0]);
auto* sourcePlayer = PlayerManager::GetPlayer(splitArgs[0]);
if (!x && !sourcePlayer) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid x or source player not found.");
return;
}
if (sourcePlayer) sourceEntity = sourcePlayer;
const auto x = GeneralUtils::TryParse<float>(splitArgs.at(0));
if (!x) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid x.");
const auto z = ParseRelativeAxis(sourcePos.z, splitArgs[1]);
const auto* const targetPlayer = PlayerManager::GetPlayer(splitArgs[1]);
if (!z && !targetPlayer) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid z or target player not found.");
return;
}
const auto z = GeneralUtils::TryParse<float>(splitArgs.at(1));
if (!z) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid z.");
if (x && z) {
pos.SetX(x.value());
pos.SetY(0.0f);
pos.SetZ(z.value());
} else if (sourcePlayer && targetPlayer) {
pos = targetPlayer->GetPosition();
} else {
ChatPackets::SendSystemMessage(sysAddr, u"Unable to teleport.");
return;
}
pos.SetX(x.value());
pos.SetY(0.0f);
pos.SetZ(z.value());
LOG("Teleporting objectID: %llu to X: %f, Z: %f", entity->GetObjectID(), pos.x, pos.z);
GameMessages::SendTeleport(entity->GetObjectID(), pos, NiQuaternion(), sysAddr);
} else {
ChatPackets::SendSystemMessage(sysAddr, u"Correct usage: /teleport <x> (<y>) <z> - if no Y given, will teleport to the height of the terrain (or any physics object).");
}
GameMessages::SendTeleport(sourceEntity->GetObjectID(), pos, sourceEntity->GetRotation(), sourceEntity->GetSystemAddress());
auto* possessorComponent = entity->GetComponent<PossessorComponent>();
auto* possessorComponent = sourceEntity->GetComponent<PossessorComponent>();
if (possessorComponent) {
auto* possassableEntity = Game::entityManager->GetEntity(possessorComponent->GetPossessable());

View File

@@ -15,7 +15,7 @@
#include "PropertyManagementComponent.h"
// Enums
#include "eChatMessageType.h"
#include "MessageType/Chat.h"
#include "eServerDisconnectIdentifiers.h"
#include "eObjectBits.h"
@@ -102,7 +102,7 @@ namespace GMGreaterThanZeroCommands {
return;
}
IMail::MailInfo mailInsert;
MailInfo mailInsert;
mailInsert.senderId = entity->GetObjectID();
mailInsert.senderUsername = "Darkflame Universe";
mailInsert.receiverId = receiverID;
@@ -197,7 +197,7 @@ namespace GMGreaterThanZeroCommands {
//Notify chat about it
CBITSTREAM;
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, eChatMessageType::GM_MUTE);
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::GM_MUTE);
bitStream.Write(characterId);
bitStream.Write(expire);
@@ -292,7 +292,7 @@ namespace GMGreaterThanZeroCommands {
bool displayZoneData = true;
bool displayIndividualPlayers = true;
const auto splitArgs = GeneralUtils::SplitString(args, ' ');
if (!splitArgs.empty() && !splitArgs.at(0).empty()) displayZoneData = splitArgs.at(0) == "1";
if (splitArgs.size() > 1) displayIndividualPlayers = splitArgs.at(1) == "1";

View File

@@ -12,6 +12,7 @@
#include "VanityUtilities.h"
#include "WorldPackets.h"
#include "ZoneInstanceManager.h"
#include "Database.h"
// Components
#include "BuffComponent.h"
@@ -216,7 +217,10 @@ namespace GMZeroCommands {
}
void RequestMailCount(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
Mail::HandleNotificationRequest(entity->GetSystemAddress(), entity->GetObjectID());
Mail::NotificationResponse response;
response.status = Mail::eNotificationResponse::NewMail;
response.mailCount = Database::Get()->GetUnreadMailCount(entity->GetCharacter()->GetID());
response.Send(sysAddr);
}
void InstanceInfo(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
@@ -225,8 +229,13 @@ namespace GMZeroCommands {
ChatPackets::SendSystemMessage(sysAddr, u"Map: " + (GeneralUtils::to_u16string(zoneId.GetMapID())) + u"\nClone: " + (GeneralUtils::to_u16string(zoneId.GetCloneID())) + u"\nInstance: " + (GeneralUtils::to_u16string(zoneId.GetInstanceID())));
}
// Display the server uptime
void ServerUptime(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
const auto time = Game::server->GetUptime();
const auto seconds = std::chrono::duration_cast<std::chrono::seconds>(time).count();
ChatPackets::SendSystemMessage(sysAddr, u"Server has been up for " + GeneralUtils::to_u16string(seconds) + u" s");
}
//For client side commands
void ClientHandled(Entity* entity, const SystemAddress& sysAddr, const std::string args) {}
};

View File

@@ -15,6 +15,7 @@ namespace GMZeroCommands {
void LeaveZone(Entity* entity, const SystemAddress& sysAddr, const std::string args);
void Resurrect(Entity* entity, const SystemAddress& sysAddr, const std::string args);
void InstanceInfo(Entity* entity, const SystemAddress& sysAddr, const std::string args);
void ServerUptime(Entity* entity, const SystemAddress& sysAddr, const std::string args);
void ClientHandled(Entity* entity, const SystemAddress& sysAddr, const std::string args);
}

View File

@@ -59,9 +59,9 @@ void VanityUtilities::SpawnVanity() {
for (const auto& npc : objects) {
if (npc.m_ID == LWOOBJID_EMPTY) continue;
if (npc.m_LOT == 176){
if (npc.m_LOT == 176) {
Game::zoneManager->RemoveSpawner(npc.m_ID);
} else{
} else {
auto* entity = Game::entityManager->GetEntity(npc.m_ID);
if (!entity) continue;
entity->Smash(LWOOBJID_EMPTY, eKillType::VIOLENT);
@@ -86,14 +86,14 @@ void VanityUtilities::SpawnVanity() {
float rate = GeneralUtils::GenerateRandomNumber<float>(0, 1);
if (location.m_Chance < rate) continue;
if (object.m_LOT == 176){
if (object.m_LOT == 176) {
object.m_ID = SpawnSpawner(object, location);
} else {
// Spawn the NPC
auto* objectEntity = SpawnObject(object, location);
if (!objectEntity) continue;
object.m_ID = objectEntity->GetObjectID();
if (!object.m_Phrases.empty()){
if (!object.m_Phrases.empty()) {
objectEntity->SetVar<std::vector<std::string>>(u"chats", object.m_Phrases);
SetupNPCTalk(objectEntity);
}
@@ -102,12 +102,12 @@ void VanityUtilities::SpawnVanity() {
}
LWOOBJID SpawnSpawner(const VanityObject& object, const VanityObjectLocation& location) {
SceneObject obj;
SceneObject obj{};
obj.lot = object.m_LOT;
// guratantee we have no collisions
do {
obj.id = ObjectIDManager::GenerateObjectID();
} while(Game::zoneManager->GetSpawner(obj.id));
} while (Game::zoneManager->GetSpawner(obj.id));
obj.position = location.m_Position;
obj.rotation = location.m_Rotation;
obj.settings = object.m_Config;
@@ -146,7 +146,7 @@ Entity* SpawnObject(const VanityObject& object, const VanityObjectLocation& loca
}
void ParseXml(const std::string& file) {
if (loadedFiles.contains(file)){
if (loadedFiles.contains(file)) {
LOG("Trying to load vanity file %s twice!!!", file.c_str());
return;
}
@@ -232,7 +232,7 @@ void ParseXml(const std::string& file) {
auto* configElement = object->FirstChildElement("config");
std::vector<std::u16string> keys = {};
std::vector<LDFBaseData*> config = {};
if(configElement) {
if (configElement) {
for (auto* key = configElement->FirstChildElement("key"); key != nullptr;
key = key->NextSiblingElement("key")) {
// Get the config data
@@ -240,7 +240,7 @@ void ParseXml(const std::string& file) {
if (!data) continue;
LDFBaseData* configData = LDFBaseData::DataFromString(data);
if (configData->GetKey() == u"useLocationsAsRandomSpawnPoint" && configData->GetValueType() == eLDFType::LDF_TYPE_BOOLEAN){
if (configData->GetKey() == u"useLocationsAsRandomSpawnPoint" && configData->GetValueType() == eLDFType::LDF_TYPE_BOOLEAN) {
useLocationsAsRandomSpawnPoint = static_cast<bool>(configData);
continue;
}
@@ -250,7 +250,7 @@ void ParseXml(const std::string& file) {
}
if (!keys.empty()) config.push_back(new LDFData<std::vector<std::u16string>>(u"syncLDF", keys));
VanityObject objectData {
VanityObject objectData{
.m_Name = name,
.m_LOT = lot,
.m_Equipment = inventory,
@@ -268,7 +268,7 @@ void ParseXml(const std::string& file) {
for (auto* location = locations->FirstChildElement("location"); location != nullptr;
location = location->NextSiblingElement("location")) {
// Get the location data
auto zoneID = GeneralUtils::TryParse<uint32_t>(location->Attribute("zone"));
auto x = GeneralUtils::TryParse<float>(location->Attribute("x"));
@@ -288,7 +288,7 @@ void ParseXml(const std::string& file) {
continue;
}
VanityObjectLocation locationData {
VanityObjectLocation locationData{
.m_Position = { x.value(), y.value(), z.value() },
.m_Rotation = { rw.value(), rx.value(), ry.value(), rz.value() },
};
@@ -403,26 +403,39 @@ void SetupNPCTalk(Entity* npc) {
npc->SetProximityRadius(20.0f, "talk");
}
void NPCTalk(Entity* npc) {
auto* proximityMonitorComponent = npc->GetComponent<ProximityMonitorComponent>();
void VanityUtilities::OnProximityUpdate(Entity* entity, Entity* other, const std::string& proxName, const std::string& name) {
if (proxName != "talk") return;
const auto* const proximityMonitorComponent = entity->GetComponent<ProximityMonitorComponent>();
if (!proximityMonitorComponent) return;
if (!proximityMonitorComponent->GetProximityObjects("talk").empty()) {
const auto& chats = npc->GetVar<std::vector<std::string>>(u"chats");
if (chats.empty()) {
return;
}
const auto& selected
= chats[GeneralUtils::GenerateRandomNumber<int32_t>(0, static_cast<int32_t>(chats.size() - 1))];
GameMessages::SendNotifyClientZoneObject(
npc->GetObjectID(), u"sendToclient_bubble", 0, 0, npc->GetObjectID(), selected, UNASSIGNED_SYSTEM_ADDRESS);
if (name == "ENTER" && !entity->HasTimer("talk")) {
NPCTalk(entity);
}
}
void VanityUtilities::OnTimerDone(Entity* npc, const std::string& name) {
if (name == "talk") {
const auto* const proximityMonitorComponent = npc->GetComponent<ProximityMonitorComponent>();
if (!proximityMonitorComponent || proximityMonitorComponent->GetProximityObjects("talk").empty()) return;
NPCTalk(npc);
}
}
void NPCTalk(Entity* npc) {
const auto& chats = npc->GetVar<std::vector<std::string>>(u"chats");
if (chats.empty()) return;
const auto& selected
= chats[GeneralUtils::GenerateRandomNumber<int32_t>(0, static_cast<int32_t>(chats.size() - 1))];
GameMessages::SendNotifyClientZoneObject(
npc->GetObjectID(), u"sendToclient_bubble", 0, 0, npc->GetObjectID(), selected, UNASSIGNED_SYSTEM_ADDRESS);
Game::entityManager->SerializeEntity(npc);
const float nextTime = GeneralUtils::GenerateRandomNumber<float>(15, 60);
npc->AddCallbackTimer(nextTime, [npc]() { NPCTalk(npc); });
npc->AddTimer("talk", nextTime);
}

View File

@@ -31,4 +31,8 @@ namespace VanityUtilities {
std::string ParseMarkdown(
const std::string& file
);
void OnProximityUpdate(Entity* entity, Entity* other, const std::string& proxName, const std::string& name);
void OnTimerDone(Entity* entity, const std::string& name);
};