Merge remote-tracking branch 'origin/main' into raw-parsing-for-scene-data

# Conflicts:
#	dNavigation/dTerrain/RawChunk.cpp
This commit is contained in:
Aaron Kimbrell
2026-06-21 01:13:34 -05:00
274 changed files with 4280 additions and 2608 deletions

View File

@@ -66,13 +66,9 @@ const BrickList& BrickDatabase::GetBricks(const LxfmlPath& lxfmlPath) {
std::string materialString(materialList);
const auto materials = GeneralUtils::SplitString(materialString, ',');
if (!materials.empty()) {
brick.materialID = std::stoi(materials[0]);
} else {
brick.materialID = 0;
}
brick.materialID = GeneralUtils::TryParse(materials[0], 0);
} else if (materialID != nullptr) {
brick.materialID = std::stoi(materialID);
brick.materialID = GeneralUtils::TryParse(materialID, 0);
} else {
brick.materialID = 0; // This is bad, makes it so the minigame can't be played
}

View File

@@ -54,17 +54,23 @@ void LogAndSaveFailedAntiCheatCheck(const LWOOBJID& id, const SystemAddress& sys
// If player exists and entity exists in world, use both for logging info.
if (entity && player) {
const auto* const playerChar = player->GetCharacter();
const auto& playerName = playerChar ? playerChar->GetName() : "(null player character)";
const auto* const entityChar = entity->GetCharacter();
const auto& entityName = entityChar ? entityChar->GetName() : "(null entity character)";
LOG("Player (%s) (%llu) at system address (%s) with sending player (%s) (%llu) does not match their own.",
player->GetCharacter()->GetName().c_str(), player->GetObjectID(),
playerName.c_str(), player->GetObjectID(),
sysAddr.ToString(),
entity->GetCharacter()->GetName().c_str(), entity->GetObjectID());
if (player->GetCharacter()) toReport = player->GetCharacter()->GetParentUser();
entityName.c_str(), entity->GetObjectID());
if (playerChar) toReport = playerChar->GetParentUser();
// In the case that the target entity id did not exist, just log the player info.
} else if (player) {
const auto* const playerChar = player->GetCharacter();
const auto& playerName = playerChar ? playerChar->GetName() : "(null player character)";
LOG("Player (%s) (%llu) at system address (%s) with sending player (%llu) does not match their own.",
player->GetCharacter()->GetName().c_str(), player->GetObjectID(),
playerName.c_str(), player->GetObjectID(),
sysAddr.ToString(), id);
if (player->GetCharacter()) toReport = player->GetCharacter()->GetParentUser();
if (playerChar) toReport = playerChar->GetParentUser();
// In the rare case that the player does not exist, just log the system address and who the target id was.
} else {
LOG("Player at system address (%s) with sending player (%llu) does not match their own.",
@@ -76,8 +82,11 @@ void LogAndSaveFailedAntiCheatCheck(const LWOOBJID& id, const SystemAddress& sys
auto* user = UserManager::Instance()->GetUser(sysAddr);
if (user) {
const auto* const lastChar = user->GetLastUsedChar();
const auto& lastName = lastChar ? lastChar->GetName() : "(null last char)";
const auto lastObjID = lastChar ? lastChar->GetObjectID() : LWOOBJID_EMPTY;
LOG("User at system address (%s) (%s) (%llu) sent a packet as (%llu) which is not an id they own.",
sysAddr.ToString(), user->GetLastUsedChar()->GetName().c_str(), user->GetLastUsedChar()->GetObjectID(), id);
sysAddr.ToString(), lastName.c_str(), lastObjID, id);
// Can't know sending player. Just log system address for IP banning.
} else {
LOG("No user found for system address (%s).", sysAddr.ToString());

View File

@@ -21,6 +21,8 @@
#include "TeamManager.h"
#include "CDObjectsTable.h"
#include "ObjectIDManager.h"
#include "CDActivitiesTable.h"
#include "ScriptedActivityComponent.h"
namespace {
std::unordered_set<uint32_t> CachedMatrices;
@@ -142,13 +144,17 @@ void DropFactionLoot(Entity& player, GameMessages::DropClientLoot& lootMsg) {
// Drops 1 token for each player on a team
// token drops are always given to every player on the team.
void DropFactionLoot(const Team& team, GameMessages::DropClientLoot& lootMsg) {
void DropFactionLoot(const Team& team, GameMessages::DropClientLoot& lootMsg, const bool noTeamLootOnDeath = false) {
for (const auto member : team.members) {
GameMessages::GetPosition memberPosMsg{};
memberPosMsg.target = member;
memberPosMsg.Send();
if (NiPoint3::Distance(memberPosMsg.pos, lootMsg.spawnPos) > g_MAX_DROP_RADIUS) continue;
GameMessages::IsDead isDeadMsg{};
// Skip dead players
if (noTeamLootOnDeath && isDeadMsg.Send(member) && isDeadMsg.bDead) continue;
GameMessages::GetFactionTokenType factionTokenType{};
factionTokenType.target = member;
// If we're not in a faction, this message will return false
@@ -186,7 +192,7 @@ void DropPowerupLoot(Entity& player, GameMessages::DropClientLoot& lootMsg) {
// Drop the power up with no owner
// Power ups can be picked up by anyone on a team, however unlike actual loot items,
// if multiple clients say they picked one up, we let them pick it up.
void DropPowerupLoot(const Team& team, GameMessages::DropClientLoot& lootMsg) {
void DropPowerupLoot(const Team& team, GameMessages::DropClientLoot& lootMsg, const bool noTeamLootOnDeath = false) {
lootMsg.lootID = ObjectIDManager::GenerateObjectID();
lootMsg.ownerID = LWOOBJID_EMPTY; // By setting ownerID to empty, any client that gets this DropClientLoot message can pick up the item.
CalcFinalDropPos(lootMsg);
@@ -198,6 +204,10 @@ void DropPowerupLoot(const Team& team, GameMessages::DropClientLoot& lootMsg) {
memberPosMsg.Send();
if (NiPoint3::Distance(memberPosMsg.pos, lootMsg.spawnPos) > g_MAX_DROP_RADIUS) continue;
GameMessages::IsDead isDeadMsg{};
// Skip dead players
if (noTeamLootOnDeath && isDeadMsg.Send(member) && isDeadMsg.bDead) continue;
lootMsg.target = member;
// By sending this message with the same ID to all players on the team, all players on the team are allowed to pick it up.
lootMsg.Send();
@@ -230,7 +240,7 @@ void DropMissionLoot(Entity& player, GameMessages::DropClientLoot& lootMsg) {
// Check if the item needs to be dropped for anyone on the team
// Only players who need the item will have it dropped
void DropMissionLoot(const Team& team, GameMessages::DropClientLoot& lootMsg) {
void DropMissionLoot(const Team& team, GameMessages::DropClientLoot& lootMsg, const bool noTeamLootOnDeath = false) {
GameMessages::MissionNeedsLot needMsg{};
needMsg.item = lootMsg.item;
for (const auto member : team.members) {
@@ -239,6 +249,10 @@ void DropMissionLoot(const Team& team, GameMessages::DropClientLoot& lootMsg) {
memberPosMsg.Send();
if (NiPoint3::Distance(memberPosMsg.pos, lootMsg.spawnPos) > g_MAX_DROP_RADIUS) continue;
GameMessages::IsDead isDeadMsg{};
// Skip dead players
if (noTeamLootOnDeath && isDeadMsg.Send(member) && isDeadMsg.bDead) continue;
needMsg.target = member;
// Will return false if the item is not required
if (needMsg.Send()) {
@@ -274,19 +288,24 @@ void DropRegularLoot(Entity& player, GameMessages::DropClientLoot& lootMsg) {
// Drop a regular piece of loot.
// Most items will go through this.
// Finds the next loot owner on the team the is in range of the kill and gives them this reward.
void DropRegularLoot(Team& team, GameMessages::DropClientLoot& lootMsg) {
void DropRegularLoot(Team& team, GameMessages::DropClientLoot& lootMsg, const bool noTeamLootOnDeath = false) {
auto earningPlayer = LWOOBJID_EMPTY;
lootMsg.lootID = ObjectIDManager::GenerateObjectID();
CalcFinalDropPos(lootMsg);
GameMessages::GetPosition memberPosMsg{};
GameMessages::IsDead isDeadMsg{};
// Find the next loot owner. Eventually this will run into the `player` passed into this function, since those will
// have the same ID, this loop will only ever run at most 4 times.
do {
earningPlayer = team.GetNextLootOwner();
memberPosMsg.target = earningPlayer;
memberPosMsg.Send();
} while (NiPoint3::Distance(memberPosMsg.pos, lootMsg.spawnPos) > g_MAX_DROP_RADIUS);
if (noTeamLootOnDeath) {
isDeadMsg.target = earningPlayer;
// Skip dead players
isDeadMsg.Send();
}
} while (isDeadMsg.bDead || (NiPoint3::Distance(memberPosMsg.pos, lootMsg.spawnPos) > g_MAX_DROP_RADIUS));
if (team.lootOption == 0 /* Shared loot */) {
lootMsg.target = earningPlayer;
lootMsg.ownerID = earningPlayer;
@@ -304,10 +323,10 @@ void DropRegularLoot(Team& team, GameMessages::DropClientLoot& lootMsg) {
DistrbuteMsgToTeam(lootMsg, team);
}
void DropLoot(Entity* player, const LWOOBJID source, const std::map<LOT, LootDropInfo>& rolledItems, uint32_t minCoins, uint32_t maxCoins) {
void DropLoot(Entity* player, const LWOOBJID source, const std::map<LOT, LootDropInfo>& rolledItems, uint32_t minCoins, uint32_t maxCoins, const bool noTeamLootOnDeath) {
player = player->GetOwner(); // if the owner is overwritten, we collect that here
const auto playerID = player->GetObjectID();
if (!player || !player->IsPlayer()) {
if (!player->IsPlayer()) {
LOG("Trying to drop loot for non-player %llu:%i", playerID, player->GetLOT());
return;
}
@@ -342,43 +361,53 @@ void DropLoot(Entity* player, const LWOOBJID source, const std::map<LOT, LootDro
const CDObjects& object = objectsTable->GetByID(lootLot);
if (lootLot == TOKEN_PROXY) {
team ? DropFactionLoot(*team, lootMsg) : DropFactionLoot(*player, lootMsg);
team ? DropFactionLoot(*team, lootMsg, noTeamLootOnDeath) : DropFactionLoot(*player, lootMsg);
} else if (info.table.MissionDrop) {
team ? DropMissionLoot(*team, lootMsg) : DropMissionLoot(*player, lootMsg);
team ? DropMissionLoot(*team, lootMsg, noTeamLootOnDeath) : DropMissionLoot(*player, lootMsg);
} else if (object.type == "Powerup") {
team ? DropPowerupLoot(*team, lootMsg) : DropPowerupLoot(*player, lootMsg);
team ? DropPowerupLoot(*team, lootMsg, noTeamLootOnDeath) : DropPowerupLoot(*player, lootMsg);
} else {
team ? DropRegularLoot(*team, lootMsg) : DropRegularLoot(*player, lootMsg);
team ? DropRegularLoot(*team, lootMsg, noTeamLootOnDeath) : DropRegularLoot(*player, lootMsg);
}
}
}
// Coin roll is divided up between the members, rounded up, then dropped for each player
const uint32_t coinRoll = static_cast<uint32_t>(minCoins + GeneralUtils::GenerateRandomNumber<float>(0, 1) * (maxCoins - minCoins));
const auto droppedCoins = team ? std::ceil(coinRoll / team->members.size()) : coinRoll;
// Filter out dead player if we need to
std::vector<LWOOBJID> lootEarners;
if (team) {
for (auto member : team->members) {
GameMessages::DropClientLoot lootMsg{};
lootMsg.target = member;
lootMsg.ownerID = member;
lootMsg.currency = droppedCoins;
lootMsg.spawnPos = spawnPosition;
lootMsg.sourceID = source;
lootMsg.item = LOT_NULL;
lootMsg.Send();
const auto* const memberEntity = Game::entityManager->GetEntity(member);
if (memberEntity) lootMsg.Send(memberEntity->GetSystemAddress());
if (noTeamLootOnDeath) {
for (const auto member : team->members) {
GameMessages::IsDead isDeadMsg{};
isDeadMsg.target = member;
if (isDeadMsg.Send() && !isDeadMsg.bDead) {
lootEarners.push_back(member);
}
}
} else {
lootEarners = team->members;
}
} else {
lootEarners.push_back(playerID);
}
// Coin roll is divided up between the members, rounded up, then dropped for each player
const uint32_t coinRoll = static_cast<uint32_t>(minCoins + GeneralUtils::GenerateRandomNumber<float>(0, 1) * (maxCoins - minCoins));
// Just in case its empty don't allow divide by 0
const auto droppedCoins = lootEarners.empty() ? coinRoll : static_cast<uint32_t>(std::ceil(static_cast<float>(coinRoll) / lootEarners.size()));
// Drops coins for each alive member of a team (or just a player)
for (auto member : lootEarners) {
GameMessages::DropClientLoot lootMsg{};
lootMsg.target = playerID;
lootMsg.ownerID = playerID;
lootMsg.target = member;
lootMsg.ownerID = member;
lootMsg.currency = droppedCoins;
lootMsg.spawnPos = spawnPosition;
lootMsg.sourceID = source;
lootMsg.item = LOT_NULL;
CalcFinalDropPos(lootMsg);
lootMsg.Send();
lootMsg.Send(player->GetSystemAddress());
const auto* const memberEntity = Game::entityManager->GetEntity(member);
if (memberEntity) lootMsg.Send(memberEntity->GetSystemAddress());
}
}
@@ -531,6 +560,9 @@ void Loot::GiveActivityLoot(Entity* player, const LWOOBJID source, uint32_t acti
void Loot::DropLoot(Entity* player, const LWOOBJID source, uint32_t matrixIndex, uint32_t minCoins, uint32_t maxCoins) {
player = player->GetOwner(); // if the owner is overwritten, we collect that here
auto* scriptedActivityComponent = Game::entityManager->GetZoneControlEntity()->GetComponent<ScriptedActivityComponent>();
const bool noTeamLootOnDeath = scriptedActivityComponent ? scriptedActivityComponent->GetNoTeamLootOnDeath() : false;
auto* inventoryComponent = player->GetComponent<InventoryComponent>();
if (!inventoryComponent)
@@ -538,7 +570,7 @@ void Loot::DropLoot(Entity* player, const LWOOBJID source, uint32_t matrixIndex,
const auto result = ::RollLootMatrix(matrixIndex);
::DropLoot(player, source, result, minCoins, maxCoins);
::DropLoot(player, source, result, minCoins, maxCoins, noTeamLootOnDeath);
}
void Loot::DropActivityLoot(Entity* player, const LWOOBJID source, uint32_t activityID, int32_t rating) {

View File

@@ -74,69 +74,74 @@ namespace Mail {
void SendRequest::Handle() {
SendResponse response;
auto* character = player->GetCharacter();
const bool restrictMailOnMute = UserManager::Instance()->GetMuteRestrictMail() && character->GetParentUser()->GetIsMuted();
const bool restrictedMailAccess = character->HasPermission(ePermissionMap::RestrictedMailAccess);
if (character && !(restrictedMailAccess || restrictMailOnMute)) {
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, ALL, 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;
}
}
}
if (!character) {
response.status = eSendResponse::UnknownError;
} else {
response.status = eSendResponse::SenderAccountIsMuted;
const bool restrictMailOnMute = UserManager::Instance()->GetMuteRestrictMail() && character->GetParentUser()->GetIsMuted();
const bool restrictedMailAccess = character->HasPermission(ePermissionMap::RestrictedMailAccess);
if (character && !(restrictedMailAccess || restrictMailOnMute)) {
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, ALL, 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;
}
}
LOG("Finished send with status %s", StringifiedEnum::ToString(response.status).data());
response.Send(sysAddr);
}
@@ -193,7 +198,7 @@ namespace Mail {
if (mailID > 0 && playerID == player->GetObjectID() && inv) {
auto playerMail = Database::Get()->GetMail(mailID);
if (!playerMail) {
if (!playerMail || playerMail->receiverId != player->GetObjectID()) {
response.status = eAttachmentCollectResponse::MailNotFound;
} else if (!inv->HasSpaceForLoot({ {playerMail->itemLOT, playerMail->itemCount} })) {
response.status = eAttachmentCollectResponse::NoSpaceInInventory;
@@ -225,15 +230,21 @@ namespace Mail {
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;
const auto mailData = Database::Get()->GetMail(mailID);
response.status = eDeleteResponse::NotFound;
if (mailData) {
if (mailData->receiverId != playerID) {
LOG("Player %llu attempted to delete mail owned by %llu. Possible spoof?", playerID, mailData->receiverId);
} else {
if (!(mailData->itemLOT > 0 && mailData->itemCount > 0)) {
Database::Get()->DeleteMail(mailID);
response.status = eDeleteResponse::Success;
} else if (mailData->itemLOT > 0 && mailData->itemCount > 0) {
response.status = eDeleteResponse::HasAttachments;
}
}
}
LOG("DeleteRequest status %s", StringifiedEnum::ToString(response.status).data());
response.Send(sysAddr);
}
@@ -253,11 +264,19 @@ namespace Mail {
void ReadRequest::Handle() {
ReadResponse response;
response.status = eReadResponse::UnknownError;
response.mailID = mailID;
if (Database::Get()->GetMail(mailID)) {
response.status = eReadResponse::Success;
Database::Get()->MarkMailRead(mailID);
const auto mail = Database::Get()->GetMail(mailID);
if (mail) {
if (mail->receiverId == player->GetObjectID()) {
response.status = eReadResponse::Success;
Database::Get()->MarkMailRead(mailID);
} else {
LOG("Player %llu tried to mark mail read for player %llu", mail->receiverId, player->GetObjectID());
}
} else {
LOG("No mail by ID %llu found to mark as read.", mailID);
}
LOG("ReadRequest %s", StringifiedEnum::ToString(response.status).data());

View File

@@ -13,6 +13,7 @@
#include "DestroyableComponent.h"
#include "GameMessages.h"
#include "eMissionState.h"
#include "PetComponent.h"
std::map<uint32_t, Precondition*> Preconditions::cache = {};
@@ -79,6 +80,9 @@ bool Precondition::Check(Entity* player, bool evaluateCosts) const {
case PreconditionType::DoesNotHaveRacingLicence:
case PreconditionType::LegoClubMember:
case PreconditionType::NoInteraction:
case PreconditionType::NotFreeTrial:
case PreconditionType::MissionActive:
case PreconditionType::DoesNotHaveFlag:
any = true;
break;
case PreconditionType::DoesNotHaveItem:
@@ -114,11 +118,13 @@ bool Precondition::Check(Entity* player, bool evaluateCosts) const {
bool Precondition::CheckValue(Entity* player, const uint32_t value, bool evaluateCosts) const {
auto* missionComponent = player->GetComponent<MissionComponent>();
auto* inventoryComponent = player->GetComponent<InventoryComponent>();
auto* destroyableComponent = player->GetComponent<DestroyableComponent>();
auto* levelComponent = player->GetComponent<LevelProgressionComponent>();
auto* character = player->GetCharacter();
auto [missionComponent, inventoryComponent, destroyableComponent, levelComponent] =
player->GetComponentsMut<const MissionComponent, /* not const */ InventoryComponent, const DestroyableComponent, const LevelProgressionComponent>();
if (!missionComponent || !inventoryComponent || !destroyableComponent || !levelComponent || !character) {
return false;
}
Mission* mission;
@@ -152,7 +158,7 @@ bool Precondition::CheckValue(Entity* player, const uint32_t value, bool evaluat
if (missionComponent == nullptr) return false;
return missionComponent->GetMissionState(value) >= eMissionState::COMPLETE;
case PreconditionType::PetDeployed:
return false; // TODO
return PetComponent::GetActivePet(player->GetObjectID()) != nullptr;
case PreconditionType::HasFlag:
return character->GetPlayerFlag(value);
case PreconditionType::WithinShape:
@@ -160,9 +166,9 @@ bool Precondition::CheckValue(Entity* player, const uint32_t value, bool evaluat
case PreconditionType::InBuild:
return character->GetBuildMode();
case PreconditionType::TeamCheck:
return false; // TODO
return false; // TODO: requires knowing the player's minigame team assignment (red/blue etc.); DLU does not track this per-player
case PreconditionType::IsPetTaming:
return false; // TODO
return PetComponent::GetTamingPet(player->GetObjectID()) != nullptr;
case PreconditionType::HasFaction:
for (const auto faction : destroyableComponent->GetFactionIDs()) {
if (faction == static_cast<int>(value)) {
@@ -180,15 +186,24 @@ bool Precondition::CheckValue(Entity* player, const uint32_t value, bool evaluat
return true;
case PreconditionType::HasRacingLicence:
return false; // TODO
return false; // TODO: requires a racing licence level on the player; DLU does not track this
case PreconditionType::DoesNotHaveRacingLicence:
return false; // TODO
return false; // TODO: requires a racing licence level on the player; DLU does not track this
case PreconditionType::LegoClubMember:
return false; // TODO
return true; // Live LU opened LEGO CLUB to All players at some point, so always return true
case PreconditionType::NoInteraction:
return false; // TODO
return false; // TODO: requires tracking the player's currently active interaction object; DLU does not track this
case PreconditionType::HasLevel:
return levelComponent->GetLevel() >= value;
case PreconditionType::NotFreeTrial:
return true; // DLU does not support free trial accounts; all players pass this check
case PreconditionType::MissionActive: {
if (missionComponent == nullptr) return false;
const auto state = missionComponent->GetMissionState(value);
return state == eMissionState::ACTIVE || state == eMissionState::COMPLETE_ACTIVE;
}
case PreconditionType::DoesNotHaveFlag:
return !character->GetPlayerFlag(value);
default:
return true; // There are a couple more unknown preconditions. Always return true in this case.
}
@@ -228,6 +243,7 @@ PreconditionExpression::PreconditionExpression(const std::string& conditions) {
case '&':
case ';':
case '(':
case ':':
b << conditions.substr(i + 1);
done = true;
break;

View File

@@ -26,7 +26,10 @@ enum class PreconditionType
DoesNotHaveRacingLicence,
LegoClubMember,
NoInteraction,
HasLevel = 22
NotFreeTrial,
MissionActive,
HasLevel,
DoesNotHaveFlag = 23
};

View File

@@ -261,7 +261,7 @@ void SlashCommandHandler::Startup() {
Command TestMapCommand{
.help = "Transfers you to the given zone",
.info = "Transfers you to the given zone by id and clone id. Add \"force\" to skip checking if the zone is accessible (this can softlock your character, though, if you e.g. try to teleport to Frostburgh).",
.info = "Transfers you to the given zone by id and clone id and then spawns you at the specified spawn point if one was specified. Ignores instance-id for now.",
.aliases = { "testmap", "tm" },
.handle = DEVGMCommands::TestMap,
.requiredLevel = eGameMasterLevel::FORUM_MODERATOR
@@ -468,7 +468,7 @@ void SlashCommandHandler::Startup() {
Command InspectCommand{
.help = "Inspect an object",
.info = "Finds the closest entity with the given component or LNV variable (ignoring players and racing cars), printing its ID, distance from the player, and whether it is sleeping, as well as the the IDs of all components the entity has. See detailed usage in the DLU docs",
.info = "Finds the closest entity with the given component or LNV variable (ignoring players and racing cars), printing its ID, distance from the player, and whether it is sleeping, as well as the IDs of all components the entity has. Use `localCharacter` or `zoneControl` to inspect your current character or the zone control object.",
.aliases = { "inspect" },
.handle = DEVGMCommands::Inspect,
.requiredLevel = eGameMasterLevel::DEVELOPER
@@ -929,10 +929,10 @@ void SlashCommandHandler::Startup() {
Command GmInvisCommand{
.help = "Toggles invisibility for the character",
.info = "Toggles invisibility for the character, though it's currently a bit buggy. Requires nonzero GM Level for the character, but the account must have a GM level of 8",
.info = "Toggles invisibility for the character, making them invisible to other players and lower GM levels",
.aliases = { "gminvis" },
.handle = GMGreaterThanZeroCommands::GmInvis,
.requiredLevel = eGameMasterLevel::DEVELOPER
.requiredLevel = eGameMasterLevel::FORUM_MODERATOR
};
RegisterCommand(GmInvisCommand);
@@ -1088,7 +1088,7 @@ void SlashCommandHandler::Startup() {
.info = "Resurrects the player",
.aliases = { "resurrect" },
.handle = GMZeroCommands::Resurrect,
.requiredLevel = eGameMasterLevel::CIVILIAN
.requiredLevel = eGameMasterLevel::DEVELOPER
};
RegisterCommand(ResurrectCommand);

View File

@@ -13,7 +13,7 @@
#include "dpShapeSphere.h"
#include "dZoneManager.h"
#include "EntityInfo.h"
#include "Metrics.hpp"
#include "Metrics.h"
#include "PlayerManager.h"
#include "SlashCommandHandler.h"
#include "UserManager.h"
@@ -26,6 +26,8 @@
#include "Database.h"
#include "CDObjectsTable.h"
#include "CDRewardCodesTable.h"
#include "CDLootMatrixTable.h"
#include "CDLootTableTable.h"
// Components
#include "BuffComponent.h"
@@ -89,7 +91,8 @@ namespace DEVGMCommands {
GameMessages::SendChatModeUpdate(entity->GetObjectID(), eGameMasterLevel::CIVILIAN);
entity->SetGMLevel(eGameMasterLevel::CIVILIAN);
GameMessages::SendToggleGMInvis(entity->GetObjectID(), false, UNASSIGNED_SYSTEM_ADDRESS);
GameMessages::ToggleGMInvis msg;
msg.Send(entity->GetObjectID());
GameMessages::SendSlashCommandFeedbackText(entity, u"Your game master level has been changed, you may not be able to use all commands.");
}
@@ -176,14 +179,15 @@ namespace DEVGMCommands {
charComp->m_Character->SetRightHand(minifigItemId);
} else {
Game::entityManager->ConstructEntity(entity);
Game::entityManager->ConstructEntity(entity, entity->GetSystemAddress());
ChatPackets::SendSystemMessage(sysAddr, u"Invalid Minifig item to change, try one of the following: Eyebrows, Eyes, HairColor, HairStyle, Pants, LeftHand, Mouth, RightHand, Shirt, Hands");
return;
}
Game::entityManager->ConstructEntity(entity);
Game::entityManager->ConstructEntity(entity, entity->GetSystemAddress());
ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(lowerName) + u" set to " + (GeneralUtils::to_u16string(minifigItemId)));
GameMessages::SendToggleGMInvis(entity->GetObjectID(), false, UNASSIGNED_SYSTEM_ADDRESS); // need to retoggle because it gets reenabled on creation of new character
}
void PlayAnimation(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
@@ -377,8 +381,6 @@ namespace DEVGMCommands {
line.erase(std::remove(line.begin(), line.end(), '\r'), line.end());
SlashCommandHandler::HandleChatCommand(GeneralUtils::ASCIIToUTF16(line), &entity, sysAddr);
}
} else {
ChatPackets::SendSystemMessage(sysAddr, u"Unknown macro! Is the filename right?");
}
}
@@ -743,11 +745,40 @@ namespace DEVGMCommands {
auto tables = query.execQuery();
std::map<LOT, std::string> lotToName{};
std::map<std::string, LOT> nameToLot{};
while (!tables.eof()) {
std::string message = std::to_string(tables.getIntField("id")) + " - " + tables.getStringField("name");
ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::UTF8ToUTF16(message, message.size()));
const auto lot = tables.getIntField("id");
const auto name = tables.getStringField("name");
lotToName[lot] = name;
nameToLot[name] = lot;
tables.nextRow();
}
// if there arent a ton of results, print them to chat instead
if (lotToName.size() < 5) {
std::stringstream ss;
ss << "Lookup results for \"" << args << "\":";
for (const auto& [lot, name] : lotToName) {
ss << "\nLOT: " << lot << " - Name: " << name;
}
ChatPackets::SendSystemMessage(sysAddr, ss.str());
} else {
AMFArrayValue response;
response.Insert("visible", true);
response.Insert("objectID", "Search Results for: " + args);
response.Insert("serverInfo", true);
auto* const info = response.InsertArray("data");
auto& lotSort = info->PushDebug("Sorted by LOT");
for (const auto& [lot, name] : lotToName) {
auto& entry = lotSort.PushDebug<AMFStringValue>(std::to_string(lot)) = name;
}
auto& nameSort = info->PushDebug("Sorted by Name");
for (const auto& [name, lot] : nameToLot) {
auto& entry = nameSort.PushDebug<AMFStringValue>(name) = std::to_string(lot);
}
GameMessages::SendUIMessageServerToSingleClient("ToggleObjectDebugger", response, sysAddr);
}
}
void Spawn(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
@@ -771,7 +802,7 @@ namespace DEVGMCommands {
info.spawner = nullptr;
info.spawnerID = entity->GetObjectID();
info.spawnerNodeID = 0;
info.settings = { new LDFData<bool>(u"SpawnedFromSlashCommand", true) };
info.settings.Insert<bool>(u"SpawnedFromSlashCommand", true);
Entity* newEntity = Game::entityManager->CreateEntity(info, nullptr);
@@ -794,7 +825,7 @@ namespace DEVGMCommands {
}
const auto numberToSpawnOptional = GeneralUtils::TryParse<uint32_t>(splitArgs[1]);
if (!numberToSpawnOptional && numberToSpawnOptional.value() > 0) {
if (!numberToSpawnOptional) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid number of enemies to spawn.");
return;
}
@@ -802,7 +833,7 @@ namespace DEVGMCommands {
// Must spawn within a radius of at least 0.0f
const auto radiusToSpawnWithinOptional = GeneralUtils::TryParse<float>(splitArgs[2]);
if (!radiusToSpawnWithinOptional && radiusToSpawnWithinOptional.value() < 0.0f) {
if (!radiusToSpawnWithinOptional || radiusToSpawnWithinOptional.value() < 0.0f) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid radius to spawn within.");
return;
}
@@ -813,7 +844,7 @@ namespace DEVGMCommands {
info.spawner = nullptr;
info.spawnerID = entity->GetObjectID();
info.spawnerNodeID = 0;
info.settings = { new LDFData<bool>(u"SpawnedFromSlashCommand", true) };
info.settings.Insert(u"SpawnedFromSlashCommand", true);
auto playerPosition = entity->GetPosition();
while (numberToSpawn > 0) {
@@ -1020,7 +1051,8 @@ namespace DEVGMCommands {
ChatPackets::SendSystemMessage(sysAddr, u"Requesting map change...");
LWOCLONEID cloneId = 0;
bool force = false;
LWOINSTANCEID instanceID{};
std::string targetScene;
const auto reqZoneOptional = GeneralUtils::TryParse<LWOMAPID>(splitArgs[0]);
if (!reqZoneOptional) {
@@ -1030,29 +1062,34 @@ namespace DEVGMCommands {
const LWOMAPID reqZone = reqZoneOptional.value();
if (splitArgs.size() > 1) {
auto index = 1;
if (splitArgs[index] == "force") {
index++;
force = true;
const auto cloneIdOptional = GeneralUtils::TryParse<LWOCLONEID>(splitArgs[1]);
if (!cloneIdOptional) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid clone id.");
return;
}
if (splitArgs.size() > index) {
const auto cloneIdOptional = GeneralUtils::TryParse<LWOCLONEID>(splitArgs[index]);
if (!cloneIdOptional) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid clone id.");
cloneId = cloneIdOptional.value();
if (splitArgs.size() > 2) {
const auto instanceIDVal = GeneralUtils::TryParse<LWOINSTANCEID>(splitArgs[2]);
if (!instanceIDVal) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid instance id.");
return;
}
cloneId = cloneIdOptional.value();
instanceID = instanceIDVal.value();
}
if (splitArgs.size() > 3) {
targetScene = splitArgs[3];
}
}
const auto objid = entity->GetObjectID();
if (force || Game::zoneManager->CheckIfAccessibleZone(reqZone)) { // to prevent tomfoolery
if (Game::zoneManager->CheckIfAccessibleZone(reqZone)) { // to prevent tomfoolery
ZoneInstanceManager::Instance()->RequestZoneTransfer(Game::server, reqZone, cloneId, false, [objid](bool mythranShift, uint32_t zoneID, uint32_t zoneInstance, uint32_t zoneClone, std::string serverIP, uint16_t serverPort) {
ZoneInstanceManager::Instance()->RequestZoneTransfer(Game::server, reqZone, cloneId, false, [objid, targetScene](bool mythranShift, uint32_t zoneID, uint32_t zoneInstance, uint32_t zoneClone, std::string serverIP, uint16_t serverPort) {
auto* entity = Game::entityManager->GetEntity(objid);
if (!entity) return;
@@ -1070,6 +1107,7 @@ namespace DEVGMCommands {
entity->GetCharacter()->SetZoneID(zoneID);
entity->GetCharacter()->SetZoneInstance(zoneInstance);
entity->GetCharacter()->SetZoneClone(zoneClone);
entity->GetCharacter()->SetTargetScene(targetScene);
entity->GetComponent<CharacterComponent>()->SetLastRocketConfig(u"");
}
@@ -1102,6 +1140,10 @@ namespace DEVGMCommands {
}
const auto& password = splitArgs[2];
if (password.length() >= 50) {
ChatPackets::SendSystemMessage(sysAddr, u"Password is too long.");
return;
}
ZoneInstanceManager::Instance()->CreatePrivateZone(Game::server, zone.value(), clone.value(), password);
@@ -1233,10 +1275,10 @@ namespace DEVGMCommands {
auto* inventoryComponent = entity->GetComponent<InventoryComponent>();
if (!inventoryComponent) return;
std::vector<LDFBaseData*> data{};
data.push_back(new LDFData<int32_t>(u"reforgedLOT", reforgedItem.value()));
LwoNameValue config;
config.Insert<LOT>(u"reforgedLOT", reforgedItem.value());
inventoryComponent->AddItem(baseItem.value(), 1, eLootSourceType::MODERATION, eInventoryType::INVALID, data);
inventoryComponent->AddItem(baseItem.value(), 1, eLootSourceType::MODERATION, eInventoryType::INVALID, config);
}
void Crash(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
@@ -1247,38 +1289,26 @@ namespace DEVGMCommands {
}
void Metrics(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
AMFArrayValue response;
response.Insert("visible", true);
response.Insert("objectID", "Metrics");
response.Insert("serverInfo", true);
auto* info = response.InsertArray("data");
for (const auto variable : Metrics::GetAllMetrics()) {
auto* metric = Metrics::GetMetric(variable);
auto& metricData = info->PushDebug(Metrics::MetricVariableToString(variable));
if (metric == nullptr) {
continue;
}
const auto& metric = Metrics::GetMetric(variable);
ChatPackets::SendSystemMessage(
sysAddr,
GeneralUtils::ASCIIToUTF16(Metrics::MetricVariableToString(variable)) +
u": " +
GeneralUtils::to_u16string(Metrics::ToMiliseconds(metric->average)) +
u"ms"
);
metricData.PushDebug<AMFStringValue>("Maximum") = std::to_string(Metrics::ToMiliseconds(metric.max)) + "ms";
metricData.PushDebug<AMFStringValue>("Minimum") = std::to_string(Metrics::ToMiliseconds(metric.min)) + "ms";
metricData.PushDebug<AMFStringValue>("Average") = std::to_string(Metrics::ToMiliseconds(metric.average)) + "ms";
metricData.PushDebug<AMFStringValue>("Measurements Count") = std::to_string(metric.measurementSize);
}
ChatPackets::SendSystemMessage(
sysAddr,
u"Peak RSS: " + GeneralUtils::to_u16string(static_cast<float>(static_cast<double>(Metrics::GetPeakRSS()) / 1.024e6)) +
u"MB"
);
ChatPackets::SendSystemMessage(
sysAddr,
u"Current RSS: " + GeneralUtils::to_u16string(static_cast<float>(static_cast<double>(Metrics::GetCurrentRSS()) / 1.024e6)) +
u"MB"
);
ChatPackets::SendSystemMessage(
sysAddr,
u"Process ID: " + GeneralUtils::to_u16string(Metrics::GetProcessID())
);
auto& processInfo = info->PushDebug("Process Info");
processInfo.PushDebug<AMFStringValue>("Peak RSS") = std::to_string(static_cast<double>(Metrics::GetPeakRSS()) / 1.024e6) + "MB";
processInfo.PushDebug<AMFStringValue>("Current RSS") = std::to_string(static_cast<double>(Metrics::GetCurrentRSS()) / 1.024e6) + "MB";
processInfo.PushDebug<AMFIntValue>("Process ID") = Metrics::GetProcessID();
GameMessages::SendUIMessageServerToSingleClient("ToggleObjectDebugger", response, sysAddr);
}
void ReloadConfig(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
@@ -1310,19 +1340,30 @@ namespace DEVGMCommands {
const auto loops = GeneralUtils::TryParse<uint32_t>(splitArgs[2]);
if (!loops) return;
auto* const lootMatrixTable = CDClientManager::GetTable<CDLootMatrixTable>();
auto* const lootTableTable = CDClientManager::GetTable<CDLootTableTable>();
bool found = false;
for (const auto& entry : lootMatrixTable->GetMatrix(lootMatrixIndex.value())) {
for (const auto& loot : lootTableTable->GetTable(entry.LootTableIndex)) {
found = targetLot.value() == loot.itemid;
if (found) break;
}
}
if (!found) {
std::stringstream ss;
ss << "Target LOT " << targetLot.value() << " not found in loot matrix " << lootMatrixIndex.value() << ".";
ChatPackets::SendSystemMessage(sysAddr, ss.str());
return;
}
uint64_t totalRuns = 0;
for (uint32_t i = 0; i < loops; i++) {
while (true) {
const auto lootRoll = Loot::RollLootMatrix(nullptr, lootMatrixIndex.value());
totalRuns += 1;
bool doBreak = false;
for (const auto& kv : lootRoll) {
if (static_cast<uint32_t>(kv.first) == targetLot) {
doBreak = true;
}
}
if (doBreak) break;
if (lootRoll.contains(targetLot.value())) break;
}
}
@@ -1479,7 +1520,15 @@ namespace DEVGMCommands {
void Inspect(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
const auto splitArgs = GeneralUtils::SplitString(args, ' ');
if (splitArgs.empty()) return;
const auto idParsed = GeneralUtils::TryParse<LWOOBJID>(splitArgs[0]);
std::optional<LWOOBJID> idIntermed;
if (splitArgs[0] == "zoneControl") {
idIntermed = 0x3FFF'FFFFFFFE;
} else if (splitArgs[0] == "localCharacter") {
idIntermed = entity->GetObjectID();
} else {
idIntermed = GeneralUtils::TryParse<LWOOBJID>(splitArgs[0]);
}
const auto idParsed = idIntermed;
// First try to get the object by its ID if provided.
// Second try to get the object by player name.

View File

@@ -275,7 +275,8 @@ namespace GMGreaterThanZeroCommands {
}
void GmInvis(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
GameMessages::SendToggleGMInvis(entity->GetObjectID(), true, UNASSIGNED_SYSTEM_ADDRESS);
GameMessages::ToggleGMInvis msg;
msg.Send(entity->GetObjectID());
}
void SetName(Entity* entity, const SystemAddress& sysAddr, const std::string args) {

View File

@@ -187,8 +187,13 @@ namespace GMZeroCommands {
auto splitArgs = GeneralUtils::SplitString(args, ' ');
if (splitArgs.empty()) return;
ChatPackets::SendSystemMessage(sysAddr, u"Requesting private map...");
const auto& password = splitArgs[0];
if (password.length() >= 50) {
ChatPackets::SendSystemMessage(sysAddr, u"Password is too long.");
return;
}
ChatPackets::SendSystemMessage(sysAddr, u"Requesting private map...");
ZoneInstanceManager::Instance()->RequestPrivateZone(Game::server, false, password, [=](bool mythranShift, uint32_t zoneID, uint32_t zoneInstance, uint32_t zoneClone, std::string serverIP, uint16_t serverPort) {
LOG("Transferring %s to Zone %i (Instance %i | Clone %i | Mythran Shift: %s) with IP %s and Port %i", sysAddr.ToString(), zoneID, zoneInstance, zoneClone, mythranShift == true ? "true" : "false", serverIP.c_str(), serverPort);

View File

@@ -45,10 +45,8 @@ void VanityUtilities::SpawnVanity() {
info.pos = { 259.5f, 246.4f, -705.2f };
info.rot = { 0.0f, 0.0f, 1.0f, 0.0f };
info.spawnerID = Game::entityManager->GetZoneControlEntity()->GetObjectID();
info.settings = {
new LDFData<bool>(u"hasCustomText", true),
new LDFData<std::string>(u"customText", ParseMarkdown((BinaryPathFinder::GetBinaryDir() / "vanity/TESTAMENT.md").string()))
};
info.settings.Insert<bool>(u"hasCustomText", true);
info.settings.Insert<std::string>(u"customText", ParseMarkdown((BinaryPathFinder::GetBinaryDir() / "vanity/TESTAMENT.md").string()));
auto* entity = Game::entityManager->CreateEntity(info);
Game::entityManager->ConstructEntity(entity);
@@ -231,7 +229,7 @@ void ParseXml(const std::string& file) {
auto* configElement = object->FirstChildElement("config");
std::vector<std::u16string> keys = {};
std::vector<LDFBaseData*> config = {};
LwoNameValue config;
if (configElement) {
for (auto* key = configElement->FirstChildElement("key"); key != nullptr;
key = key->NextSiblingElement("key")) {
@@ -239,16 +237,16 @@ void ParseXml(const std::string& file) {
auto* data = key->GetText();
if (!data) continue;
LDFBaseData* configData = LDFBaseData::DataFromString(data);
const auto& configData = config.ParseInsert(data);
if (configData->GetKey() == u"useLocationsAsRandomSpawnPoint" && configData->GetValueType() == eLDFType::LDF_TYPE_BOOLEAN) {
useLocationsAsRandomSpawnPoint = static_cast<bool>(configData);
useLocationsAsRandomSpawnPoint = static_cast<const LDFData<bool>*>(configData.get())->GetValue();
config.Erase(u"useLocationsAsRandomSpawnPoint");
continue;
}
keys.push_back(configData->GetKey());
config.push_back(configData);
}
}
if (!keys.empty()) config.push_back(new LDFData<std::vector<std::u16string>>(u"syncLDF", keys));
if (!keys.empty()) config.Insert<std::vector<std::u16string>>(u"syncLDF", keys);
VanityObject objectData{
.m_Name = name,

View File

@@ -19,7 +19,7 @@ struct VanityObject {
std::vector<LOT> m_Equipment;
std::vector<std::string> m_Phrases;
std::map<uint32_t, std::vector<VanityObjectLocation>> m_Locations;
std::vector<LDFBaseData*> m_Config;
LwoNameValue m_Config;
};