Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 39 additions & 6 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Modernize an old Legion-based TrinityCore source into a modern, stable, maintain
| 4 | [Dependency Updates](#phase-4--dependency-updates) | **Validated** |
| 5 | [Code Modernization](#phase-5--code-modernization) | **Complete** |
| 6 | [CI, Testing, and Profiling](#phase-6--ci-testing-and-profiling) | **Complete** |
| 7 | [Modular System](#phase-7--modular-system) | **Complete** |
| 7 | [Modular System](#phase-7--modular-system) | **In progress** |
| 8 | [Safe Async Systems](#phase-8--safe-async-systems) | **Complete** |
| 9 | [Map Threading Research](#phase-9--map-threading-research) | **Complete** |
| 10 | [World Layering](#phase-10--world-layering) | **Not started** |
Expand Down Expand Up @@ -286,7 +286,7 @@ reading the code and removes the implicit "these are second-class citizens" sign

**Goals:** Introduce modular architecture slowly.

### Tasks
### C++ Module System

- [x] Create `modules/` directory structure
- [x] Add `modules/cmake/AddModule.cmake` — `add_argus_module()` helper function
Expand All @@ -296,16 +296,48 @@ reading the code and removes the implicit "these are second-class citizens" sign
- [x] Link `modules-loader` + all module libs into worldserver from root CMakeLists.txt
- [x] Hook `AddModuleScripts()` into `AddScripts()` in `ScriptLoader.cpp.in.cmake` (static builds only)

### Database Update Layering

Introduce AzerothCore-style update layers so modules, pending changes, and server-local overrides each have their own tracked directory and state. All four databases (world, auth, characters, hotfixes) are covered.

**Update layers:**

| State | Directory | Purpose |
|---|---|---|
| `RELEASED` | `sql/updates/<db>/master/` | Committed, stable updates |
| `ARCHIVED` | `sql/old/` | Legacy history |
| `CUSTOM` | `sql/custom/<db>/` | Server-local overrides, never committed |
| `PENDING` | `sql/pending/<db>/` | WIP / pre-review SQL |
| `MODULE` | `modules/mod-*/sql/<db>/` | Module-owned migrations |

**Completed:**

- [x] Expand `updates` and `updates_include` state enum to `RELEASED | ARCHIVED | CUSTOM | MODULE | PENDING` — SQL update files for all 4 DBs (`2026_05_24_01_world.sql`, `2026_05_24_00_auth.sql`, `2026_05_24_00_characters.sql`, `2026_05_24_00_hotfixes.sql`)
- [x] Create `sql/pending/<db>/` directories with `.gitkeep` for all 4 DBs
- [x] Create `.gitkeep` files in existing `sql/custom/<db>/` directories
- [x] Register `sql/pending/<db>/` as `PENDING` and `sql/custom/<db>/` as `CUSTOM` in `updates_include` via the SQL update files
- [x] Expand `UpdateFetcher::State` enum — add `CUSTOM`, `MODULE`, `PENDING` to `UpdateFetcher.h`
- [x] Update `AppliedFileEntry::StateConvert` (both overloads) to handle all 5 state values correctly
- [x] Fix `Update()` counter: only `ARCHIVED` increments `countArchivedUpdates`; `CUSTOM`/`MODULE`/`PENDING` count as recent
- [x] Add `tools/create_sql.ps1` — generates `sql/pending/<db>/rev_<epoch_ms>.sql` and opens it in `$env:EDITOR`

**Pending:**

- [ ] Module SQL auto-discovery in `DBUpdater.cpp` — at startup, scan `modules/mod-*/sql/<db>/` directories and register them as `MODULE`-state include paths so module migrations are applied automatically without any manual `updates_include` entries

### How to create a module

```
modules/mod-myfeature/
CMakeLists.txt — calls add_argus_module(NAME "MyFeature")
CMakeLists.txt — calls add_argus_module(NAME "MyFeature")
src/
mod_myfeature.cpp — defines void AddMyFeatureScripts()
mod_myfeature.cpp — defines void AddMyFeatureScripts()
mod_myfeature.h
conf/ — optional: .conf.dist files
sql/ — optional: database migrations
conf/ — optional: .conf.dist files copied next to binary
sql/
world/ — optional: YYYY_MM_DD_NN.sql world migrations
auth/ — optional: auth migrations
characters/ — optional: character migrations
```

### First Modular Targets (future)
Expand All @@ -323,6 +355,7 @@ modules/mod-myfeature/
### Validation

- [ ] Modules load correctly
- [ ] Module SQL migrations apply on first startup without manual DB edits
- [ ] Server remains stable
- [ ] Gameplay unchanged

Expand Down
12 changes: 12 additions & 0 deletions sql/updates/auth/master/2026_05_24_01_auth.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
DELETE FROM `rbac_linked_permissions` WHERE `linkedId` IN (884,885,886);
DELETE FROM `rbac_permissions` WHERE `id` IN (884,885,886);

INSERT INTO `rbac_permissions` (`id`,`name`) VALUES
(884,'Command: layer info'),
(885,'Command: layer list'),
(886,'Command: layer migrate');

INSERT INTO `rbac_linked_permissions` (`id`,`linkedId`) VALUES
(197,884),
(197,885),
(196,886);
6 changes: 6 additions & 0 deletions sql/updates/characters/master/2026_05_24_01_characters.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- Phase 10: World Layering — persist last-known layer so the 30-min cooldown
-- survives server restarts and so /who-style tools can report layer per player.
ALTER TABLE `characters`
ADD COLUMN `world_layer` INT UNSIGNED NOT NULL DEFAULT 0
COMMENT 'Open-world layer the player was last assigned to (0 = default)'
AFTER `honorRestBonus`;
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ void CharacterDatabaseConnection::DoPrepareStatements()
"resettalents_time, primarySpecialization, trans_x, trans_y, trans_z, trans_o, transguid, extra_flags, summonedPetNumber, at_login, zone, online, death_expire_time, taxi_path, dungeonDifficulty, "
"totalKills, todayKills, yesterdayKills, chosenTitle, watchedFaction, drunk, "
"health, power1, power2, power3, power4, power5, power6, instance_id, activeTalentGroup, lootSpecId, exploredZones, knownTitles, actionBars, grantableLevels, raidDifficulty, legacyRaidDifficulty, fishingSteps, "
"honor, honorLevel, prestigeLevel, honorRestState, honorRestBonus "
"honor, honorLevel, prestigeLevel, honorRestState, honorRestBonus, world_layer "
"FROM characters c LEFT JOIN character_fishingsteps cfs ON c.guid = cfs.guid WHERE c.guid = ?", CONNECTION_ASYNC);

PrepareStatement(CHAR_SEL_GROUP_MEMBER, "SELECT guid FROM group_member WHERE memberGuid = ?", CONNECTION_BOTH);
Expand Down Expand Up @@ -426,7 +426,7 @@ void CharacterDatabaseConnection::DoPrepareStatements()
"logout_time=?,is_logout_resting=?,resettalents_cost=?,resettalents_time=?,primarySpecialization=?,extra_flags=?,summonedPetNumber=?,at_login=?,zone=?,death_expire_time=?,taxi_path=?,"
"totalKills=?,todayKills=?,yesterdayKills=?,chosenTitle=?,"
"watchedFaction=?,drunk=?,health=?,power1=?,power2=?,power3=?,power4=?,power5=?,power6=?,latency=?,activeTalentGroup=?,lootSpecId=?,exploredZones=?,"
"equipmentCache=?,knownTitles=?,actionBars=?,grantableLevels=?,online=?,honor=?,honorLevel=?,prestigeLevel=?,honorRestState=?,honorRestBonus=?,lastLoginBuild=? WHERE guid=?", CONNECTION_ASYNC);
"equipmentCache=?,knownTitles=?,actionBars=?,grantableLevels=?,online=?,honor=?,honorLevel=?,prestigeLevel=?,honorRestState=?,honorRestBonus=?,world_layer=?,lastLoginBuild=? WHERE guid=?", CONNECTION_ASYNC);

PrepareStatement(CHAR_UPD_ADD_AT_LOGIN_FLAG, "UPDATE characters SET at_login = at_login | ? WHERE guid = ?", CONNECTION_ASYNC);
PrepareStatement(CHAR_UPD_REM_AT_LOGIN_FLAG, "UPDATE characters set at_login = at_login & ~ ? WHERE guid = ?", CONNECTION_ASYNC);
Expand Down
6 changes: 5 additions & 1 deletion src/server/game/Accounts/RBAC.h
Original file line number Diff line number Diff line change
Expand Up @@ -752,7 +752,11 @@ enum RBACPermissions
//
// IF YOU ADD NEW PERMISSIONS, ADD THEM IN 3.3.5 BRANCH AS WELL!
//
// custom permissions 1000+
// Phase 10 — World Layering GM commands (ArgusCore custom)
RBAC_PERM_COMMAND_LAYER_INFO = 884,
RBAC_PERM_COMMAND_LAYER_LIST = 885,
RBAC_PERM_COMMAND_LAYER_MIGRATE = 886,
//
RBAC_PERM_MAX
};

Expand Down
29 changes: 23 additions & 6 deletions src/server/game/Entities/Player/Player.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
#include "LootItemStorage.h"
#include "LootMgr.h"
#include "LootPackets.h"
#include "LayerManager.h"
#include "Mail.h"
#include "MailPackets.h"
#include "MapManager.h"
Expand Down Expand Up @@ -1338,7 +1339,9 @@ bool Player::TeleportTo(TeleportLocation const& teleportLocation, TeleportToOpti
if (duel && GetMapId() != teleportLocation.Location.GetMapId() && GetMap()->GetGameObject(GetGuidValue(PLAYER_DUEL_ARBITER)))
DuelComplete(DUEL_FLED);

if (GetMapId() == teleportLocation.Location.GetMapId() && (!teleportLocation.InstanceId || GetInstanceId() == teleportLocation.InstanceId))
// Layer migration must take the far-teleport path even though mapId is identical,
// so that HandleMoveWorldportAck calls CreateMap and picks up the pending layer.
if (GetMapId() == teleportLocation.Location.GetMapId() && (!teleportLocation.InstanceId || GetInstanceId() == teleportLocation.InstanceId) && !(options & TELE_TO_LAYER_MIGRATION))
{
//lets reset far teleport flag if it wasn't reset during chained teleport
SetSemaphoreTeleportFar(false);
Expand Down Expand Up @@ -1402,11 +1405,15 @@ bool Player::TeleportTo(TeleportLocation const& teleportLocation, TeleportToOpti
return false;
}

// Seamless teleport can happen only if cosmetic maps match
if (!oldmap ||
(oldmap->GetEntry()->CosmeticParentMapID != int32(teleportLocation.Location.GetMapId()) && int32(GetMapId()) != mEntry->CosmeticParentMapID &&
!((oldmap->GetEntry()->CosmeticParentMapID != -1) ^ (oldmap->GetEntry()->CosmeticParentMapID != mEntry->CosmeticParentMapID))))
options &= ~TELE_TO_SEAMLESS;
// Seamless teleport can happen only if cosmetic maps match.
// Layer migration never sets TELE_TO_SEAMLESS so this check is a no-op for it.
if (!(options & TELE_TO_LAYER_MIGRATION))
{
if (!oldmap ||
(oldmap->GetEntry()->CosmeticParentMapID != int32(teleportLocation.Location.GetMapId()) && int32(GetMapId()) != mEntry->CosmeticParentMapID &&
!((oldmap->GetEntry()->CosmeticParentMapID != -1) ^ (oldmap->GetEntry()->CosmeticParentMapID != mEntry->CosmeticParentMapID))))
options &= ~TELE_TO_SEAMLESS;
}

//lets reset near teleport flag if it wasn't reset during chained teleports
SetSemaphoreTeleportNear(false);
Expand Down Expand Up @@ -17208,6 +17215,7 @@ bool Player::LoadFromDB(ObjectGuid guid, CharacterDatabaseQueryHolder const& hol
uint32 prestigeLevel;
PlayerRestState honorRestState;
float honorRestBonus;
uint32 world_layer;

explicit PlayerLoadData(Field const* fields)
{
Expand Down Expand Up @@ -17293,6 +17301,7 @@ bool Player::LoadFromDB(ObjectGuid guid, CharacterDatabaseQueryHolder const& hol
prestigeLevel = fields[i++].GetUInt32();
honorRestState = PlayerRestState(fields[i++].GetUInt8());
honorRestBonus = fields[i++].GetFloat();
world_layer = fields[i++].GetUInt32();
}

} fields(result->Fetch());
Expand Down Expand Up @@ -18015,6 +18024,12 @@ bool Player::LoadFromDB(ObjectGuid guid, CharacterDatabaseQueryHolder const& hol
_InitHonorLevelOnLoadFromDB(fields.honor, fields.honorLevel, fields.prestigeLevel);

_restMgr->LoadRestBonus(REST_TYPE_HONOR, fields.honorRestState, fields.honorRestBonus);

// Seed the layer cooldown state so the 30-min anti-farm timer survives restarts.
// AssignLayer will still run full population-balancing rules on zone entry;
// this only pre-populates _playerStates so Rule 2 (cooldown) has data to check.
if (fields.world_layer != 0)
sLayerMgr->RecordPlayerLayer(GetGUID(), fields.map, fields.world_layer);
if (time_diff > 0)
{
//speed collect rest bonus in offline, in logout, far from tavern, city (section/in hour)
Expand Down Expand Up @@ -19868,6 +19883,8 @@ void Player::SaveToDB(LoginDatabaseTransaction loginTransaction, CharacterDataba
stmt->setUInt32(index++, GetPrestigeLevel());
stmt->setUInt8(index++, uint8(GetUInt32Value(PLAYER_FIELD_REST_INFO + AsUnderlyingType(REST_STATE_HONOR))));
stmt->setFloat(index++, finiteAlways(_restMgr->GetRestBonus(REST_TYPE_HONOR)));
// Save the open-world layer; store 0 for instanced maps (irrelevant there).
stmt->setUInt32(index++, GetMap() && !GetMap()->Instanceable() ? GetMap()->GetWorldLayer() : 0);
if (std::shared_ptr<Realm const> currentRealm = sRealmList->GetCurrentRealm())
stmt->setUInt32(index++, ClientBuild::GetMinorMajorBugfixVersionForBuild(currentRealm->Build));
else
Expand Down
3 changes: 2 additions & 1 deletion src/server/game/Entities/Player/Player.h
Original file line number Diff line number Diff line change
Expand Up @@ -853,7 +853,8 @@ enum TeleportToOptions
TELE_TO_SPELL = 0x10,
TELE_TO_TRANSPORT_TELEPORT = 0x20, // 3.3.5 only
TELE_REVIVE_AT_TELEPORT = 0x40,
TELE_TO_SEAMLESS = 0x80
TELE_TO_SEAMLESS = 0x80,
TELE_TO_LAYER_MIGRATION = 0x100 // world-layer hop: forces far-teleport path, keeps seamless
};

DEFINE_ENUM_FLAG(TeleportToOptions);
Expand Down
23 changes: 23 additions & 0 deletions src/server/game/Groups/Group.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

#include "Group.h"
#include "Battleground.h"
#include "LayerManager.h"
#include "Map.h"
#include "BattlegroundMgr.h"
#include "CharacterCache.h"
#include "DatabaseEnv.h"
Expand Down Expand Up @@ -499,6 +501,27 @@ bool Group::AddMember(Player* player)
player->SetLegacyRaidDifficultyID(GetLegacyRaidDifficultyID());
player->SendRaidDifficulty(true);
}

// Layer migration: if player and leader are already on the same open-world
// map but different layers, migrate the new member seamlessly now.
// If they're on different maps entirely, AssignLayer Rule 1 handles it
// automatically when the player next enters a zone (no action needed here).
{
ObjectGuid leaderGuid = GetLeaderGUID();
if (Player* leader = ObjectAccessor::FindConnectedPlayer(leaderGuid))
{
Map* leaderMap = leader->GetMap();
Map* playerMap = player->GetMap();
if (leaderMap && playerMap
&& !leaderMap->Instanceable()
&& leaderMap->GetId() == playerMap->GetId()
&& leaderMap->GetInstanceId() == playerMap->GetInstanceId()
&& leaderMap->GetWorldLayer() != playerMap->GetWorldLayer())
{
sLayerMgr->MigratePlayerToLayer(player, leaderMap->GetWorldLayer());
}
}
}
}

player->SetGroupUpdateFlag(GROUP_UPDATE_FULL);
Expand Down
Loading
Loading