wip
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(command -v lua5.1)",
|
||||
"Bash(command -v lua)",
|
||||
"Read(//home/sindo/azeroth-server/**)",
|
||||
"Read(//home/sindo/**)",
|
||||
"Bash(docker image *)",
|
||||
"Bash(docker restart *)",
|
||||
"Bash(bash import-custom-sql.sh)",
|
||||
"Bash(awk '/^ ac-worldserver:/,/^ [a-z]/' /home/sindo/moonwell-core/docker-compose.yml)",
|
||||
"Bash(awk '/ac-worldserver:/{flag=1} flag{print; if\\(/depends_on:/\\){flag=0}}' /home/sindo/moonwell-core/docker-compose.yml)",
|
||||
"Bash(grep -n \"LoadAdditionalFile\\\\|\\\\.conf.dist\\\\|conf$\" /home/sindo/moonwell-core/src/common/Configuration/Config.cpp)",
|
||||
"Bash(grep -n \"^$\\\\|ENCOUNTER\" /home/sindo/moonwell-core/conf/dist/env.ac)",
|
||||
"Bash(grep -v \"^information_schema\\\\|^mysql$\\\\|^performance_schema\\\\|^sys$\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
Vendored
+10
@@ -19,3 +19,13 @@ AC_CCACHE=true
|
||||
AC_RESTARTER_BINPATH=
|
||||
AC_RESTARTER_BINFILE=
|
||||
AC_RESTARTER_WITHGDB=
|
||||
|
||||
#
|
||||
# mod-encounter-journal
|
||||
# This deployment reads only .conf.dist files, so module conf overrides
|
||||
# live here as env vars. ExportOnStart=1 writes JSON/Lua right after the
|
||||
# custom_ej_* tables load. EXPORT_PATH must point at a directory that's
|
||||
# mounted on the host so the file appears outside the container.
|
||||
#
|
||||
AC_ENCOUNTER_JOURNAL_EXPORT_ON_START=1
|
||||
AC_ENCOUNTER_JOURNAL_EXPORT_PATH=/azerothcore/env/dist/logs
|
||||
|
||||
@@ -14,6 +14,7 @@ fi
|
||||
|
||||
IMPORT_MYTHICPLUS_SQL="${ACORE_IMPORT_MYTHICPLUS_SQL:-1}"
|
||||
IMPORT_STORE_SQL="${ACORE_IMPORT_STORE_SQL:-1}"
|
||||
IMPORT_ENCOUNTER_JOURNAL_SQL="${ACORE_IMPORT_ENCOUNTER_JOURNAL_SQL:-1}"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
@@ -234,6 +235,49 @@ import_aoe_loot_sql() {
|
||||
import_sql_file "acore_world" "$aoe_loot_sql_file"
|
||||
}
|
||||
|
||||
import_encounter_journal_sql() {
|
||||
local schema_file="$ROOT_DIR/modules/mod-encounter-journal/data/sql/db-world/base/01_encounter_journal_schema.sql"
|
||||
local sample_file="$ROOT_DIR/modules/mod-encounter-journal/data/sql/db-world/base/02_encounter_journal_sample.sql"
|
||||
|
||||
if [[ "$IMPORT_ENCOUNTER_JOURNAL_SQL" == "0" ]]; then
|
||||
log "Encounter Journal SQL import disabled, skipped"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ ! -f "$schema_file" ]]; then
|
||||
log "mod-encounter-journal schema SQL not found, skipped"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Schema file uses CREATE TABLE IF NOT EXISTS, but we still gate on the
|
||||
# canonical tier table to avoid noisy re-imports on every bootstrap.
|
||||
if ! table_exists "acore_world" "custom_ej_tier"; then
|
||||
import_sql_file "acore_world" "$schema_file"
|
||||
else
|
||||
log "custom_ej_* schema already present, skipping"
|
||||
fi
|
||||
|
||||
# Sample data only on a fresh install (no rows in any custom_ej_* table).
|
||||
# Re-running it would clobber hand-edited rows in the 9000-9999 id range.
|
||||
if [[ -f "$sample_file" ]]; then
|
||||
local total
|
||||
total="$(mysql_query "acore_world" "
|
||||
SELECT (SELECT COUNT(*) FROM custom_ej_tier)
|
||||
+ (SELECT COUNT(*) FROM custom_ej_instance)
|
||||
+ (SELECT COUNT(*) FROM custom_ej_encounter)
|
||||
+ (SELECT COUNT(*) FROM custom_ej_creature)
|
||||
+ (SELECT COUNT(*) FROM custom_ej_section)
|
||||
+ (SELECT COUNT(*) FROM custom_ej_loot);
|
||||
")"
|
||||
|
||||
if [[ "$total" == "0" ]]; then
|
||||
import_sql_file "acore_world" "$sample_file"
|
||||
else
|
||||
log "custom_ej_* tables already populated, skipping sample data"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
if (($# > 0)); then
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
@@ -250,6 +294,7 @@ fi
|
||||
|
||||
require_integer_flag "ACORE_IMPORT_MYTHICPLUS_SQL" "$IMPORT_MYTHICPLUS_SQL"
|
||||
require_integer_flag "ACORE_IMPORT_STORE_SQL" "$IMPORT_STORE_SQL"
|
||||
require_integer_flag "ACORE_IMPORT_ENCOUNTER_JOURNAL_SQL" "$IMPORT_ENCOUNTER_JOURNAL_SQL"
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
@@ -260,6 +305,7 @@ fi
|
||||
|
||||
import_mythicplus_sql
|
||||
import_aoe_loot_sql
|
||||
import_encounter_journal_sql
|
||||
import_store_sql
|
||||
|
||||
log "custom SQL bootstrap completed"
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
This module is part of the AzerothCore project and is distributed under the
|
||||
terms of the GNU General Public License v2 (GPLv2). See the AzerothCore LICENSE
|
||||
file for the full license text:
|
||||
|
||||
https://github.com/azerothcore/azerothcore-wotlk/blob/master/LICENSE
|
||||
@@ -0,0 +1,191 @@
|
||||
# mod-encounter-journal
|
||||
|
||||
Source-of-truth for the **MoonWellClient EncounterJournal** addon. Stores
|
||||
all data (tiers, instances, encounters, sections, creatures, loot) in
|
||||
`acore_world` under `custom_ej_*` tables and exports them as JSON / Lua
|
||||
that the client can consume directly. No DBC dependency.
|
||||
|
||||
## Layout
|
||||
|
||||
```text
|
||||
mod-encounter-journal/
|
||||
├── conf/ module configuration
|
||||
├── data/sql/db-world/base/
|
||||
│ ├── 01_encounter_journal_schema.sql table definitions
|
||||
│ └── 02_encounter_journal_sample.sql sample data (Deadmines, Naxxramas)
|
||||
├── data/sql/db-world/updates/ future incremental migrations
|
||||
├── docs/encounter_journal_format.md output structure reference
|
||||
├── src/ C++ implementation (manager + commands)
|
||||
└── tools/json_to_lua.py offline JSON → Lua fallback
|
||||
```
|
||||
|
||||
## Tables
|
||||
|
||||
| Table | Purpose |
|
||||
| --------------------------- | ---------------------------------------------------------- |
|
||||
| `custom_ej_tier` | Top-level expansion / content bucket |
|
||||
| `custom_ej_instance` | Dungeon / raid header |
|
||||
| `custom_ej_tier_instance` | N:M tier ↔ instance |
|
||||
| `custom_ej_encounter` | Boss / encounter under an instance |
|
||||
| `custom_ej_creature` | Creature card (portrait, name, description) per encounter |
|
||||
| `custom_ej_section` | Tree of Overview / Abilities sections |
|
||||
| `custom_ej_loot` | Explicit loot table per encounter |
|
||||
|
||||
References to standard AzerothCore tables:
|
||||
|
||||
- `custom_ej_instance.map_id` ↔ `instance_template.map`
|
||||
- `custom_ej_creature.creature_id` ↔ `creature_template.entry`
|
||||
- `custom_ej_loot.item_id` ↔ `item_template.entry`
|
||||
- `custom_ej_creature.creature_display_id` is optional and only used by the
|
||||
client's 3D portrait tab; pull it from
|
||||
`creature_template.modelid1..modelid4` when populating manually.
|
||||
|
||||
## Bitmasks
|
||||
|
||||
### `instance.flags`
|
||||
|
||||
| Bit | Meaning |
|
||||
| ------- | ----------------- |
|
||||
| `0x000` | dungeon (default) |
|
||||
| `0x010` | raid |
|
||||
| `0x040` | hide difficulty |
|
||||
| `0x800` | open world |
|
||||
|
||||
### `encounter.difficulty_mask`
|
||||
|
||||
For dungeons:
|
||||
|
||||
| Bit | Meaning |
|
||||
| ----- | --------------- |
|
||||
| `0x1` | normal |
|
||||
| `0x2` | heroic |
|
||||
| `0x4` | mythic (custom) |
|
||||
|
||||
For raids:
|
||||
|
||||
| Bit | Meaning |
|
||||
| ----- | --------- |
|
||||
| `0x1` | 10 normal |
|
||||
| `0x2` | 25 normal |
|
||||
| `0x4` | 10 heroic |
|
||||
| `0x8` | 25 heroic |
|
||||
|
||||
### `section.flags`
|
||||
|
||||
| Bit | Meaning |
|
||||
| ------ | ------------ |
|
||||
| `0x1` | starts open |
|
||||
| `0x2` | heroic only |
|
||||
|
||||
## Console / GM commands
|
||||
|
||||
All commands require **administrator** rights, except `info` which is
|
||||
gamemaster-only. They run from the in-game chat **and** the worldserver
|
||||
console.
|
||||
|
||||
| Command | What it does |
|
||||
| ---------------- | ---------------------------------------------------------------------------- |
|
||||
| `.ej info` | Print row counts of the in-memory cache. |
|
||||
| `.ej reload` | Re-read all `custom_ej_*` tables. |
|
||||
| `.ej validate` | Run integrity checks (see "Validation" below). |
|
||||
| `.ej importloot` | Auto-populate `custom_ej_loot` from `creature_loot_template` (skips dupes). |
|
||||
| `.ej export` | Write `EncounterJournalData.json` (and `.lua`) to the configured path. |
|
||||
|
||||
## Configuration
|
||||
|
||||
`conf/mod_encounter_journal.conf.dist` (rename to `.conf` to override):
|
||||
|
||||
| Option | Default | Description |
|
||||
| ----------------------------------- | ------------------------------ | --------------------------------------------------------------------------------------------- |
|
||||
| `EncounterJournal.Enable` | `1` | Master switch. |
|
||||
| `EncounterJournal.Export.Path` | (current working dir) | Output directory; must exist & be writable. |
|
||||
| `EncounterJournal.Export.JsonFile` | `EncounterJournalData.json` | JSON filename. |
|
||||
| `EncounterJournal.Export.LuaFile` | `EncounterJournalData.lua` | Lua filename. Empty string skips Lua generation. |
|
||||
| `EncounterJournal.Export.LuaGlobal` | `MoonWellEncounterJournalData` | Name of the Lua global the addon reads. |
|
||||
| `EncounterJournal.ExportOnStart` | `0` | When `1`, exports JSON/Lua right after startup load (useful for headless docker without TTY). |
|
||||
|
||||
## Output format
|
||||
|
||||
The exporter writes the following top-level structure (full per-row layout
|
||||
in [`docs/encounter_journal_format.md`](docs/encounter_journal_format.md)):
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"tiers": [ /* [id, name, flags, sort_order] */ ],
|
||||
"instances": { /* "instance_id": [...] */ },
|
||||
"tierInstances": { /* "tier_id": [instance_id, ...] */ },
|
||||
"encounters": { /* "instance_id": [[...], [...]] */ },
|
||||
"creatures": { /* "encounter_id": [[...], [...]] */ },
|
||||
"sections": { /* "section_id": [...] */ },
|
||||
"items": { /* "encounter_id": [[...], [...]] */ }
|
||||
}
|
||||
```
|
||||
|
||||
Field order inside each row matches the layout the MoonWellClient
|
||||
EncounterJournal addon expects, so the Lua file can be loaded directly:
|
||||
|
||||
```lua
|
||||
-- in your addon Init() or once on load
|
||||
local data = MoonWellEncounterJournalData
|
||||
for _, tier in ipairs(data.tiers) do
|
||||
-- tier[1]=id, tier[2]=name, tier[3]=flags, tier[4]=sort_order
|
||||
end
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Edit data** — INSERT/UPDATE rows in `custom_ej_*` tables.
|
||||
2. **Validate** — `.ej validate` (no errors expected).
|
||||
3. **Export** — `.ej export` (writes JSON + Lua to `Export.Path`).
|
||||
4. **Ship** — copy `EncounterJournalData.lua` into the
|
||||
MoonWellClient EncounterJournal addon directory.
|
||||
|
||||
For loot, you can shortcut step 1:
|
||||
|
||||
1. Insert the encounter and a `custom_ej_creature` row with `creature_id`
|
||||
set to the boss NPC id.
|
||||
2. Run `.ej importloot` — entries from `creature_loot_template` (and one
|
||||
level of `reference_loot_template`) are inserted into `custom_ej_loot`
|
||||
with `difficulty_mask` copied from the creature card. Existing rows are
|
||||
skipped via the unique key.
|
||||
3. Manually clean up: drop trash items, set `faction_mask` /
|
||||
`class_mask` / `sort_order` where needed.
|
||||
4. Re-run `.ej validate` and `.ej export`.
|
||||
|
||||
## Validation
|
||||
|
||||
`.ej validate` verifies:
|
||||
|
||||
- every `tier_instance.tier_id` and `tier_instance.instance_id` exists;
|
||||
- every `encounter.instance_id` exists;
|
||||
- every `encounter.first_section_id` exists or equals `0`;
|
||||
- `encounter.difficulty_mask` only uses bits valid for the parent
|
||||
instance type (dungeon → `0x7`, raid → `0xF`);
|
||||
- every `creature.encounter_id` exists;
|
||||
- every `creature.creature_id` (if non-zero) exists in `creature_template`;
|
||||
- every `section.encounter_id` exists;
|
||||
- every `section.parent_section_id` / `next_section_id` /
|
||||
`sub_section_id` (if non-zero) exists;
|
||||
- every `loot.encounter_id` exists;
|
||||
- every `loot.item_id` exists in `item_template`;
|
||||
- no `loot` duplicates by
|
||||
`(encounter_id, item_id, difficulty_mask, faction_mask, class_mask)`.
|
||||
|
||||
`sort_order` stability is enforced by the schema (column has no
|
||||
`AUTO_INCREMENT`/random default, exporter sorts on it).
|
||||
|
||||
## Offline JSON → Lua
|
||||
|
||||
If you produce the JSON outside the worldserver (e.g., from a CI job that
|
||||
talks to MySQL directly), use the bundled converter:
|
||||
|
||||
```sh
|
||||
python3 modules/mod-encounter-journal/tools/json_to_lua.py \
|
||||
--input EncounterJournalData.json \
|
||||
--output EncounterJournalData.lua \
|
||||
--global MoonWellEncounterJournalData
|
||||
```
|
||||
|
||||
The output uses the same field order, key sort, and global name as
|
||||
`.ej export`, so the addon does not care which path produced the file.
|
||||
Whitespace / float formatting may differ slightly.
|
||||
@@ -0,0 +1,83 @@
|
||||
#
|
||||
# This file is part of the AzerothCore project.
|
||||
#
|
||||
|
||||
########################################
|
||||
# mod-encounter-journal configuration
|
||||
########################################
|
||||
|
||||
#
|
||||
# EncounterJournal.Enable
|
||||
# Description: Master switch for the module. When 0 the module
|
||||
# does not register the .ej commands.
|
||||
# Default: 1
|
||||
#
|
||||
|
||||
EncounterJournal.Enable = 1
|
||||
|
||||
#
|
||||
# EncounterJournal.Export.Path
|
||||
# Description: Directory where exported files are written. The
|
||||
# directory must already exist and be writable by the
|
||||
# worldserver process. Trailing slash is optional.
|
||||
# Empty string == current working directory.
|
||||
# Tip (Docker): set to "/azerothcore/env/dist/logs"
|
||||
# so files appear on the host under ./env/dist/logs.
|
||||
# Default: ""
|
||||
#
|
||||
|
||||
EncounterJournal.Export.Path = "/env/dist/logs"
|
||||
|
||||
#
|
||||
# EncounterJournal.Export.JsonFile
|
||||
# Description: Filename of the JSON export written under Export.Path.
|
||||
# Default: "EncounterJournalData.json"
|
||||
#
|
||||
|
||||
EncounterJournal.Export.JsonFile = "EncounterJournalData.json"
|
||||
|
||||
#
|
||||
# EncounterJournal.Export.LuaFile
|
||||
# Description: Filename of the Lua export written under Export.Path.
|
||||
# Set to empty string to skip Lua generation.
|
||||
# Default: "EncounterJournalData.lua"
|
||||
#
|
||||
|
||||
EncounterJournal.Export.LuaFile = "EncounterJournalData.lua"
|
||||
|
||||
#
|
||||
# EncounterJournal.Export.LuaGlobal
|
||||
# Description: Name of the Lua global the .lua file assigns its
|
||||
# payload to. The MoonWellClient EncounterJournal addon
|
||||
# reads this global on load.
|
||||
# Default: "MoonWellEncounterJournalData"
|
||||
#
|
||||
|
||||
EncounterJournal.Export.LuaGlobal = "MoonWellEncounterJournalData"
|
||||
|
||||
#
|
||||
# EncounterJournal.ExportOnStart
|
||||
# Description: When 1, the worldserver writes the JSON/Lua files to
|
||||
# EncounterJournal.Export.Path immediately after the
|
||||
# custom_ej_* tables are loaded at startup. Useful when
|
||||
# the in-game/CLI .ej commands are not available
|
||||
# (CI builds, headless docker without an attached TTY).
|
||||
# Default: 0
|
||||
#
|
||||
|
||||
EncounterJournal.ExportOnStart = 1
|
||||
|
||||
#
|
||||
# EncounterJournal.ImportOnStart
|
||||
# Description: When 1, on startup the worldserver auto-populates
|
||||
# custom_ej_* tables from instance_template,
|
||||
# instance_encounters and creature_template, then runs
|
||||
# the loot importer. Existing rows are kept (INSERT
|
||||
# IGNORE on PK), so manual edits are preserved.
|
||||
# Use this once after a fresh install or after wiping
|
||||
# the auto-imported rows; descriptions stay empty and
|
||||
# must be filled by hand.
|
||||
# Default: 0
|
||||
#
|
||||
|
||||
EncounterJournal.ImportOnStart = 0
|
||||
@@ -0,0 +1,115 @@
|
||||
--
|
||||
-- Encounter Journal: schema for custom_ej_* tables.
|
||||
-- Source of truth for client EncounterJournal data lives here, not in DBC.
|
||||
--
|
||||
-- Tables are CREATE TABLE IF NOT EXISTS so re-running the file is safe.
|
||||
-- DBUpdater re-applies the file when its hash changes.
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `custom_ej_tier` (
|
||||
`id` INT UNSIGNED NOT NULL,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`flags` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`sort_order` INT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
COMMENT='Encounter Journal: tier (expansion/content bucket).';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `custom_ej_instance` (
|
||||
`id` INT UNSIGNED NOT NULL,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`description` TEXT NOT NULL,
|
||||
`button_icon` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`small_button_icon` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`background` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`lore_background` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`map_id` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`area_id` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`world_map_area_id` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`flags` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`sort_order` INT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
COMMENT='Encounter Journal: dungeon/raid/scenario.';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `custom_ej_tier_instance` (
|
||||
`tier_id` INT UNSIGNED NOT NULL,
|
||||
`instance_id` INT UNSIGNED NOT NULL,
|
||||
`sort_order` INT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`tier_id`, `instance_id`),
|
||||
KEY `idx_instance` (`instance_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
COMMENT='Encounter Journal: tier <-> instance N:M.';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `custom_ej_encounter` (
|
||||
`id` INT UNSIGNED NOT NULL,
|
||||
`instance_id` INT UNSIGNED NOT NULL,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`description` TEXT NOT NULL,
|
||||
`map_position_x` FLOAT NOT NULL DEFAULT 0,
|
||||
`map_position_y` FLOAT NOT NULL DEFAULT 0,
|
||||
`floor_index` INT UNSIGNED NOT NULL DEFAULT 1,
|
||||
`world_map_area_id` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`first_section_id` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`difficulty_mask` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`flags` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`sort_order` INT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_instance` (`instance_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
COMMENT='Encounter Journal: boss / encounter.';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `custom_ej_creature` (
|
||||
`id` INT UNSIGNED NOT NULL,
|
||||
`encounter_id` INT UNSIGNED NOT NULL,
|
||||
`creature_id` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`description` TEXT NOT NULL,
|
||||
`creature_display_id` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`icon` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`difficulty_mask` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`sort_order` INT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_encounter` (`encounter_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
COMMENT='Encounter Journal: creature card under an encounter.';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `custom_ej_section` (
|
||||
`id` INT UNSIGNED NOT NULL,
|
||||
`encounter_id` INT UNSIGNED NOT NULL,
|
||||
`parent_section_id` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`next_section_id` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`sub_section_id` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`description` TEXT NOT NULL,
|
||||
`creature_display_id` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`description_spell_id` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`icon_spell_id` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`icon` VARCHAR(255) NULL,
|
||||
`flags` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`icon_flags` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`sort_order` INT NOT NULL DEFAULT 0,
|
||||
`type` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`difficulty_mask` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`creature_id` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_encounter` (`encounter_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
COMMENT='Encounter Journal: encounter section (Overview/Abilities tree).';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `custom_ej_loot` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`encounter_id` INT UNSIGNED NOT NULL,
|
||||
`item_id` INT UNSIGNED NOT NULL,
|
||||
`difficulty_mask` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`faction_mask` INT NOT NULL DEFAULT -1,
|
||||
`class_mask` INT NOT NULL DEFAULT -1,
|
||||
`flags` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`sort_order` INT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_encounter` (`encounter_id`),
|
||||
KEY `idx_item` (`item_id`),
|
||||
UNIQUE KEY `uk_loot_dedup`
|
||||
(`encounter_id`, `item_id`, `difficulty_mask`, `faction_mask`, `class_mask`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
COMMENT='Encounter Journal: explicit loot table per encounter.';
|
||||
@@ -0,0 +1,179 @@
|
||||
--
|
||||
-- Encounter Journal: minimal sample data.
|
||||
-- Showcases one dungeon (Deadmines, map 36) and one raid (Naxxramas, map 533),
|
||||
-- with one encounter each, a creature card, a section tree (overview + 1 ability)
|
||||
-- and a couple of loot rows. Loot item_ids reference real WotLK item_template entries.
|
||||
--
|
||||
-- IDs used by the sample stay above 9000 to avoid collisions with future imports.
|
||||
--
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
DELETE FROM `custom_ej_loot` WHERE `encounter_id` BETWEEN 9000 AND 9999;
|
||||
DELETE FROM `custom_ej_section` WHERE `encounter_id` BETWEEN 9000 AND 9999;
|
||||
DELETE FROM `custom_ej_creature` WHERE `encounter_id` BETWEEN 9000 AND 9999;
|
||||
DELETE FROM `custom_ej_encounter` WHERE `id` BETWEEN 9000 AND 9999;
|
||||
DELETE FROM `custom_ej_tier_instance` WHERE `instance_id` BETWEEN 9000 AND 9999;
|
||||
DELETE FROM `custom_ej_instance` WHERE `id` BETWEEN 9000 AND 9999;
|
||||
DELETE FROM `custom_ej_tier` WHERE `id` BETWEEN 9000 AND 9999;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Tiers
|
||||
-- ---------------------------------------------------------------------------
|
||||
INSERT INTO `custom_ej_tier` (`id`, `name`, `flags`, `sort_order`) VALUES
|
||||
(9001, 'Classic', 0, 10),
|
||||
(9002, 'Wrath of the Lich King', 0, 20);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Dungeon: The Deadmines (map 36)
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- instance.flags = 0 (dungeon)
|
||||
INSERT INTO `custom_ej_instance`
|
||||
(`id`, `name`, `description`, `button_icon`, `small_button_icon`,
|
||||
`background`, `lore_background`, `map_id`, `area_id`, `world_map_area_id`,
|
||||
`flags`, `sort_order`)
|
||||
VALUES
|
||||
(9001, 'The Deadmines',
|
||||
'Beneath the quiet town of Moonbrook lies the Defias Brotherhood''s '
|
||||
'shipyard, where Edwin VanCleef forges his revenge against the city of '
|
||||
'Stormwind.',
|
||||
'Interface\\EncounterJournal\\UI-EJ-DUNGEONBUTTON-DEADMINES',
|
||||
'Interface\\EncounterJournal\\UI-EJ-SMALLBUTTON-DEADMINES',
|
||||
'Interface\\EncounterJournal\\UI-EJ-DUNGEON-DEADMINES',
|
||||
'',
|
||||
36, 1581, 0,
|
||||
0, 10);
|
||||
|
||||
INSERT INTO `custom_ej_tier_instance` (`tier_id`, `instance_id`, `sort_order`)
|
||||
VALUES (9001, 9001, 10);
|
||||
|
||||
-- Encounter: Edwin VanCleef
|
||||
INSERT INTO `custom_ej_encounter`
|
||||
(`id`, `instance_id`, `name`, `description`,
|
||||
`map_position_x`, `map_position_y`, `floor_index`, `world_map_area_id`,
|
||||
`first_section_id`, `difficulty_mask`, `flags`, `sort_order`)
|
||||
VALUES
|
||||
(9001, 9001, 'Edwin VanCleef',
|
||||
'Once a master stonemason, VanCleef built much of postwar Stormwind. '
|
||||
'Cheated of his pay, he founded the Defias Brotherhood to take revenge.',
|
||||
0.85, 0.65, 1, 0,
|
||||
9001, 0x1, 0, 100);
|
||||
|
||||
-- Creature card
|
||||
INSERT INTO `custom_ej_creature`
|
||||
(`id`, `encounter_id`, `creature_id`, `name`, `description`,
|
||||
`creature_display_id`, `icon`, `difficulty_mask`, `sort_order`)
|
||||
VALUES
|
||||
(9001, 9001, 639, 'Edwin VanCleef',
|
||||
'Leader of the Defias Brotherhood.',
|
||||
822, '', 0x1, 10);
|
||||
|
||||
-- Sections
|
||||
-- Tree: 9001 (Overview) -> next 9002 (Abilities)
|
||||
-- sub 9003 (Strike)
|
||||
INSERT INTO `custom_ej_section`
|
||||
(`id`, `encounter_id`, `parent_section_id`, `next_section_id`,
|
||||
`sub_section_id`, `name`, `description`,
|
||||
`creature_display_id`, `description_spell_id`, `icon_spell_id`,
|
||||
`icon`, `flags`, `icon_flags`, `sort_order`, `type`,
|
||||
`difficulty_mask`, `creature_id`)
|
||||
VALUES
|
||||
(9001, 9001, 0, 9002, 0,
|
||||
'Overview',
|
||||
'VanCleef calls in waves of his lieutenants before engaging the party '
|
||||
'directly. Stay mobile and interrupt his casts.',
|
||||
0, 0, 0, NULL, 0x1, 0, 10, 0, 0x1, 639),
|
||||
|
||||
(9002, 9001, 0, 0, 9003,
|
||||
'Abilities',
|
||||
'', 0, 0, 0, NULL, 0, 0, 20, 0, 0x1, 639),
|
||||
|
||||
(9003, 9001, 9002, 0, 0,
|
||||
'Strike',
|
||||
'A heavy melee strike on VanCleef''s current target.',
|
||||
0, 6975, 6975, NULL, 0, 0, 10, 0, 0x1, 639);
|
||||
|
||||
-- Loot (real WotLK items): Cruel Barb (mh sword), Buccaneer''s Bracers
|
||||
INSERT INTO `custom_ej_loot`
|
||||
(`encounter_id`, `item_id`, `difficulty_mask`, `faction_mask`,
|
||||
`class_mask`, `flags`, `sort_order`)
|
||||
VALUES
|
||||
(9001, 1937, 0x1, -1, -1, 0, 10), -- Cruel Barb
|
||||
(9001, 2169, 0x1, -1, -1, 0, 20); -- Buccaneer''s Bracers
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Raid: Naxxramas (map 533)
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- instance.flags = 0x10 (raid)
|
||||
INSERT INTO `custom_ej_instance`
|
||||
(`id`, `name`, `description`, `button_icon`, `small_button_icon`,
|
||||
`background`, `lore_background`, `map_id`, `area_id`, `world_map_area_id`,
|
||||
`flags`, `sort_order`)
|
||||
VALUES
|
||||
(9101, 'Naxxramas',
|
||||
'Necropolis of the lich Kel''Thuzad, anchored above the Dragonblight. '
|
||||
'Liberated and reactivated as a fortress of the Scourge.',
|
||||
'Interface\\EncounterJournal\\UI-EJ-RAIDBUTTON-NAXXRAMAS',
|
||||
'Interface\\EncounterJournal\\UI-EJ-SMALLBUTTON-NAXXRAMAS',
|
||||
'Interface\\EncounterJournal\\UI-EJ-RAID-NAXXRAMAS',
|
||||
'',
|
||||
533, 3456, 0,
|
||||
0x10, 10);
|
||||
|
||||
INSERT INTO `custom_ej_tier_instance` (`tier_id`, `instance_id`, `sort_order`)
|
||||
VALUES (9002, 9101, 10);
|
||||
|
||||
-- Encounter: Patchwerk (10/25, normal/heroic combinations -> mask 0xF)
|
||||
INSERT INTO `custom_ej_encounter`
|
||||
(`id`, `instance_id`, `name`, `description`,
|
||||
`map_position_x`, `map_position_y`, `floor_index`, `world_map_area_id`,
|
||||
`first_section_id`, `difficulty_mask`, `flags`, `sort_order`)
|
||||
VALUES
|
||||
(9101, 9101, 'Patchwerk',
|
||||
'Kel''Thuzad''s favored creation: an abomination stitched from the '
|
||||
'corpses of more than a hundred victims.',
|
||||
0.40, 0.50, 1, 0,
|
||||
9101, 0xF, 0, 100);
|
||||
|
||||
INSERT INTO `custom_ej_creature`
|
||||
(`id`, `encounter_id`, `creature_id`, `name`, `description`,
|
||||
`creature_display_id`, `icon`, `difficulty_mask`, `sort_order`)
|
||||
VALUES
|
||||
(9101, 9101, 16028, 'Patchwerk',
|
||||
'Kel''Thuzad''s flagship abomination.',
|
||||
23323, '', 0xF, 10);
|
||||
|
||||
INSERT INTO `custom_ej_section`
|
||||
(`id`, `encounter_id`, `parent_section_id`, `next_section_id`,
|
||||
`sub_section_id`, `name`, `description`,
|
||||
`creature_display_id`, `description_spell_id`, `icon_spell_id`,
|
||||
`icon`, `flags`, `icon_flags`, `sort_order`, `type`,
|
||||
`difficulty_mask`, `creature_id`)
|
||||
VALUES
|
||||
(9101, 9101, 0, 9102, 0,
|
||||
'Overview',
|
||||
'Tank-and-spank with a fixed enrage timer. Two well-geared tanks must '
|
||||
'soak Hateful Strikes; healers run hot from the start.',
|
||||
0, 0, 0, NULL, 0x1, 0, 10, 0, 0xF, 16028),
|
||||
|
||||
(9102, 9101, 0, 0, 9103,
|
||||
'Abilities',
|
||||
'', 0, 0, 0, NULL, 0, 0, 20, 0, 0xF, 16028),
|
||||
|
||||
(9103, 9101, 9102, 0, 0,
|
||||
'Hateful Strike',
|
||||
'Patchwerk strikes the highest-health target within melee range for '
|
||||
'massive physical damage.',
|
||||
0, 28308, 28308, NULL, 0, 0, 10, 0, 0xF, 16028);
|
||||
|
||||
-- Loot: a few real Patchwerk drops (10-normal mask 0x1, 25-normal mask 0x2)
|
||||
INSERT INTO `custom_ej_loot`
|
||||
(`encounter_id`, `item_id`, `difficulty_mask`, `faction_mask`,
|
||||
`class_mask`, `flags`, `sort_order`)
|
||||
VALUES
|
||||
(9101, 39262, 0x1, -1, -1, 0, 10), -- Stained-Glass Shard of Light
|
||||
(9101, 39263, 0x1, -1, -1, 0, 20), -- Wraith Spear
|
||||
(9101, 40548, 0x2, -1, -1, 0, 30), -- Patchwork Bracers (25-man)
|
||||
(9101, 40549, 0x2, -1, -1, 0, 40); -- Severed Visionary Tendrils (25-man)
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,125 @@
|
||||
# EncounterJournal export format
|
||||
|
||||
This document fixes the field order inside each row of the JSON / Lua
|
||||
export. Same order in JSON arrays and Lua tables; the only difference is
|
||||
the surrounding container syntax.
|
||||
|
||||
## `tiers`
|
||||
|
||||
Array. Stable order: `sort_order ASC, id ASC`.
|
||||
|
||||
```text
|
||||
[ id, name, flags, sort_order ]
|
||||
```
|
||||
|
||||
## `instances` — keyed by `instance_id`
|
||||
|
||||
```text
|
||||
[
|
||||
name,
|
||||
description,
|
||||
button_icon,
|
||||
small_button_icon,
|
||||
background,
|
||||
lore_background,
|
||||
map_id,
|
||||
area_id,
|
||||
sort_order,
|
||||
flags,
|
||||
id,
|
||||
world_map_area_id
|
||||
]
|
||||
```
|
||||
|
||||
## `tierInstances` — keyed by `tier_id`
|
||||
|
||||
```text
|
||||
[ instance_id, instance_id, ... ]
|
||||
```
|
||||
|
||||
Stable order: `tier_instance.sort_order ASC, instance_id ASC`.
|
||||
|
||||
## `encounters` — keyed by `instance_id`
|
||||
|
||||
Array of rows; stable order `sort_order ASC, id ASC`.
|
||||
|
||||
```text
|
||||
[
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
map_position_x,
|
||||
map_position_y,
|
||||
floor_index,
|
||||
world_map_area_id,
|
||||
first_section_id,
|
||||
instance_id,
|
||||
difficulty_mask,
|
||||
flags,
|
||||
sort_order
|
||||
]
|
||||
```
|
||||
|
||||
## `creatures` — keyed by `encounter_id`
|
||||
|
||||
Array of rows; stable order `sort_order ASC, id ASC`.
|
||||
|
||||
```text
|
||||
[
|
||||
name,
|
||||
description,
|
||||
creature_display_id,
|
||||
icon,
|
||||
encounter_id,
|
||||
sort_order,
|
||||
id,
|
||||
creature_id,
|
||||
difficulty_mask
|
||||
]
|
||||
```
|
||||
|
||||
## `sections` — keyed by `section_id`
|
||||
|
||||
Single row per key.
|
||||
|
||||
```text
|
||||
[
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
creature_display_id,
|
||||
description_spell_id,
|
||||
icon_spell_id,
|
||||
encounter_id,
|
||||
next_section_id,
|
||||
sub_section_id,
|
||||
parent_section_id,
|
||||
flags,
|
||||
icon_flags,
|
||||
sort_order,
|
||||
type,
|
||||
difficulty_mask,
|
||||
creature_id
|
||||
]
|
||||
```
|
||||
|
||||
## `items` — keyed by `encounter_id`
|
||||
|
||||
Array of rows; stable order `sort_order ASC, id ASC`.
|
||||
|
||||
```text
|
||||
[
|
||||
item_id,
|
||||
encounter_id,
|
||||
difficulty_mask,
|
||||
faction_mask,
|
||||
flags,
|
||||
id,
|
||||
class_mask
|
||||
]
|
||||
```
|
||||
|
||||
> Item name / icon / quality are intentionally **not** part of this
|
||||
> payload. The MoonWellClient EncounterJournal addon resolves them via
|
||||
> the regular `GetItemInfo` API; if you need an offline cache, generate
|
||||
> a separate `ItemCache.lua` from `item_template`.
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env bash
|
||||
MOD_ENCOUNTER_JOURNAL_ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/" && pwd )"
|
||||
@@ -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
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Convert an EncounterJournal JSON dump into a Lua file the
|
||||
MoonWellClient EncounterJournal addon can `loadstring`/`include`.
|
||||
|
||||
Field order, key sort and structure mirror what the C++ exporter
|
||||
in mod-encounter-journal writes via .ej export. Use this when the
|
||||
JSON is produced outside the worldserver (e.g., from a CI job that
|
||||
queries acore_world directly).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
|
||||
def lua_str(value: Any) -> str:
|
||||
s = str(value)
|
||||
s = s.replace("\\", "\\\\")
|
||||
s = s.replace("'", "\\'")
|
||||
s = s.replace("\n", "\\n")
|
||||
s = s.replace("\r", "\\r")
|
||||
return f"'{s}'"
|
||||
|
||||
|
||||
def lua_value(value: Any) -> str:
|
||||
if isinstance(value, bool):
|
||||
return "true" if value else "false"
|
||||
if isinstance(value, (int, float)):
|
||||
# Integers stay int; floats use repr for round-trip stability.
|
||||
if isinstance(value, float) and value.is_integer():
|
||||
return str(int(value))
|
||||
return repr(value) if isinstance(value, float) else str(value)
|
||||
if value is None:
|
||||
return "nil"
|
||||
return lua_str(value)
|
||||
|
||||
|
||||
def lua_array(values: Iterable[Any]) -> str:
|
||||
return "{ " + ", ".join(lua_value(v) for v in values) + " }"
|
||||
|
||||
|
||||
def emit_keyed_rows(out: list[str], name: str, mapping: dict, indent: str) -> None:
|
||||
out.append(f"{indent}{name} = {{\n")
|
||||
for key in sorted(mapping.keys(), key=lambda k: int(k)):
|
||||
rows = mapping[key]
|
||||
if rows and isinstance(rows[0], list):
|
||||
out.append(f"{indent} [{int(key)}] = {{\n")
|
||||
for row in rows:
|
||||
out.append(f"{indent} {lua_array(row)},\n")
|
||||
out.append(f"{indent} }},\n")
|
||||
else:
|
||||
# Single row OR flat list of ids (tierInstances).
|
||||
out.append(f"{indent} [{int(key)}] = {lua_array(rows)},\n")
|
||||
out.append(f"{indent}}},\n")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--input", "-i", required=True,
|
||||
help="Path to EncounterJournalData.json")
|
||||
parser.add_argument("--output", "-o", required=True,
|
||||
help="Path to write EncounterJournalData.lua")
|
||||
parser.add_argument("--global", "-g", dest="global_name",
|
||||
default="MoonWellEncounterJournalData",
|
||||
help="Name of the Lua global to assign")
|
||||
args = parser.parse_args()
|
||||
|
||||
src = Path(args.input)
|
||||
if not src.exists():
|
||||
print(f"input file not found: {src}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
with src.open("r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
out: list[str] = []
|
||||
out.append("-- Generated by mod-encounter-journal/tools/json_to_lua.py.\n")
|
||||
out.append("-- Source of truth: custom_ej_* tables in acore_world.\n\n")
|
||||
out.append(f"{args.global_name} = {{\n")
|
||||
|
||||
# tiers (flat array)
|
||||
out.append(" tiers = {\n")
|
||||
for tier in data.get("tiers", []):
|
||||
out.append(f" {lua_array(tier)},\n")
|
||||
out.append(" },\n")
|
||||
|
||||
# instances : map id -> single row
|
||||
instances = data.get("instances", {})
|
||||
out.append(" instances = {\n")
|
||||
for key in sorted(instances.keys(), key=lambda k: int(k)):
|
||||
out.append(f" [{int(key)}] = {lua_array(instances[key])},\n")
|
||||
out.append(" },\n")
|
||||
|
||||
# tierInstances : map tier_id -> [instance_id, ...]
|
||||
emit_keyed_rows(out, "tierInstances", data.get("tierInstances", {}), " ")
|
||||
|
||||
# encounters : map instance_id -> [[row], ...]
|
||||
emit_keyed_rows(out, "encounters", data.get("encounters", {}), " ")
|
||||
|
||||
# creatures : map encounter_id -> [[row], ...]
|
||||
emit_keyed_rows(out, "creatures", data.get("creatures", {}), " ")
|
||||
|
||||
# sections : map section_id -> single row
|
||||
sections = data.get("sections", {})
|
||||
out.append(" sections = {\n")
|
||||
for key in sorted(sections.keys(), key=lambda k: int(k)):
|
||||
out.append(f" [{int(key)}] = {lua_array(sections[key])},\n")
|
||||
out.append(" },\n")
|
||||
|
||||
# items : map encounter_id -> [[row], ...]
|
||||
emit_keyed_rows(out, "items", data.get("items", {}), " ")
|
||||
|
||||
out.append("}\n")
|
||||
|
||||
Path(args.output).write_text("".join(out), encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user