6 Commits

63 changed files with 239401 additions and 1105 deletions
+27
View File
@@ -0,0 +1,27 @@
{
"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$\")",
"Bash(python3 /home/sindo/moonwell-core/modules/mod-encounter-journal/tools/build_item_cache.py -j /home/sindo/moonwell-core/env/dist/logs/EncounterJournalData.json -d /home/sindo/moonwell-core/Generated_ItemsCache1.lua /home/sindo/moonwell-core/Generated_ItemsCache2.lua /home/sindo/moonwell-core/Generated_ItemsCache3.lua -o /tmp/EncounterJournalItemCache.lua)",
"Bash(awk 'NR>1 && !/^\\\\s*\\\\[[0-9]+\\\\]\\\\s*=\\\\s*\\\\{.*\\\\},?\\\\s*$/ && !/^\\\\}$/' /home/sindo/moonwell-core/Generated_ItemsCache3.lua)",
"Bash(awk '/^\\\\s*\\\\[[0-9]+\\\\]\\\\s*=\\\\s*\\\\{/' /home/sindo/moonwell-core/Generated_ItemsCache3.lua)",
"Bash(grep -n \"^[A-Z_]* = {$\\\\|^[A-Z_]* = {\" /home/sindo/moonwell-core/Generated_EncounterJournal.lua)",
"Bash(python3 /home/sindo/moonwell-core/modules/mod-encounter-journal/tools/import_donor_journal.py -i /home/sindo/moonwell-core/Generated_EncounterJournal.lua -o /tmp/donor_import.sql)",
"Bash(python3 /home/sindo/moonwell-core/modules/mod-encounter-journal/tools/import_donor_journal.py -i /home/sindo/moonwell-core/Generated_EncounterJournal.lua -o /tmp/donor_import_filtered.sql --known-maps /tmp/known_maps.txt --known-creatures /tmp/known_creatures.txt --known-items /tmp/known_items.txt)",
"Bash(sudo grep -c \"Encounter Journal loaded\\\\|mod-encounter-journal\" /var/lib/docker/containers/8a3d7a23f170597b5a8eae754ef3259ac6f2af352d78958707ef5cf3010133e3/8a3d7a23f170597b5a8eae754ef3259ac6f2af352d78958707ef5cf3010133e3-json.log)",
"Bash(xargs -I {} stat -c '%y %n' {})",
"Bash(grep -n \"INSERT INTO \\\\`custom_ej_instance\\\\`\" /home/sindo/moonwell-core/modules/mod-encounter-journal/data/sql/db-world/base/02_encounter_journal_sample.sql)"
]
}
}
-1
View File
@@ -108,4 +108,3 @@ local.properties
# !modules/yourmodule
#
# ==================
.claude
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+10
View File
@@ -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
@@ -1,45 +0,0 @@
-- MySQL dump 10.13 Distrib 8.4.3, for Win64 (x86_64)
--
-- Host: localhost Database: acore_characters
-- ------------------------------------------------------
-- Server version 8.4.3
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!50503 SET NAMES utf8mb4 */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `mod_gamemodes_characters`
--
DROP TABLE IF EXISTS `mod_gamemodes_characters`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `mod_gamemodes_characters` (
`guid` int unsigned NOT NULL,
`game_mode` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '0 = NORMAL, 1 = TRAITOR',
PRIMARY KEY (`guid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Character Game Modes';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `mod_gamemodes_characters`
--
LOCK TABLES `mod_gamemodes_characters` WRITE;
/*!40000 ALTER TABLE `mod_gamemodes_characters` DISABLE KEYS */;
/*!40000 ALTER TABLE `mod_gamemodes_characters` ENABLE KEYS */;
UNLOCK TABLES;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+46
View File
@@ -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"
-86
View File
@@ -1,86 +0,0 @@
local AIO = AIO or require("AIO")
local ADDON_NAME = "MoonWellClient"
local HANDLER_NAME = "Init"
local GAME_MODE_NORMAL = 0
local GAME_MODE_TRAITOR = 1
local PLAYER_EVENT_ON_LOGIN = 3
local AIO_INIT_SEND_DELAY_MS = 1000
local MoonWellAIO = rawget(_G, "MoonWellAIO") or {}
_G.MoonWellAIO = MoonWellAIO
local PendingSyncEvents = {}
local function GetPlayerGameMode(player)
if not player then
return GAME_MODE_NORMAL
end
local query = CharDBQuery(
"SELECT game_mode FROM mod_gamemodes_characters WHERE guid = " .. player:GetGUIDLow() .. " LIMIT 1"
)
if not query or query:IsNull(0) then
return GAME_MODE_NORMAL
end
local gameMode = query:GetUInt8(0)
if gameMode == GAME_MODE_TRAITOR then
return GAME_MODE_TRAITOR
end
return GAME_MODE_NORMAL
end
function MoonWellAIO.GetPlayerGameMode(player)
return GetPlayerGameMode(player)
end
function MoonWellAIO.SendPlayerGameMode(player)
if not player then
return GAME_MODE_NORMAL
end
local gameMode = GetPlayerGameMode(player)
AIO.Handle(player, ADDON_NAME, HANDLER_NAME, gameMode)
return gameMode
end
local function CancelPendingSync(playerGuidLow)
local eventId = PendingSyncEvents[playerGuidLow]
if eventId then
RemoveEventById(eventId)
PendingSyncEvents[playerGuidLow] = nil
end
end
function MoonWellAIO.SchedulePlayerGameMode(player, delayMs)
if not player then
return
end
local playerGuidLow = player:GetGUIDLow()
local playerGuid = player:GetGUID()
CancelPendingSync(playerGuidLow)
PendingSyncEvents[playerGuidLow] = CreateLuaEvent(function()
PendingSyncEvents[playerGuidLow] = nil
local onlinePlayer = GetPlayerByGUID(playerGuid)
if onlinePlayer then
MoonWellAIO.SendPlayerGameMode(onlinePlayer)
end
end, delayMs or AIO_INIT_SEND_DELAY_MS, 1)
end
-- Do not append MoonWellClient blocks directly into the AIO init packet:
-- the client addon may register its handlers slightly later than AIO itself.
AIO.AddOnInit(function(msg, player)
MoonWellAIO.SchedulePlayerGameMode(player, AIO_INIT_SEND_DELAY_MS)
return msg
end)
RegisterPlayerEvent(PLAYER_EVENT_ON_LOGIN, function(_, player)
MoonWellAIO.SchedulePlayerGameMode(player, AIO_INIT_SEND_DELAY_MS)
end)
@@ -1207,9 +1207,6 @@ public:
void OnHeal(Unit* healer, Unit* receiver, uint32& gain) override
{
if (!healer)
return;
if (healer->IsPlayer())
sEluna->OnPlayerHeal(healer->ToPlayer(), receiver, gain);
@@ -1219,9 +1216,6 @@ public:
void OnDamage(Unit* attacker, Unit* receiver, uint32& damage) override
{
if (!attacker)
return;
if (attacker->IsPlayer())
sEluna->OnPlayerDamage(attacker->ToPlayer(), receiver, damage);
+5
View File
@@ -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
+195
View File
@@ -0,0 +1,195 @@
# 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 |
| ---------------- | ---------------------------------------------------------------------------- |
| 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 importskeleton` | Generate instance/encounter/creature/section rows from `instance_template` + `instance_encounters`. |
| `.ej importloot` | Auto-populate `custom_ej_loot` from `creature_loot_template` (skips dupes). |
| `.ej importall` | Run `importskeleton` then `importloot` back-to-back. |
| `.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`.
+2
View File
@@ -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,164 @@
#!/usr/bin/env python3
"""
Build a trimmed item cache for the MoonWellClient EncounterJournal addon.
Reads one or more donor cache files (Sirus-style Generated_ItemsCache*.lua,
each holding rows like `[id] = { "name_en", "name_ru", quality, ..., "icon",
... }`), intersects them with the item ids referenced by
EncounterJournalData.json (the file produced by .ej export) and emits a
Lua file containing only the matching rows.
Output schema (single global, Lua):
MoonWellEncounterJournalItemCache = {
[<item_id>] = { name_en, name_ru, quality, icon },
...
}
Multiple donor files are merged left-to-right; later files override earlier
ones for duplicate ids. Items missing from every donor are reported on
stderr and skipped (the client falls back to GetItemInfo for those).
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
from typing import Dict, Iterable, Set, Tuple
# Row layout per Sirus convention:
# [id] = { "name_en", "name_ru", quality, classId, subClass,
# inventoryType, slot, stackable, idx, "icon", spellId },
ROW_RE = re.compile(
r"""^\s*\[(?P<id>\d+)\]\s*=\s*\{\s*
"(?P<en>(?:[^"\\]|\\.)*)"\s*,\s*
"(?P<ru>(?:[^"\\]|\\.)*)"\s*,\s*
(?P<quality>-?\d+)\s*,\s*
-?\d+\s*,\s* # field 4
-?\d+\s*,\s* # field 5
-?\d+\s*,\s* # field 6
-?\d+\s*,\s* # field 7
-?\d+\s*,\s* # field 8
-?\d+\s*,\s* # field 9
"(?P<icon>(?:[^"\\]|\\.)*)"\s*,\s*
-?\d+\s*
\}\s*,?\s*$""",
re.VERBOSE,
)
def parse_donor(path: Path) -> Dict[int, Tuple[str, str, int, str]]:
"""Returns {id: (name_en, name_ru, quality, icon)} from one donor file."""
out: Dict[int, Tuple[str, str, int, str]] = {}
bad = 0
with path.open("r", encoding="utf-8") as f:
for line in f:
m = ROW_RE.match(line)
if not m:
stripped = line.strip()
if stripped and not stripped.startswith(
("--", "Items", "ItemsCache", "}", "{")
):
bad += 1
continue
out[int(m.group("id"))] = (
m.group("en"),
m.group("ru"),
int(m.group("quality")),
m.group("icon"),
)
if bad:
print(f"[{path.name}] skipped {bad} unparseable lines", file=sys.stderr)
return out
def collect_needed_ids(journal_json: Path) -> Set[int]:
"""Returns the set of item ids referenced in EncounterJournalData.json."""
with journal_json.open("r", encoding="utf-8") as f:
data = json.load(f)
needed: Set[int] = set()
for rows in data.get("items", {}).values():
for row in rows:
# items[encounterID] row layout per docs/encounter_journal_format.md:
# [item_id, encounter_id, difficulty_mask, faction_mask, flags,
# id, class_mask]
if row:
needed.add(int(row[0]))
return needed
def lua_str(s: str) -> str:
s = s.replace("\\", "\\\\").replace("'", "\\'")
s = s.replace("\n", "\\n").replace("\r", "\\r")
return f"'{s}'"
def write_lua(out_path: Path, global_name: str,
cache: Dict[int, Tuple[str, str, int, str]]) -> None:
with out_path.open("w", encoding="utf-8") as f:
f.write("-- Generated by mod-encounter-journal/tools/"
"build_item_cache.py.\n")
f.write("-- Trimmed donor item cache for items referenced by "
"EncounterJournalData.\n\n")
f.write(f"{global_name} = {{\n")
for item_id in sorted(cache.keys()):
en, ru, quality, icon = cache[item_id]
f.write(
f" [{item_id}] = {{ {lua_str(en)}, {lua_str(ru)}, "
f"{quality}, {lua_str(icon)} }},\n"
)
f.write("}\n")
def merge_donors(donor_paths: Iterable[Path]
) -> Dict[int, Tuple[str, str, int, str]]:
merged: Dict[int, Tuple[str, str, int, str]] = {}
for p in donor_paths:
rows = parse_donor(p)
print(f"[{p.name}] parsed {len(rows)} rows", file=sys.stderr)
merged.update(rows)
return merged
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--journal", "-j", required=True, type=Path,
help="EncounterJournalData.json (from .ej export)")
parser.add_argument("--donor", "-d", required=True, type=Path, nargs="+",
help="One or more Generated_ItemsCache*.lua files")
parser.add_argument("--output", "-o", required=True, type=Path,
help="Where to write EncounterJournalItemCache.lua")
parser.add_argument("--global", dest="global_name",
default="MoonWellEncounterJournalItemCache",
help="Lua global name (default: %(default)s)")
args = parser.parse_args()
needed = collect_needed_ids(args.journal)
print(f"EJ references {len(needed)} unique item ids", file=sys.stderr)
donors = merge_donors(args.donor)
print(f"donor cache holds {len(donors)} unique item ids", file=sys.stderr)
trimmed = {i: donors[i] for i in needed if i in donors}
missing = sorted(needed - donors.keys())
write_lua(args.output, args.global_name, trimmed)
print(
f"wrote {len(trimmed)} rows to {args.output} "
f"(missing in donor: {len(missing)})",
file=sys.stderr,
)
if missing:
sample = ", ".join(str(i) for i in missing[:15])
more = "" if len(missing) <= 15 else f" ... and {len(missing) - 15} more"
print(f"missing item ids (client will fall back to GetItemInfo): "
f"{sample}{more}", file=sys.stderr)
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,577 @@
#!/usr/bin/env python3
"""
Import a Sirus-style Generated_EncounterJournal.lua dump into the
mod-encounter-journal custom_ej_* tables.
The dump holds seven top-level Lua tables that map 1:1 to our schema:
JOURNALINSTANCE -> custom_ej_instance
JOURNALENCOUNTER -> custom_ej_encounter
JOURNALENCOUNTERCREATURE -> custom_ej_creature
JOURNALENCOUNTERSECTION -> custom_ej_section
JOURNALENCOUNTERITEM -> custom_ej_loot
JOURNALTIER -> custom_ej_tier
JOURNALTIERXINSTANCE -> custom_ej_tier_instance
The script emits a single .sql file (or writes to stdout) that uses
REPLACE INTO so re-running is safe. Pipe it into your MySQL container:
python3 import_donor_journal.py -i Generated_EncounterJournal.lua \
-o donor_import.sql
docker exec -i ac-database mysql -uroot -p$PASS acore_world \
< donor_import.sql
Each row from the dump goes through a small tokenizer that understands
quoted strings ("..."), long-bracket strings ([[...]]), numbers, and
nested braces. The dump only uses single-line strings, so no multi-line
handling is needed.
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
from typing import Any, Iterable, List, Optional, TextIO, Tuple
# ---------------------------------------------------------------------------
# Tokeniser / value parser
# ---------------------------------------------------------------------------
class LuaParseError(RuntimeError):
pass
def _skip_ws(src: str, pos: int) -> int:
while pos < len(src) and src[pos] in " \t\r\n,":
pos += 1
return pos
def _parse_quoted_string(src: str, pos: int) -> Tuple[str, int]:
assert src[pos] == '"'
pos += 1
out: List[str] = []
while pos < len(src):
c = src[pos]
if c == "\\" and pos + 1 < len(src):
nxt = src[pos + 1]
# Lua escapes: \", \', \\, \n, \r, \t — keep verbatim semantics.
out.append({
'"': '"', "'": "'", "\\": "\\",
"n": "\n", "r": "\r", "t": "\t",
}.get(nxt, nxt))
pos += 2
continue
if c == '"':
return "".join(out), pos + 1
out.append(c)
pos += 1
raise LuaParseError("unterminated quoted string")
def _parse_long_bracket_string(src: str, pos: int) -> Tuple[str, int]:
assert src.startswith("[[", pos)
pos += 2
end = src.find("]]", pos)
if end < 0:
raise LuaParseError("unterminated long-bracket string")
return src[pos:end], end + 2
_NUM_RE = re.compile(r"-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?")
def _parse_number(src: str, pos: int) -> Tuple[float, int]:
m = _NUM_RE.match(src, pos)
if not m:
raise LuaParseError(f"expected number at {pos}: {src[pos:pos+20]!r}")
txt = m.group(0)
val: Any = float(txt) if ("." in txt or "e" in txt or "E" in txt) else int(txt)
return val, m.end()
def _parse_table(src: str, pos: int) -> Tuple[list, int]:
"""Parses `{ v1, v2, ... }` and returns the list of values."""
if src[pos] != "{":
raise LuaParseError(f"expected '{{' at {pos}")
pos += 1
out: list = []
while True:
pos = _skip_ws(src, pos)
if pos >= len(src):
raise LuaParseError("unterminated table literal")
if src[pos] == "}":
return out, pos + 1
value, pos = _parse_value(src, pos)
out.append(value)
def _parse_value(src: str, pos: int) -> Tuple[Any, int]:
pos = _skip_ws(src, pos)
c = src[pos]
if c == '"':
return _parse_quoted_string(src, pos)
if src.startswith("[[", pos):
return _parse_long_bracket_string(src, pos)
if c == "{":
return _parse_table(src, pos)
return _parse_number(src, pos)
# ---------------------------------------------------------------------------
# Top-level table extractor
# ---------------------------------------------------------------------------
def _find_top_level_tables(text: str) -> dict:
"""
Returns {table_name: dict_or_list}. Iterates the source once, splitting
at top-level `NAME = {` declarations and parsing the brace-balanced body.
"""
tables: dict = {}
pos = 0
pat = re.compile(r"^([A-Z_]+)\s*=\s*\{", re.MULTILINE)
while True:
m = pat.search(text, pos)
if not m:
break
name = m.group(1)
# Find brace-balanced end.
body_start = m.end() - 1 # position of '{'
depth = 0
i = body_start
in_str = None # '"' or '[' for long bracket
while i < len(text):
ch = text[i]
if in_str == '"':
if ch == "\\":
i += 2
continue
if ch == '"':
in_str = None
i += 1
continue
if in_str == "[":
if text.startswith("]]", i):
in_str = None
i += 2
continue
i += 1
continue
if ch == '"':
in_str = '"'
i += 1
continue
if text.startswith("[[", i):
in_str = "["
i += 2
continue
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
body = text[body_start:i + 1]
tables[name] = _parse_top_level_body(name, body)
pos = i + 1
break
i += 1
else:
raise LuaParseError(f"unterminated table {name}")
return tables
def _parse_top_level_body(name: str, body: str) -> Any:
"""
Parses the contents of one top-level table, which can have either
keyed entries `[id] = { ... }` or sequential entries `{ ... }`.
Returns dict for keyed entries, list for sequential.
"""
assert body.startswith("{") and body.endswith("}")
inner = body[1:-1]
i = 0
keyed: dict = {}
seq: list = []
while True:
i = _skip_ws(inner, i)
if i >= len(inner):
break
if inner[i] == "[":
# `[key] = value`
j = inner.index("]", i)
key_val, _ = _parse_value(inner, i + 1)
i = j + 1
i = _skip_ws(inner, i)
if inner[i] != "=":
raise LuaParseError(f"{name}: expected '=' at {i}")
i = _skip_ws(inner, i + 1)
value, i = _parse_value(inner, i)
keyed[key_val] = value
else:
value, i = _parse_value(inner, i)
seq.append(value)
if keyed and seq:
raise LuaParseError(f"{name}: mix of keyed/sequential entries")
return keyed if keyed else seq
# ---------------------------------------------------------------------------
# SQL emission
# ---------------------------------------------------------------------------
def _sql_str(s: Any) -> str:
s = str(s)
return "'" + s.replace("\\", "\\\\").replace("'", "''") + "'"
def _to_int(v: Any, default: int = 0) -> int:
if v is None:
return default
if isinstance(v, bool):
return int(v)
return int(v)
def _to_uint(v: Any) -> int:
"""Sirus uses -1 as 'all difficulties'; our schema is UNSIGNED → store 0."""
n = _to_int(v)
return n if n >= 0 else 0
def _to_signed(v: Any) -> int:
return _to_int(v)
def _to_float(v: Any) -> float:
if v is None:
return 0.0
return float(v)
def _load_id_set(path: Optional[Path]) -> Optional[set]:
"""Reads one id per line, returns a set of ints. None when path is None."""
if path is None:
return None
out: set = set()
with path.open("r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
try:
out.add(int(line))
except ValueError:
pass
return out
def emit(tables: dict, out: TextIO,
known_maps: Optional[set] = None,
known_creatures: Optional[set] = None,
known_items: Optional[set] = None,
dropped: Optional[dict] = None) -> dict:
"""
Writes REPLACE INTO statements for every donor table. Returns per-table
insert counts for the run summary.
When @known_maps/@known_creatures/@known_items are provided, rows that
reference an id missing from the corresponding set are skipped:
- instance with map_id not in known_maps is dropped (and all of its
encounters/creatures/sections/loot/tier_instance go with it);
- creature whose creature_id != 0 and not in known_creatures dropped;
- loot whose item_id not in known_items dropped;
- tier with no surviving tier_instance entries dropped.
"""
counts = {
"tier": 0, "instance": 0, "tier_instance": 0,
"encounter": 0, "creature": 0, "section": 0, "loot": 0,
}
if dropped is None:
dropped = {}
for k in ("instance", "creature", "loot", "tier"):
dropped.setdefault(k, 0)
out.write("-- Generated by mod-encounter-journal/tools/"
"import_donor_journal.py.\n")
out.write("-- Source: Sirus-style Generated_EncounterJournal.lua dump.\n")
out.write("-- Safe to re-run: every INSERT uses REPLACE semantics on PK.\n")
out.write("SET NAMES utf8mb4;\n")
out.write("START TRANSACTION;\n\n")
# First pass: figure out which instance_ids survive the map filter so we
# can transitively drop their encounters/creatures/sections/loot.
surviving_instances: set = set()
for inst_id, row in tables.get("JOURNALINSTANCE", {}).items():
if len(row) < 12:
row = row + [0] * (12 - len(row))
map_id = _to_int(row[6])
if known_maps is not None and map_id not in known_maps:
dropped["instance"] += 1
continue
surviving_instances.add(_to_int(inst_id))
# encounter_id -> instance_id (used by creature/section/loot filters)
encounter_to_instance: dict = {}
for inst_id, rows in tables.get("JOURNALENCOUNTER", {}).items():
for row in rows:
if len(row) < 12:
continue
encounter_to_instance[_to_int(row[0])] = _to_int(inst_id)
def _enc_survives(enc_id: int) -> bool:
inst = encounter_to_instance.get(enc_id)
return inst is not None and inst in surviving_instances
# ---- tiers ----
# Build the set of tiers that retain at least one surviving instance
# via JOURNALTIERXINSTANCE so we don't write orphan tiers.
surviving_tiers: set = set()
for inst_id, tirow in tables.get("JOURNALTIERXINSTANCE", {}).items():
if _to_int(inst_id) in surviving_instances and len(tirow) >= 1:
surviving_tiers.add(_to_int(tirow[0]))
for row in tables.get("JOURNALTIER", []):
# {id, name, flags}
tid, name, flags = row[0], row[1], row[2] if len(row) > 2 else 0
if surviving_tiers and _to_int(tid) not in surviving_tiers:
dropped["tier"] += 1
continue
out.write(
"REPLACE INTO `custom_ej_tier` "
"(`id`, `name`, `flags`, `sort_order`) VALUES "
f"({_to_int(tid)}, {_sql_str(name)}, {_to_int(flags)}, "
f"{_to_int(tid)});\n"
)
counts["tier"] += 1
out.write("\n")
# ---- instances ----
for inst_id, row in tables.get("JOURNALINSTANCE", {}).items():
if _to_int(inst_id) not in surviving_instances:
continue
if len(row) < 12:
row = row + [0] * (12 - len(row))
(name, desc, btn, sbtn, bg, lbg, map_id, area, sort_o,
flags, src_id, wmaa) = row[:12]
out.write(
"REPLACE 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 ("
f"{_to_int(inst_id)}, {_sql_str(name)}, {_sql_str(desc)}, "
f"{_sql_str(btn)}, {_sql_str(sbtn)}, {_sql_str(bg)}, "
f"{_sql_str(lbg)}, {_to_int(map_id)}, {_to_int(area)}, "
f"{_to_int(wmaa)}, {_to_int(flags)}, {_to_int(sort_o)}"
");\n"
)
counts["instance"] += 1
out.write("\n")
# ---- tier_instance ----
# JOURNALTIERXINSTANCE[instance_id] = {tier_id, sort_order}
for inst_id, row in tables.get("JOURNALTIERXINSTANCE", {}).items():
if _to_int(inst_id) not in surviving_instances:
continue
tier_id = row[0]
sort_o = row[1] if len(row) > 1 else 0
out.write(
"REPLACE INTO `custom_ej_tier_instance` "
"(`tier_id`, `instance_id`, `sort_order`) VALUES "
f"({_to_int(tier_id)}, {_to_int(inst_id)}, {_to_int(sort_o)});\n"
)
counts["tier_instance"] += 1
out.write("\n")
# ---- encounters ----
# JOURNALENCOUNTER[instance_id] = list of rows
# {id, name, desc, map_x, map_y, floor, world_map_area, first_section,
# instance_id, difficulty_mask, flags, sort_order}
for inst_id, rows in tables.get("JOURNALENCOUNTER", {}).items():
if _to_int(inst_id) not in surviving_instances:
continue
for row in rows:
if len(row) < 12:
row = row + [0] * (12 - len(row))
(eid, name, desc, mx, my, floor, wmaa, first_sec, src_inst,
diff, flags, sort_o) = row[:12]
out.write(
"REPLACE 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 ("
f"{_to_int(eid)}, {_to_int(inst_id)}, {_sql_str(name)}, "
f"{_sql_str(desc)}, {_to_float(mx):.6f}, {_to_float(my):.6f}, "
f"{_to_uint(floor)}, {_to_uint(wmaa)}, {_to_int(first_sec)}, "
f"{_to_uint(diff)}, {_to_uint(flags)}, {_to_int(sort_o)}"
");\n"
)
counts["encounter"] += 1
out.write("\n")
# ---- creatures ----
# JOURNALENCOUNTERCREATURE[encounter_id] = list of rows
# {name, desc, display_id, icon, encounter_id, sort, id, creature_id,
# difficulty_mask}
for enc_id, rows in tables.get("JOURNALENCOUNTERCREATURE", {}).items():
if not _enc_survives(_to_int(enc_id)):
continue
for row in rows:
if len(row) < 9:
row = row + [0] * (9 - len(row))
(name, desc, display_id, icon, src_enc, sort_o, cid, cr_id,
diff) = row[:9]
cr_id_i = _to_int(cr_id)
if (known_creatures is not None and cr_id_i != 0
and cr_id_i not in known_creatures):
dropped["creature"] += 1
continue
out.write(
"REPLACE INTO `custom_ej_creature` "
"(`id`, `encounter_id`, `creature_id`, `name`, `description`, "
"`creature_display_id`, `icon`, `difficulty_mask`, "
"`sort_order`) VALUES ("
f"{_to_int(cid)}, {_to_int(enc_id)}, {_to_int(cr_id)}, "
f"{_sql_str(name)}, {_sql_str(desc)}, {_to_uint(display_id)}, "
f"{_sql_str(icon)}, {_to_uint(diff)}, {_to_int(sort_o)}"
");\n"
)
counts["creature"] += 1
out.write("\n")
# ---- sections ----
# JOURNALENCOUNTERSECTION[section_id] = single row
# {id, name, desc, display_id, desc_spell, icon_spell, encounter_id,
# next, sub, parent, flags, icon_flags, sort, type, difficulty,
# creature_id}
for sid, row in tables.get("JOURNALENCOUNTERSECTION", {}).items():
if len(row) < 16:
row = row + [0] * (16 - len(row))
(src_sid, name, desc, display_id, desc_spell, icon_spell, enc_id,
next_sid, sub_sid, parent_sid, flags, icon_flags, sort_o, stype,
diff, cr_id) = row[:16]
if not _enc_survives(_to_int(enc_id)):
continue
out.write(
"REPLACE 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 ("
f"{_to_int(sid)}, {_to_int(enc_id)}, {_to_int(parent_sid)}, "
f"{_to_int(next_sid)}, {_to_int(sub_sid)}, {_sql_str(name)}, "
f"{_sql_str(desc)}, {_to_uint(display_id)}, "
f"{_to_uint(desc_spell)}, {_to_uint(icon_spell)}, NULL, "
f"{_to_uint(flags)}, {_to_uint(icon_flags)}, {_to_int(sort_o)}, "
f"{_to_uint(stype)}, {_to_uint(diff)}, {_to_uint(cr_id)}"
");\n"
)
counts["section"] += 1
out.write("\n")
# ---- loot ----
# JOURNALENCOUNTERITEM[encounter_id] = list of rows
# {item_id, encounter_id, difficulty_mask, faction_mask, flags, id,
# class_mask}
for enc_id, rows in tables.get("JOURNALENCOUNTERITEM", {}).items():
if not _enc_survives(_to_int(enc_id)):
continue
for row in rows:
if len(row) < 7:
row = row + [0] * (7 - len(row))
(item_id, src_enc, diff, faction, flags, lid, class_mask) = row[:7]
item_i = _to_int(item_id)
if known_items is not None and item_i not in known_items:
dropped["loot"] += 1
continue
out.write(
"REPLACE INTO `custom_ej_loot` "
"(`id`, `encounter_id`, `item_id`, `difficulty_mask`, "
"`faction_mask`, `class_mask`, `flags`, `sort_order`) "
"VALUES ("
f"{_to_int(lid)}, {_to_int(enc_id)}, {_to_int(item_id)}, "
f"{_to_uint(diff)}, {_to_signed(faction)}, "
f"{_to_signed(class_mask)}, {_to_uint(flags)}, "
f"{_to_int(lid)}"
");\n"
)
counts["loot"] += 1
out.write("\n")
out.write("COMMIT;\n")
return counts
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main() -> int:
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--input", "-i", required=True, type=Path,
help="Generated_EncounterJournal.lua")
p.add_argument("--output", "-o", type=Path,
help="Where to write the .sql file (default: stdout)")
p.add_argument("--known-maps", type=Path,
help="File with allowed map ids (one per line). Donor "
"instances whose map_id is not listed are dropped, "
"together with their encounters/creatures/sections/"
"loot.")
p.add_argument("--known-creatures", type=Path,
help="File with allowed creature_template entries. Donor "
"creature cards with creature_id not in the list are "
"dropped.")
p.add_argument("--known-items", type=Path,
help="File with allowed item_template entries. Donor loot "
"rows referencing missing items are dropped.")
args = p.parse_args()
text = args.input.read_text(encoding="utf-8")
tables = _find_top_level_tables(text)
print("parsed tables:", file=sys.stderr)
for name, body in tables.items():
size = len(body) if isinstance(body, (dict, list)) else "?"
print(f" {name:30s} {size}", file=sys.stderr)
known_maps = _load_id_set(args.known_maps)
known_creatures = _load_id_set(args.known_creatures)
known_items = _load_id_set(args.known_items)
if known_maps is not None:
print(f"map whitelist: {len(known_maps)} ids", file=sys.stderr)
if known_creatures is not None:
print(f"creature whitelist: {len(known_creatures)} ids",
file=sys.stderr)
if known_items is not None:
print(f"item whitelist: {len(known_items)} ids", file=sys.stderr)
dropped: dict = {}
if args.output:
with args.output.open("w", encoding="utf-8") as f:
counts = emit(tables, f, known_maps, known_creatures,
known_items, dropped)
else:
counts = emit(tables, sys.stdout, known_maps, known_creatures,
known_items, dropped)
print("emitted rows:", file=sys.stderr)
for k, v in counts.items():
print(f" {k:14s} {v}", file=sys.stderr)
if any(dropped.values()):
print("dropped (custom Sirus entries):", file=sys.stderr)
for k, v in dropped.items():
if v:
print(f" {k:14s} {v}", file=sys.stderr)
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -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())
-1
View File
@@ -1 +0,0 @@
@@ -1,25 +0,0 @@
#
# Game Modes module configuration
#
########################################
# GameModes settings
########################################
#
# GameModes.Enable
# Description: Enable game modes system
# Default: 1 - (Enabled)
# 0 - (Disabled)
#
GameModes.Enable = 1
#
# GameModes.Traitor.Enable
# Description: Enable Traitor game mode
# Default: 1 - (Enabled)
# 0 - (Disabled)
#
GameModes.Traitor.Enable = 1
@@ -1,6 +0,0 @@
DROP TABLE IF EXISTS `mod_gamemodes_characters`;
CREATE TABLE `mod_gamemodes_characters` (
`guid` INT UNSIGNED NOT NULL,
`game_mode` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 = NORMAL, 1 = TRAITOR',
PRIMARY KEY (`guid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-1
View File
@@ -1 +0,0 @@
-69
View File
@@ -1,69 +0,0 @@
#include "GameModes.h"
#include "DatabaseEnv.h"
#include "Log.h"
GameModeMgr* GameModeMgr::Instance()
{
static GameModeMgr instance;
return &instance;
}
void GameModeMgr::LoadFromDB()
{
uint32 oldMSTime = getMSTime();
_gameModes.clear();
QueryResult result = CharacterDatabase.Query("SELECT `guid`, `game_mode` FROM `mod_gamemodes_characters`");
if (!result)
{
LOG_INFO("module", ">> Loaded 0 character game modes. Table `mod_gamemodes_characters` is empty.");
return;
}
uint32 count = 0;
do
{
Field* fields = result->Fetch();
ObjectGuid::LowType guid = fields[0].Get<uint32>();
uint8 mode = fields[1].Get<uint8>();
if (mode >= GAMEMODE_MAX)
{
LOG_ERROR("module", "mod-gamemodes: Invalid game mode {} for character {}, skipping.", mode, guid);
continue;
}
_gameModes[guid] = static_cast<GameMode>(mode);
++count;
} while (result->NextRow());
LOG_INFO("module", ">> Loaded {} character game modes in {} ms", count, GetMSTimeDiffToNow(oldMSTime));
}
void GameModeMgr::SaveGameMode(ObjectGuid::LowType guid, GameMode mode)
{
_gameModes[guid] = mode;
CharacterDatabase.Execute("REPLACE INTO `mod_gamemodes_characters` (`guid`, `game_mode`) VALUES ({}, {})",
guid, static_cast<uint8>(mode));
}
void GameModeMgr::DeleteGameMode(ObjectGuid::LowType guid)
{
_gameModes.erase(guid);
CharacterDatabase.Execute("DELETE FROM `mod_gamemodes_characters` WHERE `guid` = {}", guid);
}
GameMode GameModeMgr::GetGameMode(ObjectGuid::LowType guid) const
{
auto it = _gameModes.find(guid);
if (it != _gameModes.end())
return it->second;
return GAMEMODE_NORMAL;
}
bool GameModeMgr::HasGameMode(ObjectGuid::LowType guid, GameMode mode) const
{
return GetGameMode(guid) == mode;
}
-37
View File
@@ -1,37 +0,0 @@
#ifndef MOD_GAMEMODES_H
#define MOD_GAMEMODES_H
#include "Player.h"
#include <unordered_map>
enum GameMode : uint8
{
GAMEMODE_NORMAL = 0,
GAMEMODE_TRAITOR = 1,
GAMEMODE_MAX
};
// Custom bit in SMSG_CHAR_ENUM character flags.
// Chosen as a high unused bit so the packet layout stays unchanged.
constexpr uint32 GAMEMODE_CHARACTER_FLAG_TRAITOR = 0x40000000;
class GameModeMgr
{
public:
static GameModeMgr* Instance();
void LoadFromDB();
void SaveGameMode(ObjectGuid::LowType guid, GameMode mode);
void DeleteGameMode(ObjectGuid::LowType guid);
GameMode GetGameMode(ObjectGuid::LowType guid) const;
bool HasGameMode(ObjectGuid::LowType guid, GameMode mode) const;
private:
GameModeMgr() = default;
std::unordered_map<ObjectGuid::LowType, GameMode> _gameModes;
};
#define sGameModeMgr GameModeMgr::Instance()
#endif // MOD_GAMEMODES_H
@@ -1,6 +0,0 @@
void AddSC_TraitorMode();
void Addmod_gamemodesScripts()
{
AddSC_TraitorMode();
}
@@ -1,291 +0,0 @@
#include "TraitorMode.h"
#include "GameModes.h"
#include "Config.h"
#include "DBCStores.h"
#include "DatabaseEnv.h"
#include "Log.h"
#include "ObjectMgr.h"
#include "ReputationMgr.h"
#include "ScriptMgr.h"
#include "WorldPacket.h"
#include "WorldSession.h"
namespace TraitorMode
{
namespace
{
void NormalizeFactionState(Player* player, FactionEntry const* factionEntry, bool visible, bool atWar)
{
if (!factionEntry || factionEntry->reputationListID < 0)
return;
FactionState* factionState = const_cast<FactionState*>(player->GetReputationMgr().GetState(factionEntry));
if (!factionState)
return;
factionState->Flags &= ~(FACTION_FLAG_VISIBLE | FACTION_FLAG_HIDDEN | FACTION_FLAG_INVISIBLE_FORCED |
FACTION_FLAG_PEACE_FORCED | FACTION_FLAG_INACTIVE);
if (visible)
factionState->Flags |= FACTION_FLAG_VISIBLE;
else
factionState->Flags |= FACTION_FLAG_HIDDEN | FACTION_FLAG_INVISIBLE_FORCED;
if (atWar)
factionState->Flags |= FACTION_FLAG_AT_WAR;
else
factionState->Flags &= ~FACTION_FLAG_AT_WAR;
factionState->needSend = true;
factionState->needSave = true;
}
void SetFactionStanding(Player* player, uint32 factionId, int32 targetDisplayed, bool visible, bool atWar)
{
FactionEntry const* factionEntry = sFactionStore.LookupEntry(factionId);
if (!factionEntry)
return;
// ReputationMgr::GetBaseReputation now always uses the player's actual race
// mask, so SetOneFactionReputation is safe to call directly.
player->GetReputationMgr().SetOneFactionReputation(factionEntry, static_cast<float>(targetDisplayed), false);
NormalizeFactionState(player, factionEntry, visible, atWar);
}
void NormalizeTraitorReputationStates(Player* player)
{
TeamId originalTeam = player->GetTeamId(true);
uint32 const* newFactions = (originalTeam == TEAM_ALLIANCE)
? HORDE_REPUTATION_FACTIONS
: ALLIANCE_REPUTATION_FACTIONS;
uint32 const* oldFactions = (originalTeam == TEAM_ALLIANCE)
? ALLIANCE_REPUTATION_FACTIONS
: HORDE_REPUTATION_FACTIONS;
auto normalizeFactionAndParent = [player](uint32 factionId, bool visible, bool atWar)
{
if (FactionEntry const* factionEntry = sFactionStore.LookupEntry(factionId))
{
NormalizeFactionState(player, factionEntry, visible, atWar);
if (factionEntry->team)
NormalizeFactionState(player, sFactionStore.LookupEntry(factionEntry->team), visible, false);
}
};
for (uint32 i = 0; i < FACTION_COUNT; ++i)
{
normalizeFactionAndParent(newFactions[i], true, false);
normalizeFactionAndParent(oldFactions[i], false, true);
}
}
void SendFactionVisiblePacket(Player* player, FactionEntry const* factionEntry)
{
if (!factionEntry || factionEntry->reputationListID < 0)
return;
WorldPacket data(SMSG_SET_FACTION_VISIBLE, 4);
data << uint32(factionEntry->reputationListID);
player->SendDirectMessage(&data);
}
void SendTraitorFactionVisibility(Player* player)
{
TeamId originalTeam = player->GetTeamId(true);
uint32 const* newFactions = (originalTeam == TEAM_ALLIANCE)
? HORDE_REPUTATION_FACTIONS
: ALLIANCE_REPUTATION_FACTIONS;
auto sendFactionAndParent = [player](uint32 factionId)
{
if (FactionEntry const* factionEntry = sFactionStore.LookupEntry(factionId))
{
SendFactionVisiblePacket(player, factionEntry);
if (factionEntry->team)
SendFactionVisiblePacket(player, sFactionStore.LookupEntry(factionEntry->team));
}
};
for (uint32 i = 0; i < FACTION_COUNT; ++i)
sendFactionAndParent(newFactions[i]);
}
}
void ApplyFactionSwap(Player* player)
{
TeamId originalTeam = player->GetTeamId(true);
TeamId newTeam = (originalTeam == TEAM_ALLIANCE) ? TEAM_HORDE : TEAM_ALLIANCE;
player->setTeamId(newTeam);
// Derive faction template from the canonical race for the new team (same
// logic as SetFactionForRace), so we never rely on hardcoded DBC IDs.
uint8 representativeRace = (newTeam == TEAM_HORDE) ? HORDE_DEFAULT_RACE : ALLIANCE_DEFAULT_RACE;
if (ChrRacesEntry const* rEntry = sChrRacesStore.LookupEntry(representativeRace))
player->SetFaction(rEntry->FactionID);
LOG_INFO("module", "mod-gamemodes: ApplyFactionSwap for {} (GUID: {}), original team = {}, new team = {}, factionTemplate = {}",
player->GetName(), player->GetGUID().GetCounter(),
(originalTeam == TEAM_ALLIANCE ? "Alliance" : "Horde"),
(newTeam == TEAM_ALLIANCE ? "Alliance" : "Horde"),
player->GetFaction());
}
void InitTraitorReputations(Player* player)
{
TeamId originalTeam = player->GetTeamId(true);
// New faction reputations -> Neutral (standing = 0)
const uint32* newFactions = (originalTeam == TEAM_ALLIANCE)
? HORDE_REPUTATION_FACTIONS
: ALLIANCE_REPUTATION_FACTIONS;
for (uint32 i = 0; i < FACTION_COUNT; ++i)
SetFactionStanding(player, newFactions[i], 0, true, false);
// Old faction reputations -> Hostile (standing = -6000)
const uint32* oldFactions = (originalTeam == TEAM_ALLIANCE)
? ALLIANCE_REPUTATION_FACTIONS
: HORDE_REPUTATION_FACTIONS;
for (uint32 i = 0; i < FACTION_COUNT; ++i)
SetFactionStanding(player, oldFactions[i], -6000, false, true);
LOG_INFO("module", "mod-gamemodes: InitTraitorReputations for {} (GUID: {})",
player->GetName(), player->GetGUID().GetCounter());
}
void TeleportToNewFactionStart(Player* player)
{
TeamId originalTeam = player->GetTeamId(true);
uint8 spawnRace = (originalTeam == TEAM_ALLIANCE)
? HORDE_DEFAULT_RACE
: ALLIANCE_DEFAULT_RACE;
PlayerInfo const* info = sObjectMgr->GetPlayerInfo(spawnRace, player->getClass());
if (!info)
{
// Fallback: try warrior (class 1) for that race
info = sObjectMgr->GetPlayerInfo(spawnRace, 1);
}
if (!info)
{
LOG_ERROR("module", "mod-gamemodes: No spawn info for race {} class {}", spawnRace, player->getClass());
return;
}
player->TeleportTo(info->mapId, info->positionX, info->positionY, info->positionZ, info->orientation);
LOG_INFO("module", "mod-gamemodes: Teleported {} to faction start (map {}, {:.1f}, {:.1f}, {:.1f})",
player->GetName(), info->mapId, info->positionX, info->positionY, info->positionZ);
}
}
class TraitorModePlayer : public PlayerScript
{
public:
TraitorModePlayer() : PlayerScript("TraitorModePlayer",
{PLAYERHOOK_ON_CREATE, PLAYERHOOK_ON_LOGIN, PLAYERHOOK_ON_REPUTATION_LOAD_FROM_DB, PLAYERHOOK_ON_UPDATE_FACTION}) {}
// OnPlayerCreate fires on a temporary Player object AFTER SaveToDB.
// Changes to faction/reputation here are lost. We only save the game mode to DB.
void OnPlayerCreate(Player* player) override
{
LOG_INFO("module", "mod-gamemodes: OnPlayerCreate for {} (GUID: {}), OutfitId = {}",
player->GetName(), player->GetGUID().GetCounter(), player->GetOutfitId());
if (!sConfigMgr->GetOption<bool>("GameModes.Enable", true))
return;
if (!sConfigMgr->GetOption<bool>("GameModes.Traitor.Enable", true))
return;
if (player->GetOutfitId() != GAMEMODE_TRAITOR)
return;
sGameModeMgr->SaveGameMode(player->GetGUID().GetCounter(), GAMEMODE_TRAITOR);
CharacterDatabase.Execute("UPDATE `characters` SET `cinematic` = 1 WHERE `guid` = {}", player->GetGUID().GetCounter());
LOG_INFO("module", "mod-gamemodes: Saved TRAITOR mode for {} (GUID: {})",
player->GetName(), player->GetGUID().GetCounter());
}
// Reputation load completes before initial packets are sent to the client.
// Traitor mode must rewrite faction/reputation state here, otherwise the client
// builds the reputation window from the original race defaults.
void OnPlayerReputationLoadFromDB(Player* player) override
{
if (!sConfigMgr->GetOption<bool>("GameModes.Enable", true))
return;
if (!sConfigMgr->GetOption<bool>("GameModes.Traitor.Enable", true))
return;
if (!sGameModeMgr->HasGameMode(player->GetGUID().GetCounter(), GAMEMODE_TRAITOR))
return;
TraitorMode::ApplyFactionSwap(player);
if (player->HasAtLoginFlag(AT_LOGIN_FIRST))
TraitorMode::InitTraitorReputations(player);
TraitorMode::NormalizeTraitorReputationStates(player);
}
void OnPlayerLogin(Player* player) override
{
if (!sConfigMgr->GetOption<bool>("GameModes.Enable", true))
return;
if (!sConfigMgr->GetOption<bool>("GameModes.Traitor.Enable", true))
return;
if (!sGameModeMgr->HasGameMode(player->GetGUID().GetCounter(), GAMEMODE_TRAITOR))
return;
TraitorMode::ApplyFactionSwap(player);
TraitorMode::NormalizeTraitorReputationStates(player);
player->GetReputationMgr().SendInitialReputations();
TraitorMode::SendTraitorFactionVisibility(player);
player->SetPlayerFlag(PLAYER_FLAGS_TRAITOR);
if (player->HasAtLoginFlag(AT_LOGIN_FIRST))
TraitorMode::TeleportToNewFactionStart(player);
LOG_INFO("module", "mod-gamemodes: OnPlayerLogin END for {} — TeamId = {}, FactionTemplate = {}, OriginalTeam = {}",
player->GetName(),
static_cast<uint32>(player->GetTeamId()),
player->GetFaction(),
static_cast<uint32>(player->GetTeamId(true)));
}
// OnPlayerUpdateFaction fires every time SetFactionForRace is called
// (login, race change, etc). Re-apply the swap so it's never lost.
void OnPlayerUpdateFaction(Player* player) override
{
if (!sConfigMgr->GetOption<bool>("GameModes.Enable", true))
return;
if (!sConfigMgr->GetOption<bool>("GameModes.Traitor.Enable", true))
return;
if (!sGameModeMgr->HasGameMode(player->GetGUID().GetCounter(), GAMEMODE_TRAITOR))
return;
TraitorMode::ApplyFactionSwap(player);
}
};
class TraitorModeWorld : public WorldScript
{
public:
TraitorModeWorld() : WorldScript("TraitorModeWorld",
{WORLDHOOK_ON_LOAD_CUSTOM_DATABASE_TABLE}) {}
void OnLoadCustomDatabaseTable() override
{
sGameModeMgr->LoadFromDB();
}
};
void AddSC_TraitorMode()
{
new TraitorModePlayer();
new TraitorModeWorld();
}
@@ -1,49 +0,0 @@
#ifndef MOD_GAMEMODES_TRAITOR_H
#define MOD_GAMEMODES_TRAITOR_H
#include "Player.h"
namespace TraitorMode
{
// Reputation faction IDs: capital cities + PvP battleground factions
constexpr uint32 ALLIANCE_REPUTATION_FACTIONS[] = {
72, // Stormwind
47, // Ironforge
69, // Darnassus
54, // Gnomeregan Exiles
930, // Exodar
890, // Silverwing Sentinels (WSG)
730, // Stormpike Guard (AV)
509, // League of Arathor (AB)
};
constexpr uint32 HORDE_REPUTATION_FACTIONS[] = {
76, // Orgrimmar
81, // Thunder Bluff
68, // Undercity
530, // Darkspear Trolls
911, // Silvermoon City
889, // Warsong Outriders (WSG)
729, // Frostwolf Clan (AV)
510, // The Defilers (AB)
};
constexpr uint32 FACTION_COUNT = 8;
// Default spawn points for each faction (race 2 = Orc for Horde, race 1 = Human for Alliance)
constexpr uint8 HORDE_DEFAULT_RACE = 2; // Orc
constexpr uint8 ALLIANCE_DEFAULT_RACE = 1; // Human
// Apply faction swap on login/faction update
void ApplyFactionSwap(Player* player);
// Set new faction reputations to Neutral (standing 0)
void InitTraitorReputations(Player* player);
// Teleport traitor to the opposite faction's starting zone
void TeleportToNewFactionStart(Player* player);
}
void AddSC_TraitorMode();
#endif // MOD_GAMEMODES_TRAITOR_H
@@ -336,3 +336,40 @@ IndividualProgression.ExcludeAccounts = 1
# Default: "^RNDBOT.*"
#
IndividualProgression.ExcludedAccountsRegex = "^RNDBOT.*"
########################################
# Player Guide (Путеводитель)
########################################
# IndividualProgression.PlayerGuide.Enable
#
# Description: Enable server-side Player Guide payload generation
# and addon-channel delivery. When disabled the
# module does not push any data to clients and
# ignores client commands.
#
# Default: 1 - Enabled
# 0 - Disabled
#
IndividualProgression.PlayerGuide.Enable = 1
# IndividualProgression.PlayerGuide.ChunkSize
#
# Description: Maximum size, in bytes, of a single addon-channel
# chunk used to deliver guide payloads. Lower values
# produce more chunks; higher values risk hitting
# the 255-byte client limit. Clamped to [64, 240].
#
# Default: 230
#
IndividualProgression.PlayerGuide.ChunkSize = 230
# IndividualProgression.PlayerGuide.LogPayloads
#
# Description: Emit a debug log line for each payload pushed to
# a client. Useful when developing the client UI.
#
# Default: 0 - Disabled
# 1 - Enabled
#
IndividualProgression.PlayerGuide.LogPayloads = 0
@@ -42,6 +42,7 @@ void AddSC_ipp_spell_scripts();
void AddSC_individualProgression_commandscript();
void AddSC_mod_individual_progression_awareness();
void AddSC_mod_individual_progression_player();
void AddSC_mod_individual_progression_player_guide();
void AddSC_npc_archmage_timear();
void AddSC_karazhan_70();
void AddSC_the_eye_70();
@@ -88,6 +89,7 @@ void Addmod_individual_progressionScripts()
AddSC_individualProgression_commandscript();
AddSC_mod_individual_progression_awareness();
AddSC_mod_individual_progression_player();
AddSC_mod_individual_progression_player_guide();
AddSC_npc_archmage_timear();
AddSC_karazhan_70();
AddSC_the_eye_70();
@@ -0,0 +1,649 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>,
* released under GNU AGPL v3 license:
* https://github.com/azerothcore/azerothcore-wotlk/blob/master/LICENSE-AGPL3
*/
#include "PlayerGuideMgr.h"
#include "../IndividualProgression.h"
#include "Config.h"
#include "DatabaseEnv.h"
#include "Field.h"
#include "Log.h"
#include "Player.h"
#include "QueryResult.h"
#include "QuestDef.h"
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <set>
#include <sstream>
#include <unordered_set>
namespace MoonWell::PlayerGuide
{
namespace
{
std::string EscapeJson(std::string const& in)
{
std::string out;
out.reserve(in.size() + 2);
for (unsigned char c : in)
{
switch (c)
{
case '"': out += "\\\""; break;
case '\\': out += "\\\\"; break;
case '\b': out += "\\b"; break;
case '\f': out += "\\f"; break;
case '\n': out += "\\n"; break;
case '\r': out += "\\r"; break;
case '\t': out += "\\t"; break;
default:
if (c < 0x20)
{
char buf[8];
std::snprintf(buf, sizeof(buf), "\\u%04x", c);
out += buf;
}
else
out += static_cast<char>(c);
break;
}
}
return out;
}
// Writes "key":number when value > 0.
void WriteOptUInt(std::ostringstream& s, char const* key, uint32 v,
bool& first)
{
if (!v)
return;
if (!first) s << ',';
s << '"' << key << "\":" << v;
first = false;
}
void WriteString(std::ostringstream& s, char const* key,
std::string const& v, bool& first)
{
if (!first) s << ',';
s << '"' << key << "\":\"" << EscapeJson(v) << '"';
first = false;
}
void WriteUInt(std::ostringstream& s, char const* key, uint32 v,
bool& first)
{
if (!first) s << ',';
s << '"' << key << "\":" << v;
first = false;
}
void WriteBool(std::ostringstream& s, char const* key, bool v,
bool& first)
{
if (!first) s << ',';
s << '"' << key << "\":" << (v ? "true" : "false");
first = false;
}
// Maps an IPP ProgressionState into a target stage (1..N).
// The stage ids are kept stable so the client can key
// localisations off of them.
struct StageInfo
{
uint32 id;
char const* name;
char const* description;
};
StageInfo const STAGES[] =
{
{ 1, "Начало пути",
"Завершите низкоуровневое содержимое родного континента." },
{ 2, "Огненные Недра",
"Победите Рагнароса в Огненных Недрах." },
{ 3, "Логово Ониксии",
"Победите Ониксию." },
{ 4, "Логово Крыла Тьмы",
"Победите Нефариана в Логове Крыла Тьмы." },
{ 5, "Подготовка к АQ",
"Завершите подготовку к открытию ворот Ан'Киража." },
{ 6, "Военный поход АQ",
"Окажите помощь военным усилиям в Силитусе." },
{ 7, "Храм Ан'Киража",
"Победите Ц'Туна в Храме Ан'Киража." },
{ 8, "Наксрамас (40)",
"Победите КелТузада в Наксрамасе." },
{ 9, "Подготовка к Запределью",
"Откройте Тёмный портал и подготовьтесь к TBC." },
{ 10, "Каражан и Гру'ул",
"Завершите первый ярус рейдов TBC." },
{ 11, "Пещера Змеиных Недр / Око Бури",
"Завершите рейды второго яруса TBC." },
{ 12, "Вершина Хиджала и Чёрный храм",
"Победите Иллидана в Чёрном храме." },
{ 13, "Зул'Аман",
"Получите доступ к Зул'Аману и завершите его." },
{ 14, "Плато Солнечного Колодца",
"Победите Кил'джедена на Плато Солнечного Колодца." },
{ 15, "Наксрамас (Нордскол)",
"Завершите рейды первого яруса WotLK." },
{ 16, "Ульдуар",
"Победите Йогг-Сарона в Ульдуаре." },
{ 17, "Испытание крестоносца",
"Завершите Испытание крестоносца." },
{ 18, "Ледяная Корона",
"Победите Короля-лича." },
{ 19, "Рубиновое святилище",
"Победите Халиона в Рубиновом святилище." }
};
StageInfo const* FindStage(uint32 id)
{
for (StageInfo const& s : STAGES)
if (s.id == id)
return &s;
return nullptr;
}
// Tiered dungeon recommendations matched to the player level/stage.
struct DungeonRec
{
uint32 stage; // recommended starting from this stage
uint32 maxStage; // hidden after this stage (inclusive)
uint8 minLevel;
uint8 maxLevel;
uint32 mapId;
char const* title;
};
DungeonRec const DUNGEONS[] =
{
{ 1, 1, 15, 21, 36, "Мёртвые копи" },
{ 1, 1, 17, 24, 33, "Курганы Иглошкурых" },
{ 1, 2, 22, 30, 47, "Шахта Каражан" },
{ 1, 2, 24, 32, 70, "Ульдаман" },
{ 1, 3, 27, 38, 109, "Затонувший храм" },
{ 1, 3, 30, 42, 209, "Зул'Фаррак" },
{ 1, 4, 46, 54, 229, "Верхняя часть Чёрной горы" },
{ 1, 4, 55, 60, 230, "Глубины Чёрной горы" },
{ 1, 4, 58, 60, 533, "Наксрамас" },
{ 9, 12, 58, 70, 540, "Кузня Крови" },
{ 9, 12, 60, 70, 542, "Цитадель Адского пламени" },
{ 9, 12, 65, 70, 553, "Ботаника" },
{ 9, 12, 67, 70, 555, "Гробница Ткача Смерти" },
{14, 19, 68, 80, 574, "Утгард" },
{14, 19, 70, 80, 600, "Драк'Тарон" },
{14, 19, 72, 80, 619, "Ан'Кахет: Старое Королевство" },
{14, 19, 74, 80, 595, "Чертоги Камней" },
{14, 19, 76, 80, 658, "Очищение Стратхольма" }
};
}
Mgr* Mgr::instance()
{
static Mgr inst;
return &inst;
}
void Mgr::LoadConfig()
{
_enabled = sConfigMgr->GetOption<bool>(
"IndividualProgression.PlayerGuide.Enable", true);
_logPayloads = sConfigMgr->GetOption<bool>(
"IndividualProgression.PlayerGuide.LogPayloads", false);
_chunkSize = sConfigMgr->GetOption<uint32>(
"IndividualProgression.PlayerGuide.ChunkSize", 230);
// The addon channel hard-caps a single message body to 255
// characters. We leave room for the framing header.
if (_chunkSize < 64) _chunkSize = 64;
if (_chunkSize > 240) _chunkSize = 240;
ReloadEncounterJournalCache();
}
void Mgr::ReloadEncounterJournalCache()
{
_ejByMapId.clear();
// mod-encounter-journal may be disabled or its table absent.
// The query then yields no result and we keep an empty cache;
// recommendations that would have used EJ ids are skipped.
QueryResult result = WorldDatabase.Query(
"SELECT `id`, `map_id`, `name` "
"FROM `custom_ej_instance`");
if (!result)
{
LOG_INFO("server.loading",
">> PlayerGuide: no custom_ej_instance rows found. "
"Recommendations will be empty.");
return;
}
do
{
Field* f = result->Fetch();
EjInstanceRef ref;
ref.ejId = f[0].Get<uint32>();
uint32 mapId = f[1].Get<uint32>();
ref.name = f[2].Get<std::string>();
// First entry wins. If the EJ has duplicates for the same
// map (e.g. heroic variants) the lowest id is taken via the
// INSERT order; we don't reorder.
_ejByMapId.emplace(mapId, std::move(ref));
} while (result->NextRow());
LOG_INFO("server.loading",
">> PlayerGuide: cached {} EJ instance entries by mapId.",
_ejByMapId.size());
}
std::string Mgr::StageName(uint32 stage) const
{
StageInfo const* s = FindStage(stage);
return s ? s->name : "Этап";
}
std::string Mgr::StageDescription(uint32 stage) const
{
StageInfo const* s = FindStage(stage);
return s ? s->description : "";
}
void Mgr::AppendLevelObjective(Payload& out, Player* player,
uint32 requiredLevel, uint32 order) const
{
Objective o;
o.id = order;
o.type = "level";
char buf[64];
std::snprintf(buf, sizeof(buf), "Достигнуть %u уровня",
requiredLevel);
o.title = buf;
o.progress = player->GetLevel();
o.required = requiredLevel;
o.completed = o.progress >= o.required;
o.targetId = requiredLevel;
o.order = order;
out.objectives.push_back(std::move(o));
}
void Mgr::AppendQuestObjective(Payload& out, Player* player,
uint32 questId,
std::string const& title,
uint32 order) const
{
QuestStatus st = player->GetQuestStatus(questId);
Objective o;
o.id = order;
o.type = "quest";
o.title = title;
o.required = 1;
o.progress = (st == QUEST_STATUS_REWARDED ||
st == QUEST_STATUS_COMPLETE) ? 1 : 0;
o.completed = (st == QUEST_STATUS_REWARDED);
o.targetId = questId;
o.questId = questId;
o.order = order;
out.objectives.push_back(std::move(o));
}
void Mgr::AppendDungeonObjective(Payload& out, uint32 mapId,
std::string const& title,
uint32 order) const
{
auto it = _ejByMapId.find(mapId);
if (it == _ejByMapId.end())
return; // no EJ entry -> skip rather than emit a stub.
Objective o;
o.id = order;
o.type = "dungeon";
o.title = it->second.name.empty() ? title : it->second.name;
o.required = 1;
o.targetId = it->second.ejId; // primary id for click
o.ejInstanceId = it->second.ejId; // JOURNALINSTANCE key
o.mapId = mapId; // WoW map id (auxiliary)
o.order = order;
out.objectives.push_back(std::move(o));
}
void Mgr::AppendRaidObjective(Payload& out, uint32 mapId,
std::string const& title,
uint32 order) const
{
auto it = _ejByMapId.find(mapId);
if (it == _ejByMapId.end())
return;
Objective o;
o.id = order;
o.type = "raid";
o.title = it->second.name.empty() ? title : it->second.name;
o.required = 1;
o.targetId = it->second.ejId;
o.ejInstanceId = it->second.ejId;
o.mapId = mapId;
o.order = order;
out.objectives.push_back(std::move(o));
}
void Mgr::AppendRecommendations(Payload& out, Player* player) const
{
uint8 level = player->GetLevel();
uint32 stage = out.stageId;
std::unordered_set<uint32> seen;
uint32 recId = 1;
for (DungeonRec const& d : DUNGEONS)
{
if (stage < d.stage || stage > d.maxStage)
continue;
if (level + 2 < d.minLevel || level > d.maxLevel + 1)
continue;
// Resolve EJ id from custom_ej_instance. If the EJ does
// not know about this map, we cannot link to a card, so
// we skip the recommendation entirely. This keeps the
// contract that recommendations.instanceID is always a
// valid EJ id.
auto it = _ejByMapId.find(d.mapId);
if (it == _ejByMapId.end())
continue;
if (!seen.insert(it->second.ejId).second)
continue;
Recommendation r;
r.id = recId++;
r.type = "dungeon";
r.title = it->second.name.empty() ? d.title
: it->second.name;
r.ejInstanceId = it->second.ejId; // JOURNALINSTANCE key
r.mapId = d.mapId; // WoW map id (auxiliary)
r.priority = 100 -
static_cast<uint32>(std::abs(int32(level) -
int32((d.minLevel + d.maxLevel) / 2)));
r.description = "Подходит для вашего текущего этапа.";
out.recommendations.push_back(std::move(r));
if (out.recommendations.size() >= 6)
break;
}
}
static uint32 ProgressFor(uint32 stage, uint8 level)
{
// Rough %: scaled across the three expansion bands.
uint32 p = 0;
if (stage < PROGRESSION_PRE_TBC)
p = std::min<uint32>(60u, (level * 60u) / 60u);
else if (stage < PROGRESSION_WOTLK_TIER_1)
p = 60u + std::min<uint32>(20u,
((level > 60 ? level - 60 : 0) * 20u) / 10u);
else
p = 80u + std::min<uint32>(20u,
((level > 70 ? level - 70 : 0) * 20u) / 10u);
// Add a small bump per cleared major raid stage above 1.
if (stage > 1)
p = std::min<uint32>(100u, p + (stage - 1) * 2u);
return p;
}
Payload Mgr::BuildFor(Player* player) const
{
Payload out;
out.version = PAYLOAD_VERSION;
out.characterGuid = player->GetGUID().GetRawValue();
// ProgressionState as set by IPP. Default is 0 (start of vanilla).
uint32 ipStage = player->GetPlayerSetting(
"mod-individual-progression", SETTING_PROGRESSION_STATE).value;
// Player's *next* stage is what the guide should be helping with.
// STAGES use 1-based ids where stage id = ProgressionState+1.
out.stageId = std::min<uint32>(ipStage + 1, 19);
out.stageName = StageName(out.stageId);
out.stageDescription = StageDescription(out.stageId);
out.progress = ProgressFor(ipStage, player->GetLevel());
uint32 order = 1;
switch (ipStage)
{
case PROGRESSION_START:
AppendLevelObjective(out, player, 60, order++);
AppendRaidObjective(out, 409,
"Победить Рагнароса в Огненных Недрах", order++);
break;
case PROGRESSION_MOLTEN_CORE:
AppendLevelObjective(out, player, 60, order++);
AppendRaidObjective(out, 249,
"Победить Ониксию", order++);
AppendRaidObjective(out, 469,
"Победить Нефариана в Логове Крыла Тьмы", order++);
break;
case PROGRESSION_ONYXIA:
AppendRaidObjective(out, 469,
"Победить Нефариана в Логове Крыла Тьмы", order++);
AppendRaidObjective(out, 309,
"Зул'Гуруб (опционально)", order++);
break;
case PROGRESSION_BLACKWING_LAIR:
AppendQuestObjective(out, player, MIGHT_OF_KALIMDOR,
"Сдать ресурсы для военного похода", order++);
AppendQuestObjective(out, player, BANG_A_GONG,
"Открыть ворота Ан'Киража", order++);
break;
case PROGRESSION_PRE_AQ:
AppendQuestObjective(out, player, CHAOS_AND_DESTRUCTION,
"Завершить полевые задания в Силитусе", order++);
AppendRaidObjective(out, 509,
"Руины Ан'Киража", order++);
break;
case PROGRESSION_AQ_WAR:
AppendRaidObjective(out, 531,
"Победить Ц'Туна в Храме Ан'Киража", order++);
break;
case PROGRESSION_AQ:
AppendRaidObjective(out, 533,
"Победить КелТузада в Наксрамасе", order++);
break;
case PROGRESSION_NAXX40:
AppendQuestObjective(out, player, INTO_THE_BREACH,
"Открыть Тёмный портал", order++);
AppendLevelObjective(out, player, 70, order++);
break;
case PROGRESSION_PRE_TBC:
AppendLevelObjective(out, player, 70, order++);
AppendRaidObjective(out, 532,
"Каражан", order++);
AppendRaidObjective(out, 565,
"Логово Груула", order++);
AppendRaidObjective(out, 544,
"Логово Магтеридона", order++);
break;
case PROGRESSION_TBC_TIER_1:
AppendRaidObjective(out, 548,
"Пещера Змеиных Недр", order++);
AppendRaidObjective(out, 550,
"Око Бури — Крепость Бурь", order++);
break;
case PROGRESSION_TBC_TIER_2:
AppendRaidObjective(out, 534,
"Битва за гору Хиджал", order++);
AppendRaidObjective(out, 564,
"Чёрный храм", order++);
break;
case PROGRESSION_TBC_TIER_3:
AppendRaidObjective(out, 568,
"Зул'Аман", order++);
break;
case PROGRESSION_TBC_TIER_4:
AppendRaidObjective(out, 580,
"Плато Солнечного Колодца", order++);
break;
case PROGRESSION_TBC_TIER_5:
AppendLevelObjective(out, player, 80, order++);
AppendRaidObjective(out, 533,
"Наксрамас (Нордскол)", order++);
AppendRaidObjective(out, 616,
"Око Вечности", order++);
AppendRaidObjective(out, 615,
"Камера Сокровищ Обсидиановой драконьей стаи",
order++);
break;
case PROGRESSION_WOTLK_TIER_1:
AppendRaidObjective(out, 603,
"Победить Йогг-Сарона в Ульдуаре", order++);
break;
case PROGRESSION_WOTLK_TIER_2:
AppendRaidObjective(out, 649,
"Испытание крестоносца", order++);
break;
case PROGRESSION_WOTLK_TIER_3:
AppendRaidObjective(out, 631,
"Цитадель Ледяной Короны", order++);
break;
case PROGRESSION_WOTLK_TIER_4:
AppendRaidObjective(out, 724,
"Рубиновое святилище", order++);
break;
default:
break;
}
AppendRecommendations(out, player);
return out;
}
std::string Mgr::SerializeJson(Objective const& o) const
{
std::ostringstream s;
s << '{';
bool first = true;
WriteUInt(s, "id", o.id, first);
WriteString(s, "type", o.type, first);
WriteString(s, "title", o.title, first);
if (!o.description.empty())
WriteString(s, "description", o.description, first);
WriteBool(s, "completed", o.completed, first);
WriteUInt(s, "progress", o.progress, first);
WriteUInt(s, "required", o.required, first);
WriteUInt(s, "targetID", o.targetId, first);
WriteUInt(s, "order", o.order, first);
WriteOptUInt(s, "questID", o.questId, first);
WriteOptUInt(s, "questChainID", o.questChainId, first);
WriteOptUInt(s, "ej_instanceID", o.ejInstanceId, first);
WriteOptUInt(s, "mapID", o.mapId, first);
WriteOptUInt(s, "encounterID", o.encounterId, first);
WriteOptUInt(s, "achievementID", o.achievementId, first);
WriteOptUInt(s, "factionID", o.factionId, first);
WriteOptUInt(s, "professionID", o.professionId, first);
WriteOptUInt(s, "itemID", o.itemId, first);
WriteOptUInt(s, "currencyID", o.currencyId, first);
s << '}';
return s.str();
}
std::string Mgr::SerializeJson(Payload const& p) const
{
std::ostringstream s;
s << '{';
s << "\"version\":" << p.version
<< ",\"characterGuid\":" << p.characterGuid
<< ",\"stageID\":" << p.stageId
<< ",\"stageName\":\"" << EscapeJson(p.stageName) << '"'
<< ",\"stageDescription\":\""
<< EscapeJson(p.stageDescription) << '"'
<< ",\"progress\":" << std::min<uint32>(p.progress, 100u);
// Objectives: collapse duplicate ids and clamp progress.
s << ",\"objectives\":[";
std::set<uint32> seenIds;
bool firstObj = true;
for (Objective const& src : p.objectives)
{
if (!seenIds.insert(src.id).second)
continue;
Objective o = src;
if (o.required && o.progress > o.required)
o.progress = o.required;
if (!firstObj) s << ',';
s << SerializeJson(o);
firstObj = false;
}
s << "]";
// Recommendations.
s << ",\"recommendations\":[";
bool firstRec = true;
for (Recommendation const& r : p.recommendations)
{
if (!firstRec) s << ',';
s << '{';
bool f = true;
WriteUInt(s, "id", r.id, f);
WriteString(s, "type", r.type, f);
WriteString(s, "title", r.title, f);
if (!r.description.empty())
WriteString(s, "description", r.description, f);
WriteOptUInt(s, "ej_instanceID", r.ejInstanceId, f);
WriteOptUInt(s, "mapID", r.mapId, f);
WriteOptUInt(s, "encounterID", r.encounterId, f);
WriteOptUInt(s, "questID", r.questId, f);
WriteOptUInt(s, "itemID", r.itemId, f);
WriteUInt(s, "priority", r.priority, f);
s << '}';
firstRec = false;
}
s << "]";
// Rewards.
s << ",\"rewards\":[";
bool firstRew = true;
for (Reward const& r : p.rewards)
{
if (!firstRew) s << ',';
s << '{';
bool f = true;
WriteString(s, "type", r.type, f);
WriteOptUInt(s, "itemID", r.itemId, f);
WriteOptUInt(s, "currencyID", r.currencyId, f);
WriteUInt(s, "count", std::max<uint32>(r.count, 1), f);
WriteOptUInt(s, "amount", r.amount, f);
s << '}';
firstRew = false;
}
s << "]";
s << '}';
return s.str();
}
}
@@ -0,0 +1,159 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>,
* released under GNU AGPL v3 license:
* https://github.com/azerothcore/azerothcore-wotlk/blob/master/LICENSE-AGPL3
*/
#ifndef MOONWELL_PLAYER_GUIDE_MGR_H
#define MOONWELL_PLAYER_GUIDE_MGR_H
#include "Define.h"
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
class Player;
namespace MoonWell::PlayerGuide
{
enum : uint32 { PAYLOAD_VERSION = 1 };
struct Objective
{
uint32 id = 0;
std::string type; // level | quest | quest_chain | dungeon |
// raid | boss | achievement |
// reputation | profession | item |
// currency | custom
std::string title;
std::string description;
bool completed = false;
uint32 progress = 0;
uint32 required = 0;
uint32 targetId = 0;
uint32 order = 0;
// Optional type-specific helpers. Zero means "not set" and
// serialiser omits it.
uint32 questId = 0;
uint32 questChainId = 0;
// ej_instanceID: key into client's JOURNALINSTANCE table.
// Consumed by EncounterJournal_DisplayInstance / EJ_SelectInstance.
uint32 ejInstanceId = 0;
// WoW map id (for telemetry / teleport / fallback). NOT used by
// the client to open EJ.
uint32 mapId = 0;
uint32 encounterId = 0;
uint32 achievementId = 0;
uint32 factionId = 0;
uint32 professionId = 0;
uint32 itemId = 0;
uint32 currencyId = 0;
};
struct Recommendation
{
uint32 id = 0;
std::string type;
std::string title;
std::string description;
uint32 ejInstanceId = 0; // JOURNALINSTANCE key
uint32 mapId = 0; // WoW map id (auxiliary)
uint32 encounterId = 0;
uint32 questId = 0;
uint32 itemId = 0;
uint32 priority = 0;
};
struct Reward
{
std::string type;
uint32 itemId = 0;
uint32 count = 1;
uint32 currencyId = 0;
uint32 amount = 0;
};
struct Payload
{
uint32 version = PAYLOAD_VERSION;
uint64 characterGuid = 0;
uint32 stageId = 0;
std::string stageName;
std::string stageDescription;
uint32 progress = 0; // 0..100
std::vector<Objective> objectives;
std::vector<Recommendation> recommendations;
std::vector<Reward> rewards;
};
class Mgr
{
public:
static Mgr* instance();
// Reads configuration. Safe to call multiple times.
void LoadConfig();
// Builds the per-character guide payload from IPP state.
// The function is read-only with respect to the player.
Payload BuildFor(Player* player) const;
// Serialises a payload to a single JSON string.
// Validates uniqueness of objective ids and clamps progress.
std::string SerializeJson(Payload const& payload) const;
std::string SerializeJson(Objective const& objective) const;
bool IsEnabled() const { return _enabled; }
bool LogPayloads() const { return _logPayloads; }
uint32 ChunkSize() const { return _chunkSize; }
// Reloads (map_id -> ej_instance.id/name) cache from
// custom_ej_instance. Called from LoadConfig() and from the
// OnLoadCustomDatabaseTable world hook.
void ReloadEncounterJournalCache();
private:
Mgr() = default;
// Stage helpers based on ProgressionState.
std::string StageName(uint32 stage) const;
std::string StageDescription(uint32 stage) const;
// Filling helpers.
void AppendLevelObjective(Payload& out, Player* player,
uint32 requiredLevel, uint32 order) const;
void AppendQuestObjective(Payload& out, Player* player,
uint32 questId, std::string const& title,
uint32 order) const;
void AppendDungeonObjective(Payload& out, uint32 mapId,
std::string const& title,
uint32 order) const;
void AppendRaidObjective(Payload& out, uint32 mapId,
std::string const& title,
uint32 order) const;
void AppendRecommendations(Payload& out, Player* player) const;
struct EjInstanceRef
{
uint32 ejId = 0; // custom_ej_instance.id
std::string name; // custom_ej_instance.name
};
// mapId -> EJ instance ref. Filled at config load.
std::unordered_map<uint32, EjInstanceRef> _ejByMapId;
bool _enabled = true;
bool _logPayloads = false;
uint32 _chunkSize = 230; // per-frame body capacity (addon channel)
};
}
#define sPlayerGuideMgr ::MoonWell::PlayerGuide::Mgr::instance()
#endif // MOONWELL_PLAYER_GUIDE_MGR_H
@@ -0,0 +1,196 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>,
* released under GNU AGPL v3 license:
* https://github.com/azerothcore/azerothcore-wotlk/blob/master/LICENSE-AGPL3
*/
#include "PlayerGuideMgr.h"
#include "PlayerGuideTransport.h"
#include "../IndividualProgression.h"
#include "Chat.h"
#include "Config.h"
#include "Creature.h"
#include "Log.h"
#include "Player.h"
#include "QuestDef.h"
#include "ScriptMgr.h"
#include "SharedDefines.h"
#include <cstdlib>
#include <string>
namespace MoonWell::PlayerGuide
{
namespace
{
void Push(Player* player)
{
if (!sPlayerGuideMgr->IsEnabled())
return;
if (!player || !player->GetSession() || player->IsBeingTeleported())
return;
Payload p = sPlayerGuideMgr->BuildFor(player);
std::string json = sPlayerGuideMgr->SerializeJson(p);
if (sPlayerGuideMgr->LogPayloads())
LOG_DEBUG("module", "[PlayerGuide] push {} bytes -> {}",
json.size(), player->GetName());
Transport::SendSetData(player, json);
}
// Splits "MWPG\t<cmd>\t<arg>" -> (cmd, arg). Returns false if
// the message does not match the expected addon prefix.
bool ParseClientMessage(std::string const& in, std::string& cmd,
std::string& arg)
{
// Expected prefix: "MWPG\t".
if (in.size() < 6)
return false;
if (in.compare(0, 4, ADDON_PREFIX) != 0)
return false;
if (in[4] != '\t')
return false;
std::size_t end = in.find('\t', 5);
if (end == std::string::npos)
{
cmd.assign(in, 5, in.size() - 5);
arg.clear();
return true;
}
cmd.assign(in, 5, end - 5);
arg.assign(in, end + 1, in.size() - (end + 1));
return true;
}
}
class PlayerGuide_PlayerScript : public PlayerScript
{
public:
PlayerGuide_PlayerScript()
: PlayerScript("PlayerGuide_PlayerScript",
{
PLAYERHOOK_ON_LOGIN,
PLAYERHOOK_ON_LEVEL_CHANGED,
PLAYERHOOK_ON_PLAYER_COMPLETE_QUEST,
PLAYERHOOK_ON_CREATURE_KILL,
PLAYERHOOK_ON_BEFORE_SEND_CHAT_MESSAGE
}) { }
void OnPlayerLogin(Player* player) override
{
Push(player);
}
void OnPlayerLevelChanged(Player* player, uint8 /*oldLevel*/) override
{
Push(player);
}
void OnPlayerCompleteQuest(Player* player,
Quest const* /*quest*/) override
{
Push(player);
}
void OnPlayerCreatureKill(Player* killer, Creature* killed) override
{
if (!killer || !killed)
return;
if (killed->GetCreatureTemplate()->rank <= CREATURE_ELITE_NORMAL)
return;
Push(killer);
}
void OnPlayerBeforeSendChatMessage(Player* player, uint32& type,
uint32& lang, std::string& msg) override
{
if (!sPlayerGuideMgr->IsEnabled())
return;
if (type != CHAT_MSG_WHISPER || lang != LANG_ADDON)
return;
if (sPlayerGuideMgr->LogPayloads())
LOG_DEBUG("module",
"[PlayerGuide] raw addon <- {} ({} bytes): '{}'",
player->GetName(), msg.size(), msg);
std::string cmd, arg;
if (!ParseClientMessage(msg, cmd, arg))
return;
LOG_INFO("module",
"[PlayerGuide] <- {} cmd='{}' arg='{}' ({} bytes)",
player->GetName(), cmd, arg, msg.size());
if (cmd == ClientCmd::REQUEST_REFRESH)
{
Push(player);
}
else if (cmd == ClientCmd::REQUEST_TRACK_OBJECTIVE)
{
// Tracking is purely client-side state for now; we just
// re-send the payload so the client can update its
// tracked id atomically.
Push(player);
}
else if (cmd == ClientCmd::REQUEST_OPEN_TARGET)
{
// Reserved for future: server-driven open-target action
// (e.g. queue LFG). For MVP a refresh is enough.
Push(player);
}
else
{
LOG_WARN("module",
"[PlayerGuide] unknown cmd='{}' from {}",
cmd, player->GetName());
return;
}
// Swallow the message so it does not get relayed as a real
// whisper. Clearing the body is enough — ChatHandler
// returns early on empty addon messages.
msg.clear();
}
};
class PlayerGuide_WorldScript : public WorldScript
{
public:
PlayerGuide_WorldScript()
: WorldScript("PlayerGuide_WorldScript",
{
WORLDHOOK_ON_AFTER_CONFIG_LOAD,
WORLDHOOK_ON_LOAD_CUSTOM_DATABASE_TABLE
}) { }
void OnAfterConfigLoad(bool /*reload*/) override
{
sPlayerGuideMgr->LoadConfig();
LOG_INFO("server.loading",
">> PlayerGuide module: {}.",
sPlayerGuideMgr->IsEnabled() ? "enabled" : "disabled");
}
void OnLoadCustomDatabaseTable() override
{
// Re-read custom_ej_instance after the EJ loader has had
// a chance to populate it. Order between modules on this
// hook is undefined, but we are reading the table itself
// (not EJ's in-memory cache) so we are fine as long as
// the SQL has been applied.
sPlayerGuideMgr->ReloadEncounterJournalCache();
}
};
}
void AddSC_mod_individual_progression_player_guide()
{
new MoonWell::PlayerGuide::PlayerGuide_PlayerScript();
new MoonWell::PlayerGuide::PlayerGuide_WorldScript();
}
@@ -0,0 +1,96 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>,
* released under GNU AGPL v3 license:
* https://github.com/azerothcore/azerothcore-wotlk/blob/master/LICENSE-AGPL3
*/
#include "PlayerGuideTransport.h"
#include "PlayerGuideMgr.h"
#include "Chat.h"
#include "Player.h"
#include "WorldPacket.h"
#include "WorldSession.h"
#include <algorithm>
#include <cstdio>
#include <vector>
namespace MoonWell::PlayerGuide
{
namespace
{
// The client-visible chat body must stay strictly below the
// 255-byte addon limit. We reserve room for the framing header.
// The mgr clamps its configured chunk size into a safe range.
constexpr std::size_t HARD_LIMIT = 250;
void SendOneChunk(Player* player, std::string_view command,
uint32 seq, uint32 total, std::string_view body)
{
std::string msg;
msg.reserve(ADDON_PREFIX[0] ? body.size() + 32 : body.size());
msg.append(ADDON_PREFIX);
msg.push_back('\t');
msg.append(command.data(), command.size());
msg.push_back('\t');
char buf[24];
std::snprintf(buf, sizeof(buf), "%u\t%u\t", seq, total);
msg.append(buf);
msg.append(body.data(), body.size());
if (msg.size() > HARD_LIMIT)
msg.resize(HARD_LIMIT);
WorldPacket data;
ChatHandler::BuildChatPacket(data, CHAT_MSG_WHISPER,
LANG_ADDON, player, player, msg);
player->GetSession()->SendPacket(&data);
}
}
void Transport::Send(Player* player, std::string_view command,
std::string_view body)
{
if (!player || !player->GetSession())
return;
// How much body fits into a chunk after framing.
// Framing = prefix(4) + '\t' + cmd(N) + '\t' + "65535\t65535\t".
// Use Mgr's configured chunk size when sensible.
std::size_t framing = 4 + 1 + command.size() + 1 + 16;
std::size_t budget = sPlayerGuideMgr->ChunkSize() > framing
? (sPlayerGuideMgr->ChunkSize() - framing)
: 64;
if (body.size() <= budget)
{
SendOneChunk(player, command, 1, 1, body);
return;
}
// Multi-chunk: split blindly by byte budget. JSON tolerates
// concatenation on the client.
uint32 total = static_cast<uint32>(
(body.size() + budget - 1) / budget);
uint32 seq = 1;
for (std::size_t off = 0; off < body.size(); off += budget, ++seq)
{
std::size_t take = std::min(budget, body.size() - off);
SendOneChunk(player, command, seq, total,
std::string_view(body.data() + off, take));
}
}
void Transport::SendSetData(Player* player, std::string const& json)
{
Send(player, ServerCmd::SET_DATA, json);
}
void Transport::SendNotify(Player* player, std::string const& text)
{
Send(player, ServerCmd::NOTIFY, text);
}
}
@@ -0,0 +1,57 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>,
* released under GNU AGPL v3 license:
* https://github.com/azerothcore/azerothcore-wotlk/blob/master/LICENSE-AGPL3
*/
#ifndef MOONWELL_PLAYER_GUIDE_TRANSPORT_H
#define MOONWELL_PLAYER_GUIDE_TRANSPORT_H
#include "Define.h"
#include <string>
#include <string_view>
class Player;
namespace MoonWell::PlayerGuide
{
// Addon channel prefix used on both ends. The client AIO bridge
// routes messages with this prefix to the MoonWellPlayerGuide
// handler.
constexpr char const* ADDON_PREFIX = "MWPG";
// Commands sent server -> client.
namespace ServerCmd
{
constexpr char const* SET_DATA = "SetData";
constexpr char const* SET_STAGE = "SetStage";
constexpr char const* UPDATE_OBJECTIVE = "UpdateObjective";
constexpr char const* NOTIFY = "Notify";
}
// Commands sent client -> server.
namespace ClientCmd
{
constexpr char const* REQUEST_REFRESH = "RequestRefresh";
constexpr char const* REQUEST_TRACK_OBJECTIVE = "RequestTrackObjective";
constexpr char const* REQUEST_OPEN_TARGET = "RequestOpenTarget";
}
class Transport
{
public:
// Sends a single logical message split into addon-channel
// chunks. Wire format per chunk:
// "MWPG\t<cmd>\t<seq>\t<total>\t<body>"
// Single-chunk messages still use seq=1 total=1.
static void Send(Player* player, std::string_view command,
std::string_view body);
// Convenience wrappers.
static void SendSetData(Player* player, std::string const& json);
static void SendNotify(Player* player, std::string const& text);
};
}
#endif // MOONWELL_PLAYER_GUIDE_TRANSPORT_H
@@ -4772,10 +4772,7 @@ uint32 PlayerbotAI::AutoScaleActivity(uint32 mod)
return static_cast<uint32>(mod * (1 - lagProgress));
}
bool PlayerbotAI::IsOpposing(Player* player)
{
return player && bot && player->GetTeamId() != bot->GetTeamId();
}
bool PlayerbotAI::IsOpposing(Player* player) { return IsOpposing(player->getRace(), bot->getRace()); }
bool PlayerbotAI::IsOpposing(uint8 race1, uint8 race2)
{
+4 -40
View File
@@ -82,9 +82,6 @@ FORCE_PLAYERBOTS="${FORCE_PLAYERBOTS:-${ACORE_PLAYERBOTS_FORCE:-0}}"
: "${ACORE_QUEST_PARTY_ENABLE:=1}"
: "${ACORE_QUEST_PARTY_MESSAGE:=0}"
: "${ACORE_GAMEMODES_ENABLE:=1}"
: "${ACORE_GAMEMODES_TRAITOR_ENABLE:=1}"
: "${ACORE_ELUNA_ENABLED:=true}"
: "${ACORE_ELUNA_TRACEBACK:=false}"
: "${ACORE_ELUNA_SCRIPT_PATH:=lua_scripts}"
@@ -269,9 +266,8 @@ database_service_running() {
docker compose ps --status running --services 2>/dev/null | grep -qx 'ac-database'
}
import_sql_file() {
import_world_sql_file() {
local file_path="$1"
local database="$2"
if [[ ! -f "$file_path" ]]; then
log "skip missing ${file_path#$ROOT_DIR/}"
@@ -279,21 +275,13 @@ import_sql_file() {
fi
if (( DRY_RUN )); then
log "would import ${file_path#$ROOT_DIR/} into ${database}"
log "would import ${file_path#$ROOT_DIR/} into acore_world"
return
fi
docker compose exec -T ac-database bash -lc \
"mysql --default-character-set=utf8mb4 -uroot -p\"\$MYSQL_ROOT_PASSWORD\" ${database}" < "$file_path"
log "imported ${file_path#$ROOT_DIR/} into ${database}"
}
import_world_sql_file() {
import_sql_file "$1" "acore_world"
}
import_characters_sql_file() {
import_sql_file "$1" "acore_characters"
"mysql --default-character-set=utf8mb4 -uroot -p\"\$MYSQL_ROOT_PASSWORD\" acore_world" < "$file_path"
log "imported ${file_path#$ROOT_DIR/} into acore_world"
}
import_local_lua_module_sql() {
@@ -312,15 +300,6 @@ import_local_lua_module_sql() {
import_world_sql_file "$start_zone_race_teleporters_sql"
}
import_gamemodes_sql() {
if ! database_service_running; then
log "skip gamemodes SQL: ac-database is not running"
return
fi
import_characters_sql_file "$ROOT_DIR/modules/mod-gamemodes/data/sql/db-characters/base/mod_gamemodes_characters.sql"
}
ensure_world_conf() {
if [[ -f "$WORLD_CONF" ]]; then
return
@@ -360,8 +339,6 @@ sync_module_dists() {
"$MODULES_ETC_DIR/mod_learnspells.conf.dist"
sync_file "$ROOT_DIR/modules/mod-playerbots/conf/playerbots.conf.dist" \
"$MODULES_ETC_DIR/playerbots.conf.dist"
sync_file "$ROOT_DIR/modules/mod-gamemodes/conf/mod_gamemodes.conf.dist" \
"$MODULES_ETC_DIR/mod_gamemodes.conf.dist"
sync_file "$ROOT_DIR/modules/mod-quest-loot-party/conf/mod-quest-loot-party.conf.dist" \
"$MODULES_ETC_DIR/mod-quest-loot-party.conf.dist"
}
@@ -505,16 +482,6 @@ playerbots_supported() {
return 0
}
write_gamemodes_conf() {
write_file "$MODULES_ETC_DIR/mod_gamemodes.conf.dist" <<EOF
[worldserver]
# Managed by ./setup-modules.sh
GameModes.Enable = ${ACORE_GAMEMODES_ENABLE}
GameModes.Traitor.Enable = ${ACORE_GAMEMODES_TRAITOR_ENABLE}
EOF
}
write_playerbots_conf() {
if ! [[ -f "$ROOT_DIR/modules/mod-playerbots/conf/playerbots.conf.dist" ]]; then
log "skip playerbots: module config source not found"
@@ -603,7 +570,6 @@ cleanup_legacy_module_overrides() {
"$MODULES_ETC_DIR/mod_eluna.conf"
"$MODULES_ETC_DIR/mod_learnspells.conf"
"$MODULES_ETC_DIR/mod-quest-loot-party.conf"
"$MODULES_ETC_DIR/mod_gamemodes.conf"
"$MODULES_ETC_DIR/playerbots.conf"
)
@@ -660,10 +626,8 @@ main() {
write_quest_loot_party_conf
write_eluna_conf
write_learnspells_conf
write_gamemodes_conf
write_playerbots_conf
import_local_lua_module_sql
import_gamemodes_sql
log "module bootstrap completed"
}
@@ -41,18 +41,14 @@ void CharacterDatabaseConnection::DoPrepareStatements()
PrepareStatement(CHAR_SEL_BANINFO_LIST, "SELECT bandate, unbandate, bannedby, banreason FROM character_banned WHERE guid = ? ORDER BY unbandate", CONNECTION_SYNCH);
PrepareStatement(CHAR_SEL_BANNED_NAME, "SELECT characters.name FROM characters, character_banned WHERE character_banned.guid = ? AND character_banned.guid = characters.guid", CONNECTION_SYNCH);
PrepareStatement(CHAR_SEL_ENUM, "SELECT c.guid, c.name, c.race, c.class, c.gender, c.skin, c.face, c.hairStyle, c.hairColor, c.facialStyle, c.level, c.zone, c.map, c.position_x, c.position_y, c.position_z, "
"gm.guildid, c.playerFlags, c.at_login, cp.entry, cp.modelid, cp.level, c.equipmentCache, cb.guid, c.extra_flags, COALESCE(mgc.game_mode, 0) "
"gm.guildid, c.playerFlags, c.at_login, cp.entry, cp.modelid, cp.level, c.equipmentCache, cb.guid, c.extra_flags "
"FROM characters AS c LEFT JOIN character_pet AS cp ON c.guid = cp.owner AND cp.slot = ? LEFT JOIN guild_member AS gm ON c.guid = gm.guid "
"LEFT JOIN character_banned AS cb ON c.guid = cb.guid AND cb.active = 1 "
"LEFT JOIN mod_gamemodes_characters AS mgc ON c.guid = mgc.guid "
"WHERE c.account = ? AND c.deleteInfos_Name IS NULL ORDER BY COALESCE(c.order, c.guid)", CONNECTION_ASYNC);
"LEFT JOIN character_banned AS cb ON c.guid = cb.guid AND cb.active = 1 WHERE c.account = ? AND c.deleteInfos_Name IS NULL ORDER BY COALESCE(c.order, c.guid)", CONNECTION_ASYNC);
PrepareStatement(CHAR_SEL_ENUM_DECLINED_NAME, "SELECT c.guid, c.name, c.race, c.class, c.gender, c.skin, c.face, c.hairStyle, c.hairColor, c.facialStyle, c.level, c.zone, c.map, "
"c.position_x, c.position_y, c.position_z, gm.guildid, c.playerFlags, c.at_login, cp.entry, cp.modelid, cp.level, c.equipmentCache, "
"cb.guid, c.extra_flags, COALESCE(mgc.game_mode, 0), cd.genitive FROM characters AS c LEFT JOIN character_pet AS cp ON c.guid = cp.owner AND cp.slot = ? "
"cb.guid, c.extra_flags, cd.genitive FROM characters AS c LEFT JOIN character_pet AS cp ON c.guid = cp.owner AND cp.slot = ? "
"LEFT JOIN character_declinedname AS cd ON c.guid = cd.guid LEFT JOIN guild_member AS gm ON c.guid = gm.guid "
"LEFT JOIN character_banned AS cb ON c.guid = cb.guid AND cb.active = 1 "
"LEFT JOIN mod_gamemodes_characters AS mgc ON c.guid = mgc.guid "
"WHERE c.account = ? AND c.deleteInfos_Name IS NULL ORDER BY COALESCE(c.order, c.guid)", CONNECTION_ASYNC);
"LEFT JOIN character_banned AS cb ON c.guid = cb.guid AND cb.active = 1 WHERE c.account = ? AND c.deleteInfos_Name IS NULL ORDER BY COALESCE(c.order, c.guid)", CONNECTION_ASYNC);
PrepareStatement(CHAR_SEL_FREE_NAME, "SELECT guid, name, at_login FROM characters WHERE guid = ? AND account = ? AND NOT EXISTS (SELECT NULL FROM characters WHERE name = ?)", CONNECTION_ASYNC);
PrepareStatement(CHAR_SEL_CHAR_ZONE, "SELECT zone FROM characters WHERE guid = ?", CONNECTION_SYNCH);
PrepareStatement(CHAR_SEL_CHARACTER_NAME_DATA, "SELECT race, class, gender, level FROM characters WHERE guid = ?", CONNECTION_SYNCH);
@@ -70,13 +66,12 @@ void CharacterDatabaseConnection::DoPrepareStatements()
PrepareStatement(CHAR_INS_BATTLEGROUND_RANDOM, "INSERT INTO character_battleground_random (guid) VALUES (?)", CONNECTION_ASYNC);
// Start LoginQueryHolder content
PrepareStatement(CHAR_SEL_CHARACTER, "SELECT characters.guid, account, name, race, class, gender, level, xp, money, skin, face, hairStyle, hairColor, facialStyle, bankSlots, restState, playerFlags, "
PrepareStatement(CHAR_SEL_CHARACTER, "SELECT guid, account, name, race, class, gender, level, xp, money, skin, face, hairStyle, hairColor, facialStyle, bankSlots, restState, playerFlags, "
"position_x, position_y, position_z, map, orientation, taximask, cinematic, totaltime, leveltime, rest_bonus, logout_time, is_logout_resting, resettalents_cost, "
"resettalents_time, trans_x, trans_y, trans_z, trans_o, transguid, extra_flags, stable_slots, at_login, zone, online, death_expire_time, taxi_path, instance_mode_mask, "
"arenaPoints, totalHonorPoints, todayHonorPoints, yesterdayHonorPoints, totalKills, todayKills, yesterdayKills, chosenTitle, knownCurrencies, watchedFaction, drunk, "
"health, power1, power2, power3, power4, power5, power6, power7, instance_id, talentGroupsCount, activeTalentGroup, exploredZones, equipmentCache, ammoId, "
"knownTitles, actionBars, grantableLevels, innTriggerId, extraBonusTalentCount, UNIX_TIMESTAMP(creation_date), COALESCE(mgc.game_mode, 0) "
"FROM characters LEFT JOIN mod_gamemodes_characters AS mgc ON characters.guid = mgc.guid WHERE characters.guid = ?", CONNECTION_ASYNC);
"knownTitles, actionBars, grantableLevels, innTriggerId, extraBonusTalentCount, UNIX_TIMESTAMP(creation_date) FROM characters WHERE guid = ?", CONNECTION_ASYNC);
PrepareStatement(CHAR_SEL_CHARACTER_AURAS, "SELECT casterGuid, itemGuid, spell, effectMask, recalculateMask, stackCount, amount0, amount1, amount2, "
"base_amount0, base_amount1, base_amount2, maxDuration, remainTime, remainCharges FROM character_aura WHERE guid = ?", CONNECTION_ASYNC);
@@ -114,8 +109,6 @@ void CharacterDatabaseConnection::DoPrepareStatements()
PrepareStatement(CHAR_SEL_CHARACTER_RANDOMBG, "SELECT guid FROM character_battleground_random WHERE guid = ?", CONNECTION_ASYNC);
PrepareStatement(CHAR_SEL_CHARACTER_BANNED, "SELECT guid FROM character_banned WHERE guid = ? AND active = 1", CONNECTION_ASYNC);
PrepareStatement(CHAR_SEL_CHARACTER_QUESTSTATUSREW, "SELECT quest FROM character_queststatus_rewarded WHERE guid = ? AND active = 1", CONNECTION_ASYNC);
PrepareStatement(CHAR_REP_CHAR_GAME_MODE, "REPLACE INTO mod_gamemodes_characters (guid, game_mode) VALUES (?, ?)", CONNECTION_ASYNC);
PrepareStatement(CHAR_DEL_CHAR_GAME_MODE, "DELETE FROM mod_gamemodes_characters WHERE guid = ?", CONNECTION_ASYNC);
PrepareStatement(CHAR_SEL_ACCOUNT_INSTANCELOCKTIMES, "SELECT instanceId, releaseTime FROM account_instance_times WHERE accountId = ?", CONNECTION_ASYNC);
PrepareStatement(CHAR_SEL_BREW_OF_THE_MONTH, "SELECT lastEventId FROM character_brew_of_the_month WHERE guid = ?", CONNECTION_ASYNC);
PrepareStatement(CHAR_REP_BREW_OF_THE_MONTH, "REPLACE INTO character_brew_of_the_month (guid, lastEventId) VALUES (?, ?)", CONNECTION_ASYNC);
@@ -97,8 +97,6 @@ enum CharacterDatabaseStatements : uint32
CHAR_SEL_CHARACTER_RANDOMBG,
CHAR_SEL_CHARACTER_BANNED,
CHAR_SEL_CHARACTER_QUESTSTATUSREW,
CHAR_REP_CHAR_GAME_MODE,
CHAR_DEL_CHAR_GAME_MODE,
CHAR_SEL_ACCOUNT_INSTANCELOCKTIMES,
CHAR_SEL_MAILITEMS,
CHAR_SEL_BREW_OF_THE_MONTH,
@@ -333,7 +333,7 @@ bool AchievementCriteriaData::Meets(uint32 criteria_id, Player const* source, Un
if (Player const* player = target->ToPlayer())
if (player->GetDeathTimer() != 0)
// flag set == must be same team, not set == different team
return (player->GetBgTeamId() == source->GetBgTeamId()) == (player_dead.own_team_flag != 0);
return (player->GetTeamId() == source->GetTeamId()) == (player_dead.own_team_flag != 0);
return false;
case ACHIEVEMENT_CRITERIA_DATA_TYPE_S_AURA:
return source->HasAuraEffect(aura.spell_id, aura.effect_idx);
@@ -389,7 +389,7 @@ bool AchievementCriteriaData::Meets(uint32 criteria_id, Player const* source, Un
if (!bg)
return false;
uint32 score = bg->GetTeamScore(source->GetBgTeamId() == TEAM_ALLIANCE ? TEAM_HORDE : TEAM_ALLIANCE);
uint32 score = bg->GetTeamScore(source->GetTeamId() == TEAM_ALLIANCE ? TEAM_HORDE : TEAM_ALLIANCE);
return score >= bg_loss_team_score.min_score && score <= bg_loss_team_score.max_score;
}
case ACHIEVEMENT_CRITERIA_DATA_TYPE_INSTANCE_SCRIPT:
@@ -462,7 +462,7 @@ bool AchievementCriteriaData::Meets(uint32 criteria_id, Player const* source, Un
uint32 winnnerScore = bg->GetTeamScore(winnerTeam);
uint32 loserScore = bg->GetTeamScore(TeamId(!uint32(winnerTeam)));
return source->GetBgTeamId() == winnerTeam && winnnerScore == teams_scores.winner_score && loserScore == teams_scores.loser_score;
return source->GetTeamId() == winnerTeam && winnnerScore == teams_scores.winner_score && loserScore == teams_scores.loser_score;
}
default:
break;
@@ -635,7 +635,7 @@ void AchievementMgr::LoadFromDB(PreparedQueryResult achievementResult, PreparedQ
// title achievement rewards are retroactive
if (AchievementReward const* reward = sAchievementMgr->GetAchievementReward(achievement))
if (uint32 titleId = reward->titleId[uint8(GetPlayer()->GetTeamId())])
if (uint32 titleId = reward->titleId[Player::TeamIdForRace(GetPlayer()->getRace())])
if (CharTitlesEntry const* titleEntry = sCharTitlesStore.LookupEntry(titleId))
if (!GetPlayer()->HasTitle(titleEntry))
GetPlayer()->SetTitle(titleEntry);
@@ -2448,8 +2448,8 @@ bool AchievementMgr::CanUpdateCriteria(AchievementCriteriaEntry const* criteria,
if (achievement->mapID != -1 && GetPlayer()->GetMapId() != uint32(achievement->mapID))
return false;
if ((achievement->requiredFaction == ACHIEVEMENT_FACTION_HORDE && GetPlayer()->GetTeamId() != TEAM_HORDE) ||
(achievement->requiredFaction == ACHIEVEMENT_FACTION_ALLIANCE && GetPlayer()->GetTeamId() != TEAM_ALLIANCE))
if ((achievement->requiredFaction == ACHIEVEMENT_FACTION_HORDE && GetPlayer()->GetTeamId(true) != TEAM_HORDE) ||
(achievement->requiredFaction == ACHIEVEMENT_FACTION_ALLIANCE && GetPlayer()->GetTeamId(true) != TEAM_ALLIANCE))
return false;
for (uint32 i = 0; i < MAX_CRITERIA_REQUIREMENTS; ++i)
+7 -41
View File
@@ -28,16 +28,6 @@ namespace
{
std::unordered_map<ObjectGuid, CharacterCacheEntry> _characterCacheStore;
std::unordered_map<std::string, CharacterCacheEntry*> _characterCacheByNameStore;
TeamId GetEffectiveCharacterTeam(uint8 race, uint32 playerFlags, uint8 gameMode)
{
TeamId teamId = Player::TeamIdForRace(race);
if (Player::ResolveGameModeFromStorage(playerFlags, gameMode) == PLAYER_GAMEMODE_TRAITOR)
return teamId == TEAM_ALLIANCE ? TEAM_HORDE : TEAM_ALLIANCE;
return teamId;
}
}
CharacterCache* CharacterCache::instance()
@@ -72,11 +62,7 @@ void CharacterCache::LoadCharacterCacheStorage()
_characterCacheStore.clear();
uint32 oldMSTime = getMSTime();
QueryResult result = CharacterDatabase.Query(R"(
SELECT c.guid, c.name, c.account, c.race, c.gender, c.class, c.level, c.playerFlags, COALESCE(mgc.game_mode, 0)
FROM characters c
LEFT JOIN mod_gamemodes_characters mgc ON mgc.guid = c.guid
)");
QueryResult result = CharacterDatabase.Query("SELECT guid, name, account, race, gender, class, level FROM characters");
if (!result)
{
LOG_INFO("server.loading", "No character name data loaded, empty query!");
@@ -87,8 +73,7 @@ void CharacterCache::LoadCharacterCacheStorage()
{
Field* fields = result->Fetch();
AddCharacterCacheEntry(ObjectGuid::Create<HighGuid::Player>(fields[0].Get<uint32>()) /*guid*/, fields[2].Get<uint32>() /*account*/, fields[1].Get<std::string>() /*name*/,
fields[4].Get<uint8>() /*gender*/, fields[3].Get<uint8>() /*race*/, fields[5].Get<uint8>() /*class*/, fields[6].Get<uint8>() /*level*/,
fields[7].Get<uint32>() /*playerFlags*/, fields[8].Get<uint8>() /*gameMode*/);
fields[4].Get<uint8>() /*gender*/, fields[3].Get<uint8>() /*race*/, fields[5].Get<uint8>() /*class*/, fields[6].Get<uint8>() /*level*/);
} while (result->NextRow());
QueryResult mailCountResult = CharacterDatabase.Query("SELECT receiver, COUNT(receiver) FROM mail GROUP BY receiver");
@@ -107,12 +92,7 @@ void CharacterCache::LoadCharacterCacheStorage()
void CharacterCache::RefreshCacheEntry(uint32 lowGuid)
{
QueryResult result = CharacterDatabase.Query(R"(
SELECT c.guid, c.name, c.account, c.race, c.gender, c.class, c.level, c.playerFlags, COALESCE(mgc.game_mode, 0)
FROM characters c
LEFT JOIN mod_gamemodes_characters mgc ON mgc.guid = c.guid
WHERE c.guid = {}
)", lowGuid);
QueryResult result = CharacterDatabase.Query("SELECT guid, name, account, race, gender, class, level FROM characters WHERE guid = {}", lowGuid);
if (!result)
{
return;
@@ -122,9 +102,7 @@ void CharacterCache::RefreshCacheEntry(uint32 lowGuid)
{
Field* fields = result->Fetch();
DeleteCharacterCacheEntry(ObjectGuid::Create<HighGuid::Player>(lowGuid), fields[1].Get<std::string>());
AddCharacterCacheEntry(ObjectGuid::Create<HighGuid::Player>(fields[0].Get<uint32>()) /*guid*/, fields[2].Get<uint32>() /*account*/, fields[1].Get<std::string>() /*name*/,
fields[4].Get<uint8>() /*gender*/, fields[3].Get<uint8>() /*race*/, fields[5].Get<uint8>() /*class*/, fields[6].Get<uint8>() /*level*/,
fields[7].Get<uint32>() /*playerFlags*/, fields[8].Get<uint8>() /*gameMode*/);
AddCharacterCacheEntry(ObjectGuid::Create<HighGuid::Player>(fields[0].Get<uint32>()) /*guid*/, fields[2].Get<uint32>() /*account*/, fields[1].Get<std::string>() /*name*/, fields[4].Get<uint8>() /*gender*/, fields[3].Get<uint8>() /*race*/, fields[5].Get<uint8>() /*class*/, fields[6].Get<uint8>() /*level*/);
} while (result->NextRow());
QueryResult mailCountResult = CharacterDatabase.Query("SELECT receiver, COUNT(receiver) FROM mail WHERE receiver = {} GROUP BY receiver", lowGuid);
@@ -141,18 +119,16 @@ void CharacterCache::RefreshCacheEntry(uint32 lowGuid)
/*
Modifying functions
*/
void CharacterCache::AddCharacterCacheEntry(ObjectGuid const& guid, uint32 accountId, std::string const& name, uint8 gender, uint8 race, uint8 playerClass, uint8 level, uint32 playerFlags, uint8 gameMode)
void CharacterCache::AddCharacterCacheEntry(ObjectGuid const& guid, uint32 accountId, std::string const& name, uint8 gender, uint8 race, uint8 playerClass, uint8 level)
{
CharacterCacheEntry& data = _characterCacheStore[guid];
data.Guid = guid;
data.Name = name;
data.AccountId = accountId;
data.PlayerFlags = playerFlags;
data.Race = race;
data.Sex = gender;
data.Class = playerClass;
data.Level = level;
data.GameMode = gameMode;
data.GuildId = 0; // Will be set in guild loading or guild setting
for (uint8 i = 0; i < MAX_ARENA_SLOT; ++i)
{
@@ -169,7 +145,7 @@ void CharacterCache::DeleteCharacterCacheEntry(ObjectGuid const& guid, std::stri
_characterCacheByNameStore.erase(name);
}
void CharacterCache::UpdateCharacterData(ObjectGuid const& guid, std::string const& name, Optional<uint8> gender /*= {}*/, Optional<uint8> race /*= {}*/, Optional<uint32> playerFlags /*= {}*/, Optional<uint8> gameMode /*= {}*/)
void CharacterCache::UpdateCharacterData(ObjectGuid const& guid, std::string const& name, Optional<uint8> gender /*= {}*/, Optional<uint8> race /*= {}*/)
{
auto itr = _characterCacheStore.find(guid);
if (itr == _characterCacheStore.end())
@@ -188,16 +164,6 @@ void CharacterCache::UpdateCharacterData(ObjectGuid const& guid, std::string con
itr->second.Race = *race;
}
if (playerFlags)
{
itr->second.PlayerFlags = *playerFlags;
}
if (gameMode)
{
itr->second.GameMode = *gameMode;
}
//WorldPackets::Misc::InvalidatePlayer packet(guid);
//sWorld->SendGlobalMessage(packet.Write());
@@ -345,7 +311,7 @@ uint32 CharacterCache::GetCharacterTeamByGuid(ObjectGuid guid) const
return 0;
}
return GetEffectiveCharacterTeam(itr->second.Race, itr->second.PlayerFlags, itr->second.GameMode);
return Player::TeamIdForRace(itr->second.Race);
}
uint32 CharacterCache::GetCharacterAccountIdByGuid(ObjectGuid guid) const
+2 -4
View File
@@ -29,12 +29,10 @@ struct CharacterCacheEntry
ObjectGuid Guid;
std::string Name;
uint32 AccountId;
uint32 PlayerFlags = 0;
uint8 Class;
uint8 Race;
uint8 Sex;
uint8 Level;
uint8 GameMode = 0;
uint8 MailCount;
ObjectGuid::LowType GuildId;
std::array<uint32, MAX_ARENA_SLOT> ArenaTeamId;
@@ -51,10 +49,10 @@ class AC_GAME_API CharacterCache
void LoadCharacterCacheStorage();
void RefreshCacheEntry(uint32 lowGuid);
void AddCharacterCacheEntry(ObjectGuid const& guid, uint32 accountId, std::string const& name, uint8 gender, uint8 race, uint8 playerClass, uint8 level, uint32 playerFlags = 0, uint8 gameMode = 0);
void AddCharacterCacheEntry(ObjectGuid const& guid, uint32 accountId, std::string const& name, uint8 gender, uint8 race, uint8 playerClass, uint8 level);
void DeleteCharacterCacheEntry(ObjectGuid const& guid, std::string const& name);
void UpdateCharacterData(ObjectGuid const& guid, std::string const& name, Optional<uint8> gender = {}, Optional<uint8> race = {}, Optional<uint32> playerFlags = {}, Optional<uint8> gameMode = {});
void UpdateCharacterData(ObjectGuid const& guid, std::string const& name, Optional<uint8> gender = {}, Optional<uint8> race = {});
void UpdateCharacterLevel(ObjectGuid const& guid, uint8 level);
void UpdateCharacterAccountId(ObjectGuid const& guid, uint32 accountId);
void UpdateCharacterGuildId(ObjectGuid const& guid, ObjectGuid::LowType guildId);
+3 -3
View File
@@ -430,7 +430,7 @@ namespace lfg
{
if (!itemRequirement->checkLeaderOnly || !group || group->GetLeaderGUID() == player->GetGUID())
{
if (itemRequirement->faction == TEAM_NEUTRAL || itemRequirement->faction == player->GetTeamId())
if (itemRequirement->faction == TEAM_NEUTRAL || itemRequirement->faction == player->GetTeamId(true))
{
if (!player->HasItemCount(itemRequirement->id, 1))
{
@@ -446,7 +446,7 @@ namespace lfg
{
if (!questRequirement->checkLeaderOnly || !group || group->GetLeaderGUID() == player->GetGUID())
{
if (questRequirement->faction == TEAM_NEUTRAL || questRequirement->faction == player->GetTeamId())
if (questRequirement->faction == TEAM_NEUTRAL || questRequirement->faction == player->GetTeamId(true))
{
if (!player->GetQuestRewardStatus(questRequirement->id))
{
@@ -468,7 +468,7 @@ namespace lfg
{
if (!achievementRequirement->checkLeaderOnly || !group || group->GetLeaderGUID() == player->GetGUID())
{
if (achievementRequirement->faction == TEAM_NEUTRAL || achievementRequirement->faction == player->GetTeamId())
if (achievementRequirement->faction == TEAM_NEUTRAL || achievementRequirement->faction == player->GetTeamId(true))
{
if (!player->HasAchieved(achievementRequirement->id))
{
+23 -240
View File
@@ -147,80 +147,6 @@ enum CharacterCustomizeFlags
CHAR_CUSTOMIZE_FLAG_RACE = 0x00100000 // name, gender, race, etc...
};
namespace
{
constexpr uint32 ALLIANCE_TRAITOR_REPUTATION_FACTIONS[] = { 72, 47, 69, 54, 930, 890, 730, 509 };
constexpr uint32 HORDE_TRAITOR_REPUTATION_FACTIONS[] = { 76, 81, 68, 530, 911, 889, 729, 510 };
constexpr uint32 TRAITOR_REPUTATION_FACTION_COUNT = 8;
bool IsGameModeEnabled(PlayerGameMode gameMode)
{
if (gameMode == PLAYER_GAMEMODE_NORMAL)
return true;
if (!sConfigMgr->GetOption<bool>("GameModes.Enable", true))
return false;
switch (gameMode)
{
case PLAYER_GAMEMODE_TRAITOR:
return sConfigMgr->GetOption<bool>("GameModes.Traitor.Enable", true);
case PLAYER_GAMEMODE_NORMAL:
default:
return true;
}
}
TeamId GetOppositeTeamId(TeamId team)
{
switch (team)
{
case TEAM_ALLIANCE:
return TEAM_HORDE;
case TEAM_HORDE:
return TEAM_ALLIANCE;
default:
return TEAM_NEUTRAL;
}
}
PlayerGameMode NormalizeGameModeValue(uint8 gameMode)
{
return gameMode == PLAYER_GAMEMODE_TRAITOR ? PLAYER_GAMEMODE_TRAITOR : PLAYER_GAMEMODE_NORMAL;
}
PlayerGameMode GameModeFromOutfitId(uint8 outfitId)
{
if (outfitId == 0)
return PLAYER_GAMEMODE_NORMAL;
// We repurpose the create packet's outfit byte for game mode selection.
// The stock client normally sends 0 here, so treat any non-zero custom value
// as a request for the only create-time custom mode we currently support.
PlayerGameMode gameMode = NormalizeGameModeValue(outfitId);
if (gameMode == PLAYER_GAMEMODE_NORMAL)
gameMode = PLAYER_GAMEMODE_TRAITOR;
return IsGameModeEnabled(gameMode) ? gameMode : PLAYER_GAMEMODE_NORMAL;
}
uint8 GetRepresentativeRaceForTeam(TeamId team)
{
return team == TEAM_HORDE ? RACE_ORC : RACE_HUMAN;
}
PlayerInfo const* GetRepresentativePlayerInfoForTeam(TeamId team, uint8 classId)
{
uint8 representativeRace = GetRepresentativeRaceForTeam(team);
if (PlayerInfo const* info = sObjectMgr->GetPlayerInfo(representativeRace, classId))
return info;
return sObjectMgr->GetPlayerInfo(representativeRace, CLASS_WARRIOR);
}
}
static uint32 copseReclaimDelay[MAX_DEATH_COUNT] = { 30, 60, 120 };
// we can disable this warning for this since it only
@@ -560,13 +486,12 @@ void Player::CleanupsBeforeDelete(bool finalCleanup)
bool Player::Create(ObjectGuid::LowType guidlow, CharacterCreateInfo* createInfo)
{
m_outfitId = createInfo->OutfitId;
// FIXME: outfitId not used in player creating
/// @todo: need more checks against packet modifications
// should check that skin, face, hair* are valid via DBC per race/class
// also do it in Player::BuildEnumData, Player::LoadFromDB
Object::_Create(guidlow, 0, HighGuid::Player);
SetGameMode(GameModeFromOutfitId(m_outfitId));
m_name = createInfo->Name;
@@ -581,6 +506,8 @@ bool Player::Create(ObjectGuid::LowType guidlow, CharacterCreateInfo* createInfo
for (uint8 i = 0; i < PLAYER_SLOTS_COUNT; i++)
m_items[i] = nullptr;
Relocate(info->positionX, info->positionY, info->positionZ, info->orientation);
ChrClassesEntry const* cEntry = sChrClassesStore.LookupEntry(createInfo->Class);
if (!cEntry)
{
@@ -589,6 +516,8 @@ bool Player::Create(ObjectGuid::LowType guidlow, CharacterCreateInfo* createInfo
return false;
}
SetMap(sMapMgr->CreateMap(info->mapId, this));
uint8 powertype = cEntry->powerType;
SetObjectScale(1.0f);
@@ -596,6 +525,8 @@ bool Player::Create(ObjectGuid::LowType guidlow, CharacterCreateInfo* createInfo
m_realRace = createInfo->Race; // set real race flag
m_race = createInfo->Race; // set real race flag
SetFactionForRace(createInfo->Race);
if (!IsValidGender(createInfo->Gender))
{
LOG_ERROR("entities.player", "Player::Create: Possible hacking-attempt: Account {} tried creating a character named '{}' with an invalid gender ({}) - refusing to do so",
@@ -604,14 +535,8 @@ bool Player::Create(ObjectGuid::LowType guidlow, CharacterCreateInfo* createInfo
}
uint32 RaceClassGender = (createInfo->Race) | (createInfo->Class << 8) | (createInfo->Gender << 16);
SetUInt32Value(UNIT_FIELD_BYTES_0, (RaceClassGender | (powertype << 24)));
SetFactionForRace(createInfo->Race);
WorldLocation startPosition = GetStartPosition();
Relocate(startPosition);
SetMap(sMapMgr->CreateMap(startPosition.GetMapId(), this));
InitDisplayIds();
if (sWorld->getIntConfig(CONFIG_GAME_TYPE) == REALM_TYPE_PVP || sWorld->getIntConfig(CONFIG_GAME_TYPE) == REALM_TYPE_RPPVP)
{
@@ -1186,8 +1111,8 @@ bool Player::BuildEnumData(PreparedQueryResult result, WorldPacket* data)
// characters.hairColor, characters.facialStyle, character.level, characters.zone, characters.map, characters.position_x, characters.position_y, characters.position_z,
// 16 17 18 19 20 21 22 23
// guild_member.guildid, characters.playerFlags, characters.at_login, character_pet.entry, character_pet.modelid, character_pet.level, characters.equipmentCache, character_banned.guid,
// 24 25 26
// characters.extra_flags, mod_gamemodes_characters.game_mode, character_declinedname.genitive
// 24 25
// characters.extra_flags, character_declinedname.genitive
Field* fields = result->Fetch();
@@ -1259,15 +1184,12 @@ bool Player::BuildEnumData(PreparedQueryResult result, WorldPacket* data)
charFlags |= CHARACTER_FLAG_LOCKED_BY_BILLING;
if (sWorld->getBoolConfig(CONFIG_DECLINED_NAMES_USED))
{
if (!fields[26].Get<std::string>().empty())
if (!fields[25].Get<std::string>().empty())
charFlags |= CHARACTER_FLAG_DECLINED;
}
else
charFlags |= CHARACTER_FLAG_DECLINED;
if (fields[25].Get<uint8>() == 1 /* GAMEMODE_TRAITOR */)
charFlags |= CHARACTER_FLAG_UNK31;
*data << uint32(charFlags); // character flags
// character customize flags
@@ -4143,10 +4065,6 @@ void Player::DeleteFromDB(ObjectGuid::LowType lowGuid, uint32 accountId, bool up
stmt->SetData(0, lowGuid);
trans->Append(stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_GAME_MODE);
stmt->SetData(0, lowGuid);
trans->Append(stmt);
stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_PLAYER_ACCOUNT_DATA);
stmt->SetData(0, lowGuid);
trans->Append(stmt);
@@ -5932,46 +5850,17 @@ TeamId Player::TeamIdForRace(uint8 race)
void Player::SetFactionForRace(uint8 race)
{
TeamId nativeTeam = TeamIdForRace(race);
m_team = IsTraitor() ? GetOppositeTeamId(nativeTeam) : nativeTeam;
m_team = TeamIdForRace(race);
sScriptMgr->OnPlayerUpdateFaction(this);
uint8 factionRace = IsTraitor() ? GetRepresentativeRaceForTeam(m_team) : race;
ChrRacesEntry const* rEntry = sChrRacesStore.LookupEntry(factionRace);
if (GetTeamId(true) != GetTeamId())
return;
ChrRacesEntry const* rEntry = sChrRacesStore.LookupEntry(race);
SetFaction(rEntry ? rEntry->FactionID : 0);
}
PlayerGameMode Player::ResolveGameModeFromStorage(uint32 playerFlags, uint8 gameMode)
{
PlayerGameMode resolvedGameMode = NormalizeGameModeValue(gameMode);
if (resolvedGameMode == PLAYER_GAMEMODE_NORMAL && (playerFlags & PLAYER_FLAGS_TRAITOR))
resolvedGameMode = PLAYER_GAMEMODE_TRAITOR;
return IsGameModeEnabled(resolvedGameMode) ? resolvedGameMode : PLAYER_GAMEMODE_NORMAL;
}
PlayerInfo const* Player::GetStartPlayerInfo() const
{
if (IsTraitor())
return GetRepresentativePlayerInfoForTeam(GetTeamId(), getClass());
return sObjectMgr->GetPlayerInfo(getRace(true), getClass());
}
void Player::SetGameMode(PlayerGameMode gameMode)
{
m_gameMode = NormalizeGameModeValue(static_cast<uint8>(gameMode));
if (!IsGameModeEnabled(m_gameMode))
m_gameMode = PLAYER_GAMEMODE_NORMAL;
if (m_gameMode == PLAYER_GAMEMODE_TRAITOR)
SetPlayerFlag(PLAYER_FLAGS_TRAITOR);
else
RemovePlayerFlag(PLAYER_FLAGS_TRAITOR);
}
ReputationRank Player::GetReputationRank(uint32 faction) const
{
FactionEntry const* factionEntry = sFactionStore.LookupEntry(faction);
@@ -6083,7 +5972,7 @@ void Player::RewardReputation(Unit* victim)
ChampioningFaction = GetChampioningFaction();
}
TeamId teamId = GetTeamId();
TeamId teamId = GetTeamId(true); // Always check player original reputation when rewarding
if (Rep->RepFaction1 && (!Rep->TeamDependent || teamId == TEAM_ALLIANCE))
{
@@ -6197,78 +6086,6 @@ void Player::RewardReputation(Quest const* quest)
}
}
void Player::ApplyTraitorReputationState()
{
if (!IsTraitor())
return;
auto normalizeFactionState = [this](FactionEntry const* factionEntry, bool visible, bool atWar)
{
if (!factionEntry || factionEntry->reputationListID < 0)
return;
FactionState* factionState = const_cast<FactionState*>(GetReputationMgr().GetState(factionEntry));
if (!factionState)
return;
factionState->Flags &= ~(FACTION_FLAG_VISIBLE | FACTION_FLAG_HIDDEN | FACTION_FLAG_INVISIBLE_FORCED |
FACTION_FLAG_PEACE_FORCED | FACTION_FLAG_INACTIVE);
if (visible)
factionState->Flags |= FACTION_FLAG_VISIBLE;
else
factionState->Flags |= FACTION_FLAG_HIDDEN | FACTION_FLAG_INVISIBLE_FORCED;
if (atWar)
factionState->Flags |= FACTION_FLAG_AT_WAR;
else
factionState->Flags &= ~FACTION_FLAG_AT_WAR;
factionState->needSend = true;
factionState->needSave = true;
};
auto setFactionStanding = [this, &normalizeFactionState](uint32 factionId, int32 targetDisplayed, bool visible, bool atWar)
{
FactionEntry const* factionEntry = sFactionStore.LookupEntry(factionId);
if (!factionEntry)
return;
GetReputationMgr().SetOneFactionReputation(factionEntry, static_cast<float>(targetDisplayed), false);
normalizeFactionState(factionEntry, visible, atWar);
};
auto normalizeFactionAndParent = [&normalizeFactionState](uint32 factionId, bool visible, bool atWar)
{
if (FactionEntry const* factionEntry = sFactionStore.LookupEntry(factionId))
{
normalizeFactionState(factionEntry, visible, atWar);
if (factionEntry->team)
normalizeFactionState(sFactionStore.LookupEntry(factionEntry->team), visible, false);
}
};
TeamId nativeTeam = GetTeamId(true);
uint32 const* friendlyFactions = nativeTeam == TEAM_ALLIANCE ? HORDE_TRAITOR_REPUTATION_FACTIONS : ALLIANCE_TRAITOR_REPUTATION_FACTIONS;
uint32 const* hostileFactions = nativeTeam == TEAM_ALLIANCE ? ALLIANCE_TRAITOR_REPUTATION_FACTIONS : HORDE_TRAITOR_REPUTATION_FACTIONS;
if (HasAtLoginFlag(AT_LOGIN_FIRST))
{
for (uint32 i = 0; i < TRAITOR_REPUTATION_FACTION_COUNT; ++i)
setFactionStanding(friendlyFactions[i], 0, true, false);
for (uint32 i = 0; i < TRAITOR_REPUTATION_FACTION_COUNT; ++i)
setFactionStanding(hostileFactions[i], -6000, false, true);
}
for (uint32 i = 0; i < TRAITOR_REPUTATION_FACTION_COUNT; ++i)
{
normalizeFactionAndParent(friendlyFactions[i], true, false);
normalizeFactionAndParent(hostileFactions[i], false, true);
}
}
void Player::RewardExtraBonusTalentPoints(uint32 bonusTalentPoints)
{
if (bonusTalentPoints)
@@ -10484,7 +10301,7 @@ bool Player::ActivateTaxiPathTo(std::vector<uint32> const& nodes, Creature* npc
// only one mount ID for both sides. Probably not good to use 315 in case DBC nodes
// change but I couldn't find a suitable alternative. OK to use class because only DK
// can use this taxi.
uint32 mount_display_id = sObjectMgr->GetTaxiMountDisplayId(sourcenode, GetTeamId(), npc == nullptr || (sourcenode == 315 && IsClass(CLASS_DEATH_KNIGHT, CLASS_CONTEXT_TAXI)));
uint32 mount_display_id = sObjectMgr->GetTaxiMountDisplayId(sourcenode, GetTeamId(true), npc == nullptr || (sourcenode == 315 && IsClass(CLASS_DEATH_KNIGHT, CLASS_CONTEXT_TAXI)));
// in spell case allow 0 model
if ((mount_display_id == 0 && spellid == 0) || sourcepath == 0)
@@ -10579,7 +10396,7 @@ void Player::ContinueTaxiFlight()
LOG_DEBUG("entities.unit", "WORLD: Restart character {} taxi flight", GetGUID().ToString());
uint32 mountDisplayId = sObjectMgr->GetTaxiMountDisplayId(sourceNode, GetTeamId(), true);
uint32 mountDisplayId = sObjectMgr->GetTaxiMountDisplayId(sourceNode, GetTeamId(true), true);
if (!mountDisplayId)
return;
@@ -10858,7 +10675,7 @@ bool Player::BuyItemFromVendorSlot(ObjectGuid vendorguid, uint32 vendorslot, uin
return false;
}
if (!IsGameMaster() && ((pProto->HasFlag2(ITEM_FLAG2_FACTION_HORDE) && GetTeamId() == TEAM_ALLIANCE) || (pProto->HasFlag2(ITEM_FLAG2_FACTION_ALLIANCE) && GetTeamId() == TEAM_HORDE)))
if (!IsGameMaster() && ((pProto->HasFlag2(ITEM_FLAG2_FACTION_HORDE) && GetTeamId(true) == TEAM_ALLIANCE) || (pProto->HasFlag2(ITEM_FLAG2_FACTION_ALLIANCE) && GetTeamId(true) == TEAM_HORDE)))
{
return false;
}
@@ -11521,7 +11338,7 @@ void Player::ReportedAfkBy(Player* reporter)
WorldLocation Player::GetStartPosition() const
{
PlayerInfo const* info = GetStartPlayerInfo();
PlayerInfo const* info = sObjectMgr->GetPlayerInfo(getRace(true), getClass());
uint32 mapId = info->mapId;
if (IsClass(CLASS_DEATH_KNIGHT, CLASS_CONTEXT_INIT) && HasSpell(50977))
return WorldLocation(0, 2352.0f, -5709.0f, 154.5f, 0.0f);
@@ -14881,7 +14698,6 @@ void Player::_SaveEntryPoint(CharacterDatabaseTransaction trans)
stmt->SetData(7, m_entryPointData.taxiPath[1]);
stmt->SetData(8, m_entryPointData.mountSpell);
trans->Append(stmt);
}
void Player::DeleteEquipmentSet(uint64 setGuid)
@@ -14949,25 +14765,6 @@ void Player::_SaveCharacter(bool create, CharacterDatabaseTransaction trans)
uint8 index = 0;
auto finiteAlways = [](float f) { return std::isfinite(f) ? f : 0.0f; };
PlayerGameMode persistedGameMode = GetGameMode();
// Character enum and login restore traitor mode from persisted storage, not from the
// transient create packet. Re-derive the initial mode from the raw create byte before
// the first save in case later create initialization has reset the in-memory state.
if (create && persistedGameMode == PLAYER_GAMEMODE_NORMAL && m_outfitId != 0)
{
persistedGameMode = GameModeFromOutfitId(m_outfitId);
if (persistedGameMode != GetGameMode())
{
SetGameMode(persistedGameMode);
SetFactionForRace(getRace(true));
}
}
uint32 playerFlagsForSave = GetPlayerFlags();
if (create && persistedGameMode == PLAYER_GAMEMODE_TRAITOR)
playerFlagsForSave |= PLAYER_FLAGS_TRAITOR;
if (create)
{
@@ -14990,7 +14787,7 @@ void Player::_SaveCharacter(bool create, CharacterDatabaseTransaction trans)
stmt->SetData(index++, GetByteValue(PLAYER_BYTES_2, 0));
stmt->SetData(index++, GetByteValue(PLAYER_BYTES_2, 2));
stmt->SetData(index++, GetByteValue(PLAYER_BYTES_2, 3));
stmt->SetData(index++, playerFlagsForSave);
stmt->SetData(index++, (uint32)GetPlayerFlags());
stmt->SetData(index++, (uint16)GetMapId());
stmt->SetData(index++, (uint32)GetInstanceId());
stmt->SetData(index++, (uint8(GetDungeonDifficulty()) | uint8(GetRaidDifficulty()) << 4));
@@ -15108,7 +14905,7 @@ void Player::_SaveCharacter(bool create, CharacterDatabaseTransaction trans)
stmt->SetData(index++, GetByteValue(PLAYER_BYTES_2, 0));
stmt->SetData(index++, GetByteValue(PLAYER_BYTES_2, 2));
stmt->SetData(index++, GetByteValue(PLAYER_BYTES_2, 3));
stmt->SetData(index++, playerFlagsForSave);
stmt->SetData(index++, GetPlayerFlags());
if (!IsBeingTeleported())
{
@@ -15236,20 +15033,6 @@ void Player::_SaveCharacter(bool create, CharacterDatabaseTransaction trans)
}
trans->Append(stmt);
if (persistedGameMode == PLAYER_GAMEMODE_TRAITOR)
{
stmt = CharacterDatabase.GetPreparedStatement(CHAR_REP_CHAR_GAME_MODE);
stmt->SetData(0, GetGUID().GetCounter());
stmt->SetData(1, uint8(persistedGameMode));
}
else
{
stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_CHAR_GAME_MODE);
stmt->SetData(0, GetGUID().GetCounter());
}
trans->Append(stmt);
}
void Player::_LoadGlyphs(PreparedQueryResult result)
+2 -20
View File
@@ -454,13 +454,6 @@ enum DrunkenState
#define MAX_DRUNKEN 4
enum PlayerGameMode : uint8
{
PLAYER_GAMEMODE_NORMAL = 0,
PLAYER_GAMEMODE_TRAITOR = 1,
PLAYER_GAMEMODE_MAX
};
enum PlayerFlags : uint32
{
PLAYER_FLAGS_GROUP_LEADER = 0x00000001,
@@ -493,7 +486,7 @@ enum PlayerFlags : uint32
PLAYER_FLAGS_UNK27 = 0x08000000,
PLAYER_FLAGS_UNK28 = 0x10000000,
PLAYER_FLAGS_UNK29 = 0x20000000,
PLAYER_FLAGS_TRAITOR = 0x40000000, // mod-gamemodes: player is playing for the opposite faction
PLAYER_FLAGS_UNK30 = 0x40000000,
PLAYER_FLAGS_UNK31 = 0x80000000,
};
@@ -1163,7 +1156,7 @@ public:
PlayerSocial* GetSocial() { return m_social; }
PlayerTaxi m_taxi;
void InitTaxiNodesForLevel() { m_taxi.InitTaxiNodesForLevel(IsTraitor() ? (GetTeamId() == TEAM_ALLIANCE ? RACE_HUMAN : RACE_ORC) : getRace(), getClass(), GetLevel()); }
void InitTaxiNodesForLevel() { m_taxi.InitTaxiNodesForLevel(getRace(), getClass(), GetLevel()); }
bool ActivateTaxiPathTo(std::vector<uint32> const& nodes, Creature* npc = nullptr, uint32 spellid = 1);
bool ActivateTaxiPathTo(uint32 taxi_path_id, uint32 spellid = 1);
void CleanupAfterTaxiFlight();
@@ -2133,18 +2126,10 @@ public:
void CheckAreaExploreAndOutdoor();
static TeamId TeamIdForRace(uint8 race);
[[nodiscard]] static PlayerGameMode ResolveGameModeFromStorage(uint32 playerFlags, uint8 gameMode);
[[nodiscard]] TeamId GetTeamId(bool original = false) const { return original ? TeamIdForRace(getRace(true)) : m_team; };
[[nodiscard]] PlayerGameMode GetGameMode() const { return m_gameMode; }
[[nodiscard]] bool HasGameMode(PlayerGameMode gameMode) const { return m_gameMode == gameMode; }
[[nodiscard]] bool IsTraitor() const { return HasGameMode(PLAYER_GAMEMODE_TRAITOR); }
[[nodiscard]] PlayerInfo const* GetStartPlayerInfo() const;
void SetGameMode(PlayerGameMode gameMode);
void SetFactionForRace(uint8 race);
void setTeamId(TeamId teamid) { m_team = teamid; };
[[nodiscard]] uint8 GetOutfitId() const { return m_outfitId; }
void InitDisplayIds();
bool IsAtGroupRewardDistance(WorldObject const* pRewardSource) const;
@@ -2163,7 +2148,6 @@ public:
[[nodiscard]] ReputationRank GetReputationRank(uint32 faction_id) const;
void RewardReputation(Unit* victim);
void RewardReputation(Quest const* quest);
void ApplyTraitorReputationState();
float CalculateReputationGain(ReputationSource source, uint32 creatureOrQuestLevel, float rep, int32 faction, bool noQuestBonus = false);
@@ -2839,8 +2823,6 @@ protected:
ObjectGuid m_lootGuid;
TeamId m_team;
PlayerGameMode m_gameMode{PLAYER_GAMEMODE_NORMAL};
uint8 m_outfitId{0};
uint32 m_nextSave; // pussywizard
uint16 m_additionalSaveTimer; // pussywizard
uint8 m_additionalSaveMask; // pussywizard
@@ -1106,18 +1106,8 @@ bool Player::SatisfyQuestRace(Quest const* qInfo, bool msg) const
uint32 reqraces = qInfo->GetAllowableRaces();
if (reqraces == 0)
return true;
if ((reqraces & getRaceMask()) == 0)
{
// Allow faction-swapped characters to take quests restricted to their active faction,
// while still respecting truly race-specific quests on the opposite side.
if (GetTeamId() != GetTeamId(true))
{
uint32 currentFactionRaceMask = (GetTeamId() == TEAM_ALLIANCE) ? RACEMASK_ALLIANCE : RACEMASK_HORDE;
if ((reqraces & currentFactionRaceMask) == reqraces)
return true;
}
if (msg)
SendCanTakeQuestResponse(INVALIDREASON_QUEST_FAILED_WRONG_RACE);
return false;
@@ -65,7 +65,6 @@
#include "Util.h"
#include "World.h"
#include "WorldPacket.h"
#include <cmath>
/// @todo: this import is not necessary for compilation and marked as unused by the IDE
// however, for some reasons removing it would cause a damn linking issue
@@ -2379,12 +2378,12 @@ InventoryResult Player::CanUseItem(ItemTemplate const* proto) const
return EQUIP_ERR_ITEM_NOT_FOUND;
}
if (proto->HasFlag2(ITEM_FLAG2_FACTION_HORDE) && GetTeamId() != TEAM_HORDE)
if (proto->HasFlag2(ITEM_FLAG2_FACTION_HORDE) && GetTeamId(true) != TEAM_HORDE)
{
return EQUIP_ERR_YOU_CAN_NEVER_USE_THAT_ITEM;
}
if (proto->HasFlag2(ITEM_FLAG2_FACTION_ALLIANCE) && GetTeamId() != TEAM_ALLIANCE)
if (proto->HasFlag2(ITEM_FLAG2_FACTION_ALLIANCE) && GetTeamId(true) != TEAM_ALLIANCE)
{
return EQUIP_ERR_YOU_CAN_NEVER_USE_THAT_ITEM;
}
@@ -5015,8 +5014,8 @@ bool Player::LoadFromDB(ObjectGuid playerGuid, CharacterDatabaseQueryHolder cons
//"arenaPoints, totalHonorPoints, todayHonorPoints, yesterdayHonorPoints, totalKills, todayKills, yesterdayKills, chosenTitle, knownCurrencies, watchedFaction, drunk, "
// 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
//"health, power1, power2, power3, power4, power5, power6, power7, instance_id, talentGroupsCount, activeTalentGroup, exploredZones, equipmentCache, ammoId, knownTitles,
// 70 71 72 73 74 75
//"actionBars, grantableLevels, innTriggerId, extraBonusTalentCount, UNIX_TIMESTAMP(creation_date), game_mode FROM characters WHERE guid = '{}'", guid);
// 70 71 72 73 74
//"actionBars, grantableLevels, innTriggerId, extraBonusTalentCount, UNIX_TIMESTAMP(creation_date) FROM characters WHERE guid = '{}'", guid);
PreparedQueryResult result = holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_FROM);
if (!result)
@@ -5094,8 +5093,6 @@ bool Player::LoadFromDB(ObjectGuid playerGuid, CharacterDatabaseQueryHolder cons
// load character creation date, relevant for achievements of type average
SetCreationTime(fields[74].Get<Seconds>());
PlayerGameMode storedGameMode = ResolveGameModeFromStorage(fields[16].Get<uint32>(), fields[75].Get<uint8>());
SetGameMode(storedGameMode);
// load achievements before anything else to prevent multiple gains for the same achievement/criteria on every loading (as loading does call UpdateAchievementCriteria)
m_achievementMgr->LoadFromDB(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_ACHIEVEMENTS), holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_CRITERIA_PROGRESS), holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_OFFLINE_ACHIEVEMENTS_UPDATES));
@@ -5115,7 +5112,6 @@ bool Player::LoadFromDB(ObjectGuid playerGuid, CharacterDatabaseQueryHolder cons
SetByteValue(PLAYER_BYTES_3, 0, fields[5].Get<uint8>());
SetByteValue(PLAYER_BYTES_3, 1, fields[54].Get<uint8>());
ReplaceAllPlayerFlags((PlayerFlags)fields[16].Get<uint32>());
SetGameMode(storedGameMode);
SetInt32Value(PLAYER_FIELD_WATCHED_FACTION_INDEX, fields[53].Get<uint32>());
SetUInt64Value(PLAYER_FIELD_KNOWN_CURRENCIES, fields[52].Get<uint64>());
@@ -5153,22 +5149,12 @@ bool Player::LoadFromDB(ObjectGuid playerGuid, CharacterDatabaseQueryHolder cons
InitPrimaryProfessions(); // to max set before any spell loaded
bool isFirstLogin = (fields[38].Get<uint16>() & AT_LOGIN_FIRST) != 0;
// init saved position, and fix it later if problematic
int32 transLowGUID = fields[35].Get<int32>();
Relocate(fields[17].Get<float>(), fields[18].Get<float>(), fields[19].Get<float>(), fields[21].Get<float>());
uint32 mapId = fields[20].Get<uint16>();
uint32 instanceId = fields[63].Get<uint32>();
if (isFirstLogin && IsTraitor())
{
WorldLocation startPosition = GetStartPosition();
Relocate(startPosition);
mapId = startPosition.GetMapId();
instanceId = 0;
}
uint32 dungeonDiff = fields[43].Get<uint8>() & 0x0F;
if (dungeonDiff >= MAX_DUNGEON_DIFFICULTY)
dungeonDiff = DUNGEON_DIFFICULTY_NORMAL;
@@ -5325,7 +5311,7 @@ bool Player::LoadFromDB(ObjectGuid playerGuid, CharacterDatabaseQueryHolder cons
else if (!taxi_nodes.empty())
{
instanceId = 0;
if (!m_taxi.LoadTaxiDestinationsFromString(taxi_nodes, GetTeamId()))
if (!m_taxi.LoadTaxiDestinationsFromString(taxi_nodes, GetTeamId(true)))
{
// xinef: could no load valid data for taxi, relocate to homebind and clear
m_taxi.ClearTaxiDestinations();
@@ -5557,13 +5543,6 @@ bool Player::LoadFromDB(ObjectGuid playerGuid, CharacterDatabaseQueryHolder cons
// must be before inventory (some items required reputation check)
m_reputationMgr->LoadFromDB(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_REPUTATION));
sScriptMgr->OnPlayerReputationLoadFromDB(this);
// Traitor game mode swaps the player's team without changing race, but the
// reputation list is always rebuilt from the native race. Re-apply the
// traitor reputation state here so the new faction's reputations stay
// visible/friendly and the native ones hidden/at-war on every login.
ApplyTraitorReputationState();
// xinef: load mails before inventory, so problematic items can be added to already loaded mails
_LoadMail(holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_MAILS), holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_MAIL_ITEMS));
@@ -6871,7 +6850,7 @@ bool Player::Satisfy(DungeonProgressionRequirements const* ar, uint32 target_map
missingItems = &missingLeaderItems;
}
if (itemRequirement->faction == TEAM_NEUTRAL || itemRequirement->faction == checkPlayer->GetTeamId())
if (itemRequirement->faction == TEAM_NEUTRAL || itemRequirement->faction == checkPlayer->GetTeamId(true))
{
if (!checkPlayer->HasItemCount(itemRequirement->id, 1))
{
@@ -6893,7 +6872,7 @@ bool Player::Satisfy(DungeonProgressionRequirements const* ar, uint32 target_map
missingAchievements = &missingLeaderAchievements;
}
if (achievementRequirement->faction == TEAM_NEUTRAL || achievementRequirement->faction == GetTeamId())
if (achievementRequirement->faction == TEAM_NEUTRAL || achievementRequirement->faction == GetTeamId(true))
{
if (!checkPlayer || !checkPlayer->HasAchieved(achievementRequirement->id))
{
@@ -6915,7 +6894,7 @@ bool Player::Satisfy(DungeonProgressionRequirements const* ar, uint32 target_map
missingQuests = &missingLeaderQuests;
}
if (questRequirement->faction == TEAM_NEUTRAL || questRequirement->faction == checkPlayer->GetTeamId())
if (questRequirement->faction == TEAM_NEUTRAL || questRequirement->faction == checkPlayer->GetTeamId(true))
{
if (!checkPlayer->GetQuestRewardStatus(questRequirement->id))
{
@@ -7102,9 +7081,8 @@ bool Player::CheckInstanceCount(uint32 instanceId) const
bool Player::_LoadHomeBind(PreparedQueryResult result)
{
PlayerInfo const* info = GetStartPlayerInfo();
PlayerInfo const* nativeInfo = sObjectMgr->GetPlayerInfo(getRace(true), getClass());
if (!info || !nativeInfo)
PlayerInfo const* info = sObjectMgr->GetPlayerInfo(getRace(true), getClass());
if (!info)
{
LOG_ERROR("entities.player", "Player (Name {}) has incorrect race/class pair. Can't be loaded.", GetName());
return false;
@@ -7136,25 +7114,6 @@ bool Player::_LoadHomeBind(PreparedQueryResult result)
}
}
if (ok && IsTraitor())
{
auto sameCoord = [](float left, float right) { return std::fabs(left - right) < 0.01f; };
bool matchesNativeHomebind = m_homebindMapId == nativeInfo->mapId &&
m_homebindAreaId == nativeInfo->areaId &&
sameCoord(m_homebindX, nativeInfo->positionX) &&
sameCoord(m_homebindY, nativeInfo->positionY) &&
sameCoord(m_homebindZ, nativeInfo->positionZ);
bool matchesEffectiveHomebind = m_homebindMapId == info->mapId &&
m_homebindAreaId == info->areaId &&
sameCoord(m_homebindX, info->positionX) &&
sameCoord(m_homebindY, info->positionY) &&
sameCoord(m_homebindZ, info->positionZ);
if (matchesNativeHomebind && !matchesEffectiveHomebind)
SetHomebind(WorldLocation(info->mapId, info->positionX, info->positionY, info->positionZ, 0.0f), info->areaId);
}
if (!ok)
{
m_homebindMapId = info->mapId;
@@ -1244,7 +1244,7 @@ void Player::UpdateArea(uint32 newArea)
else
RemoveByteFlag(UNIT_FIELD_BYTES_2, 1, UNIT_BYTE2_FLAG_SANCTUARY);
uint32 const areaRestFlag = (GetTeamId() == TEAM_ALLIANCE)
uint32 const areaRestFlag = (GetTeamId(true) == TEAM_ALLIANCE)
? AREA_FLAG_REST_ZONE_ALLIANCE
: AREA_FLAG_REST_ZONE_HORDE;
if (area && area->flags & areaRestFlag)
@@ -1305,12 +1305,12 @@ void Player::UpdateZone(uint32 newZone, uint32 newArea, bool force)
{
case AREATEAM_ALLY:
pvpInfo.IsInHostileArea =
GetTeamId() != TEAM_ALLIANCE &&
GetTeamId(true) != TEAM_ALLIANCE &&
(sWorld->IsPvPRealm() || zone->flags & AREA_FLAG_CAPITAL);
break;
case AREATEAM_HORDE:
pvpInfo.IsInHostileArea =
GetTeamId() != TEAM_HORDE &&
GetTeamId(true) != TEAM_HORDE &&
(sWorld->IsPvPRealm() || zone->flags & AREA_FLAG_CAPITAL);
break;
case AREATEAM_NONE:
+1 -1
View File
@@ -1483,7 +1483,7 @@ void Guild::HandleInviteMember(WorldSession* session, std::string const& name)
return;
}
if (!sWorld->getBoolConfig(CONFIG_ALLOW_TWO_SIDE_INTERACTION_GUILD) && pInvitee->GetTeamId() != player->GetTeamId())
if (!sWorld->getBoolConfig(CONFIG_ALLOW_TWO_SIDE_INTERACTION_GUILD) && pInvitee->GetTeamId(true) != player->GetTeamId(true))
{
SendCommandResult(session, GUILD_COMMAND_INVITE, ERR_GUILD_NOT_ALLIED, name);
return;
@@ -548,9 +548,7 @@ void WorldSession::HandleCharCreateOpcode(WorldPacket& recvData)
return;
}
if (newChar->IsTraitor())
newChar->setCinematic(1);
else if ((haveSameRace && skipCinematics == 1) || skipCinematics == 2)
if ((haveSameRace && skipCinematics == 1) || skipCinematics == 2)
newChar->setCinematic(1); // not show intro
newChar->SetAtLoginFlag(AT_LOGIN_FIRST); // First login
@@ -560,7 +558,6 @@ void WorldSession::HandleCharCreateOpcode(WorldPacket& recvData)
// Player created, save it now
newChar->SaveToDB(characterTransaction, true, false);
createInfo->CharCount++;
LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_DEL_REALM_CHARACTERS_BY_REALM);
@@ -582,8 +579,7 @@ void WorldSession::HandleCharCreateOpcode(WorldPacket& recvData)
{
LOG_INFO("entities.player.character", "Account: {} (IP: {}) Create Character: {} {}", GetAccountId(), GetRemoteAddress(), newChar->GetName(), newChar->GetGUID().ToString());
sScriptMgr->OnPlayerCreate(newChar.get());
sCharacterCache->AddCharacterCacheEntry(newChar->GetGUID(), GetAccountId(), newChar->GetName(), newChar->getGender(), newChar->getRace(), newChar->getClass(), newChar->GetLevel(),
newChar->GetUInt32Value(PLAYER_FLAGS), static_cast<uint8>(newChar->GetGameMode()));
sCharacterCache->AddCharacterCacheEntry(newChar->GetGUID(), GetAccountId(), newChar->GetName(), newChar->getGender(), newChar->getRace(), newChar->getClass(), newChar->GetLevel());
SendCharCreate(CHAR_CREATE_SUCCESS);
}
else
+2 -2
View File
@@ -446,12 +446,12 @@ bool LootItem::AllowedForPlayer(Player const* player, ObjectGuid source) const
}
// not show loot for not own team
if (pProto->HasFlag2(ITEM_FLAG2_FACTION_HORDE) && player->GetTeamId() != TEAM_HORDE)
if (pProto->HasFlag2(ITEM_FLAG2_FACTION_HORDE) && player->GetTeamId(true) != TEAM_HORDE)
{
return false;
}
if (pProto->HasFlag2(ITEM_FLAG2_FACTION_ALLIANCE) && player->GetTeamId() != TEAM_ALLIANCE)
if (pProto->HasFlag2(ITEM_FLAG2_FACTION_ALLIANCE) && player->GetTeamId(true) != TEAM_ALLIANCE)
{
return false;
}
+2 -24
View File
@@ -29,20 +29,6 @@ const int32 ReputationMgr::PointsInRank[MAX_REPUTATION_RANK] = {36000, 3000, 300
const int32 ReputationMgr::Reputation_Cap = 42999;
const int32 ReputationMgr::Reputation_Bottom = -42000;
namespace
{
uint32 GetEffectiveReputationRaceMask(Player const* player)
{
// Always use the player's actual race mask so the server BaseRep calculation
// matches what the client computes (client always uses the original race, not
// the current team). This is correct for both the race-change service (where
// getRace() already reflects the new race) and for game modes like Traitor
// that swap team without changing race (where the original race mask is the
// right baseline for reputation display and incremental gain alike).
return player->getRaceMask();
}
}
ReputationRank ReputationMgr::ReputationToRank(int32 standing)
{
int32 limit = Reputation_Cap + 1;
@@ -107,7 +93,7 @@ int32 ReputationMgr::GetBaseReputation(FactionEntry const* factionEntry) const
if (!factionEntry)
return 0;
uint32 raceMask = GetEffectiveReputationRaceMask(_player);
uint32 raceMask = _player->getRaceMask();
uint32 classMask = _player->getClassMask();
for (int i = 0; i < 4; i++)
{
@@ -161,7 +147,7 @@ uint32 ReputationMgr::GetDefaultStateFlags(FactionEntry const* factionEntry) con
if (!factionEntry)
return 0;
uint32 raceMask = GetEffectiveReputationRaceMask(_player);
uint32 raceMask = _player->getRaceMask();
uint32 classMask = _player->getClassMask();
for (int i = 0; i < 4; i++)
{
@@ -478,17 +464,9 @@ void ReputationMgr::SetVisible(FactionTemplateEntry const* factionTemplateEntry)
return;
if (FactionEntry const* factionEntry = sFactionStore.LookupEntry(factionTemplateEntry->faction))
{
if (_player->GetTeamId() != _player->GetTeamId(true))
{
SetVisible(factionEntry);
return;
}
// Never show factions of the opposing team
if (!(factionEntry->BaseRepRaceMask[1] & _player->getRaceMask() && factionEntry->BaseRepValue[1] == Reputation_Bottom))
SetVisible(factionEntry);
}
}
void ReputationMgr::SetVisible(FactionEntry const* factionEntry)
@@ -215,11 +215,6 @@ void ScriptMgr::OnPlayerLoadFromDB(Player* player)
CALL_ENABLED_HOOKS(PlayerScript, PLAYERHOOK_ON_LOAD_FROM_DB, script->OnPlayerLoadFromDB(player));
}
void ScriptMgr::OnPlayerReputationLoadFromDB(Player* player)
{
CALL_ENABLED_HOOKS(PlayerScript, PLAYERHOOK_ON_REPUTATION_LOAD_FROM_DB, script->OnPlayerReputationLoadFromDB(player));
}
void ScriptMgr::OnPlayerBeforeLogout(Player* player)
{
CALL_ENABLED_HOOKS(PlayerScript, PLAYERHOOK_ON_BEFORE_LOGOUT, script->OnPlayerBeforeLogout(player));
@@ -67,7 +67,6 @@ enum PlayerHook
PLAYERHOOK_ON_TEXT_EMOTE,
PLAYERHOOK_ON_SPELL_CAST,
PLAYERHOOK_ON_LOAD_FROM_DB,
PLAYERHOOK_ON_REPUTATION_LOAD_FROM_DB,
PLAYERHOOK_ON_LOGIN,
PLAYERHOOK_ON_BEFORE_LOGOUT,
PLAYERHOOK_ON_LOGOUT,
@@ -321,7 +320,6 @@ public:
// Called during data loading
virtual void OnPlayerLoadFromDB(Player* /*player*/) { };
virtual void OnPlayerReputationLoadFromDB(Player* /*player*/) { };
// Called when a player logs in.
virtual void OnPlayerLogin(Player* /*player*/) { }
-1
View File
@@ -346,7 +346,6 @@ public: /* PlayerScript */
void OnPlayerSpellCast(Player* player, Spell* spell, bool skipCheck);
void OnPlayerLogin(Player* player);
void OnPlayerLoadFromDB(Player* player);
void OnPlayerReputationLoadFromDB(Player* player);
void OnPlayerBeforeLogout(Player* player);
void OnPlayerLogout(Player* player);
void OnPlayerCreate(Player* player);
+2 -4
View File
@@ -111,14 +111,12 @@ public:
{
if (sCharacterCache->HasCharacterCacheEntry(cPlayer->GetGUID()))
{
sCharacterCache->UpdateCharacterData(cPlayer->GetGUID(), cPlayer->GetName(), cPlayer->getGender(), cPlayer->getRace(),
cPlayer->GetUInt32Value(PLAYER_FLAGS), static_cast<uint8>(cPlayer->GetGameMode()));
sCharacterCache->UpdateCharacterData(cPlayer->GetGUID(), cPlayer->GetName(), cPlayer->getGender(), cPlayer->getRace());
}
else
{
sCharacterCache->AddCharacterCacheEntry(cPlayer->GetGUID(), cPlayer->GetSession()->GetAccountId(), cPlayer->GetName(),
cPlayer->getGender(), cPlayer->getRace(), cPlayer->getClass(), cPlayer->GetLevel(),
cPlayer->GetUInt32Value(PLAYER_FLAGS), static_cast<uint8>(cPlayer->GetGameMode()));
cPlayer->getGender(), cPlayer->getRace(), cPlayer->getClass(), cPlayer->GetLevel());
}
sCharacterCache->UpdateCharacterAccountId(cPlayer->GetGUID(), cPlayer->GetSession()->GetAccountId());