mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2025-10-10 17:38:08 +00:00

* feat: re-write persistent object ID tracker Features: - Remove random objectIDs entirely - Replace random objectIDs with persistentIDs - Remove the need to contact the MASTER server for a persistent ID - Add persistent ID logic to WorldServers that use transactions to guarantee unique IDs no matter when they are generated - Default character xml version to be the most recent one Fixes: - Return optional from GetModel (and check for nullopt where it may exist) - Regenerate inventory item ids on first login to be unique item IDs (fixes all those random IDs Pet IDs and subkeys are left alone and are assumed to be reserved (checks are there to prevent this) There is also duplicate check logic in place for properties and UGC/Models * Update comment and log * fix: sqlite transaction bug * fix colliding temp item ids temp items should not be saved. would cause issues between worlds as experienced before this commit
296 lines
12 KiB
C++
296 lines
12 KiB
C++
#include "ControlBehaviors.h"
|
|
|
|
#include "Amf3.h"
|
|
#include "Entity.h"
|
|
#include "Game.h"
|
|
#include "GameMessages.h"
|
|
#include "ModelComponent.h"
|
|
#include "ObjectIDManager.h"
|
|
#include "Logger.h"
|
|
#include "BehaviorStates.h"
|
|
#include "AssetManager.h"
|
|
#include "BlockDefinition.h"
|
|
#include "User.h"
|
|
#include "tinyxml2.h"
|
|
#include "CDClientDatabase.h"
|
|
#include "CharacterComponent.h"
|
|
|
|
// Message includes
|
|
#include "Action.h"
|
|
#include "AddActionMessage.h"
|
|
#include "AddStripMessage.h"
|
|
#include "AddMessage.h"
|
|
#include "MigrateActionsMessage.h"
|
|
#include "MoveToInventoryMessage.h"
|
|
#include "MergeStripsMessage.h"
|
|
#include "RearrangeStripMessage.h"
|
|
#include "RemoveActionsMessage.h"
|
|
#include "RemoveStripMessage.h"
|
|
#include "RenameMessage.h"
|
|
#include "SplitStripMessage.h"
|
|
#include "UpdateActionMessage.h"
|
|
#include "UpdateStripUiMessage.h"
|
|
#include "eObjectBits.h"
|
|
|
|
void ControlBehaviors::RequestUpdatedID(ControlBehaviorContext& context) {
|
|
BehaviorMessageBase msgBase{ context.arguments };
|
|
const auto oldBehaviorID = msgBase.GetBehaviorId();
|
|
if (!context) {
|
|
LOG("Model to update behavior ID for is null. Cannot update ID.");
|
|
return;
|
|
}
|
|
LWOOBJID persistentIdBig = ObjectIDManager::GetPersistentID();
|
|
// This updates the behavior ID of the behavior should this be a new behavior
|
|
AMFArrayValue args;
|
|
|
|
args.Insert("behaviorID", std::to_string(persistentIdBig));
|
|
args.Insert("objectID", std::to_string(context.modelComponent->GetParent()->GetObjectID()));
|
|
|
|
GameMessages::SendUIMessageServerToSingleClient(context.modelOwner, context.modelOwner->GetSystemAddress(), "UpdateBehaviorID", args);
|
|
context.modelComponent->UpdatePendingBehaviorId(persistentIdBig, oldBehaviorID);
|
|
|
|
ControlBehaviors::Instance().SendBehaviorListToClient(context);
|
|
}
|
|
|
|
void ControlBehaviors::SendBehaviorListToClient(const ControlBehaviorContext& context) {
|
|
if (!context) return;
|
|
|
|
AMFArrayValue behaviorsToSerialize;
|
|
context.modelComponent->SendBehaviorListToClient(behaviorsToSerialize);
|
|
|
|
GameMessages::SendUIMessageServerToSingleClient(context.modelOwner, context.modelOwner->GetSystemAddress(), "UpdateBehaviorList", behaviorsToSerialize);
|
|
}
|
|
|
|
// TODO This is also supposed to serialize the state of the behaviors in progress but those aren't implemented yet
|
|
void ControlBehaviors::SendBehaviorBlocksToClient(ControlBehaviorContext& context) {
|
|
if (!context) return;
|
|
BehaviorMessageBase behaviorMsg{ context.arguments };
|
|
|
|
context.modelComponent->VerifyBehaviors();
|
|
AMFArrayValue behavior;
|
|
context.modelComponent->SendBehaviorBlocksToClient(behaviorMsg.GetBehaviorId(), behavior);
|
|
GameMessages::SendUIMessageServerToSingleClient(context.modelOwner, context.modelOwner->GetSystemAddress(), "UpdateBehaviorBlocks", behavior);
|
|
}
|
|
|
|
void ControlBehaviors::UpdateAction(const AMFArrayValue& arguments) {
|
|
UpdateActionMessage updateActionMessage{ arguments };
|
|
auto blockDefinition = GetBlockInfo(updateActionMessage.GetAction().GetType());
|
|
|
|
if (!blockDefinition) {
|
|
LOG("Received undefined block type %s. Ignoring.", updateActionMessage.GetAction().GetType().data());
|
|
return;
|
|
}
|
|
|
|
if (updateActionMessage.GetAction().GetValueParameterString().size() > 0) {
|
|
if (updateActionMessage.GetAction().GetValueParameterString().size() < blockDefinition->GetMinimumValue() ||
|
|
updateActionMessage.GetAction().GetValueParameterString().size() > blockDefinition->GetMaximumValue()) {
|
|
LOG("Updated block %s is out of range. Ignoring update", updateActionMessage.GetAction().GetType().data());
|
|
return;
|
|
}
|
|
} else {
|
|
if (updateActionMessage.GetAction().GetValueParameterDouble() < blockDefinition->GetMinimumValue() ||
|
|
updateActionMessage.GetAction().GetValueParameterDouble() > blockDefinition->GetMaximumValue()) {
|
|
LOG("Updated block %s is out of range. Ignoring update", updateActionMessage.GetAction().GetType().data());
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ControlBehaviors::ProcessCommand(Entity* const modelEntity, const AMFArrayValue& arguments, const std::string_view command, Entity* const modelOwner) {
|
|
if (!isInitialized || !modelEntity || !modelOwner) return;
|
|
auto* const modelComponent = modelEntity->GetComponent<ModelComponent>();
|
|
|
|
if (!modelComponent) return;
|
|
|
|
ControlBehaviorContext context{ arguments, modelComponent, modelOwner };
|
|
bool needsNewBehaviorID = false;
|
|
|
|
if (command == "sendBehaviorListToClient") {
|
|
SendBehaviorListToClient(context);
|
|
} else if (command == "sendBehaviorBlocksToClient") {
|
|
SendBehaviorBlocksToClient(context);
|
|
} else if (command == "modelTypeChanged") {
|
|
const auto* const modelType = arguments.Get<double>("ModelType");
|
|
if (!modelType) return;
|
|
|
|
modelEntity->SetVar<int>(u"modelType", modelType->GetValue());
|
|
} else if (command == "toggleExecutionUpdates") {
|
|
// TODO
|
|
} else if (command == "addStrip") {
|
|
needsNewBehaviorID = context.modelComponent->HandleControlBehaviorsMsg<AddStripMessage>(context.arguments);
|
|
} else if (command == "removeStrip") {
|
|
needsNewBehaviorID = context.modelComponent->HandleControlBehaviorsMsg<RemoveStripMessage>(arguments);
|
|
} else if (command == "mergeStrips") {
|
|
needsNewBehaviorID = context.modelComponent->HandleControlBehaviorsMsg<MergeStripsMessage>(arguments);
|
|
} else if (command == "splitStrip") {
|
|
needsNewBehaviorID = context.modelComponent->HandleControlBehaviorsMsg<SplitStripMessage>(arguments);
|
|
} else if (command == "updateStripUI") {
|
|
needsNewBehaviorID = context.modelComponent->HandleControlBehaviorsMsg<UpdateStripUiMessage>(arguments);
|
|
} else if (command == "addAction") {
|
|
needsNewBehaviorID = context.modelComponent->HandleControlBehaviorsMsg<AddActionMessage>(arguments);
|
|
} else if (command == "migrateActions") {
|
|
needsNewBehaviorID = context.modelComponent->HandleControlBehaviorsMsg<MigrateActionsMessage>(arguments);
|
|
} else if (command == "rearrangeStrip") {
|
|
needsNewBehaviorID = context.modelComponent->HandleControlBehaviorsMsg<RearrangeStripMessage>(arguments);
|
|
} else if (command == "removeActions") {
|
|
needsNewBehaviorID = context.modelComponent->HandleControlBehaviorsMsg<RemoveActionsMessage>(arguments);
|
|
} else if (command == "updateAction") {
|
|
needsNewBehaviorID = context.modelComponent->HandleControlBehaviorsMsg<UpdateActionMessage>(arguments);
|
|
} else if (command == "rename") {
|
|
needsNewBehaviorID = context.modelComponent->HandleControlBehaviorsMsg<RenameMessage>(arguments);
|
|
|
|
// Send the list back to the client so the name is updated.
|
|
SendBehaviorListToClient(context);
|
|
} else if (command == "add") {
|
|
AddMessage msg{ context.arguments, context.modelOwner->GetObjectID() };
|
|
context.modelComponent->AddBehavior(msg);
|
|
SendBehaviorListToClient(context);
|
|
} else if (command == "moveToInventory" || command == "remove") {
|
|
// both moveToInventory and remove use the same args
|
|
const bool isRemove = command != "remove";
|
|
MoveToInventoryMessage msg{ arguments, modelOwner->GetObjectID() };
|
|
context.modelComponent->RemoveBehavior(msg, isRemove);
|
|
auto* characterComponent = modelOwner->GetComponent<CharacterComponent>();
|
|
if (!characterComponent) return;
|
|
|
|
if (!isRemove) {
|
|
AMFArrayValue args;
|
|
args.Insert("BehaviorID", std::to_string(msg.GetBehaviorId()));
|
|
GameMessages::SendUIMessageServerToSingleClient(modelOwner, characterComponent->GetSystemAddress(), "BehaviorRemoved", args);
|
|
}
|
|
|
|
SendBehaviorListToClient(context);
|
|
} else {
|
|
LOG("Unknown behavior command (%s)", command.data());
|
|
}
|
|
|
|
if (needsNewBehaviorID) RequestUpdatedID(context);
|
|
}
|
|
|
|
ControlBehaviors::ControlBehaviors() {
|
|
auto blocksBuffer = Game::assetManager->GetFile("ui\\ingame\\blocksdef.xml");
|
|
if (!blocksBuffer) {
|
|
LOG("Failed to open blocksdef.xml, property behaviors will be disabled for this zone! "
|
|
"(This is a necessary file for cheat detection and ensuring we do not send unexpected values to the client)");
|
|
return;
|
|
}
|
|
|
|
tinyxml2::XMLDocument m_Doc;
|
|
|
|
std::string read;
|
|
|
|
std::string buffer;
|
|
bool commentBlockStart = false;
|
|
while (std::getline(blocksBuffer, read)) {
|
|
// tinyxml2 should handle comment blocks but the client has one that fails the processing.
|
|
// This preprocessing just removes all comments from the read file out of an abundance of caution.
|
|
if (read.find("<!--") != std::string::npos) {
|
|
commentBlockStart = true;
|
|
}
|
|
if (read.find("-->") != std::string::npos) {
|
|
commentBlockStart = false;
|
|
continue;
|
|
}
|
|
if (!commentBlockStart) buffer += read;
|
|
}
|
|
|
|
auto ret = m_Doc.Parse(buffer.c_str());
|
|
if (ret == tinyxml2::XML_SUCCESS) {
|
|
LOG_DEBUG("Successfully parsed the blocksdef file!");
|
|
} else {
|
|
LOG("Failed to parse BlocksDef xmlData due to error %i!", ret);
|
|
return;
|
|
}
|
|
auto* blockLibrary = m_Doc.FirstChildElement();
|
|
if (!blockLibrary) {
|
|
LOG("No Block Library child element found.");
|
|
return;
|
|
}
|
|
|
|
// Now parse the blocksdef for the cheat detection server side.
|
|
// The client does these checks, but a bad actor can bypass the client checks
|
|
auto* blockSections = blockLibrary->FirstChildElement();
|
|
while (blockSections) {
|
|
auto* block = blockSections->FirstChildElement();
|
|
std::string blockName{};
|
|
while (block) {
|
|
blockName = block->Name();
|
|
|
|
auto& blockDefinition = blockTypes[blockName];
|
|
std::string name{};
|
|
std::string typeName{};
|
|
|
|
auto* argument = block->FirstChildElement("Argument");
|
|
if (argument) {
|
|
auto* defaultDefinition = argument->FirstChildElement("DefaultValue");
|
|
if (defaultDefinition) blockDefinition.SetDefaultValue(defaultDefinition->GetText());
|
|
|
|
auto* typeDefinition = argument->FirstChildElement("Type");
|
|
if (typeDefinition) typeName = typeDefinition->GetText();
|
|
|
|
auto* nameDefinition = argument->FirstChildElement("Name");
|
|
if (nameDefinition) name = nameDefinition->GetText();
|
|
|
|
// Now we parse the blocksdef file for the relevant information
|
|
if (typeName == "String") {
|
|
blockDefinition.SetMaximumValue(50); // The client has a hardcoded limit of 50 characters in a string field
|
|
} else if (typeName == "Float" || typeName == "Integer") {
|
|
auto* maximumDefinition = argument->FirstChildElement("Maximum");
|
|
if (maximumDefinition) blockDefinition.SetMaximumValue(std::stof(maximumDefinition->GetText()));
|
|
|
|
auto* minimumDefinition = argument->FirstChildElement("Minimum");
|
|
if (minimumDefinition) blockDefinition.SetMinimumValue(std::stof(minimumDefinition->GetText()));
|
|
} else if (typeName == "Enumeration") {
|
|
auto* values = argument->FirstChildElement("Values");
|
|
if (values) {
|
|
auto* value = values->FirstChildElement("Value");
|
|
while (value) {
|
|
if (value->GetText() == blockDefinition.GetDefaultValue()) blockDefinition.SetDefaultValue(std::to_string(blockDefinition.GetMaximumValue()));
|
|
blockDefinition.SetMaximumValue(blockDefinition.GetMaximumValue() + 1);
|
|
value = value->NextSiblingElement("Value");
|
|
}
|
|
blockDefinition.SetMaximumValue(blockDefinition.GetMaximumValue() - 1); // Maximum value is 0 indexed
|
|
} else {
|
|
values = argument->FirstChildElement("EnumerationSource");
|
|
if (!values) {
|
|
LOG("Failed to parse EnumerationSource from block (%s)", blockName.c_str());
|
|
continue;
|
|
}
|
|
|
|
auto* serviceNameNode = values->FirstChildElement("ServiceName");
|
|
if (!serviceNameNode) {
|
|
LOG("Failed to parse ServiceName from block (%s)", blockName.c_str());
|
|
continue;
|
|
}
|
|
|
|
std::string serviceName = serviceNameNode->GetText();
|
|
if (serviceName == "GetBehaviorSoundList") {
|
|
auto res = CDClientDatabase::ExecuteQuery("SELECT MAX(id) as countSounds FROM UGBehaviorSounds;");
|
|
blockDefinition.SetMaximumValue(res.getIntField("countSounds"));
|
|
blockDefinition.SetDefaultValue("0");
|
|
} else {
|
|
LOG("Unsupported Enumeration ServiceType (%s)", serviceName.c_str());
|
|
continue;
|
|
}
|
|
}
|
|
} else {
|
|
LOG("Unsupported block value type (%s)!", typeName.c_str());
|
|
continue;
|
|
}
|
|
}
|
|
block = block->NextSiblingElement();
|
|
}
|
|
blockSections = blockSections->NextSiblingElement();
|
|
}
|
|
isInitialized = true;
|
|
LOG_DEBUG("Created all base block classes");
|
|
for (auto& [name, block] : blockTypes) {
|
|
LOG_DEBUG("block name is %s default %s min %f max %f", name.data(), block.GetDefaultValue().data(), block.GetMinimumValue(), block.GetMaximumValue());
|
|
}
|
|
}
|
|
|
|
std::optional<BlockDefinition> ControlBehaviors::GetBlockInfo(const std::string_view blockName) {
|
|
auto blockDefinition = blockTypes.find(blockName);
|
|
return blockDefinition != blockTypes.end() ? std::optional(blockDefinition->second) : std::nullopt;
|
|
}
|