Merge branch 'main' into leaderboards-again

This commit is contained in:
David Markowitz
2025-05-14 00:35:35 -07:00
72 changed files with 2090 additions and 1084 deletions

View File

@@ -545,9 +545,8 @@ void Entity::Initialize() {
// ZoneControl script
if (m_TemplateID == 2365) {
CDZoneTableTable* zoneTable = CDClientManager::GetTable<CDZoneTableTable>();
const auto zoneID = Game::zoneManager->GetZoneID();
const CDZoneTable* zoneData = zoneTable->Query(zoneID.GetMapID());
const CDZoneTable* zoneData = CDZoneTableTable::Query(zoneID.GetMapID());
if (zoneData != nullptr) {
int zoneScriptID = zoneData->scriptID;

View File

@@ -99,7 +99,7 @@ Entity* EntityManager::CreateEntity(EntityInfo info, User* user, Entity* parentE
}
// Exclude the zone control object from any flags
if (!controller && info.lot != 14) {
if (!controller) {
// The client flags means the client should render the entity
GeneralUtils::SetBit(id, eObjectBits::CLIENT);

View File

@@ -20,7 +20,7 @@ void BasicAttackBehavior::Handle(BehaviorContext* context, RakNet::BitStream& bi
//Handle player damage cooldown
if (entity->IsPlayer() && !this->m_DontApplyImmune) {
const float immunityTime = Game::zoneManager->GetWorldConfig()->globalImmunityTime;
const float immunityTime = Game::zoneManager->GetWorldConfig().globalImmunityTime;
destroyableComponent->SetDamageCooldownTimer(immunityTime);
}
}
@@ -214,7 +214,7 @@ void BasicAttackBehavior::DoBehaviorCalculation(BehaviorContext* context, RakNet
//Handle player damage cooldown
if (isSuccess && targetEntity->IsPlayer() && !this->m_DontApplyImmune) {
destroyableComponent->SetDamageCooldownTimer(Game::zoneManager->GetWorldConfig()->globalImmunityTime);
destroyableComponent->SetDamageCooldownTimer(Game::zoneManager->GetWorldConfig().globalImmunityTime);
}
eBasicAttackSuccessTypes successState = eBasicAttackSuccessTypes::FAILIMMUNE;

View File

@@ -774,10 +774,10 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType
if (Game::zoneManager->GetPlayerLoseCoinOnDeath()) {
auto* character = m_Parent->GetCharacter();
uint64_t coinsTotal = character->GetCoins();
const uint64_t minCoinsToLose = Game::zoneManager->GetWorldConfig()->coinsLostOnDeathMin;
const uint64_t minCoinsToLose = Game::zoneManager->GetWorldConfig().coinsLostOnDeathMin;
if (coinsTotal >= minCoinsToLose) {
const uint64_t maxCoinsToLose = Game::zoneManager->GetWorldConfig()->coinsLostOnDeathMax;
const float coinPercentageToLose = Game::zoneManager->GetWorldConfig()->coinsLostOnDeathPercent;
const uint64_t maxCoinsToLose = Game::zoneManager->GetWorldConfig().coinsLostOnDeathMax;
const float coinPercentageToLose = Game::zoneManager->GetWorldConfig().coinsLostOnDeathPercent;
uint64_t coinsToLose = std::max(static_cast<uint64_t>(coinsTotal * coinPercentageToLose), minCoinsToLose);
coinsToLose = std::min(maxCoinsToLose, coinsToLose);

View File

@@ -10,12 +10,50 @@
#include "SimplePhysicsComponent.h"
#include "Database.h"
#include "DluAssert.h"
ModelComponent::ModelComponent(Entity* parent) : Component(parent) {
m_OriginalPosition = m_Parent->GetDefaultPosition();
m_OriginalRotation = m_Parent->GetDefaultRotation();
m_IsPaused = false;
m_NumListeningInteract = 0;
m_userModelID = m_Parent->GetVarAs<LWOOBJID>(u"userModelID");
RegisterMsg(MessageType::Game::REQUEST_USE, this, &ModelComponent::OnRequestUse);
RegisterMsg(MessageType::Game::RESET_MODEL_TO_DEFAULTS, this, &ModelComponent::OnResetModelToDefaults);
}
bool ModelComponent::OnResetModelToDefaults(GameMessages::GameMsg& msg) {
auto& reset = static_cast<GameMessages::ResetModelToDefaults&>(msg);
for (auto& behavior : m_Behaviors) behavior.HandleMsg(reset);
GameMessages::UnSmash unsmash;
unsmash.target = GetParent()->GetObjectID();
unsmash.duration = 0.0f;
unsmash.Send(UNASSIGNED_SYSTEM_ADDRESS);
m_NumListeningInteract = 0;
m_NumActiveUnSmash = 0;
m_Dirty = true;
Game::entityManager->SerializeEntity(GetParent());
return true;
}
bool ModelComponent::OnRequestUse(GameMessages::GameMsg& msg) {
bool toReturn = false;
if (!m_IsPaused) {
auto& requestUse = static_cast<GameMessages::RequestUse&>(msg);
for (auto& behavior : m_Behaviors) behavior.HandleMsg(requestUse);
toReturn = true;
}
return toReturn;
}
void ModelComponent::Update(float deltaTime) {
if (m_IsPaused) return;
for (auto& behavior : m_Behaviors) {
behavior.Update(deltaTime, *this);
}
}
void ModelComponent::LoadBehaviors() {
@@ -29,9 +67,9 @@ void ModelComponent::LoadBehaviors() {
LOG_DEBUG("Loading behavior %d", behaviorId.value());
auto& inserted = m_Behaviors.emplace_back();
inserted.SetBehaviorId(*behaviorId);
const auto behaviorStr = Database::Get()->GetBehavior(behaviorId.value());
tinyxml2::XMLDocument behaviorXml;
auto res = behaviorXml.Parse(behaviorStr.c_str(), behaviorStr.size());
LOG_DEBUG("Behavior %i %d: %s", res, behaviorId.value(), behaviorStr.c_str());
@@ -45,6 +83,11 @@ void ModelComponent::LoadBehaviors() {
}
}
void ModelComponent::Resume() {
m_Dirty = true;
m_IsPaused = false;
}
void ModelComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) {
// ItemComponent Serialization. Pets do not get this serialization.
if (!m_Parent->HasComponent(eReplicaComponentType::PET)) {
@@ -56,14 +99,14 @@ void ModelComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialU
//actual model component:
outBitStream.Write1(); // Yes we are writing model info
outBitStream.Write0(); // Is pickable
outBitStream.Write(m_NumListeningInteract > 0); // Is pickable
outBitStream.Write<uint32_t>(2); // Physics type
outBitStream.Write(m_OriginalPosition); // Original position
outBitStream.Write(m_OriginalRotation); // Original rotation
outBitStream.Write1(); // We are writing behavior info
outBitStream.Write<uint32_t>(0); // Number of behaviors
outBitStream.Write1(); // Is this model paused
outBitStream.Write<uint32_t>(m_Behaviors.size()); // Number of behaviors
outBitStream.Write(m_IsPaused); // Is this model paused
if (bIsInitialUpdate) outBitStream.Write0(); // We are not writing model editing info
}
@@ -135,3 +178,28 @@ std::array<std::pair<int32_t, std::string>, 5> ModelComponent::GetBehaviorsForSa
}
return toReturn;
}
void ModelComponent::AddInteract() {
LOG_DEBUG("Adding interact %i", m_NumListeningInteract);
m_Dirty = true;
m_NumListeningInteract++;
}
void ModelComponent::RemoveInteract() {
DluAssert(m_NumListeningInteract > 0);
LOG_DEBUG("Removing interact %i", m_NumListeningInteract);
m_Dirty = true;
m_NumListeningInteract--;
}
void ModelComponent::AddUnSmash() {
LOG_DEBUG("Adding UnSmash %i", m_NumActiveUnSmash);
m_NumActiveUnSmash++;
}
void ModelComponent::RemoveUnSmash() {
// Players can assign an UnSmash without a Smash so an assert would be bad here
if (m_NumActiveUnSmash == 0) return;
LOG_DEBUG("Removing UnSmash %i", m_NumActiveUnSmash);
m_NumActiveUnSmash--;
}

View File

@@ -30,6 +30,10 @@ public:
ModelComponent(Entity* parent);
void LoadBehaviors();
void Update(float deltaTime) override;
bool OnRequestUse(GameMessages::GameMsg& msg);
bool OnResetModelToDefaults(GameMessages::GameMsg& msg);
void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) override;
@@ -59,7 +63,7 @@ public:
/**
* Main gateway for all behavior messages to be passed to their respective behaviors.
*
*
* @tparam Msg The message type to pass
* @param args the arguments of the message to be deserialized
*/
@@ -68,7 +72,7 @@ public:
static_assert(std::is_base_of_v<BehaviorMessageBase, Msg>, "Msg must be a BehaviorMessageBase");
Msg msg{ args };
for (auto&& behavior : m_Behaviors) {
if (behavior.GetBehaviorId() == msg.GetBehaviorId()) {
if (behavior.GetBehaviorId() == msg.GetBehaviorId()) {
behavior.HandleMsg(msg);
return;
}
@@ -109,12 +113,35 @@ public:
void SendBehaviorListToClient(AMFArrayValue& args) const;
void SendBehaviorBlocksToClient(int32_t behaviorToSend, AMFArrayValue& args) const;
void VerifyBehaviors();
std::array<std::pair<int32_t, std::string>, 5> GetBehaviorsForSave() const;
const std::vector<PropertyBehavior>& GetBehaviors() const { return m_Behaviors; };
void AddInteract();
void RemoveInteract();
void Pause() { m_Dirty = true; m_IsPaused = true; }
void AddUnSmash();
void RemoveUnSmash();
bool IsUnSmashing() const { return m_NumActiveUnSmash != 0; }
void Resume();
private:
// Number of Actions that are awaiting an UnSmash to finish.
uint32_t m_NumActiveUnSmash{};
// Whether or not this component needs to have its extra data serialized.
bool m_Dirty{};
// The number of strips listening for a RequestUse GM to come in.
uint32_t m_NumListeningInteract{};
// Whether or not the model is paused and should reject all interactions regarding behaviors.
bool m_IsPaused{};
/**
* The behaviors of the model
* Note: This is a vector because the order of the behaviors matters when serializing to the client.

View File

@@ -26,6 +26,7 @@
#include <vector>
#include "CppScripts.h"
#include <ranges>
#include "dConfig.h"
PropertyManagementComponent* PropertyManagementComponent::instance = nullptr;
@@ -151,7 +152,11 @@ void PropertyManagementComponent::SetPrivacyOption(PropertyPrivacyOption value)
info.rejectionReason = rejectionReason;
info.modApproved = 0;
Database::Get()->UpdatePropertyModerationInfo(info);
if (models.empty() && Game::config->GetValue("auto_reject_empty_properties") == "1") {
UpdateApprovedStatus(false, "Your property is empty. Please place a model to have a public property.");
} else {
Database::Get()->UpdatePropertyModerationInfo(info);
}
}
void PropertyManagementComponent::UpdatePropertyDetails(std::string name, std::string description) {
@@ -255,6 +260,18 @@ void PropertyManagementComponent::OnStartBuilding() {
// Push equipped items
if (inventoryComponent) inventoryComponent->PushEquippedItems();
for (auto modelID : models | std::views::keys) {
auto* model = Game::entityManager->GetEntity(modelID);
if (model) {
auto* modelComponent = model->GetComponent<ModelComponent>();
if (modelComponent) modelComponent->Pause();
Game::entityManager->SerializeEntity(model);
GameMessages::ResetModelToDefaults reset;
reset.target = modelID;
model->HandleMsg(reset);
}
}
}
void PropertyManagementComponent::OnFinishBuilding() {
@@ -267,6 +284,18 @@ void PropertyManagementComponent::OnFinishBuilding() {
UpdateApprovedStatus(false);
Save();
for (auto modelID : models | std::views::keys) {
auto* model = Game::entityManager->GetEntity(modelID);
if (model) {
auto* modelComponent = model->GetComponent<ModelComponent>();
if (modelComponent) modelComponent->Resume();
Game::entityManager->SerializeEntity(model);
GameMessages::ResetModelToDefaults reset;
reset.target = modelID;
model->HandleMsg(reset);
}
}
}
void PropertyManagementComponent::UpdateModelPosition(const LWOOBJID id, const NiPoint3 position, NiQuaternion rotation) {
@@ -318,6 +347,8 @@ void PropertyManagementComponent::UpdateModelPosition(const LWOOBJID id, const N
Entity* newEntity = Game::entityManager->CreateEntity(info);
if (newEntity != nullptr) {
Game::entityManager->ConstructEntity(newEntity);
auto* modelComponent = newEntity->GetComponent<ModelComponent>();
if (modelComponent) modelComponent->Pause();
// Make sure the propMgmt doesn't delete our model after the server dies
// Trying to do this after the entity is constructed. Shouldn't really change anything but
@@ -363,6 +394,8 @@ void PropertyManagementComponent::UpdateModelPosition(const LWOOBJID id, const N
info.nodes[0]->config.push_back(new LDFData<int>(u"componentWhitelist", 1));
auto* model = spawner->Spawn();
auto* modelComponent = model->GetComponent<ModelComponent>();
if (modelComponent) modelComponent->Pause();
models.insert_or_assign(model->GetObjectID(), spawnerId);
@@ -537,14 +570,14 @@ void PropertyManagementComponent::DeleteModel(const LWOOBJID id, const int delet
}
}
void PropertyManagementComponent::UpdateApprovedStatus(const bool value) {
void PropertyManagementComponent::UpdateApprovedStatus(const bool value, const std::string& rejectionReason) {
if (owner == LWOOBJID_EMPTY) return;
IProperty::Info info;
info.id = propertyId;
info.modApproved = value;
info.privacyOption = static_cast<uint32_t>(privacyOption);
info.rejectionReason = "";
info.rejectionReason = rejectionReason;
Database::Get()->UpdatePropertyModerationInfo(info);
}

View File

@@ -135,7 +135,7 @@ public:
* Updates whether or not this property is approved by a moderator
* @param value true if the property should be approved, false otherwise
*/
void UpdateApprovedStatus(bool value);
void UpdateApprovedStatus(bool value, const std::string& rejectionReason = "");
/**
* Loads all the models on this property from the database

View File

@@ -101,7 +101,7 @@ void VendorComponent::SetupConstants() {
std::vector<CDVendorComponent> vendorComps = vendorComponentTable->Query([=](CDVendorComponent entry) { return (entry.id == componentID); });
if (vendorComps.empty()) return;
auto vendorData = vendorComps.at(0);
if (vendorData.buyScalar == 0.0) m_BuyScalar = Game::zoneManager->GetWorldConfig()->vendorBuyMultiplier;
if (vendorData.buyScalar == 0.0) m_BuyScalar = Game::zoneManager->GetWorldConfig().vendorBuyMultiplier;
else m_BuyScalar = vendorData.buyScalar;
m_SellScalar = vendorData.sellScalar;
m_RefreshTimeSeconds = vendorData.refreshTimeSeconds;

View File

@@ -45,6 +45,7 @@ namespace {
using namespace GameMessages;
using MessageCreator = std::function<std::unique_ptr<GameMessages::GameMsg>()>;
std::map<MessageType::Game, MessageCreator> g_MessageHandlers = {
{ REQUEST_USE, []() { return std::make_unique<RequestUse>(); }},
{ REQUEST_SERVER_OBJECT_INFO, []() { return std::make_unique<RequestServerObjectInfo>(); } },
{ SHOOTING_GALLERY_FIRE, []() { return std::make_unique<ShootingGalleryFire>(); } },
};
@@ -118,11 +119,6 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System
break;
}
case MessageType::Game::REQUEST_USE: {
GameMessages::HandleRequestUse(inStream, entity, sysAddr);
break;
}
case MessageType::Game::SET_FLAG: {
GameMessages::HandleSetFlag(inStream, entity);
break;

View File

@@ -102,6 +102,8 @@
#include "CDComponentsRegistryTable.h"
#include "CDObjectsTable.h"
#include "eItemType.h"
#include "Lxfml.h"
#include "Sd0.h"
void GameMessages::SendFireEventClientSide(const LWOOBJID& objectID, const SystemAddress& sysAddr, std::u16string args, const LWOOBJID& object, int64_t param1, int param2, const LWOOBJID& sender) {
CBITSTREAM;
@@ -2575,18 +2577,6 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent
TODO Apparently the bricks are supposed to be taken via MoveInventoryBatch?
*/
////Decompress the SD0 from the client so we can process the lxfml properly
//uint8_t* outData = new uint8_t[327680];
//int32_t error;
//int32_t size = ZCompression::Decompress(inData, lxfmlSize, outData, 327680, error);
//if (size == -1) {
// LOG("Failed to decompress LXFML: (%i)", error);
// return;
//}
//
//std::string lxfml(reinterpret_cast<char*>(outData), size); //std::string version of the decompressed data!
//Now, the cave of dragons:
//We runs this in async because the http library here is blocking, meaning it'll halt the thread.
@@ -2614,16 +2604,25 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent
LWOOBJID propertyId = LWOOBJID_EMPTY;
if (propertyInfo) propertyId = propertyInfo->id;
//Insert into ugc:
// Save the binary data to the Sd0 buffer
std::string str(sd0Data.get(), sd0Size);
std::istringstream sd0DataStream(str);
Database::Get()->InsertNewUgcModel(sd0DataStream, blueprintIDSmall, entity->GetCharacter()->GetParentUser()->GetAccountID(), entity->GetCharacter()->GetID());
Sd0 sd0(sd0DataStream);
// Uncompress the data and normalize the position
const auto asStr = sd0.GetAsStringUncompressed();
const auto [newLxfml, newCenter] = Lxfml::NormalizePosition(asStr);
// Recompress the data and save to the database
sd0.FromData(reinterpret_cast<const uint8_t*>(newLxfml.data()), newLxfml.size());
auto sd0AsStream = sd0.GetAsStream();
Database::Get()->InsertNewUgcModel(sd0AsStream, blueprintIDSmall, entity->GetCharacter()->GetParentUser()->GetAccountID(), entity->GetCharacter()->GetID());
//Insert into the db as a BBB model:
IPropertyContents::Model model;
model.id = newIDL;
model.ugcId = blueprintIDSmall;
model.position = NiPoint3Constant::ZERO;
model.position = newCenter;
model.rotation = NiQuaternion(0.0f, 0.0f, 0.0f, 0.0f);
model.lot = 14;
Database::Get()->InsertNewPropertyModel(propertyId, model, "Objects_14_name");
@@ -2649,6 +2648,9 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent
//}
//Tell the client their model is saved: (this causes us to actually pop out of our current state):
const auto& newSd0 = sd0.GetAsVector();
uint32_t sd0Size{};
for (const auto& chunk : newSd0) sd0Size += chunk.size();
CBITSTREAM;
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, MessageType::Client::BLUEPRINT_SAVE_RESPONSE);
bitStream.Write(localId);
@@ -2656,9 +2658,9 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent
bitStream.Write<uint32_t>(1);
bitStream.Write(blueprintID);
bitStream.Write<uint32_t>(sd0Size);
bitStream.Write(sd0Size);
bitStream.WriteAlignedBytes(reinterpret_cast<unsigned char*>(sd0Data.get()), sd0Size);
for (const auto& chunk : newSd0) bitStream.WriteAlignedBytes(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size());
SEND_PACKET;
@@ -2666,7 +2668,7 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent
EntityInfo info;
info.lot = 14;
info.pos = {};
info.pos = newCenter;
info.rot = {};
info.spawner = nullptr;
info.spawnerID = entity->GetObjectID();
@@ -4955,54 +4957,6 @@ void GameMessages::HandleQuickBuildCancel(RakNet::BitStream& inStream, Entity* e
quickBuildComponent->CancelQuickBuild(Game::entityManager->GetEntity(userID), eQuickBuildFailReason::CANCELED_EARLY);
}
void GameMessages::HandleRequestUse(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) {
bool bIsMultiInteractUse = false;
unsigned int multiInteractID;
int multiInteractType;
bool secondary;
LWOOBJID objectID;
inStream.Read(bIsMultiInteractUse);
inStream.Read(multiInteractID);
inStream.Read(multiInteractType);
inStream.Read(objectID);
inStream.Read(secondary);
Entity* interactedObject = Game::entityManager->GetEntity(objectID);
if (interactedObject == nullptr) {
LOG("Object %llu tried to interact, but doesn't exist!", objectID);
return;
}
if (interactedObject->GetLOT() == 9524) {
entity->GetCharacter()->SetBuildMode(true);
}
if (bIsMultiInteractUse) {
if (multiInteractType == 0) {
auto* missionOfferComponent = static_cast<MissionOfferComponent*>(interactedObject->GetComponent(eReplicaComponentType::MISSION_OFFER));
if (missionOfferComponent != nullptr) {
missionOfferComponent->OfferMissions(entity, multiInteractID);
}
} else {
interactedObject->OnUse(entity);
}
} else {
interactedObject->OnUse(entity);
}
//Perform use task if possible:
auto missionComponent = static_cast<MissionComponent*>(entity->GetComponent(eReplicaComponentType::MISSION));
if (missionComponent == nullptr) return;
missionComponent->Progress(eMissionTaskType::TALK_TO_NPC, interactedObject->GetLOT(), interactedObject->GetObjectID());
missionComponent->Progress(eMissionTaskType::INTERACT, interactedObject->GetLOT(), interactedObject->GetObjectID());
}
void GameMessages::HandlePlayEmote(RakNet::BitStream& inStream, Entity* entity) {
int emoteID;
LWOOBJID targetID;
@@ -6444,4 +6398,70 @@ namespace GameMessages {
auto* handlingEntity = Game::entityManager->GetEntity(targetForReport);
if (handlingEntity) handlingEntity->HandleMsg(*this);
}
bool RequestUse::Deserialize(RakNet::BitStream& stream) {
if (!stream.Read(bIsMultiInteractUse)) return false;
if (!stream.Read(multiInteractID)) return false;
if (!stream.Read(multiInteractType)) return false;
if (!stream.Read(object)) return false;
if (!stream.Read(secondary)) return false;
return true;
}
void RequestUse::Handle(Entity& entity, const SystemAddress& sysAddr) {
Entity* interactedObject = Game::entityManager->GetEntity(object);
if (interactedObject == nullptr) {
LOG("Object %llu tried to interact, but doesn't exist!", object);
return;
}
if (interactedObject->GetLOT() == 9524) {
entity.GetCharacter()->SetBuildMode(true);
}
if (bIsMultiInteractUse) {
if (multiInteractType == 0) {
auto* missionOfferComponent = static_cast<MissionOfferComponent*>(interactedObject->GetComponent(eReplicaComponentType::MISSION_OFFER));
if (missionOfferComponent != nullptr) {
missionOfferComponent->OfferMissions(&entity, multiInteractID);
}
} else {
interactedObject->OnUse(&entity);
}
} else {
interactedObject->OnUse(&entity);
}
interactedObject->HandleMsg(*this);
//Perform use task if possible:
auto missionComponent = entity.GetComponent<MissionComponent>();
if (!missionComponent) return;
missionComponent->Progress(eMissionTaskType::TALK_TO_NPC, interactedObject->GetLOT(), interactedObject->GetObjectID());
missionComponent->Progress(eMissionTaskType::INTERACT, interactedObject->GetLOT(), interactedObject->GetObjectID());
}
void Smash::Serialize(RakNet::BitStream& stream) const {
stream.Write(bIgnoreObjectVisibility);
stream.Write(force);
stream.Write(ghostCapacity);
stream.Write(killerID);
}
void UnSmash::Serialize(RakNet::BitStream& stream) const {
stream.Write(builderID != LWOOBJID_EMPTY);
if (builderID != LWOOBJID_EMPTY) stream.Write(builderID);
stream.Write(duration != 3.0f);
if (builderID != 3.0f) stream.Write(duration);
}
void PlayBehaviorSound::Serialize(RakNet::BitStream& stream) const {
stream.Write(soundID != -1);
if (soundID != -1) stream.Write(soundID);
}
}

View File

@@ -631,7 +631,6 @@ namespace GameMessages {
void HandleFireEventServerSide(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr);
void HandleRequestPlatformResync(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr);
void HandleQuickBuildCancel(RakNet::BitStream& inStream, Entity* entity);
void HandleRequestUse(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr);
void HandlePlayEmote(RakNet::BitStream& inStream, Entity* entity);
void HandleModularBuildConvertModel(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr);
void HandleSetFlag(RakNet::BitStream& inStream, Entity* entity);
@@ -782,6 +781,58 @@ namespace GameMessages {
bool Deserialize(RakNet::BitStream& bitStream) override;
void Handle(Entity& entity, const SystemAddress& sysAddr) override;
};
struct RequestUse : public GameMsg {
RequestUse() : GameMsg(MessageType::Game::REQUEST_USE) {}
bool Deserialize(RakNet::BitStream& stream) override;
void Handle(Entity& entity, const SystemAddress& sysAddr) override;
LWOOBJID object{};
bool secondary{ false };
// Set to true if this coming from a multi-interaction UI on the client.
bool bIsMultiInteractUse{};
// Used only for multi-interaction
unsigned int multiInteractID{};
// Used only for multi-interaction, is of the enum type InteractionType
int multiInteractType{};
};
struct Smash : public GameMsg {
Smash() : GameMsg(MessageType::Game::SMASH) {}
void Serialize(RakNet::BitStream& stream) const;
bool bIgnoreObjectVisibility{};
bool force{};
float ghostCapacity{};
LWOOBJID killerID{};
};
struct UnSmash : public GameMsg {
UnSmash() : GameMsg(MessageType::Game::UN_SMASH) {}
void Serialize(RakNet::BitStream& stream) const;
LWOOBJID builderID{ LWOOBJID_EMPTY };
float duration{ 3.0f };
};
struct PlayBehaviorSound : public GameMsg {
PlayBehaviorSound() : GameMsg(MessageType::Game::PLAY_BEHAVIOR_SOUND) {}
void Serialize(RakNet::BitStream& stream) const;
int32_t soundID{ -1 };
};
struct ResetModelToDefaults : public GameMsg {
ResetModelToDefaults() : GameMsg(MessageType::Game::RESET_MODEL_TO_DEFAULTS) {}
};
};
#endif // GAMEMESSAGES_H

View File

@@ -470,9 +470,9 @@ void Mission::YieldRewards() {
int32_t coinsToSend = 0;
if (info.LegoScore > 0) {
eLootSourceType lootSource = info.isMission ? eLootSourceType::MISSION : eLootSourceType::ACHIEVEMENT;
if (levelComponent->GetLevel() >= Game::zoneManager->GetWorldConfig()->levelCap) {
if (levelComponent->GetLevel() >= Game::zoneManager->GetWorldConfig().levelCap) {
// Since the character is at the level cap we reward them with coins instead of UScore.
coinsToSend += info.LegoScore * Game::zoneManager->GetWorldConfig()->levelCapCurrencyConversion;
coinsToSend += info.LegoScore * Game::zoneManager->GetWorldConfig().levelCapCurrencyConversion;
} else {
characterComponent->SetUScore(characterComponent->GetUScore() + info.LegoScore);
GameMessages::SendModifyLEGOScore(entity, entity->GetSystemAddress(), info.LegoScore, lootSource);

View File

@@ -4,9 +4,13 @@
#include "BehaviorStates.h"
#include "ControlBehaviorMsgs.h"
#include "tinyxml2.h"
#include "ModelComponent.h"
#include <ranges>
PropertyBehavior::PropertyBehavior() {
m_LastEditedState = BehaviorState::HOME_STATE;
m_ActiveState = BehaviorState::HOME_STATE;
}
template<>
@@ -84,6 +88,17 @@ void PropertyBehavior::HandleMsg(AddMessage& msg) {
isLoot = m_BehaviorId != 7965;
};
template<>
void PropertyBehavior::HandleMsg(GameMessages::RequestUse& msg) {
m_States[m_ActiveState].HandleMsg(msg);
}
template<>
void PropertyBehavior::HandleMsg(GameMessages::ResetModelToDefaults& msg) {
m_ActiveState = BehaviorState::HOME_STATE;
for (auto& state : m_States | std::views::values) state.HandleMsg(msg);
}
void PropertyBehavior::SendBehaviorListToClient(AMFArrayValue& args) const {
args.Insert("id", std::to_string(m_BehaviorId));
args.Insert("name", m_Name);
@@ -153,3 +168,7 @@ void PropertyBehavior::Deserialize(const tinyxml2::XMLElement& behavior) {
m_States[static_cast<BehaviorState>(stateId)].Deserialize(*stateElement);
}
}
void PropertyBehavior::Update(float deltaTime, ModelComponent& modelComponent) {
for (auto& state : m_States | std::views::values) state.Update(deltaTime, modelComponent);
}

View File

@@ -10,6 +10,7 @@ namespace tinyxml2 {
enum class BehaviorState : uint32_t;
class AMFArrayValue;
class ModelComponent;
/**
* Represents the Entity of a Property Behavior and holds data associated with the behavior
@@ -31,7 +32,12 @@ public:
void Serialize(tinyxml2::XMLElement& behavior) const;
void Deserialize(const tinyxml2::XMLElement& behavior);
void Update(float deltaTime, ModelComponent& modelComponent);
private:
// The current active behavior state. Behaviors can only be in ONE state at a time.
BehaviorState m_ActiveState;
// The states this behavior has.
std::map<BehaviorState, State> m_States;

View File

@@ -117,6 +117,16 @@ void State::HandleMsg(MigrateActionsMessage& msg) {
}
};
template<>
void State::HandleMsg(GameMessages::RequestUse& msg) {
for (auto& strip : m_Strips) strip.HandleMsg(msg);
}
template<>
void State::HandleMsg(GameMessages::ResetModelToDefaults& msg) {
for (auto& strip : m_Strips) strip.HandleMsg(msg);
}
bool State::IsEmpty() const {
for (const auto& strip : m_Strips) {
if (!strip.IsEmpty()) return false;
@@ -152,3 +162,7 @@ void State::Deserialize(const tinyxml2::XMLElement& state) {
strip.Deserialize(*stripElement);
}
}
void State::Update(float deltaTime, ModelComponent& modelComponent) {
for (auto& strip : m_Strips) strip.Update(deltaTime, modelComponent);
}

View File

@@ -8,6 +8,7 @@ namespace tinyxml2 {
}
class AMFArrayValue;
class ModelComponent;
class State {
public:
@@ -19,7 +20,11 @@ public:
void Serialize(tinyxml2::XMLElement& state) const;
void Deserialize(const tinyxml2::XMLElement& state);
void Update(float deltaTime, ModelComponent& modelComponent);
private:
// The strips contained within this state.
std::vector<Strip> m_Strips;
};

View File

@@ -3,6 +3,11 @@
#include "Amf3.h"
#include "ControlBehaviorMsgs.h"
#include "tinyxml2.h"
#include "dEntity/EntityInfo.h"
#include "ModelComponent.h"
#include "PlayerManager.h"
#include "DluAssert.h"
template <>
void Strip::HandleMsg(AddStripMessage& msg) {
@@ -75,7 +80,138 @@ void Strip::HandleMsg(MigrateActionsMessage& msg) {
} else {
m_Actions.insert(m_Actions.begin() + msg.GetDstActionIndex(), msg.GetMigratedActions().begin(), msg.GetMigratedActions().end());
}
};
}
template<>
void Strip::HandleMsg(GameMessages::RequestUse& msg) {
if (m_PausedTime > 0.0f) return;
if (m_Actions[m_NextActionIndex].GetType() == "OnInteract") {
IncrementAction();
m_WaitingForAction = false;
}
}
template<>
void Strip::HandleMsg(GameMessages::ResetModelToDefaults& msg) {
m_WaitingForAction = false;
m_PausedTime = 0.0f;
m_NextActionIndex = 0;
}
void Strip::IncrementAction() {
if (m_Actions.empty()) return;
m_NextActionIndex++;
m_NextActionIndex %= m_Actions.size();
}
void Strip::Spawn(LOT lot, Entity& entity) {
EntityInfo info{};
info.lot = lot;
info.pos = entity.GetPosition();
info.rot = NiQuaternionConstant::IDENTITY;
info.spawnerID = entity.GetObjectID();
Game::entityManager->ConstructEntity(Game::entityManager->CreateEntity(info, nullptr, &entity));
}
// Spawns a specific drop for all
void Strip::SpawnDrop(LOT dropLOT, Entity& entity) {
for (auto* const player : PlayerManager::GetAllPlayers()) {
GameMessages::SendDropClientLoot(player, entity.GetObjectID(), dropLOT, 0, entity.GetPosition());
}
}
void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent) {
auto& entity = *modelComponent.GetParent();
auto& nextAction = GetNextAction();
auto number = nextAction.GetValueParameterDouble();
auto numberAsInt = static_cast<int32_t>(number);
auto nextActionType = GetNextAction().GetType();
if (nextActionType == "SpawnStromling") {
Spawn(10495, entity); // Stromling property
} else if (nextActionType == "SpawnPirate") {
Spawn(10497, entity); // Maelstrom Pirate property
} else if (nextActionType == "SpawnRonin") {
Spawn(10498, entity); // Dark Ronin property
} else if (nextActionType == "DropImagination") {
for (; numberAsInt > 0; numberAsInt--) SpawnDrop(935, entity); // 1 Imagination powerup
} else if (nextActionType == "DropHealth") {
for (; numberAsInt > 0; numberAsInt--) SpawnDrop(177, entity); // 1 Life powerup
} else if (nextActionType == "DropArmor") {
for (; numberAsInt > 0; numberAsInt--) SpawnDrop(6431, entity); // 1 Armor powerup
} else if (nextActionType == "Smash") {
if (!modelComponent.IsUnSmashing()) {
GameMessages::Smash smash{};
smash.target = entity.GetObjectID();
smash.killerID = entity.GetObjectID();
smash.Send(UNASSIGNED_SYSTEM_ADDRESS);
}
} else if (nextActionType == "UnSmash") {
GameMessages::UnSmash unsmash{};
unsmash.target = entity.GetObjectID();
unsmash.duration = number;
unsmash.builderID = LWOOBJID_EMPTY;
unsmash.Send(UNASSIGNED_SYSTEM_ADDRESS);
modelComponent.AddUnSmash();
m_PausedTime = number;
} else if (nextActionType == "Wait") {
m_PausedTime = number;
} else if (nextActionType == "PlaySound") {
GameMessages::PlayBehaviorSound sound;
sound.target = modelComponent.GetParent()->GetObjectID();
sound.soundID = numberAsInt;
sound.Send(UNASSIGNED_SYSTEM_ADDRESS);
} else {
static std::set<std::string> g_WarnedActions;
if (!g_WarnedActions.contains(nextActionType.data())) {
LOG("Tried to play action (%s) which is not supported.", nextActionType.data());
g_WarnedActions.insert(nextActionType.data());
}
return;
}
IncrementAction();
}
// Decrement references to the previous state if we have progressed to the next one.
void Strip::RemoveStates(ModelComponent& modelComponent) const {
const auto& prevAction = GetPreviousAction();
const auto prevActionType = prevAction.GetType();
if (prevActionType == "OnInteract") {
modelComponent.RemoveInteract();
Game::entityManager->SerializeEntity(modelComponent.GetParent());
} else if (prevActionType == "UnSmash") {
modelComponent.RemoveUnSmash();
}
}
void Strip::Update(float deltaTime, ModelComponent& modelComponent) {
m_PausedTime -= deltaTime;
if (m_PausedTime > 0.0f) return;
m_PausedTime = 0.0f;
if (m_WaitingForAction) return;
auto& entity = *modelComponent.GetParent();
auto& nextAction = GetNextAction();
RemoveStates(modelComponent);
// Check for starting blocks and if not a starting block proc this blocks action
if (m_NextActionIndex == 0) {
if (nextAction.GetType() == "OnInteract") {
modelComponent.AddInteract();
Game::entityManager->SerializeEntity(entity);
m_WaitingForAction = true;
}
} else { // should be a normal block
ProcNormalAction(deltaTime, modelComponent);
}
}
void Strip::SendBehaviorBlocksToClient(AMFArrayValue& args) const {
m_Position.SendBehaviorBlocksToClient(args);
@@ -106,3 +242,13 @@ void Strip::Deserialize(const tinyxml2::XMLElement& strip) {
action.Deserialize(*actionElement);
}
}
const Action& Strip::GetNextAction() const {
DluAssert(m_NextActionIndex < m_Actions.size()); return m_Actions[m_NextActionIndex];
}
const Action& Strip::GetPreviousAction() const {
DluAssert(m_NextActionIndex < m_Actions.size());
size_t index = m_NextActionIndex == 0 ? m_Actions.size() - 1 : m_NextActionIndex - 1;
return m_Actions[index];
}

View File

@@ -11,6 +11,7 @@ namespace tinyxml2 {
}
class AMFArrayValue;
class ModelComponent;
class Strip {
public:
@@ -22,8 +23,30 @@ public:
void Serialize(tinyxml2::XMLElement& strip) const;
void Deserialize(const tinyxml2::XMLElement& strip);
const Action& GetNextAction() const;
const Action& GetPreviousAction() const;
void IncrementAction();
void Spawn(LOT object, Entity& entity);
void Update(float deltaTime, ModelComponent& modelComponent);
void SpawnDrop(LOT dropLOT, Entity& entity);
void ProcNormalAction(float deltaTime, ModelComponent& modelComponent);
void RemoveStates(ModelComponent& modelComponent) const;
private:
// Indicates this Strip is waiting for an action to be taken upon it to progress to its actions
bool m_WaitingForAction{ false };
// The amount of time this strip is paused for. Any interactions with this strip should be bounced if this is greater than 0.
float m_PausedTime{ 0.0f };
// The index of the next action to be played. This should always be within range of [0, m_Actions.size()).
size_t m_NextActionIndex{ 0 };
// The list of actions to be executed on this behavior.
std::vector<Action> m_Actions;
// The location of this strip on the UGBehaviorEditor UI
StripUiPosition m_Position;
};

View File

@@ -81,7 +81,7 @@ namespace Mail {
} 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 mailCost = Game::zoneManager->GetWorldConfig().mailBaseFee;
uint32_t stackSize = 0;
auto inventoryComponent = player->GetComponent<InventoryComponent>();
@@ -92,7 +92,7 @@ namespace Mail {
if (hasAttachment) {
item = inventoryComponent->FindItemById(mailInfo.itemID);
if (item) {
mailCost += (item->GetInfo().baseValue * Game::zoneManager->GetWorldConfig()->mailPercentAttachmentFee);
mailCost += (item->GetInfo().baseValue * Game::zoneManager->GetWorldConfig().mailPercentAttachmentFee);
mailInfo.itemLOT = item->GetLot();
}
}