This commit is contained in:
2026-05-06 23:26:10 +04:00
parent 0a3d3b6afa
commit 5dcb8be3aa
16 changed files with 2622 additions and 0 deletions
@@ -0,0 +1,182 @@
/*
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License v2 as published by the
* Free Software Foundation.
*/
#include "EncounterJournalMgr.h"
#include "Chat.h"
#include "CommandScript.h"
#include <string>
#include <vector>
using namespace Acore::ChatCommands;
class encounter_journal_commandscript : public CommandScript
{
public:
encounter_journal_commandscript() : CommandScript("encounter_journal_commandscript") { }
ChatCommandTable GetCommands() const override
{
static ChatCommandTable ejCommandTable =
{
{ "reload", HandleReloadCommand, SEC_ADMINISTRATOR, Console::Yes },
{ "export", HandleExportCommand, SEC_ADMINISTRATOR, Console::Yes },
{ "validate", HandleValidateCommand, SEC_ADMINISTRATOR, Console::Yes },
{ "importloot", HandleImportLootCommand, SEC_ADMINISTRATOR, Console::Yes },
{ "importskeleton", HandleImportSkeletonCommand, SEC_ADMINISTRATOR, Console::Yes },
{ "importall", HandleImportAllCommand, SEC_ADMINISTRATOR, Console::Yes },
{ "info", HandleInfoCommand, SEC_GAMEMASTER, Console::Yes },
};
static ChatCommandTable commandTable =
{
{ "ej", ejCommandTable },
};
return commandTable;
}
static bool HandleReloadCommand(ChatHandler* handler)
{
sEncounterJournalMgr->Load();
handler->PSendSysMessage("Encounter Journal reloaded ({} instances, "
"{} encounters, {} sections, {} loot rows).",
sEncounterJournalMgr->InstanceCount(),
sEncounterJournalMgr->EncounterCount(),
sEncounterJournalMgr->SectionCount(),
sEncounterJournalMgr->LootCount());
return true;
}
static bool HandleExportCommand(ChatHandler* handler)
{
sEncounterJournalMgr->Load();
std::vector<std::string> paths;
std::string error;
if (!sEncounterJournalMgr->Export(paths, error))
{
handler->SendErrorMessage("Encounter Journal export failed: {}", error);
return false;
}
handler->PSendSysMessage("Encounter Journal exported {} file(s):", paths.size());
for (std::string const& p : paths)
handler->PSendSysMessage(" {}", p);
return true;
}
static bool HandleValidateCommand(ChatHandler* handler)
{
sEncounterJournalMgr->Load();
std::vector<std::string> errors;
std::size_t count = sEncounterJournalMgr->Validate(errors);
if (count == 0)
{
handler->PSendSysMessage("Encounter Journal: validation OK "
"({} instances, {} encounters, {} sections, {} loot rows).",
sEncounterJournalMgr->InstanceCount(),
sEncounterJournalMgr->EncounterCount(),
sEncounterJournalMgr->SectionCount(),
sEncounterJournalMgr->LootCount());
return true;
}
handler->PSendSysMessage("Encounter Journal: {} issue(s):", count);
// Cap output to avoid flooding chat in the in-game case.
std::size_t const limit = 50;
for (std::size_t i = 0; i < errors.size() && i < limit; ++i)
handler->PSendSysMessage(" {}", errors[i]);
if (errors.size() > limit)
handler->PSendSysMessage(" ... and {} more (run from console for full list)",
errors.size() - limit);
return true;
}
static bool HandleImportLootCommand(ChatHandler* handler)
{
sEncounterJournalMgr->Load();
std::vector<std::string> warnings;
std::size_t inserted = sEncounterJournalMgr->ImportLoot(warnings);
handler->PSendSysMessage(
"Encounter Journal loot import: inserted {} row(s).", inserted);
if (!warnings.empty())
{
handler->PSendSysMessage("{} warning(s):", warnings.size());
std::size_t const limit = 25;
for (std::size_t i = 0; i < warnings.size() && i < limit; ++i)
handler->PSendSysMessage(" {}", warnings[i]);
if (warnings.size() > limit)
handler->PSendSysMessage(" ... and {} more",
warnings.size() - limit);
}
if (inserted > 0)
handler->PSendSysMessage("Reload the manager (.ej reload) to "
"see the new rows in subsequent .ej export/validate.");
return true;
}
static bool HandleImportSkeletonCommand(ChatHandler* handler)
{
sEncounterJournalMgr->Load();
MoonWell::EncounterJournal::Mgr::SkeletonCounts c;
std::vector<std::string> warnings;
std::size_t total = sEncounterJournalMgr->ImportSkeleton(c, warnings);
handler->PSendSysMessage(
"Encounter Journal skeleton import: {} new rows.", total);
handler->PSendSysMessage(
" tiers={} instances={} tier_instances={}",
c.tiers, c.instances, c.tierInstances);
handler->PSendSysMessage(
" encounters={} creatures={} sections={}",
c.encounters, c.creatures, c.sections);
if (!warnings.empty())
{
handler->PSendSysMessage("{} warning(s):", warnings.size());
std::size_t const limit = 25;
for (std::size_t i = 0; i < warnings.size() && i < limit; ++i)
handler->PSendSysMessage(" {}", warnings[i]);
if (warnings.size() > limit)
handler->PSendSysMessage(" ... and {} more",
warnings.size() - limit);
}
if (total > 0)
handler->PSendSysMessage("Run .ej importloot next, then .ej export.");
return true;
}
static bool HandleImportAllCommand(ChatHandler* handler)
{
HandleImportSkeletonCommand(handler);
sEncounterJournalMgr->Load();
return HandleImportLootCommand(handler);
}
static bool HandleInfoCommand(ChatHandler* handler)
{
handler->PSendSysMessage("Encounter Journal cache:");
handler->PSendSysMessage(" tiers: {}", sEncounterJournalMgr->TierCount());
handler->PSendSysMessage(" instances: {}", sEncounterJournalMgr->InstanceCount());
handler->PSendSysMessage(" encounters: {}", sEncounterJournalMgr->EncounterCount());
handler->PSendSysMessage(" creatures: {}", sEncounterJournalMgr->CreatureCount());
handler->PSendSysMessage(" sections: {}", sEncounterJournalMgr->SectionCount());
handler->PSendSysMessage(" loot rows: {}", sEncounterJournalMgr->LootCount());
return true;
}
};
void AddSC_encounter_journal_commands()
{
new encounter_journal_commandscript();
}
@@ -0,0 +1,83 @@
/*
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License v2 as published by the
* Free Software Foundation.
*/
#include "Config.h"
#include "EncounterJournalMgr.h"
#include "Log.h"
#include "ScriptMgr.h"
void AddSC_encounter_journal_commands();
class encounter_journal_world_script : public WorldScript
{
public:
encounter_journal_world_script()
: WorldScript("encounter_journal_world_script",
{ WORLDHOOK_ON_LOAD_CUSTOM_DATABASE_TABLE })
{ }
void OnLoadCustomDatabaseTable() override
{
if (!sConfigMgr->GetOption<bool>("EncounterJournal.Enable", true))
{
LOG_INFO("server.loading", ">> mod-encounter-journal disabled "
"via EncounterJournal.Enable=0.");
return;
}
sEncounterJournalMgr->Load();
if (sConfigMgr->GetOption<bool>("EncounterJournal.ImportOnStart", false))
{
MoonWell::EncounterJournal::Mgr::SkeletonCounts sc;
std::vector<std::string> warnings;
std::size_t inserted =
sEncounterJournalMgr->ImportSkeleton(sc, warnings);
LOG_INFO("server.loading",
">> mod-encounter-journal: ImportOnStart added {} skeleton rows "
"(tiers={}, instances={}, encounters={}, creatures={}, sections={}).",
inserted, sc.tiers, sc.instances, sc.encounters,
sc.creatures, sc.sections);
std::vector<std::string> lootWarn;
std::size_t lootRows = sEncounterJournalMgr->ImportLoot(lootWarn);
LOG_INFO("server.loading",
">> mod-encounter-journal: ImportOnStart added {} loot rows.",
lootRows);
sEncounterJournalMgr->Load();
}
if (!sConfigMgr->GetOption<bool>("EncounterJournal.ExportOnStart", false))
return;
std::vector<std::string> paths;
std::string error;
if (!sEncounterJournalMgr->Export(paths, error))
{
LOG_ERROR("server.loading",
">> mod-encounter-journal: ExportOnStart failed: {}", error);
return;
}
for (std::string const& p : paths)
LOG_INFO("server.loading",
">> mod-encounter-journal: wrote {}", p);
}
};
void Addmod_encounter_journalScripts()
{
if (!sConfigMgr->GetOption<bool>("EncounterJournal.Enable", true))
return;
new encounter_journal_world_script();
AddSC_encounter_journal_commands();
LOG_INFO("server.loading",
">> mod-encounter-journal scripts registered (.ej commands available).");
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,197 @@
/*
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License v2 as published by the
* Free Software Foundation.
*/
#ifndef MOONWELL_ENCOUNTER_JOURNAL_MGR_H
#define MOONWELL_ENCOUNTER_JOURNAL_MGR_H
#include "Define.h"
#include <cstddef>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
namespace MoonWell::EncounterJournal
{
struct Tier
{
uint32 id = 0;
std::string name;
uint32 flags = 0;
int32 sortOrder = 0;
};
struct Instance
{
uint32 id = 0;
std::string name;
std::string description;
std::string buttonIcon;
std::string smallButtonIcon;
std::string background;
std::string loreBackground;
uint32 mapId = 0;
uint32 areaId = 0;
uint32 worldMapAreaId = 0;
uint32 flags = 0;
int32 sortOrder = 0;
};
struct TierInstance
{
uint32 tierId = 0;
uint32 instanceId = 0;
int32 sortOrder = 0;
};
struct Encounter
{
uint32 id = 0;
uint32 instanceId = 0;
std::string name;
std::string description;
float mapPositionX = 0.f;
float mapPositionY = 0.f;
uint32 floorIndex = 1;
uint32 worldMapAreaId = 0;
uint32 firstSectionId = 0;
uint32 difficultyMask = 0;
uint32 flags = 0;
int32 sortOrder = 0;
};
struct Creature
{
uint32 id = 0;
uint32 encounterId = 0;
uint32 creatureId = 0;
std::string name;
std::string description;
uint32 creatureDisplayId = 0;
std::string icon;
uint32 difficultyMask = 0;
int32 sortOrder = 0;
};
struct Section
{
uint32 id = 0;
uint32 encounterId = 0;
uint32 parentSectionId = 0;
uint32 nextSectionId = 0;
uint32 subSectionId = 0;
std::string name;
std::string description;
uint32 creatureDisplayId = 0;
uint32 descriptionSpellId = 0;
uint32 iconSpellId = 0;
std::string icon; // empty when SQL value is NULL or ''
uint32 flags = 0;
uint32 iconFlags = 0;
int32 sortOrder = 0;
uint32 type = 0;
uint32 difficultyMask = 0;
uint32 creatureId = 0;
};
struct Loot
{
uint32 id = 0;
uint32 encounterId = 0;
uint32 itemId = 0;
uint32 difficultyMask = 0;
int32 factionMask = -1;
int32 classMask = -1;
uint32 flags = 0;
int32 sortOrder = 0;
};
class Mgr
{
public:
static Mgr* instance();
// Loads all custom_ej_* tables into in-memory caches.
// Safe to call repeatedly: previous data is dropped first.
// Returns true on success (does not fail when tables are empty).
bool Load();
// Drops cached data.
void Unload();
// Writes the configured JSON file (always) and Lua file
// (when Export.LuaFile is non-empty). On success @writtenPaths
// contains the absolute paths created/overwritten.
bool Export(std::vector<std::string>& writtenPaths, std::string& error);
// Validates relational integrity. Returns the number of errors
// found and pushes one human-readable line per error into @errors.
std::size_t Validate(std::vector<std::string>& errors);
// Imports loot rows from creature_loot_template via creature_template.lootid
// for each creature card whose creature_id != 0. Reference rows are
// followed one level. Existing rows in custom_ej_loot are skipped via
// INSERT IGNORE on the unique key. Returns the number of inserted rows.
std::size_t ImportLoot(std::vector<std::string>& warnings);
// Generates skeleton EJ rows from existing AzerothCore tables:
// - custom_ej_tier from MapEntry.expansionID buckets
// - custom_ej_instance from instance_template + sMapStore (name, type)
// - custom_ej_encounter from instance_encounters (creditType=0 only)
// - custom_ej_creature from creature_template referenced by encounters
// - custom_ej_section one Overview placeholder per encounter
// Existing rows are kept (INSERT IGNORE on PK). Returns total inserted
// rows; per-table counts are reported via @counts (tier/instance/
// tier_instance/encounter/creature/section).
struct SkeletonCounts
{
std::size_t tiers = 0;
std::size_t instances = 0;
std::size_t tierInstances = 0;
std::size_t encounters = 0;
std::size_t creatures = 0;
std::size_t sections = 0;
};
std::size_t ImportSkeleton(SkeletonCounts& counts,
std::vector<std::string>& warnings);
std::size_t TierCount() const { return _tiers.size(); }
std::size_t InstanceCount() const { return _instances.size(); }
std::size_t EncounterCount() const { return _encounters.size(); }
std::size_t CreatureCount() const { return _creatures.size(); }
std::size_t SectionCount() const { return _sections.size(); }
std::size_t LootCount() const { return _loot.size(); }
private:
Mgr() = default;
std::string ResolveExportPath(std::string const& filename) const;
std::string BuildJson() const;
std::string BuildLua(std::string const& globalName) const;
std::vector<Tier> _tiers;
std::vector<TierInstance> _tierInstances;
std::unordered_map<uint32, Instance> _instances;
std::unordered_map<uint32, Encounter> _encounters;
std::unordered_map<uint32, Creature> _creatures;
std::unordered_map<uint32, Section> _sections;
std::vector<Loot> _loot;
// Sorted access caches (by sort_order, id). Filled at end of Load().
std::vector<uint32> _instancesSorted;
std::unordered_map<uint32, std::vector<uint32>> _encountersByInstance;
std::unordered_map<uint32, std::vector<uint32>> _creaturesByEncounter;
std::unordered_map<uint32, std::vector<std::size_t>> _lootByEncounter;
std::unordered_map<uint32, std::vector<uint32>> _instancesByTier;
};
}
#define sEncounterJournalMgr ::MoonWell::EncounterJournal::Mgr::instance()
#endif // MOONWELL_ENCOUNTER_JOURNAL_MGR_H