Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a5a10166d9 | |||
| 4d4463ca0b | |||
| 38dffee5f6 | |||
| 4d8a2a20e1 | |||
| 7a32675298 | |||
| e7f0ce052b | |||
| b49efcea0d | |||
| 6516b32fc6 |
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"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)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -108,3 +108,4 @@ 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
-119789
File diff suppressed because it is too large
Load Diff
Vendored
-10
@@ -19,13 +19,3 @@ 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
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
-- 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 */;
|
||||
@@ -14,7 +14,6 @@ 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
|
||||
@@ -235,49 +234,6 @@ 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)
|
||||
@@ -294,7 +250,6 @@ 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"
|
||||
|
||||
@@ -305,7 +260,6 @@ fi
|
||||
|
||||
import_mythicplus_sql
|
||||
import_aoe_loot_sql
|
||||
import_encounter_journal_sql
|
||||
import_store_sql
|
||||
|
||||
log "custom SQL bootstrap completed"
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
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,6 +1207,9 @@ public:
|
||||
|
||||
void OnHeal(Unit* healer, Unit* receiver, uint32& gain) override
|
||||
{
|
||||
if (!healer)
|
||||
return;
|
||||
|
||||
if (healer->IsPlayer())
|
||||
sEluna->OnPlayerHeal(healer->ToPlayer(), receiver, gain);
|
||||
|
||||
@@ -1216,6 +1219,9 @@ public:
|
||||
|
||||
void OnDamage(Unit* attacker, Unit* receiver, uint32& damage) override
|
||||
{
|
||||
if (!attacker)
|
||||
return;
|
||||
|
||||
if (attacker->IsPlayer())
|
||||
sEluna->OnPlayerDamage(attacker->ToPlayer(), receiver, damage);
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
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
|
||||
@@ -1,195 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,83 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,115 +0,0 @@
|
||||
--
|
||||
-- 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.';
|
||||
@@ -1,179 +0,0 @@
|
||||
--
|
||||
-- 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;
|
||||
@@ -1,125 +0,0 @@
|
||||
# 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`.
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
MOD_ENCOUNTER_JOURNAL_ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/" && pwd )"
|
||||
@@ -1,182 +0,0 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
* 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
@@ -1,197 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
@@ -1,164 +0,0 @@
|
||||
#!/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())
|
||||
@@ -1,577 +0,0 @@
|
||||
#!/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())
|
||||
@@ -1,125 +0,0 @@
|
||||
#!/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())
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
#
|
||||
# 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
|
||||
@@ -0,0 +1,6 @@
|
||||
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;
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
#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;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#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
|
||||
@@ -0,0 +1,6 @@
|
||||
void AddSC_TraitorMode();
|
||||
|
||||
void Addmod_gamemodesScripts()
|
||||
{
|
||||
AddSC_TraitorMode();
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
#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();
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#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,40 +336,3 @@ 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,7 +42,6 @@ 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();
|
||||
@@ -89,7 +88,6 @@ 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();
|
||||
|
||||
@@ -1,649 +0,0 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
@@ -1,196 +0,0 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
* 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,7 +4772,10 @@ uint32 PlayerbotAI::AutoScaleActivity(uint32 mod)
|
||||
return static_cast<uint32>(mod * (1 - lagProgress));
|
||||
}
|
||||
|
||||
bool PlayerbotAI::IsOpposing(Player* player) { return IsOpposing(player->getRace(), bot->getRace()); }
|
||||
bool PlayerbotAI::IsOpposing(Player* player)
|
||||
{
|
||||
return player && bot && player->GetTeamId() != bot->GetTeamId();
|
||||
}
|
||||
|
||||
bool PlayerbotAI::IsOpposing(uint8 race1, uint8 race2)
|
||||
{
|
||||
|
||||
+40
-4
@@ -82,6 +82,9 @@ 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}"
|
||||
@@ -266,8 +269,9 @@ database_service_running() {
|
||||
docker compose ps --status running --services 2>/dev/null | grep -qx 'ac-database'
|
||||
}
|
||||
|
||||
import_world_sql_file() {
|
||||
import_sql_file() {
|
||||
local file_path="$1"
|
||||
local database="$2"
|
||||
|
||||
if [[ ! -f "$file_path" ]]; then
|
||||
log "skip missing ${file_path#$ROOT_DIR/}"
|
||||
@@ -275,13 +279,21 @@ import_world_sql_file() {
|
||||
fi
|
||||
|
||||
if (( DRY_RUN )); then
|
||||
log "would import ${file_path#$ROOT_DIR/} into acore_world"
|
||||
log "would import ${file_path#$ROOT_DIR/} into ${database}"
|
||||
return
|
||||
fi
|
||||
|
||||
docker compose exec -T ac-database bash -lc \
|
||||
"mysql --default-character-set=utf8mb4 -uroot -p\"\$MYSQL_ROOT_PASSWORD\" acore_world" < "$file_path"
|
||||
log "imported ${file_path#$ROOT_DIR/} into acore_world"
|
||||
"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"
|
||||
}
|
||||
|
||||
import_local_lua_module_sql() {
|
||||
@@ -300,6 +312,15 @@ 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
|
||||
@@ -339,6 +360,8 @@ 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"
|
||||
}
|
||||
@@ -482,6 +505,16 @@ 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"
|
||||
@@ -570,6 +603,7 @@ 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"
|
||||
)
|
||||
|
||||
@@ -626,8 +660,10 @@ 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,14 +41,18 @@ 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 "
|
||||
"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) "
|
||||
"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 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 "
|
||||
"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);
|
||||
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, 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, 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 = ? "
|
||||
"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 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 "
|
||||
"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);
|
||||
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);
|
||||
@@ -66,12 +70,13 @@ 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 guid, account, name, race, class, gender, level, xp, money, skin, face, hairStyle, hairColor, facialStyle, bankSlots, restState, playerFlags, "
|
||||
PrepareStatement(CHAR_SEL_CHARACTER, "SELECT characters.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) FROM characters WHERE guid = ?", CONNECTION_ASYNC);
|
||||
"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);
|
||||
|
||||
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);
|
||||
@@ -109,6 +114,8 @@ 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,6 +97,8 @@ 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->GetTeamId() == source->GetTeamId()) == (player_dead.own_team_flag != 0);
|
||||
return (player->GetBgTeamId() == source->GetBgTeamId()) == (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->GetTeamId() == TEAM_ALLIANCE ? TEAM_HORDE : TEAM_ALLIANCE);
|
||||
uint32 score = bg->GetTeamScore(source->GetBgTeamId() == 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->GetTeamId() == winnerTeam && winnnerScore == teams_scores.winner_score && loserScore == teams_scores.loser_score;
|
||||
return source->GetBgTeamId() == 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[Player::TeamIdForRace(GetPlayer()->getRace())])
|
||||
if (uint32 titleId = reward->titleId[uint8(GetPlayer()->GetTeamId())])
|
||||
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(true) != TEAM_HORDE) ||
|
||||
(achievement->requiredFaction == ACHIEVEMENT_FACTION_ALLIANCE && GetPlayer()->GetTeamId(true) != TEAM_ALLIANCE))
|
||||
if ((achievement->requiredFaction == ACHIEVEMENT_FACTION_HORDE && GetPlayer()->GetTeamId() != TEAM_HORDE) ||
|
||||
(achievement->requiredFaction == ACHIEVEMENT_FACTION_ALLIANCE && GetPlayer()->GetTeamId() != TEAM_ALLIANCE))
|
||||
return false;
|
||||
|
||||
for (uint32 i = 0; i < MAX_CRITERIA_REQUIREMENTS; ++i)
|
||||
|
||||
@@ -28,6 +28,16 @@ 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()
|
||||
@@ -62,7 +72,11 @@ void CharacterCache::LoadCharacterCacheStorage()
|
||||
_characterCacheStore.clear();
|
||||
uint32 oldMSTime = getMSTime();
|
||||
|
||||
QueryResult result = CharacterDatabase.Query("SELECT guid, name, account, race, gender, class, level FROM characters");
|
||||
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
|
||||
)");
|
||||
if (!result)
|
||||
{
|
||||
LOG_INFO("server.loading", "No character name data loaded, empty query!");
|
||||
@@ -73,7 +87,8 @@ 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[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*/);
|
||||
} while (result->NextRow());
|
||||
|
||||
QueryResult mailCountResult = CharacterDatabase.Query("SELECT receiver, COUNT(receiver) FROM mail GROUP BY receiver");
|
||||
@@ -92,7 +107,12 @@ void CharacterCache::LoadCharacterCacheStorage()
|
||||
|
||||
void CharacterCache::RefreshCacheEntry(uint32 lowGuid)
|
||||
{
|
||||
QueryResult result = CharacterDatabase.Query("SELECT guid, name, account, race, gender, class, level FROM characters WHERE guid = {}", 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);
|
||||
if (!result)
|
||||
{
|
||||
return;
|
||||
@@ -102,7 +122,9 @@ 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*/);
|
||||
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*/);
|
||||
} while (result->NextRow());
|
||||
|
||||
QueryResult mailCountResult = CharacterDatabase.Query("SELECT receiver, COUNT(receiver) FROM mail WHERE receiver = {} GROUP BY receiver", lowGuid);
|
||||
@@ -119,16 +141,18 @@ 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)
|
||||
void CharacterCache::AddCharacterCacheEntry(ObjectGuid const& guid, uint32 accountId, std::string const& name, uint8 gender, uint8 race, uint8 playerClass, uint8 level, uint32 playerFlags, uint8 gameMode)
|
||||
{
|
||||
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)
|
||||
{
|
||||
@@ -145,7 +169,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 /*= {}*/)
|
||||
void CharacterCache::UpdateCharacterData(ObjectGuid const& guid, std::string const& name, Optional<uint8> gender /*= {}*/, Optional<uint8> race /*= {}*/, Optional<uint32> playerFlags /*= {}*/, Optional<uint8> gameMode /*= {}*/)
|
||||
{
|
||||
auto itr = _characterCacheStore.find(guid);
|
||||
if (itr == _characterCacheStore.end())
|
||||
@@ -164,6 +188,16 @@ 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());
|
||||
|
||||
@@ -311,7 +345,7 @@ uint32 CharacterCache::GetCharacterTeamByGuid(ObjectGuid guid) const
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Player::TeamIdForRace(itr->second.Race);
|
||||
return GetEffectiveCharacterTeam(itr->second.Race, itr->second.PlayerFlags, itr->second.GameMode);
|
||||
}
|
||||
|
||||
uint32 CharacterCache::GetCharacterAccountIdByGuid(ObjectGuid guid) const
|
||||
|
||||
@@ -29,10 +29,12 @@ 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;
|
||||
@@ -49,10 +51,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);
|
||||
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 DeleteCharacterCacheEntry(ObjectGuid const& guid, std::string const& name);
|
||||
|
||||
void UpdateCharacterData(ObjectGuid const& guid, std::string const& name, Optional<uint8> gender = {}, Optional<uint8> race = {});
|
||||
void UpdateCharacterData(ObjectGuid const& guid, std::string const& name, Optional<uint8> gender = {}, Optional<uint8> race = {}, Optional<uint32> playerFlags = {}, Optional<uint8> gameMode = {});
|
||||
void UpdateCharacterLevel(ObjectGuid const& guid, uint8 level);
|
||||
void UpdateCharacterAccountId(ObjectGuid const& guid, uint32 accountId);
|
||||
void UpdateCharacterGuildId(ObjectGuid const& guid, ObjectGuid::LowType guildId);
|
||||
|
||||
@@ -430,7 +430,7 @@ namespace lfg
|
||||
{
|
||||
if (!itemRequirement->checkLeaderOnly || !group || group->GetLeaderGUID() == player->GetGUID())
|
||||
{
|
||||
if (itemRequirement->faction == TEAM_NEUTRAL || itemRequirement->faction == player->GetTeamId(true))
|
||||
if (itemRequirement->faction == TEAM_NEUTRAL || itemRequirement->faction == player->GetTeamId())
|
||||
{
|
||||
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(true))
|
||||
if (questRequirement->faction == TEAM_NEUTRAL || questRequirement->faction == player->GetTeamId())
|
||||
{
|
||||
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(true))
|
||||
if (achievementRequirement->faction == TEAM_NEUTRAL || achievementRequirement->faction == player->GetTeamId())
|
||||
{
|
||||
if (!player->HasAchieved(achievementRequirement->id))
|
||||
{
|
||||
|
||||
@@ -147,6 +147,80 @@ 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
|
||||
@@ -486,12 +560,13 @@ void Player::CleanupsBeforeDelete(bool finalCleanup)
|
||||
|
||||
bool Player::Create(ObjectGuid::LowType guidlow, CharacterCreateInfo* createInfo)
|
||||
{
|
||||
// FIXME: outfitId not used in player creating
|
||||
m_outfitId = createInfo->OutfitId;
|
||||
/// @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;
|
||||
|
||||
@@ -506,8 +581,6 @@ 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)
|
||||
{
|
||||
@@ -516,8 +589,6 @@ bool Player::Create(ObjectGuid::LowType guidlow, CharacterCreateInfo* createInfo
|
||||
return false;
|
||||
}
|
||||
|
||||
SetMap(sMapMgr->CreateMap(info->mapId, this));
|
||||
|
||||
uint8 powertype = cEntry->powerType;
|
||||
|
||||
SetObjectScale(1.0f);
|
||||
@@ -525,8 +596,6 @@ 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",
|
||||
@@ -535,8 +604,14 @@ 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)
|
||||
{
|
||||
@@ -1111,8 +1186,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
|
||||
// characters.extra_flags, character_declinedname.genitive
|
||||
// 24 25 26
|
||||
// characters.extra_flags, mod_gamemodes_characters.game_mode, character_declinedname.genitive
|
||||
|
||||
Field* fields = result->Fetch();
|
||||
|
||||
@@ -1184,12 +1259,15 @@ bool Player::BuildEnumData(PreparedQueryResult result, WorldPacket* data)
|
||||
charFlags |= CHARACTER_FLAG_LOCKED_BY_BILLING;
|
||||
if (sWorld->getBoolConfig(CONFIG_DECLINED_NAMES_USED))
|
||||
{
|
||||
if (!fields[25].Get<std::string>().empty())
|
||||
if (!fields[26].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
|
||||
@@ -4065,6 +4143,10 @@ 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);
|
||||
@@ -5850,17 +5932,46 @@ TeamId Player::TeamIdForRace(uint8 race)
|
||||
|
||||
void Player::SetFactionForRace(uint8 race)
|
||||
{
|
||||
m_team = TeamIdForRace(race);
|
||||
TeamId nativeTeam = TeamIdForRace(race);
|
||||
m_team = IsTraitor() ? GetOppositeTeamId(nativeTeam) : nativeTeam;
|
||||
|
||||
sScriptMgr->OnPlayerUpdateFaction(this);
|
||||
|
||||
if (GetTeamId(true) != GetTeamId())
|
||||
return;
|
||||
|
||||
ChrRacesEntry const* rEntry = sChrRacesStore.LookupEntry(race);
|
||||
uint8 factionRace = IsTraitor() ? GetRepresentativeRaceForTeam(m_team) : race;
|
||||
ChrRacesEntry const* rEntry = sChrRacesStore.LookupEntry(factionRace);
|
||||
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);
|
||||
@@ -5972,7 +6083,7 @@ void Player::RewardReputation(Unit* victim)
|
||||
ChampioningFaction = GetChampioningFaction();
|
||||
}
|
||||
|
||||
TeamId teamId = GetTeamId(true); // Always check player original reputation when rewarding
|
||||
TeamId teamId = GetTeamId();
|
||||
|
||||
if (Rep->RepFaction1 && (!Rep->TeamDependent || teamId == TEAM_ALLIANCE))
|
||||
{
|
||||
@@ -6086,6 +6197,78 @@ 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)
|
||||
@@ -10301,7 +10484,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(true), npc == nullptr || (sourcenode == 315 && IsClass(CLASS_DEATH_KNIGHT, CLASS_CONTEXT_TAXI)));
|
||||
uint32 mount_display_id = sObjectMgr->GetTaxiMountDisplayId(sourcenode, GetTeamId(), 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)
|
||||
@@ -10396,7 +10579,7 @@ void Player::ContinueTaxiFlight()
|
||||
|
||||
LOG_DEBUG("entities.unit", "WORLD: Restart character {} taxi flight", GetGUID().ToString());
|
||||
|
||||
uint32 mountDisplayId = sObjectMgr->GetTaxiMountDisplayId(sourceNode, GetTeamId(true), true);
|
||||
uint32 mountDisplayId = sObjectMgr->GetTaxiMountDisplayId(sourceNode, GetTeamId(), true);
|
||||
if (!mountDisplayId)
|
||||
return;
|
||||
|
||||
@@ -10675,7 +10858,7 @@ bool Player::BuyItemFromVendorSlot(ObjectGuid vendorguid, uint32 vendorslot, uin
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsGameMaster() && ((pProto->HasFlag2(ITEM_FLAG2_FACTION_HORDE) && GetTeamId(true) == TEAM_ALLIANCE) || (pProto->HasFlag2(ITEM_FLAG2_FACTION_ALLIANCE) && GetTeamId(true) == TEAM_HORDE)))
|
||||
if (!IsGameMaster() && ((pProto->HasFlag2(ITEM_FLAG2_FACTION_HORDE) && GetTeamId() == TEAM_ALLIANCE) || (pProto->HasFlag2(ITEM_FLAG2_FACTION_ALLIANCE) && GetTeamId() == TEAM_HORDE)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -11338,7 +11521,7 @@ void Player::ReportedAfkBy(Player* reporter)
|
||||
|
||||
WorldLocation Player::GetStartPosition() const
|
||||
{
|
||||
PlayerInfo const* info = sObjectMgr->GetPlayerInfo(getRace(true), getClass());
|
||||
PlayerInfo const* info = GetStartPlayerInfo();
|
||||
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);
|
||||
@@ -14698,6 +14881,7 @@ 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)
|
||||
@@ -14765,6 +14949,25 @@ 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)
|
||||
{
|
||||
@@ -14787,7 +14990,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++, (uint32)GetPlayerFlags());
|
||||
stmt->SetData(index++, playerFlagsForSave);
|
||||
stmt->SetData(index++, (uint16)GetMapId());
|
||||
stmt->SetData(index++, (uint32)GetInstanceId());
|
||||
stmt->SetData(index++, (uint8(GetDungeonDifficulty()) | uint8(GetRaidDifficulty()) << 4));
|
||||
@@ -14905,7 +15108,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++, GetPlayerFlags());
|
||||
stmt->SetData(index++, playerFlagsForSave);
|
||||
|
||||
if (!IsBeingTeleported())
|
||||
{
|
||||
@@ -15033,6 +15236,20 @@ 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)
|
||||
|
||||
@@ -454,6 +454,13 @@ 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,
|
||||
@@ -486,7 +493,7 @@ enum PlayerFlags : uint32
|
||||
PLAYER_FLAGS_UNK27 = 0x08000000,
|
||||
PLAYER_FLAGS_UNK28 = 0x10000000,
|
||||
PLAYER_FLAGS_UNK29 = 0x20000000,
|
||||
PLAYER_FLAGS_UNK30 = 0x40000000,
|
||||
PLAYER_FLAGS_TRAITOR = 0x40000000, // mod-gamemodes: player is playing for the opposite faction
|
||||
PLAYER_FLAGS_UNK31 = 0x80000000,
|
||||
};
|
||||
|
||||
@@ -1156,7 +1163,7 @@ public:
|
||||
PlayerSocial* GetSocial() { return m_social; }
|
||||
|
||||
PlayerTaxi m_taxi;
|
||||
void InitTaxiNodesForLevel() { m_taxi.InitTaxiNodesForLevel(getRace(), getClass(), GetLevel()); }
|
||||
void InitTaxiNodesForLevel() { m_taxi.InitTaxiNodesForLevel(IsTraitor() ? (GetTeamId() == TEAM_ALLIANCE ? RACE_HUMAN : RACE_ORC) : 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();
|
||||
@@ -2126,10 +2133,18 @@ 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;
|
||||
@@ -2148,6 +2163,7 @@ 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);
|
||||
|
||||
@@ -2823,6 +2839,8 @@ 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,8 +1106,18 @@ 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,6 +65,7 @@
|
||||
#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
|
||||
@@ -2378,12 +2379,12 @@ InventoryResult Player::CanUseItem(ItemTemplate const* proto) const
|
||||
return EQUIP_ERR_ITEM_NOT_FOUND;
|
||||
}
|
||||
|
||||
if (proto->HasFlag2(ITEM_FLAG2_FACTION_HORDE) && GetTeamId(true) != TEAM_HORDE)
|
||||
if (proto->HasFlag2(ITEM_FLAG2_FACTION_HORDE) && GetTeamId() != TEAM_HORDE)
|
||||
{
|
||||
return EQUIP_ERR_YOU_CAN_NEVER_USE_THAT_ITEM;
|
||||
}
|
||||
|
||||
if (proto->HasFlag2(ITEM_FLAG2_FACTION_ALLIANCE) && GetTeamId(true) != TEAM_ALLIANCE)
|
||||
if (proto->HasFlag2(ITEM_FLAG2_FACTION_ALLIANCE) && GetTeamId() != TEAM_ALLIANCE)
|
||||
{
|
||||
return EQUIP_ERR_YOU_CAN_NEVER_USE_THAT_ITEM;
|
||||
}
|
||||
@@ -5014,8 +5015,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
|
||||
//"actionBars, grantableLevels, innTriggerId, extraBonusTalentCount, UNIX_TIMESTAMP(creation_date) FROM characters WHERE guid = '{}'", guid);
|
||||
// 70 71 72 73 74 75
|
||||
//"actionBars, grantableLevels, innTriggerId, extraBonusTalentCount, UNIX_TIMESTAMP(creation_date), game_mode FROM characters WHERE guid = '{}'", guid);
|
||||
PreparedQueryResult result = holder.GetPreparedResult(PLAYER_LOGIN_QUERY_LOAD_FROM);
|
||||
|
||||
if (!result)
|
||||
@@ -5093,6 +5094,8 @@ 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));
|
||||
@@ -5112,6 +5115,7 @@ 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>());
|
||||
@@ -5149,12 +5153,22 @@ 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;
|
||||
@@ -5311,7 +5325,7 @@ bool Player::LoadFromDB(ObjectGuid playerGuid, CharacterDatabaseQueryHolder cons
|
||||
else if (!taxi_nodes.empty())
|
||||
{
|
||||
instanceId = 0;
|
||||
if (!m_taxi.LoadTaxiDestinationsFromString(taxi_nodes, GetTeamId(true)))
|
||||
if (!m_taxi.LoadTaxiDestinationsFromString(taxi_nodes, GetTeamId()))
|
||||
{
|
||||
// xinef: could no load valid data for taxi, relocate to homebind and clear
|
||||
m_taxi.ClearTaxiDestinations();
|
||||
@@ -5543,6 +5557,13 @@ 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));
|
||||
@@ -6850,7 +6871,7 @@ bool Player::Satisfy(DungeonProgressionRequirements const* ar, uint32 target_map
|
||||
missingItems = &missingLeaderItems;
|
||||
}
|
||||
|
||||
if (itemRequirement->faction == TEAM_NEUTRAL || itemRequirement->faction == checkPlayer->GetTeamId(true))
|
||||
if (itemRequirement->faction == TEAM_NEUTRAL || itemRequirement->faction == checkPlayer->GetTeamId())
|
||||
{
|
||||
if (!checkPlayer->HasItemCount(itemRequirement->id, 1))
|
||||
{
|
||||
@@ -6872,7 +6893,7 @@ bool Player::Satisfy(DungeonProgressionRequirements const* ar, uint32 target_map
|
||||
missingAchievements = &missingLeaderAchievements;
|
||||
}
|
||||
|
||||
if (achievementRequirement->faction == TEAM_NEUTRAL || achievementRequirement->faction == GetTeamId(true))
|
||||
if (achievementRequirement->faction == TEAM_NEUTRAL || achievementRequirement->faction == GetTeamId())
|
||||
{
|
||||
if (!checkPlayer || !checkPlayer->HasAchieved(achievementRequirement->id))
|
||||
{
|
||||
@@ -6894,7 +6915,7 @@ bool Player::Satisfy(DungeonProgressionRequirements const* ar, uint32 target_map
|
||||
missingQuests = &missingLeaderQuests;
|
||||
}
|
||||
|
||||
if (questRequirement->faction == TEAM_NEUTRAL || questRequirement->faction == checkPlayer->GetTeamId(true))
|
||||
if (questRequirement->faction == TEAM_NEUTRAL || questRequirement->faction == checkPlayer->GetTeamId())
|
||||
{
|
||||
if (!checkPlayer->GetQuestRewardStatus(questRequirement->id))
|
||||
{
|
||||
@@ -7081,8 +7102,9 @@ bool Player::CheckInstanceCount(uint32 instanceId) const
|
||||
|
||||
bool Player::_LoadHomeBind(PreparedQueryResult result)
|
||||
{
|
||||
PlayerInfo const* info = sObjectMgr->GetPlayerInfo(getRace(true), getClass());
|
||||
if (!info)
|
||||
PlayerInfo const* info = GetStartPlayerInfo();
|
||||
PlayerInfo const* nativeInfo = sObjectMgr->GetPlayerInfo(getRace(true), getClass());
|
||||
if (!info || !nativeInfo)
|
||||
{
|
||||
LOG_ERROR("entities.player", "Player (Name {}) has incorrect race/class pair. Can't be loaded.", GetName());
|
||||
return false;
|
||||
@@ -7114,6 +7136,25 @@ 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(true) == TEAM_ALLIANCE)
|
||||
uint32 const areaRestFlag = (GetTeamId() == 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(true) != TEAM_ALLIANCE &&
|
||||
GetTeamId() != TEAM_ALLIANCE &&
|
||||
(sWorld->IsPvPRealm() || zone->flags & AREA_FLAG_CAPITAL);
|
||||
break;
|
||||
case AREATEAM_HORDE:
|
||||
pvpInfo.IsInHostileArea =
|
||||
GetTeamId(true) != TEAM_HORDE &&
|
||||
GetTeamId() != TEAM_HORDE &&
|
||||
(sWorld->IsPvPRealm() || zone->flags & AREA_FLAG_CAPITAL);
|
||||
break;
|
||||
case AREATEAM_NONE:
|
||||
|
||||
@@ -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(true) != player->GetTeamId(true))
|
||||
if (!sWorld->getBoolConfig(CONFIG_ALLOW_TWO_SIDE_INTERACTION_GUILD) && pInvitee->GetTeamId() != player->GetTeamId())
|
||||
{
|
||||
SendCommandResult(session, GUILD_COMMAND_INVITE, ERR_GUILD_NOT_ALLIED, name);
|
||||
return;
|
||||
|
||||
@@ -548,7 +548,9 @@ void WorldSession::HandleCharCreateOpcode(WorldPacket& recvData)
|
||||
return;
|
||||
}
|
||||
|
||||
if ((haveSameRace && skipCinematics == 1) || skipCinematics == 2)
|
||||
if (newChar->IsTraitor())
|
||||
newChar->setCinematic(1);
|
||||
else if ((haveSameRace && skipCinematics == 1) || skipCinematics == 2)
|
||||
newChar->setCinematic(1); // not show intro
|
||||
|
||||
newChar->SetAtLoginFlag(AT_LOGIN_FIRST); // First login
|
||||
@@ -558,6 +560,7 @@ 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);
|
||||
@@ -579,7 +582,8 @@ 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());
|
||||
sCharacterCache->AddCharacterCacheEntry(newChar->GetGUID(), GetAccountId(), newChar->GetName(), newChar->getGender(), newChar->getRace(), newChar->getClass(), newChar->GetLevel(),
|
||||
newChar->GetUInt32Value(PLAYER_FLAGS), static_cast<uint8>(newChar->GetGameMode()));
|
||||
SendCharCreate(CHAR_CREATE_SUCCESS);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -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(true) != TEAM_HORDE)
|
||||
if (pProto->HasFlag2(ITEM_FLAG2_FACTION_HORDE) && player->GetTeamId() != TEAM_HORDE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pProto->HasFlag2(ITEM_FLAG2_FACTION_ALLIANCE) && player->GetTeamId(true) != TEAM_ALLIANCE)
|
||||
if (pProto->HasFlag2(ITEM_FLAG2_FACTION_ALLIANCE) && player->GetTeamId() != TEAM_ALLIANCE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,20 @@ 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;
|
||||
@@ -93,7 +107,7 @@ int32 ReputationMgr::GetBaseReputation(FactionEntry const* factionEntry) const
|
||||
if (!factionEntry)
|
||||
return 0;
|
||||
|
||||
uint32 raceMask = _player->getRaceMask();
|
||||
uint32 raceMask = GetEffectiveReputationRaceMask(_player);
|
||||
uint32 classMask = _player->getClassMask();
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
@@ -147,7 +161,7 @@ uint32 ReputationMgr::GetDefaultStateFlags(FactionEntry const* factionEntry) con
|
||||
if (!factionEntry)
|
||||
return 0;
|
||||
|
||||
uint32 raceMask = _player->getRaceMask();
|
||||
uint32 raceMask = GetEffectiveReputationRaceMask(_player);
|
||||
uint32 classMask = _player->getClassMask();
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
@@ -464,9 +478,17 @@ 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,6 +215,11 @@ 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,6 +67,7 @@ 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,
|
||||
@@ -320,6 +321,7 @@ 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*/) { }
|
||||
|
||||
@@ -346,6 +346,7 @@ 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);
|
||||
|
||||
@@ -111,12 +111,14 @@ public:
|
||||
{
|
||||
if (sCharacterCache->HasCharacterCacheEntry(cPlayer->GetGUID()))
|
||||
{
|
||||
sCharacterCache->UpdateCharacterData(cPlayer->GetGUID(), cPlayer->GetName(), cPlayer->getGender(), cPlayer->getRace());
|
||||
sCharacterCache->UpdateCharacterData(cPlayer->GetGUID(), cPlayer->GetName(), cPlayer->getGender(), cPlayer->getRace(),
|
||||
cPlayer->GetUInt32Value(PLAYER_FLAGS), static_cast<uint8>(cPlayer->GetGameMode()));
|
||||
}
|
||||
else
|
||||
{
|
||||
sCharacterCache->AddCharacterCacheEntry(cPlayer->GetGUID(), cPlayer->GetSession()->GetAccountId(), cPlayer->GetName(),
|
||||
cPlayer->getGender(), cPlayer->getRace(), cPlayer->getClass(), cPlayer->GetLevel());
|
||||
cPlayer->getGender(), cPlayer->getRace(), cPlayer->getClass(), cPlayer->GetLevel(),
|
||||
cPlayer->GetUInt32Value(PLAYER_FLAGS), static_cast<uint8>(cPlayer->GetGameMode()));
|
||||
}
|
||||
|
||||
sCharacterCache->UpdateCharacterAccountId(cPlayer->GetGUID(), cPlayer->GetSession()->GetAccountId());
|
||||
|
||||
Reference in New Issue
Block a user