mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2025-08-09 20:24:16 +00:00
Merge branch 'main' into moreMovementAi
This commit is contained in:
@@ -4,4 +4,6 @@ set(DZONEMANAGER_SOURCES "dZoneManager.cpp"
|
||||
"Zone.cpp")
|
||||
|
||||
add_library(dZoneManager STATIC ${DZONEMANAGER_SOURCES})
|
||||
target_link_libraries(dZoneManager dPhysics)
|
||||
target_link_libraries(dZoneManager
|
||||
PUBLIC dPhysics
|
||||
INTERFACE dWorldServer)
|
||||
|
@@ -5,7 +5,7 @@
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include "BinaryIO.h"
|
||||
#include "dLogger.h"
|
||||
#include "Logger.h"
|
||||
#include "Spawner.h"
|
||||
#include "dZoneManager.h"
|
||||
#include "GeneralUtils.h"
|
||||
@@ -14,36 +14,90 @@
|
||||
#include "CDFeatureGatingTable.h"
|
||||
#include "CDClientManager.h"
|
||||
#include "AssetManager.h"
|
||||
#include "ClientVersion.h"
|
||||
#include "dConfig.h"
|
||||
|
||||
Level::Level(Zone* parentZone, const std::string& filepath) {
|
||||
m_ParentZone = parentZone;
|
||||
|
||||
auto buffer = Game::assetManager->GetFileAsBuffer(filepath.c_str());
|
||||
auto stream = Game::assetManager->GetFile(filepath.c_str());
|
||||
|
||||
if (!buffer.m_Success) {
|
||||
Game::logger->Log("Level", "Failed to load %s", filepath.c_str());
|
||||
if (!stream) {
|
||||
LOG("Failed to load %s", filepath.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
std::istream file(&buffer);
|
||||
ReadChunks(file);
|
||||
|
||||
buffer.close();
|
||||
|
||||
ReadChunks(stream);
|
||||
}
|
||||
|
||||
Level::~Level() {
|
||||
for (std::map<uint32_t, Header>::iterator it = m_ChunkHeaders.begin(); it != m_ChunkHeaders.end(); ++it) {
|
||||
if (it->second.id == Level::ChunkTypeID::FileInfo) delete it->second.fileInfo;
|
||||
if (it->second.id == Level::ChunkTypeID::SceneObjectData) delete it->second.sceneObjects;
|
||||
}
|
||||
}
|
||||
void Level::MakeSpawner(SceneObject obj) {
|
||||
SpawnerInfo spawnInfo = SpawnerInfo();
|
||||
SpawnerNode* node = new SpawnerNode();
|
||||
spawnInfo.templateID = obj.lot;
|
||||
spawnInfo.spawnerID = obj.id;
|
||||
spawnInfo.templateScale = obj.scale;
|
||||
node->position = obj.position;
|
||||
node->rotation = obj.rotation;
|
||||
node->config = obj.settings;
|
||||
spawnInfo.nodes.push_back(node);
|
||||
for (LDFBaseData* data : obj.settings) {
|
||||
if (!data) continue;
|
||||
if (data->GetKey() == u"spawntemplate") {
|
||||
spawnInfo.templateID = std::stoi(data->GetValueAsString());
|
||||
}
|
||||
|
||||
const void Level::PrintAllObjects() {
|
||||
for (std::map<uint32_t, Header>::iterator it = m_ChunkHeaders.begin(); it != m_ChunkHeaders.end(); ++it) {
|
||||
if (it->second.id == Level::ChunkTypeID::SceneObjectData) {
|
||||
it->second.sceneObjects->PrintAllObjects();
|
||||
if (data->GetKey() == u"spawner_node_id") {
|
||||
node->nodeID = std::stoi(data->GetValueAsString());
|
||||
}
|
||||
|
||||
if (data->GetKey() == u"spawner_name") {
|
||||
spawnInfo.name = data->GetValueAsString();
|
||||
}
|
||||
|
||||
if (data->GetKey() == u"max_to_spawn") {
|
||||
spawnInfo.maxToSpawn = std::stoi(data->GetValueAsString());
|
||||
}
|
||||
|
||||
if (data->GetKey() == u"spawner_active_on_load") {
|
||||
spawnInfo.activeOnLoad = std::stoi(data->GetValueAsString());
|
||||
}
|
||||
|
||||
if (data->GetKey() == u"active_on_load") {
|
||||
spawnInfo.activeOnLoad = std::stoi(data->GetValueAsString());
|
||||
}
|
||||
|
||||
if (data->GetKey() == u"respawn") {
|
||||
if (data->GetValueType() == eLDFType::LDF_TYPE_FLOAT) // Floats are in seconds
|
||||
{
|
||||
spawnInfo.respawnTime = std::stof(data->GetValueAsString());
|
||||
} else if (data->GetValueType() == eLDFType::LDF_TYPE_U32) // Ints are in ms?
|
||||
{
|
||||
spawnInfo.respawnTime = std::stoul(data->GetValueAsString()) / 1000;
|
||||
}
|
||||
}
|
||||
if (data->GetKey() == u"spawnsGroupOnSmash") {
|
||||
spawnInfo.spawnsOnSmash = std::stoi(data->GetValueAsString());
|
||||
}
|
||||
if (data->GetKey() == u"spawnNetNameForSpawnGroupOnSmash") {
|
||||
spawnInfo.spawnOnSmashGroupName = data->GetValueAsString();
|
||||
}
|
||||
if (data->GetKey() == u"groupID") { // Load object groups
|
||||
std::string groupStr = data->GetValueAsString();
|
||||
spawnInfo.groups = GeneralUtils::SplitString(groupStr, ';');
|
||||
if (spawnInfo.groups.back().empty()) spawnInfo.groups.erase(spawnInfo.groups.end() - 1);
|
||||
}
|
||||
if (data->GetKey() == u"no_auto_spawn") {
|
||||
spawnInfo.noAutoSpawn = static_cast<LDFData<bool>*>(data)->GetValue();
|
||||
}
|
||||
if (data->GetKey() == u"no_timed_spawn") {
|
||||
spawnInfo.noTimedSpawn = static_cast<LDFData<bool>*>(data)->GetValue();
|
||||
}
|
||||
if (data->GetKey() == u"spawnActivator") {
|
||||
spawnInfo.spawnActivator = static_cast<LDFData<bool>*>(data)->GetValue();
|
||||
}
|
||||
}
|
||||
|
||||
Game::zoneManager->MakeSpawner(spawnInfo);
|
||||
}
|
||||
|
||||
void Level::ReadChunks(std::istream& file) {
|
||||
@@ -78,11 +132,10 @@ void Level::ReadChunks(std::istream& file) {
|
||||
file.seekg(0);
|
||||
Header header;
|
||||
header.id = ChunkTypeID::FileInfo; //I guess?
|
||||
FileInfoChunk* fileInfo = new FileInfoChunk();
|
||||
BinaryIO::BinaryRead(file, header.chunkVersion);
|
||||
BinaryIO::BinaryRead(file, header.chunkType);
|
||||
file.ignore(1);
|
||||
BinaryIO::BinaryRead(file, fileInfo->revision);
|
||||
BinaryIO::BinaryRead(file, header.fileInfo.revision);
|
||||
|
||||
if (header.chunkVersion >= 45) file.ignore(4);
|
||||
file.ignore(4 * (4 * 3));
|
||||
@@ -95,9 +148,7 @@ void Level::ReadChunks(std::istream& file) {
|
||||
uint32_t s = 0;
|
||||
BinaryIO::BinaryRead(file, s);
|
||||
for (uint32_t i = 0; i < s; ++i) {
|
||||
file.ignore(4); //a uint
|
||||
file.ignore(4); //two floats
|
||||
file.ignore(4);
|
||||
file.ignore(4 * 3); //a uint and two floats
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -131,7 +182,6 @@ void Level::ReadChunks(std::istream& file) {
|
||||
BinaryIO::BinaryRead(file, count);
|
||||
file.ignore(count * 12);
|
||||
|
||||
header.fileInfo = fileInfo;
|
||||
m_ChunkHeaders.insert(std::make_pair(header.id, header));
|
||||
|
||||
//Now pretend to be a normal file and read Objects chunk:
|
||||
@@ -145,36 +195,46 @@ void Level::ReadChunks(std::istream& file) {
|
||||
}
|
||||
|
||||
void Level::ReadFileInfoChunk(std::istream& file, Header& header) {
|
||||
FileInfoChunk* fi = new FileInfoChunk;
|
||||
BinaryIO::BinaryRead(file, fi->version);
|
||||
BinaryIO::BinaryRead(file, fi->revision);
|
||||
BinaryIO::BinaryRead(file, fi->enviromentChunkStart);
|
||||
BinaryIO::BinaryRead(file, fi->objectChunkStart);
|
||||
BinaryIO::BinaryRead(file, fi->particleChunkStart);
|
||||
header.fileInfo = fi;
|
||||
BinaryIO::BinaryRead(file, header.fileInfo.version);
|
||||
BinaryIO::BinaryRead(file, header.fileInfo.revision);
|
||||
BinaryIO::BinaryRead(file, header.fileInfo.enviromentChunkStart);
|
||||
BinaryIO::BinaryRead(file, header.fileInfo.objectChunkStart);
|
||||
BinaryIO::BinaryRead(file, header.fileInfo.particleChunkStart);
|
||||
|
||||
//PATCH FOR AG: (messed up file?)
|
||||
if (header.fileInfo->revision == 3452816845 && m_ParentZone->GetZoneID().GetMapID() == 1100) header.fileInfo->revision = 26;
|
||||
if (header.fileInfo.revision == 0xCDCDCDCD && m_ParentZone->GetZoneID().GetMapID() == 1100) header.fileInfo.revision = 26;
|
||||
}
|
||||
|
||||
void Level::ReadSceneObjectDataChunk(std::istream& file, Header& header) {
|
||||
SceneObjectDataChunk* chunk = new SceneObjectDataChunk;
|
||||
uint32_t objectsCount = 0;
|
||||
BinaryIO::BinaryRead(file, objectsCount);
|
||||
|
||||
CDFeatureGatingTable* featureGatingTable = CDClientManager::Instance().GetTable<CDFeatureGatingTable>();
|
||||
CDFeatureGatingTable* featureGatingTable = CDClientManager::GetTable<CDFeatureGatingTable>();
|
||||
|
||||
CDFeatureGating gating;
|
||||
gating.major =
|
||||
GeneralUtils::TryParse<int32_t>(Game::config->GetValue("version_major")).value_or(ClientVersion::major);
|
||||
gating.current =
|
||||
GeneralUtils::TryParse<int32_t>(Game::config->GetValue("version_current")).value_or(ClientVersion::current);
|
||||
gating.minor =
|
||||
GeneralUtils::TryParse<int32_t>(Game::config->GetValue("version_minor")).value_or(ClientVersion::minor);
|
||||
|
||||
const auto zoneControlObject = Game::zoneManager->GetZoneControlObject();
|
||||
DluAssert(zoneControlObject != nullptr);
|
||||
for (uint32_t i = 0; i < objectsCount; ++i) {
|
||||
std::u16string ldfString;
|
||||
SceneObject obj;
|
||||
BinaryIO::BinaryRead(file, obj.id);
|
||||
BinaryIO::BinaryRead(file, obj.lot);
|
||||
|
||||
/*if (header.fileInfo->version >= 0x26)*/ BinaryIO::BinaryRead(file, obj.value1);
|
||||
/*if (header.fileInfo->version >= 0x20)*/ BinaryIO::BinaryRead(file, obj.value2);
|
||||
/*if (header.fileInfo->version >= 0x26)*/ BinaryIO::BinaryRead(file, obj.nodeType);
|
||||
/*if (header.fileInfo->version >= 0x20)*/ BinaryIO::BinaryRead(file, obj.glomId);
|
||||
|
||||
BinaryIO::BinaryRead(file, obj.position);
|
||||
BinaryIO::BinaryRead(file, obj.rotation);
|
||||
BinaryIO::BinaryRead(file, obj.scale);
|
||||
BinaryIO::ReadString<uint32_t>(file, ldfString);
|
||||
BinaryIO::BinaryRead(file, obj.value3);
|
||||
|
||||
//This is a little bit of a bodge, but because the alpha client (HF) doesn't store the
|
||||
//spawn position / rotation like the later versions do, we need to check the LOT for the spawn pos & set it.
|
||||
@@ -183,16 +243,6 @@ void Level::ReadSceneObjectDataChunk(std::istream& file, Header& header) {
|
||||
Game::zoneManager->GetZone()->SetSpawnRot(obj.rotation);
|
||||
}
|
||||
|
||||
std::u16string ldfString = u"";
|
||||
uint32_t length = 0;
|
||||
BinaryIO::BinaryRead(file, length);
|
||||
|
||||
for (uint32_t i = 0; i < length; ++i) {
|
||||
uint16_t data;
|
||||
BinaryIO::BinaryRead(file, data);
|
||||
ldfString.push_back(data);
|
||||
}
|
||||
|
||||
std::string sData = GeneralUtils::UTF16ToWTF8(ldfString);
|
||||
std::stringstream ssData(sData);
|
||||
std::string token;
|
||||
@@ -203,101 +253,45 @@ void Level::ReadSceneObjectDataChunk(std::istream& file, Header& header) {
|
||||
obj.settings.push_back(ldfData);
|
||||
}
|
||||
|
||||
BinaryIO::BinaryRead(file, obj.value3);
|
||||
|
||||
// Feature gating
|
||||
bool gated = false;
|
||||
// We should never have more than 1 zone control object
|
||||
bool skipLoadingObject = obj.lot == zoneControlObject->GetLOT();
|
||||
for (LDFBaseData* data : obj.settings) {
|
||||
if (!data) continue;
|
||||
if (data->GetKey() == u"gatingOnFeature") {
|
||||
std::string featureGate = data->GetValueAsString();
|
||||
|
||||
if (!featureGatingTable->FeatureUnlocked(featureGate)) {
|
||||
gated = true;
|
||||
|
||||
gating.featureName = data->GetValueAsString();
|
||||
if (gating.featureName == Game::config->GetValue("event_1")) continue;
|
||||
else if (gating.featureName == Game::config->GetValue("event_2")) continue;
|
||||
else if (gating.featureName == Game::config->GetValue("event_3")) continue;
|
||||
else if (gating.featureName == Game::config->GetValue("event_4")) continue;
|
||||
else if (gating.featureName == Game::config->GetValue("event_5")) continue;
|
||||
else if (gating.featureName == Game::config->GetValue("event_6")) continue;
|
||||
else if (gating.featureName == Game::config->GetValue("event_7")) continue;
|
||||
else if (gating.featureName == Game::config->GetValue("event_8")) continue;
|
||||
else if (!featureGatingTable->FeatureUnlocked(gating)) {
|
||||
// The feature is not unlocked, so we can skip loading this object
|
||||
skipLoadingObject = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If this is a client only object, we can skip loading it
|
||||
if (data->GetKey() == u"loadOnClientOnly") {
|
||||
skipLoadingObject |= static_cast<bool>(std::stoi(data->GetValueAsString()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (gated) {
|
||||
if (skipLoadingObject) {
|
||||
for (auto* setting : obj.settings) {
|
||||
delete setting;
|
||||
setting = nullptr;
|
||||
}
|
||||
|
||||
obj.settings.clear();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (obj.lot == 176) { //Spawner
|
||||
SpawnerInfo spawnInfo = SpawnerInfo();
|
||||
SpawnerNode* node = new SpawnerNode();
|
||||
spawnInfo.templateID = obj.lot;
|
||||
spawnInfo.spawnerID = obj.id;
|
||||
spawnInfo.templateScale = obj.scale;
|
||||
node->position = obj.position;
|
||||
node->rotation = obj.rotation;
|
||||
node->config = obj.settings;
|
||||
spawnInfo.nodes.push_back(node);
|
||||
for (LDFBaseData* data : obj.settings) {
|
||||
if (data) {
|
||||
if (data->GetKey() == u"spawntemplate") {
|
||||
spawnInfo.templateID = std::stoi(data->GetValueAsString());
|
||||
}
|
||||
|
||||
if (data->GetKey() == u"spawner_node_id") {
|
||||
node->nodeID = std::stoi(data->GetValueAsString());
|
||||
}
|
||||
|
||||
if (data->GetKey() == u"spawner_name") {
|
||||
spawnInfo.name = data->GetValueAsString();
|
||||
}
|
||||
|
||||
if (data->GetKey() == u"max_to_spawn") {
|
||||
spawnInfo.maxToSpawn = std::stoi(data->GetValueAsString());
|
||||
}
|
||||
|
||||
if (data->GetKey() == u"spawner_active_on_load") {
|
||||
spawnInfo.activeOnLoad = std::stoi(data->GetValueAsString());
|
||||
}
|
||||
|
||||
if (data->GetKey() == u"active_on_load") {
|
||||
spawnInfo.activeOnLoad = std::stoi(data->GetValueAsString());
|
||||
}
|
||||
|
||||
if (data->GetKey() == u"respawn") {
|
||||
if (data->GetValueType() == eLDFType::LDF_TYPE_FLOAT) // Floats are in seconds
|
||||
{
|
||||
spawnInfo.respawnTime = std::stof(data->GetValueAsString());
|
||||
} else if (data->GetValueType() == eLDFType::LDF_TYPE_U32) // Ints are in ms?
|
||||
{
|
||||
spawnInfo.respawnTime = std::stoul(data->GetValueAsString()) / 1000;
|
||||
}
|
||||
}
|
||||
if (data->GetKey() == u"spawnsGroupOnSmash") {
|
||||
spawnInfo.spawnsOnSmash = std::stoi(data->GetValueAsString());
|
||||
}
|
||||
if (data->GetKey() == u"spawnNetNameForSpawnGroupOnSmash") {
|
||||
spawnInfo.spawnOnSmashGroupName = data->GetValueAsString();
|
||||
}
|
||||
if (data->GetKey() == u"groupID") { // Load object groups
|
||||
std::string groupStr = data->GetValueAsString();
|
||||
spawnInfo.groups = GeneralUtils::SplitString(groupStr, ';');
|
||||
spawnInfo.groups.erase(spawnInfo.groups.end() - 1);
|
||||
}
|
||||
if (data->GetKey() == u"no_auto_spawn") {
|
||||
spawnInfo.noAutoSpawn = static_cast<LDFData<bool>*>(data)->GetValue();
|
||||
}
|
||||
if (data->GetKey() == u"no_timed_spawn") {
|
||||
spawnInfo.noTimedSpawn = static_cast<LDFData<bool>*>(data)->GetValue();
|
||||
}
|
||||
if (data->GetKey() == u"spawnActivator") {
|
||||
spawnInfo.spawnActivator = static_cast<LDFData<bool>*>(data)->GetValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
Spawner* spawner = new Spawner(spawnInfo);
|
||||
Game::zoneManager->AddSpawner(obj.id, spawner);
|
||||
MakeSpawner(obj);
|
||||
} else { //Regular object
|
||||
EntityInfo info;
|
||||
info.spawnerID = 0;
|
||||
@@ -307,45 +301,7 @@ void Level::ReadSceneObjectDataChunk(std::istream& file, Header& header) {
|
||||
info.rot = obj.rotation;
|
||||
info.settings = obj.settings;
|
||||
info.scale = obj.scale;
|
||||
|
||||
//Check to see if we shouldn't be loading this:
|
||||
bool clientOnly = false;
|
||||
bool serverOnly = false;
|
||||
std::string featureGate = "";
|
||||
for (LDFBaseData* data : obj.settings) {
|
||||
if (data) {
|
||||
if (data->GetKey() == u"loadOnClientOnly") {
|
||||
clientOnly = (bool)std::stoi(data->GetValueAsString());
|
||||
break;
|
||||
}
|
||||
if (data->GetKey() == u"loadSrvrOnly") {
|
||||
serverOnly = (bool)std::stoi(data->GetValueAsString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!clientOnly) {
|
||||
|
||||
// We should never have more than 1 zone control object
|
||||
const auto zoneControlObject = Game::zoneManager->GetZoneControlObject();
|
||||
if (zoneControlObject != nullptr && info.lot == zoneControlObject->GetLOT())
|
||||
goto deleteSettings;
|
||||
|
||||
Game::entityManager->CreateEntity(info, nullptr);
|
||||
} else {
|
||||
deleteSettings:
|
||||
|
||||
for (auto* setting : info.settings) {
|
||||
delete setting;
|
||||
setting = nullptr;
|
||||
}
|
||||
|
||||
info.settings.clear();
|
||||
obj.settings.clear();
|
||||
}
|
||||
Game::entityManager->CreateEntity(info);
|
||||
}
|
||||
}
|
||||
|
||||
header.sceneObjects = chunk;
|
||||
}
|
||||
|
@@ -27,34 +27,20 @@ public:
|
||||
uint32_t particleChunkStart;
|
||||
};
|
||||
|
||||
struct SceneObjectDataChunk {
|
||||
std::map<LWOOBJID, SceneObject> objects;
|
||||
|
||||
const void PrintAllObjects() {
|
||||
for (std::map<LWOOBJID, SceneObject>::iterator it = objects.begin(); it != objects.end(); ++it) {
|
||||
std::cout << "\t ID: " << it->first << " LOT: " << it->second.lot << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t GetObjectCount() { return objects.size(); }
|
||||
};
|
||||
|
||||
struct Header {
|
||||
uint32_t id;
|
||||
uint16_t chunkVersion;
|
||||
ChunkTypeID chunkType;
|
||||
uint32_t size;
|
||||
uint32_t startPosition;
|
||||
FileInfoChunk* fileInfo;
|
||||
SceneObjectDataChunk* sceneObjects;
|
||||
FileInfoChunk fileInfo;
|
||||
LWOSCENEID lwoSceneID;
|
||||
};
|
||||
|
||||
public:
|
||||
Level(Zone* parentZone, const std::string& filepath);
|
||||
~Level();
|
||||
|
||||
const void PrintAllObjects();
|
||||
|
||||
static void MakeSpawner(SceneObject obj);
|
||||
|
||||
std::map<uint32_t, Header> m_ChunkHeaders;
|
||||
private:
|
||||
|
@@ -1,6 +1,6 @@
|
||||
#include "Spawner.h"
|
||||
#include "EntityManager.h"
|
||||
#include "dLogger.h"
|
||||
#include "Logger.h"
|
||||
#include "Game.h"
|
||||
#include <sstream>
|
||||
#include <functional>
|
||||
@@ -25,16 +25,6 @@ Spawner::Spawner(const SpawnerInfo info) {
|
||||
|
||||
m_Start = m_Info.noTimedSpawn;
|
||||
|
||||
//ssssh...
|
||||
if (m_EntityInfo.lot == 14718) { //AG - MAELSTROM SAMPLE
|
||||
m_Info.groups.emplace_back("MaelstromSamples");
|
||||
}
|
||||
|
||||
if (m_EntityInfo.lot == 14375) //AG - SPIDER BOSS EGG
|
||||
{
|
||||
m_Info.groups.emplace_back("EGG");
|
||||
}
|
||||
|
||||
int timerCount = m_Info.amountMaintained;
|
||||
|
||||
if (m_Info.amountMaintained > m_Info.nodes.size()) {
|
||||
@@ -51,20 +41,20 @@ Spawner::Spawner(const SpawnerInfo info) {
|
||||
std::vector<Spawner*> spawnSmashSpawnersN = Game::zoneManager->GetSpawnersByName(m_Info.spawnOnSmashGroupName);
|
||||
for (Entity* ssEntity : spawnSmashEntities) {
|
||||
m_SpawnSmashFoundGroup = true;
|
||||
ssEntity->AddDieCallback([=]() {
|
||||
ssEntity->AddDieCallback([=, this]() {
|
||||
Spawn();
|
||||
});
|
||||
}
|
||||
for (Spawner* ssSpawner : spawnSmashSpawners) {
|
||||
m_SpawnSmashFoundGroup = true;
|
||||
ssSpawner->AddSpawnedEntityDieCallback([=]() {
|
||||
ssSpawner->AddSpawnedEntityDieCallback([=, this]() {
|
||||
Spawn();
|
||||
});
|
||||
}
|
||||
for (Spawner* ssSpawner : spawnSmashSpawnersN) {
|
||||
m_SpawnSmashFoundGroup = true;
|
||||
m_SpawnOnSmash = ssSpawner;
|
||||
ssSpawner->AddSpawnedEntityDieCallback([=]() {
|
||||
ssSpawner->AddSpawnedEntityDieCallback([=, this]() {
|
||||
Spawn();
|
||||
});
|
||||
}
|
||||
@@ -205,6 +195,15 @@ void Spawner::Update(const float deltaTime) {
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<LWOOBJID> Spawner::GetSpawnedObjectIDs() const {
|
||||
std::vector<LWOOBJID> ids;
|
||||
ids.reserve(m_Entities.size());
|
||||
for (const auto& [objId, spawnerNode] : m_Entities) {
|
||||
ids.push_back(objId);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
void Spawner::NotifyOfEntityDeath(const LWOOBJID& objectID) {
|
||||
for (std::function<void()> cb : m_SpawnedEntityDieCallbacks) {
|
||||
cb();
|
||||
|
@@ -12,8 +12,8 @@
|
||||
#include "EntityInfo.h"
|
||||
|
||||
struct SpawnerNode {
|
||||
NiPoint3 position = NiPoint3::ZERO;
|
||||
NiQuaternion rotation = NiQuaternion::IDENTITY;
|
||||
NiPoint3 position = NiPoint3Constant::ZERO;
|
||||
NiQuaternion rotation = NiQuaternionConstant::IDENTITY;
|
||||
uint32_t nodeID = 0;
|
||||
uint32_t nodeMax = 1;
|
||||
std::vector<LWOOBJID> entities;
|
||||
@@ -66,6 +66,7 @@ public:
|
||||
void SetRespawnTime(float time);
|
||||
void SetNumToMaintain(int32_t value);
|
||||
bool GetIsSpawnSmashGroup() const { return m_SpawnSmashFoundGroup; };
|
||||
std::vector<LWOOBJID> GetSpawnedObjectIDs() const;
|
||||
|
||||
SpawnerInfo m_Info;
|
||||
bool m_Active = true;
|
||||
|
@@ -3,7 +3,7 @@
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include "Game.h"
|
||||
#include "dLogger.h"
|
||||
#include "Logger.h"
|
||||
#include "GeneralUtils.h"
|
||||
#include "BinaryIO.h"
|
||||
#include "LUTriggers.h"
|
||||
@@ -18,10 +18,10 @@
|
||||
#include "eTriggerCommandType.h"
|
||||
#include "eTriggerEventType.h"
|
||||
#include "eWaypointCommandType.h"
|
||||
#include "dNavMesh.h"
|
||||
|
||||
Zone::Zone(const LWOMAPID& mapID, const LWOINSTANCEID& instanceID, const LWOCLONEID& cloneID) :
|
||||
m_ZoneID(mapID, instanceID, cloneID) {
|
||||
m_NumberOfScenesLoaded = 0;
|
||||
m_NumberOfObjectsLoaded = 0;
|
||||
m_NumberOfSceneTransitionsLoaded = 0;
|
||||
m_CheckSum = 0;
|
||||
@@ -30,7 +30,7 @@ Zone::Zone(const LWOMAPID& mapID, const LWOINSTANCEID& instanceID, const LWOCLON
|
||||
}
|
||||
|
||||
Zone::~Zone() {
|
||||
Game::logger->Log("Zone", "Destroying zone %i", m_ZoneID.GetMapID());
|
||||
LOG("Destroying zone %i", m_ZoneID.GetMapID());
|
||||
for (std::map<LWOSCENEID, SceneRef>::iterator it = m_Scenes.begin(); it != m_Scenes.end(); ++it) {
|
||||
if (it->second.level != nullptr) delete it->second.level;
|
||||
}
|
||||
@@ -47,14 +47,13 @@ void Zone::LoadZoneIntoMemory() {
|
||||
m_ZonePath = m_ZoneFilePath.substr(0, m_ZoneFilePath.rfind('/') + 1);
|
||||
if (m_ZoneFilePath == "ERR") return;
|
||||
|
||||
AssetMemoryBuffer buffer = Game::assetManager->GetFileAsBuffer(m_ZoneFilePath.c_str());
|
||||
auto file = Game::assetManager->GetFile(m_ZoneFilePath.c_str());
|
||||
|
||||
if (!buffer.m_Success) {
|
||||
Game::logger->Log("Zone", "Failed to load %s", m_ZoneFilePath.c_str());
|
||||
if (!file) {
|
||||
LOG("Failed to load %s", m_ZoneFilePath.c_str());
|
||||
throw std::runtime_error("Aborting Zone loading due to no Zone File.");
|
||||
}
|
||||
|
||||
std::istream file(&buffer);
|
||||
if (file) {
|
||||
BinaryIO::BinaryRead(file, m_FileFormatVersion);
|
||||
|
||||
@@ -62,7 +61,7 @@ void Zone::LoadZoneIntoMemory() {
|
||||
if (m_FileFormatVersion >= Zone::FileFormatVersion::Alpha) BinaryIO::BinaryRead(file, mapRevision);
|
||||
|
||||
BinaryIO::BinaryRead(file, m_WorldID);
|
||||
if ((uint16_t)m_WorldID != m_ZoneID.GetMapID()) Game::logger->Log("Zone", "WorldID: %i doesn't match MapID %i! Is this intended?", m_WorldID, m_ZoneID.GetMapID());
|
||||
if (static_cast<LWOMAPID>(m_WorldID) != m_ZoneID.GetMapID()) LOG("WorldID: %i doesn't match MapID %i! Is this intended?", m_WorldID, m_ZoneID.GetMapID());
|
||||
|
||||
AddRevision(LWOSCENEID_INVALID, mapRevision);
|
||||
|
||||
@@ -82,18 +81,10 @@ void Zone::LoadZoneIntoMemory() {
|
||||
}
|
||||
|
||||
//Read generic zone info:
|
||||
uint8_t stringLength;
|
||||
BinaryIO::BinaryRead(file, stringLength);
|
||||
m_ZonePath = BinaryIO::ReadString(file, stringLength);
|
||||
|
||||
BinaryIO::BinaryRead(file, stringLength);
|
||||
m_ZoneRawPath = BinaryIO::ReadString(file, stringLength);
|
||||
|
||||
BinaryIO::BinaryRead(file, stringLength);
|
||||
m_ZoneName = BinaryIO::ReadString(file, stringLength);
|
||||
|
||||
BinaryIO::BinaryRead(file, stringLength);
|
||||
m_ZoneDesc = BinaryIO::ReadString(file, stringLength);
|
||||
BinaryIO::ReadString<uint8_t>(file, m_ZonePath, BinaryIO::ReadType::String);
|
||||
BinaryIO::ReadString<uint8_t>(file, m_ZoneRawPath, BinaryIO::ReadType::String);
|
||||
BinaryIO::ReadString<uint8_t>(file, m_ZoneName, BinaryIO::ReadType::String);
|
||||
BinaryIO::ReadString<uint8_t>(file, m_ZoneDesc, BinaryIO::ReadType::String);
|
||||
|
||||
if (m_FileFormatVersion >= Zone::FileFormatVersion::PreAlpha) {
|
||||
BinaryIO::BinaryRead(file, m_NumberOfSceneTransitionsLoaded);
|
||||
@@ -109,64 +100,62 @@ void Zone::LoadZoneIntoMemory() {
|
||||
uint32_t pathCount;
|
||||
BinaryIO::BinaryRead(file, pathCount);
|
||||
|
||||
m_Paths.reserve(pathCount);
|
||||
for (uint32_t i = 0; i < pathCount; ++i) LoadPath(file);
|
||||
|
||||
for (Path path : m_Paths) {
|
||||
if (path.pathType == PathType::Spawner) {
|
||||
SpawnerInfo info = SpawnerInfo();
|
||||
for (PathWaypoint waypoint : path.pathWaypoints) {
|
||||
SpawnerNode* node = new SpawnerNode();
|
||||
node->position = waypoint.position;
|
||||
node->rotation = waypoint.rotation;
|
||||
node->nodeID = 0;
|
||||
node->config = waypoint.config;
|
||||
if (path.pathType != PathType::Spawner) continue;
|
||||
SpawnerInfo info = SpawnerInfo();
|
||||
for (PathWaypoint waypoint : path.pathWaypoints) {
|
||||
SpawnerNode* node = new SpawnerNode();
|
||||
node->position = waypoint.position;
|
||||
node->rotation = waypoint.rotation;
|
||||
node->nodeID = 0;
|
||||
node->config = waypoint.config;
|
||||
|
||||
for (LDFBaseData* data : waypoint.config) {
|
||||
if (data) {
|
||||
if (data->GetKey() == u"spawner_node_id") {
|
||||
node->nodeID = std::stoi(data->GetValueAsString());
|
||||
} else if (data->GetKey() == u"spawner_max_per_node") {
|
||||
node->nodeMax = std::stoi(data->GetValueAsString());
|
||||
} else if (data->GetKey() == u"groupID") { // Load object group
|
||||
std::string groupStr = data->GetValueAsString();
|
||||
info.groups = GeneralUtils::SplitString(groupStr, ';');
|
||||
info.groups.erase(info.groups.end() - 1);
|
||||
} else if (data->GetKey() == u"grpNameQBShowBricks") {
|
||||
if (data->GetValueAsString() == "") continue;
|
||||
/*std::string groupStr = data->GetValueAsString();
|
||||
info.groups.push_back(groupStr);*/
|
||||
info.grpNameQBShowBricks = data->GetValueAsString();
|
||||
} else if (data->GetKey() == u"spawner_name") {
|
||||
info.name = data->GetValueAsString();
|
||||
}
|
||||
}
|
||||
for (LDFBaseData* data : waypoint.config) {
|
||||
if (!data) continue;
|
||||
|
||||
if (data->GetKey() == u"spawner_node_id") {
|
||||
node->nodeID = std::stoi(data->GetValueAsString());
|
||||
} else if (data->GetKey() == u"spawner_max_per_node") {
|
||||
node->nodeMax = std::stoi(data->GetValueAsString());
|
||||
} else if (data->GetKey() == u"groupID") { // Load object group
|
||||
std::string groupStr = data->GetValueAsString();
|
||||
info.groups = GeneralUtils::SplitString(groupStr, ';');
|
||||
if (info.groups.back().empty()) info.groups.erase(info.groups.end() - 1);
|
||||
} else if (data->GetKey() == u"grpNameQBShowBricks") {
|
||||
if (data->GetValueAsString().empty()) continue;
|
||||
/*std::string groupStr = data->GetValueAsString();
|
||||
info.groups.push_back(groupStr);*/
|
||||
info.grpNameQBShowBricks = data->GetValueAsString();
|
||||
} else if (data->GetKey() == u"spawner_name") {
|
||||
info.name = data->GetValueAsString();
|
||||
}
|
||||
info.nodes.push_back(node);
|
||||
}
|
||||
info.templateID = path.spawner.spawnedLOT;
|
||||
info.spawnerID = path.spawner.spawnerObjID;
|
||||
info.respawnTime = path.spawner.respawnTime;
|
||||
info.amountMaintained = path.spawner.amountMaintained;
|
||||
info.maxToSpawn = path.spawner.maxToSpawn;
|
||||
info.activeOnLoad = path.spawner.spawnerNetActive;
|
||||
info.isNetwork = true;
|
||||
Spawner* spawner = new Spawner(info);
|
||||
Game::zoneManager->AddSpawner(info.spawnerID, spawner);
|
||||
info.nodes.push_back(node);
|
||||
}
|
||||
|
||||
info.templateID = path.spawner.spawnedLOT;
|
||||
info.spawnerID = path.spawner.spawnerObjID;
|
||||
info.respawnTime = path.spawner.respawnTime;
|
||||
info.amountMaintained = path.spawner.amountMaintained;
|
||||
info.maxToSpawn = path.spawner.maxToSpawn;
|
||||
info.activeOnLoad = path.spawner.spawnerNetActive;
|
||||
info.isNetwork = true;
|
||||
Spawner* spawner = new Spawner(info);
|
||||
Game::zoneManager->AddSpawner(info.spawnerID, spawner);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Game::logger->Log("Zone", "Failed to open: %s", m_ZoneFilePath.c_str());
|
||||
LOG("Failed to open: %s", m_ZoneFilePath.c_str());
|
||||
}
|
||||
m_ZonePath = m_ZoneFilePath.substr(0, m_ZoneFilePath.rfind('/') + 1);
|
||||
|
||||
buffer.close();
|
||||
m_ZonePath = m_ZoneFilePath.substr(0, m_ZoneFilePath.rfind('/') + 1);
|
||||
}
|
||||
|
||||
std::string Zone::GetFilePathForZoneID() {
|
||||
//We're gonna go ahead and presume we've got the db loaded already:
|
||||
CDZoneTableTable* zoneTable = CDClientManager::Instance().GetTable<CDZoneTableTable>();
|
||||
CDZoneTableTable* zoneTable = CDClientManager::GetTable<CDZoneTableTable>();
|
||||
const CDZoneTable* zone = zoneTable->Query(this->GetZoneID().GetMapID());
|
||||
if (zone != nullptr) {
|
||||
std::string toReturn = "maps/" + zone->zoneName;
|
||||
@@ -181,16 +170,16 @@ std::string Zone::GetFilePathForZoneID() {
|
||||
uint32_t Zone::CalculateChecksum() {
|
||||
uint32_t sum1 = 0xffff, sum2 = 0xffff;
|
||||
|
||||
for (std::map<LWOSCENEID, uint32_t>::const_iterator it = m_MapRevisions.cbegin(); it != m_MapRevisions.cend(); ++it) {
|
||||
uint32_t sceneID = it->first.GetSceneID();
|
||||
for (const auto& [scene, sceneRevision] : m_MapRevisions) {
|
||||
uint32_t sceneID = scene.GetSceneID();
|
||||
sum2 += sum1 += (sceneID >> 16);
|
||||
sum2 += sum1 += (sceneID & 0xffff);
|
||||
|
||||
uint32_t layerID = it->first.GetLayerID();
|
||||
uint32_t layerID = scene.GetLayerID();
|
||||
sum2 += sum1 += (layerID >> 16);
|
||||
sum2 += sum1 += (layerID & 0xffff);
|
||||
|
||||
uint32_t revision = it->second;
|
||||
uint32_t revision = sceneRevision;
|
||||
sum2 += sum1 += (revision >> 16);
|
||||
sum2 += sum1 += (revision & 0xffff);
|
||||
}
|
||||
@@ -202,30 +191,20 @@ uint32_t Zone::CalculateChecksum() {
|
||||
}
|
||||
|
||||
void Zone::LoadLevelsIntoMemory() {
|
||||
for (std::map<LWOSCENEID, SceneRef>::iterator it = m_Scenes.begin(); it != m_Scenes.end(); ++it) {
|
||||
if (it->second.level == nullptr) {
|
||||
it->second.level = new Level(this, m_ZonePath + it->second.filename);
|
||||
for (auto& [sceneID, scene] : m_Scenes) {
|
||||
if (scene.level) continue;
|
||||
scene.level = new Level(this, m_ZonePath + scene.filename);
|
||||
|
||||
if (it->second.level->m_ChunkHeaders.size() > 0) {
|
||||
it->second.level->m_ChunkHeaders.begin()->second.lwoSceneID = it->first;
|
||||
AddRevision(it->second.level->m_ChunkHeaders.begin()->second.lwoSceneID, it->second.level->m_ChunkHeaders.begin()->second.fileInfo->revision);
|
||||
}
|
||||
}
|
||||
if (scene.level->m_ChunkHeaders.empty()) continue;
|
||||
|
||||
scene.level->m_ChunkHeaders.begin()->second.lwoSceneID = sceneID;
|
||||
AddRevision(scene.level->m_ChunkHeaders.begin()->second.lwoSceneID, scene.level->m_ChunkHeaders.begin()->second.fileInfo.revision);
|
||||
}
|
||||
}
|
||||
|
||||
void Zone::AddRevision(LWOSCENEID sceneID, uint32_t revision) {
|
||||
for (std::pair<LWOSCENEID, uint32_t> item : m_MapRevisions) {
|
||||
if (item.first == sceneID) return;
|
||||
}
|
||||
|
||||
m_MapRevisions[LWOSCENEID(sceneID)] = revision;
|
||||
}
|
||||
|
||||
const void Zone::PrintAllGameObjects() {
|
||||
for (std::pair<LWOSCENEID, SceneRef> scene : m_Scenes) {
|
||||
Game::logger->Log("Zone", "In sceneID: %i", scene.first.GetSceneID());
|
||||
scene.second.level->PrintAllObjects();
|
||||
if (m_MapRevisions.find(sceneID) == m_MapRevisions.end()) {
|
||||
m_MapRevisions.insert(std::make_pair(sceneID, revision));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,17 +213,10 @@ void Zone::LoadScene(std::istream& file) {
|
||||
scene.level = nullptr;
|
||||
LWOSCENEID lwoSceneID(LWOZONEID_INVALID, 0);
|
||||
|
||||
uint8_t sceneFilenameLength;
|
||||
BinaryIO::BinaryRead(file, sceneFilenameLength);
|
||||
scene.filename = BinaryIO::ReadString(file, sceneFilenameLength);
|
||||
BinaryIO::ReadString<uint8_t>(file, scene.filename, BinaryIO::ReadType::String);
|
||||
|
||||
std::string luTriggersPath = scene.filename.substr(0, scene.filename.size() - 4) + ".lutriggers";
|
||||
std::vector<LUTriggers::Trigger*> triggers;
|
||||
if(Game::assetManager->HasFile((m_ZonePath + luTriggersPath).c_str())) triggers = LoadLUTriggers(luTriggersPath, scene.id);
|
||||
|
||||
for (LUTriggers::Trigger* trigger : triggers) {
|
||||
scene.triggers.insert({ trigger->id, trigger });
|
||||
}
|
||||
if (Game::assetManager->HasFile((m_ZonePath + luTriggersPath).c_str())) LoadLUTriggers(luTriggersPath, scene);
|
||||
|
||||
if (m_FileFormatVersion >= Zone::FileFormatVersion::LatePreAlpha || m_FileFormatVersion < Zone::FileFormatVersion::PrePreAlpha) {
|
||||
BinaryIO::BinaryRead(file, scene.id);
|
||||
@@ -254,12 +226,10 @@ void Zone::LoadScene(std::istream& file) {
|
||||
BinaryIO::BinaryRead(file, scene.sceneType);
|
||||
lwoSceneID.SetLayerID(scene.sceneType);
|
||||
|
||||
uint8_t sceneNameLength;
|
||||
BinaryIO::BinaryRead(file, sceneNameLength);
|
||||
scene.name = BinaryIO::ReadString(file, sceneNameLength);
|
||||
BinaryIO::ReadString<uint8_t>(file, scene.name, BinaryIO::ReadType::String);
|
||||
}
|
||||
|
||||
if (m_FileFormatVersion == Zone::FileFormatVersion::LatePreAlpha){
|
||||
if (m_FileFormatVersion == Zone::FileFormatVersion::LatePreAlpha) {
|
||||
BinaryIO::BinaryRead(file, scene.unknown1);
|
||||
BinaryIO::BinaryRead(file, scene.unknown2);
|
||||
}
|
||||
@@ -271,61 +241,52 @@ void Zone::LoadScene(std::istream& file) {
|
||||
}
|
||||
|
||||
m_Scenes.insert(std::make_pair(lwoSceneID, scene));
|
||||
m_NumberOfScenesLoaded++;
|
||||
}
|
||||
|
||||
std::vector<LUTriggers::Trigger*> Zone::LoadLUTriggers(std::string triggerFile, LWOSCENEID sceneID) {
|
||||
std::vector<LUTriggers::Trigger*> lvlTriggers;
|
||||
void Zone::LoadLUTriggers(std::string triggerFile, SceneRef& scene) {
|
||||
auto file = Game::assetManager->GetFile((m_ZonePath + triggerFile).c_str());
|
||||
|
||||
auto buffer = Game::assetManager->GetFileAsBuffer((m_ZonePath + triggerFile).c_str());
|
||||
|
||||
if (!buffer.m_Success) {
|
||||
Game::logger->Log("Zone", "Failed to load %s from disk. Skipping loading triggers", (m_ZonePath + triggerFile).c_str());
|
||||
return lvlTriggers;
|
||||
}
|
||||
|
||||
std::istream file(&buffer);
|
||||
std::stringstream data;
|
||||
data << file.rdbuf();
|
||||
|
||||
buffer.close();
|
||||
data.seekg(0, std::ios::end);
|
||||
int32_t size = data.tellg();
|
||||
data.seekg(0, std::ios::beg);
|
||||
|
||||
if (data.str().size() == 0) return lvlTriggers;
|
||||
if (size == 0) return;
|
||||
|
||||
tinyxml2::XMLDocument* doc = new tinyxml2::XMLDocument();
|
||||
if (!doc) return lvlTriggers;
|
||||
tinyxml2::XMLDocument doc;
|
||||
|
||||
if (doc->Parse(data.str().c_str(), data.str().size()) == 0) {
|
||||
//Game::logger->Log("Zone", "Loaded LUTriggers from file %s!", triggerFile.c_str());
|
||||
} else {
|
||||
Game::logger->Log("Zone", "Failed to load LUTriggers from file %s", triggerFile.c_str());
|
||||
return lvlTriggers;
|
||||
if (doc.Parse(data.str().c_str(), size) != tinyxml2::XML_SUCCESS) {
|
||||
LOG("Failed to load LUTriggers from file %s", triggerFile.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
tinyxml2::XMLElement* triggers = doc->FirstChildElement("triggers");
|
||||
if (!triggers) return lvlTriggers;
|
||||
auto* triggers = doc.FirstChildElement("triggers");
|
||||
if (!triggers) return;
|
||||
|
||||
auto currentTrigger = triggers->FirstChildElement("trigger");
|
||||
auto* currentTrigger = triggers->FirstChildElement("trigger");
|
||||
while (currentTrigger) {
|
||||
LUTriggers::Trigger* newTrigger = new LUTriggers::Trigger();
|
||||
currentTrigger->QueryAttribute("enabled", &newTrigger->enabled);
|
||||
currentTrigger->QueryAttribute("id", &newTrigger->id);
|
||||
|
||||
auto currentEvent = currentTrigger->FirstChildElement("event");
|
||||
auto* currentEvent = currentTrigger->FirstChildElement("event");
|
||||
while (currentEvent) {
|
||||
LUTriggers::Event* newEvent = new LUTriggers::Event();
|
||||
newEvent->id = TriggerEventType::StringToTriggerEventType(currentEvent->Attribute("id"));
|
||||
auto currentCommand = currentEvent->FirstChildElement("command");
|
||||
auto* currentCommand = currentEvent->FirstChildElement("command");
|
||||
while (currentCommand) {
|
||||
LUTriggers::Command* newCommand = new LUTriggers::Command();
|
||||
newCommand->id = TriggerCommandType::StringToTriggerCommandType(currentCommand->Attribute("id"));
|
||||
newCommand->target = currentCommand->Attribute("target");
|
||||
if (currentCommand->Attribute("targetName") != NULL) {
|
||||
if (currentCommand->Attribute("targetName")) {
|
||||
newCommand->targetName = currentCommand->Attribute("targetName");
|
||||
}
|
||||
if (currentCommand->Attribute("args") != NULL) {
|
||||
if (currentCommand->Attribute("args")) {
|
||||
newCommand->args = currentCommand->Attribute("args");
|
||||
}
|
||||
|
||||
newEvent->commands.push_back(newCommand);
|
||||
currentCommand = currentCommand->NextSiblingElement("command");
|
||||
}
|
||||
@@ -333,19 +294,18 @@ std::vector<LUTriggers::Trigger*> Zone::LoadLUTriggers(std::string triggerFile,
|
||||
currentEvent = currentEvent->NextSiblingElement("event");
|
||||
}
|
||||
currentTrigger = currentTrigger->NextSiblingElement("trigger");
|
||||
lvlTriggers.push_back(newTrigger);
|
||||
scene.triggers.insert(std::make_pair(newTrigger->id, newTrigger));
|
||||
}
|
||||
|
||||
delete doc;
|
||||
|
||||
return lvlTriggers;
|
||||
}
|
||||
|
||||
LUTriggers::Trigger* Zone::GetTrigger(uint32_t sceneID, uint32_t triggerID) {
|
||||
if (m_Scenes.find(sceneID) == m_Scenes.end()) return nullptr;
|
||||
if (m_Scenes[sceneID].triggers.find(triggerID) == m_Scenes[sceneID].triggers.end()) return nullptr;
|
||||
auto scene = m_Scenes.find(sceneID);
|
||||
if (scene == m_Scenes.end()) return nullptr;
|
||||
|
||||
return m_Scenes[sceneID].triggers[triggerID];
|
||||
auto trigger = scene->second.triggers.find(triggerID);
|
||||
if (trigger == scene->second.triggers.end()) return nullptr;
|
||||
|
||||
return trigger->second;
|
||||
}
|
||||
|
||||
const Path* Zone::GetPath(std::string name) const {
|
||||
@@ -361,15 +321,14 @@ const Path* Zone::GetPath(std::string name) const {
|
||||
void Zone::LoadSceneTransition(std::istream& file) {
|
||||
SceneTransition sceneTrans;
|
||||
if (m_FileFormatVersion < Zone::FileFormatVersion::Auramar) {
|
||||
uint8_t length;
|
||||
BinaryIO::BinaryRead(file, length);
|
||||
sceneTrans.name = BinaryIO::ReadString(file, length);
|
||||
BinaryIO::ReadString<uint8_t>(file, sceneTrans.name, BinaryIO::ReadType::String);
|
||||
BinaryIO::BinaryRead(file, sceneTrans.width);
|
||||
}
|
||||
|
||||
//BR<42>THER MAY I HAVE SOME L<><4C>PS?
|
||||
uint8_t loops = (m_FileFormatVersion <= Zone::FileFormatVersion::LatePreAlpha || m_FileFormatVersion >= Zone::FileFormatVersion::Launch) ? 2 : 5;
|
||||
|
||||
sceneTrans.points.reserve(loops);
|
||||
for (uint8_t i = 0; i < loops; ++i) {
|
||||
sceneTrans.points.push_back(LoadSceneTransitionInfo(file));
|
||||
}
|
||||
@@ -389,13 +348,7 @@ void Zone::LoadPath(std::istream& file) {
|
||||
|
||||
BinaryIO::BinaryRead(file, path.pathVersion);
|
||||
|
||||
uint8_t stringLength;
|
||||
BinaryIO::BinaryRead(file, stringLength);
|
||||
for (uint8_t i = 0; i < stringLength; ++i) {
|
||||
uint16_t character;
|
||||
BinaryIO::BinaryRead(file, character);
|
||||
path.pathName.push_back(character);
|
||||
}
|
||||
BinaryIO::ReadString<uint8_t>(file, path.pathName, BinaryIO::ReadType::WideString);
|
||||
|
||||
BinaryIO::BinaryRead(file, path.pathType);
|
||||
BinaryIO::BinaryRead(file, path.flags);
|
||||
@@ -405,13 +358,7 @@ void Zone::LoadPath(std::istream& file) {
|
||||
if (path.pathVersion >= 18) {
|
||||
BinaryIO::BinaryRead(file, path.movingPlatform.timeBasedMovement);
|
||||
} else if (path.pathVersion >= 13) {
|
||||
uint8_t count;
|
||||
BinaryIO::BinaryRead(file, count);
|
||||
for (uint8_t i = 0; i < count; ++i) {
|
||||
uint16_t character;
|
||||
BinaryIO::BinaryRead(file, character);
|
||||
path.movingPlatform.platformTravelSound.push_back(character);
|
||||
}
|
||||
BinaryIO::ReadString<uint8_t>(file, path.movingPlatform.platformTravelSound, BinaryIO::ReadType::WideString);
|
||||
}
|
||||
} else if (path.pathType == PathType::Property) {
|
||||
BinaryIO::BinaryRead(file, path.property.pathType);
|
||||
@@ -420,20 +367,8 @@ void Zone::LoadPath(std::istream& file) {
|
||||
BinaryIO::BinaryRead(file, path.property.associatedZone);
|
||||
|
||||
if (path.pathVersion >= 5) {
|
||||
uint8_t count1;
|
||||
BinaryIO::BinaryRead(file, count1);
|
||||
for (uint8_t i = 0; i < count1; ++i) {
|
||||
uint16_t character;
|
||||
BinaryIO::BinaryRead(file, character);
|
||||
path.property.displayName.push_back(character);
|
||||
}
|
||||
uint32_t count2;
|
||||
BinaryIO::BinaryRead(file, count2);
|
||||
for (uint8_t i = 0; i < count2; ++i) {
|
||||
uint16_t character;
|
||||
BinaryIO::BinaryRead(file, character);
|
||||
path.property.displayDesc.push_back(character);
|
||||
}
|
||||
BinaryIO::ReadString<uint8_t>(file, path.property.displayName, BinaryIO::ReadType::WideString);
|
||||
BinaryIO::ReadString<uint32_t>(file, path.property.displayDesc, BinaryIO::ReadType::WideString);
|
||||
}
|
||||
|
||||
if (path.pathVersion >= 6) BinaryIO::BinaryRead(file, path.property.type);
|
||||
@@ -450,13 +385,7 @@ void Zone::LoadPath(std::istream& file) {
|
||||
BinaryIO::BinaryRead(file, path.property.maxBuildHeight);
|
||||
}
|
||||
} else if (path.pathType == PathType::Camera) {
|
||||
uint8_t count;
|
||||
BinaryIO::BinaryRead(file, count);
|
||||
for (uint8_t i = 0; i < count; ++i) {
|
||||
uint16_t character;
|
||||
BinaryIO::BinaryRead(file, character);
|
||||
path.camera.nextPath.push_back(character);
|
||||
}
|
||||
BinaryIO::ReadString<uint8_t>(file, path.camera.nextPath, BinaryIO::ReadType::WideString);
|
||||
if (path.pathVersion >= 14) {
|
||||
BinaryIO::BinaryRead(file, path.camera.rotatePlayer);
|
||||
|
||||
@@ -473,7 +402,7 @@ void Zone::LoadPath(std::istream& file) {
|
||||
// Read waypoints
|
||||
|
||||
BinaryIO::BinaryRead(file, path.waypointCount);
|
||||
|
||||
path.pathWaypoints.reserve(path.waypointCount);
|
||||
for (uint32_t i = 0; i < path.waypointCount; ++i) {
|
||||
PathWaypoint waypoint = PathWaypoint();
|
||||
|
||||
@@ -494,20 +423,8 @@ void Zone::LoadPath(std::istream& file) {
|
||||
BinaryIO::BinaryRead(file, waypoint.speed);
|
||||
BinaryIO::BinaryRead(file, waypoint.movingPlatform.wait);
|
||||
if (path.pathVersion >= 13) {
|
||||
uint8_t count1;
|
||||
BinaryIO::BinaryRead(file, count1);
|
||||
for (uint8_t i = 0; i < count1; ++i) {
|
||||
uint16_t character;
|
||||
BinaryIO::BinaryRead(file, character);
|
||||
waypoint.movingPlatform.departSound.push_back(character);
|
||||
}
|
||||
uint8_t count2;
|
||||
BinaryIO::BinaryRead(file, count2);
|
||||
for (uint8_t i = 0; i < count2; ++i) {
|
||||
uint16_t character;
|
||||
BinaryIO::BinaryRead(file, character);
|
||||
waypoint.movingPlatform.arriveSound.push_back(character);
|
||||
}
|
||||
BinaryIO::ReadString<uint8_t>(file, waypoint.movingPlatform.departSound, BinaryIO::ReadType::WideString);
|
||||
BinaryIO::ReadString<uint8_t>(file, waypoint.movingPlatform.arriveSound, BinaryIO::ReadType::WideString);
|
||||
}
|
||||
} else if (path.pathType == PathType::Camera) {
|
||||
BinaryIO::BinaryRead(file, waypoint.camera.time);
|
||||
@@ -530,22 +447,11 @@ void Zone::LoadPath(std::istream& file) {
|
||||
uint32_t count;
|
||||
BinaryIO::BinaryRead(file, count);
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
uint8_t count1;
|
||||
std::string parameter;
|
||||
BinaryIO::ReadString<uint8_t>(file, parameter, BinaryIO::ReadType::WideString);
|
||||
|
||||
std::string value;
|
||||
BinaryIO::BinaryRead(file, count1);
|
||||
for (uint8_t i = 0; i < count1; ++i) {
|
||||
uint16_t character;
|
||||
BinaryIO::BinaryRead(file, character);
|
||||
parameter.push_back(character);
|
||||
}
|
||||
uint8_t count2;
|
||||
BinaryIO::BinaryRead(file, count2);
|
||||
for (uint8_t i = 0; i < count2; ++i) {
|
||||
uint16_t character;
|
||||
BinaryIO::BinaryRead(file, character);
|
||||
value.push_back(character);
|
||||
}
|
||||
BinaryIO::ReadString<uint8_t>(file, value, BinaryIO::ReadType::WideString);
|
||||
|
||||
LDFBaseData* ldfConfig = nullptr;
|
||||
if (path.pathType == PathType::Movement || path.pathType == PathType::Rail) {
|
||||
@@ -565,9 +471,9 @@ void Zone::LoadPath(std::istream& file) {
|
||||
// We verify the waypoint heights against the navmesh because in many movement paths,
|
||||
// the waypoint is located near 0 height,
|
||||
if (path.pathType == PathType::Movement) {
|
||||
if (dpWorld::Instance().IsLoaded()) {
|
||||
if (dpWorld::IsLoaded()) {
|
||||
// 2000 should be large enough for every world.
|
||||
waypoint.position.y = dpWorld::Instance().GetNavMesh()->GetHeightAtPoint(waypoint.position, 2000.0f);
|
||||
waypoint.position.y = dpWorld::GetNavMesh()->GetHeightAtPoint(waypoint.position, 2000.0f);
|
||||
}
|
||||
}
|
||||
path.pathWaypoints.push_back(waypoint);
|
||||
|
@@ -154,6 +154,8 @@ struct PropertyPath {
|
||||
float repMultiplier;
|
||||
PropertyRentalPeriod rentalPeriod;
|
||||
PropertyAchievmentRequired achievementRequired;
|
||||
|
||||
// Player respawn coordinates in the main zone (not the property zone)
|
||||
NiPoint3 playerZoneCoords;
|
||||
float maxBuildHeight;
|
||||
};
|
||||
@@ -214,7 +216,6 @@ public:
|
||||
void AddRevision(LWOSCENEID sceneID, uint32_t revision);
|
||||
const LWOZONEID& GetZoneID() const { return m_ZoneID; }
|
||||
const uint32_t GetChecksum() const { return m_CheckSum; }
|
||||
const void PrintAllGameObjects();
|
||||
LUTriggers::Trigger* GetTrigger(uint32_t sceneID, uint32_t triggerID);
|
||||
const Path* GetPath(std::string name) const;
|
||||
|
||||
@@ -232,7 +233,6 @@ public:
|
||||
private:
|
||||
LWOZONEID m_ZoneID;
|
||||
std::string m_ZoneFilePath;
|
||||
uint32_t m_NumberOfScenesLoaded;
|
||||
uint32_t m_NumberOfObjectsLoaded;
|
||||
uint32_t m_NumberOfSceneTransitionsLoaded;
|
||||
FileFormatVersion m_FileFormatVersion;
|
||||
@@ -247,18 +247,17 @@ private:
|
||||
std::string m_ZoneDesc; //Description of the zone by a level designer
|
||||
std::string m_ZoneRawPath; //Path to the .raw file of this zone.
|
||||
|
||||
std::map<LWOSCENEID, SceneRef, mapCompareLwoSceneIDs> m_Scenes;
|
||||
std::map<LWOSCENEID, SceneRef> m_Scenes;
|
||||
std::vector<SceneTransition> m_SceneTransitions;
|
||||
|
||||
uint32_t m_PathDataLength;
|
||||
uint32_t m_PathChunkVersion;
|
||||
std::vector<Path> m_Paths;
|
||||
|
||||
std::map<LWOSCENEID, uint32_t, mapCompareLwoSceneIDs> m_MapRevisions; //rhs is the revision!
|
||||
|
||||
std::map<LWOSCENEID, uint32_t> m_MapRevisions; //rhs is the revision!
|
||||
//private ("helper") functions:
|
||||
void LoadScene(std::istream& file);
|
||||
std::vector<LUTriggers::Trigger*> LoadLUTriggers(std::string triggerFile, LWOSCENEID sceneID);
|
||||
void LoadLUTriggers(std::string triggerFile, SceneRef& scene);
|
||||
void LoadSceneTransition(std::istream& file);
|
||||
SceneTransitionInfo LoadSceneTransitionInfo(std::istream& file);
|
||||
void LoadPath(std::istream& file);
|
||||
|
@@ -5,15 +5,11 @@
|
||||
#include "LDFFormat.h"
|
||||
#include <vector>
|
||||
|
||||
struct mapCompareLwoSceneIDs {
|
||||
bool operator()(const LWOSCENEID& lhs, const LWOSCENEID& rhs) const { return lhs < rhs; }
|
||||
};
|
||||
|
||||
struct SceneObject {
|
||||
LWOOBJID id;
|
||||
LOT lot;
|
||||
uint32_t value1;
|
||||
uint32_t value2;
|
||||
uint32_t nodeType;
|
||||
uint32_t glomId;
|
||||
NiPoint3 position;
|
||||
NiQuaternion rotation;
|
||||
float scale = 1.0f;
|
||||
|
@@ -2,7 +2,7 @@
|
||||
#include "dCommonVars.h"
|
||||
#include "dZoneManager.h"
|
||||
#include "EntityManager.h"
|
||||
#include "dLogger.h"
|
||||
#include "Logger.h"
|
||||
#include "dConfig.h"
|
||||
#include "InventoryComponent.h"
|
||||
#include "DestroyableComponent.h"
|
||||
@@ -15,10 +15,10 @@
|
||||
#include "CDZoneTableTable.h"
|
||||
#include "AssetManager.h"
|
||||
|
||||
#include "../dWorldServer/ObjectIDManager.h"
|
||||
#include "ObjectIDManager.h"
|
||||
|
||||
void dZoneManager::Initialize(const LWOZONEID& zoneID) {
|
||||
Game::logger->Log("dZoneManager", "Preparing zone: %i/%i/%i", zoneID.GetMapID(), zoneID.GetInstanceID(), zoneID.GetCloneID());
|
||||
LOG("Preparing zone: %i/%i/%i", zoneID.GetMapID(), zoneID.GetInstanceID(), zoneID.GetCloneID());
|
||||
|
||||
int64_t startTime = 0;
|
||||
int64_t endTime = 0;
|
||||
@@ -29,7 +29,7 @@ void dZoneManager::Initialize(const LWOZONEID& zoneID) {
|
||||
|
||||
LOT zoneControlTemplate = 2365;
|
||||
|
||||
CDZoneTableTable* zoneTable = CDClientManager::Instance().GetTable<CDZoneTableTable>();
|
||||
CDZoneTableTable* zoneTable = CDClientManager::GetTable<CDZoneTableTable>();
|
||||
if (zoneTable != nullptr) {
|
||||
const CDZoneTable* zone = zoneTable->Query(zoneID.GetMapID());
|
||||
|
||||
@@ -40,14 +40,17 @@ void dZoneManager::Initialize(const LWOZONEID& zoneID) {
|
||||
Game::entityManager->SetGhostDistanceMax(max + min);
|
||||
Game::entityManager->SetGhostDistanceMin(max);
|
||||
m_PlayerLoseCoinsOnDeath = zone->PlayerLoseCoinsOnDeath;
|
||||
m_DisableSaveLocation = zone->disableSaveLoc;
|
||||
m_MountsAllowed = zone->mountsAllowed;
|
||||
m_PetsAllowed = zone->petsAllowed;
|
||||
}
|
||||
}
|
||||
|
||||
Game::logger->Log("dZoneManager", "Creating zone control object %i", zoneControlTemplate);
|
||||
LOG("Creating zone control object %i", zoneControlTemplate);
|
||||
|
||||
// Create ZoneControl object
|
||||
if (!Game::entityManager) {
|
||||
Game::logger->Log("dZoneManager", "ERROR: No entity manager loaded. Cannot proceed.");
|
||||
LOG("ERROR: No entity manager loaded. Cannot proceed.");
|
||||
throw std::invalid_argument("No entity manager loaded. Cannot proceed.");
|
||||
}
|
||||
Game::entityManager->Initialize();
|
||||
@@ -63,7 +66,7 @@ void dZoneManager::Initialize(const LWOZONEID& zoneID) {
|
||||
|
||||
LoadWorldConfig();
|
||||
|
||||
Game::logger->Log("dZoneManager", "Zone prepared in: %llu ms", (endTime - startTime));
|
||||
LOG("Zone prepared in: %llu ms", (endTime - startTime));
|
||||
|
||||
VanityUtilities::SpawnVanity();
|
||||
}
|
||||
@@ -76,8 +79,6 @@ dZoneManager::~dZoneManager() {
|
||||
delete p.second;
|
||||
p.second = nullptr;
|
||||
}
|
||||
|
||||
m_Spawners.erase(p.first);
|
||||
}
|
||||
if (m_WorldConfig) delete m_WorldConfig;
|
||||
}
|
||||
@@ -93,33 +94,6 @@ void dZoneManager::LoadZone(const LWOZONEID& zoneID) {
|
||||
m_pZone = new Zone(zoneID.GetMapID(), zoneID.GetInstanceID(), zoneID.GetCloneID());
|
||||
}
|
||||
|
||||
void dZoneManager::NotifyZone(const dZoneNotifier& notifier, const LWOOBJID& objectID) {
|
||||
switch (notifier) {
|
||||
case dZoneNotifier::SpawnedObjectDestroyed:
|
||||
break;
|
||||
case dZoneNotifier::SpawnedChildObjectDestroyed:
|
||||
break;
|
||||
case dZoneNotifier::ReloadZone:
|
||||
Game::logger->Log("dZoneManager", "Forcing reload of zone %i", m_ZoneID.GetMapID());
|
||||
LoadZone(m_ZoneID);
|
||||
|
||||
m_pZone->Initalize();
|
||||
break;
|
||||
case dZoneNotifier::UserJoined:
|
||||
break;
|
||||
case dZoneNotifier::UserMoved:
|
||||
break;
|
||||
case dZoneNotifier::PrintAllGameObjects:
|
||||
m_pZone->PrintAllGameObjects();
|
||||
break;
|
||||
case dZoneNotifier::InvalidNotifier:
|
||||
Game::logger->Log("dZoneManager", "Got an invalid zone notifier.");
|
||||
break;
|
||||
default:
|
||||
Game::logger->Log("dZoneManager", "Unknown zone notifier: %i", int(notifier));
|
||||
}
|
||||
}
|
||||
|
||||
void dZoneManager::AddSpawner(LWOOBJID id, Spawner* spawner) {
|
||||
m_Spawners.insert_or_assign(id, spawner);
|
||||
}
|
||||
@@ -138,7 +112,7 @@ LWOOBJID dZoneManager::MakeSpawner(SpawnerInfo info) {
|
||||
auto objectId = info.spawnerID;
|
||||
|
||||
if (objectId == LWOOBJID_EMPTY) {
|
||||
objectId = ObjectIDManager::Instance()->GenerateObjectID();
|
||||
objectId = ObjectIDManager::GenerateObjectID();
|
||||
GeneralUtils::SetBit(objectId, eObjectBits::CLIENT);
|
||||
|
||||
info.spawnerID = objectId;
|
||||
@@ -174,7 +148,7 @@ void dZoneManager::RemoveSpawner(const LWOOBJID id) {
|
||||
auto* spawner = GetSpawner(id);
|
||||
|
||||
if (spawner == nullptr) {
|
||||
Game::logger->Log("dZoneManager", "Failed to find spawner (%llu)", id);
|
||||
LOG("Failed to find spawner (%llu)", id);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -184,14 +158,14 @@ void dZoneManager::RemoveSpawner(const LWOOBJID id) {
|
||||
entity->Kill();
|
||||
} else {
|
||||
|
||||
Game::logger->Log("dZoneManager", "Failed to find spawner entity (%llu)", id);
|
||||
LOG("Failed to find spawner entity (%llu)", id);
|
||||
}
|
||||
|
||||
spawner->DestroyAllEntities();
|
||||
|
||||
spawner->Deactivate();
|
||||
|
||||
Game::logger->Log("dZoneManager", "Destroying spawner (%llu)", id);
|
||||
LOG("Destroying spawner (%llu)", id);
|
||||
|
||||
m_Spawners.erase(id);
|
||||
|
||||
@@ -234,7 +208,7 @@ uint32_t dZoneManager::GetUniqueMissionIdStartingValue() {
|
||||
|
||||
bool dZoneManager::CheckIfAccessibleZone(LWOMAPID zoneID) {
|
||||
//We're gonna go ahead and presume we've got the db loaded already:
|
||||
CDZoneTableTable* zoneTable = CDClientManager::Instance().GetTable<CDZoneTableTable>();
|
||||
CDZoneTableTable* zoneTable = CDClientManager::GetTable<CDZoneTableTable>();
|
||||
const CDZoneTable* zone = zoneTable->Query(zoneID);
|
||||
if (zone != nullptr) {
|
||||
return Game::assetManager->HasFile(("maps/" + zone->zoneName).c_str());
|
||||
@@ -244,14 +218,14 @@ bool dZoneManager::CheckIfAccessibleZone(LWOMAPID zoneID) {
|
||||
}
|
||||
|
||||
void dZoneManager::LoadWorldConfig() {
|
||||
Game::logger->Log("dZoneManager", "Loading WorldConfig into memory");
|
||||
LOG("Loading WorldConfig into memory");
|
||||
|
||||
auto worldConfig = CDClientDatabase::ExecuteQuery("SELECT * FROM WorldConfig;");
|
||||
|
||||
if (!m_WorldConfig) m_WorldConfig = new WorldConfig();
|
||||
|
||||
if (worldConfig.eof()) {
|
||||
Game::logger->Log("dZoneManager", "WorldConfig table is empty. Is this intended?");
|
||||
LOG("WorldConfig table is empty. Is this intended?");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -314,5 +288,5 @@ void dZoneManager::LoadWorldConfig() {
|
||||
m_WorldConfig->characterVersion = worldConfig.getIntField("CharacterVersion");
|
||||
m_WorldConfig->levelCapCurrencyConversion = worldConfig.getIntField("LevelCapCurrencyConversion");
|
||||
worldConfig.finalize();
|
||||
Game::logger->Log("dZoneManager", "Loaded WorldConfig into memory");
|
||||
LOG("Loaded WorldConfig into memory");
|
||||
}
|
||||
|
@@ -14,7 +14,6 @@ public:
|
||||
ReloadZone, //Forces the server and all connects clients to reload the map
|
||||
UserJoined,
|
||||
UserMoved,
|
||||
PrintAllGameObjects, //Using this is a BAD idea in production
|
||||
InvalidNotifier
|
||||
};
|
||||
|
||||
@@ -30,7 +29,6 @@ public:
|
||||
|
||||
Zone* GetZone(); //Gets a pointer to the currently loaded zone.
|
||||
void LoadZone(const LWOZONEID& zoneID); //Discard the current zone (if any) and loads a new zone.
|
||||
void NotifyZone(const dZoneNotifier& notifier, const LWOOBJID& objectID); //Notifies the zone of a certain event or command.
|
||||
void AddSpawner(LWOOBJID id, Spawner* spawner);
|
||||
LWOZONEID GetZoneID() const;
|
||||
LWOOBJID MakeSpawner(SpawnerInfo info);
|
||||
@@ -41,6 +39,9 @@ public:
|
||||
void Update(float deltaTime);
|
||||
Entity* GetZoneControlObject() { return m_ZoneControlObject; }
|
||||
bool GetPlayerLoseCoinOnDeath() { return m_PlayerLoseCoinsOnDeath; }
|
||||
bool GetDisableSaveLocation() { return m_DisableSaveLocation; }
|
||||
bool GetMountsAllowed() { return m_MountsAllowed; }
|
||||
bool GetPetsAllowed() { return m_PetsAllowed; }
|
||||
uint32_t GetUniqueMissionIdStartingValue();
|
||||
bool CheckIfAccessibleZone(LWOMAPID zoneID);
|
||||
|
||||
@@ -58,7 +59,10 @@ private:
|
||||
|
||||
Zone* m_pZone = nullptr;
|
||||
LWOZONEID m_ZoneID;
|
||||
bool m_PlayerLoseCoinsOnDeath; //Do players drop coins in this zone when smashed
|
||||
bool m_PlayerLoseCoinsOnDeath = false;
|
||||
bool m_DisableSaveLocation = false;
|
||||
bool m_MountsAllowed = true;
|
||||
bool m_PetsAllowed = true;
|
||||
std::map<LWOOBJID, Spawner*> m_Spawners;
|
||||
WorldConfig* m_WorldConfig = nullptr;
|
||||
|
||||
|
Reference in New Issue
Block a user