Skip to content

refactor(Core/Player): compute playable class count and mask dynamically via ClassMgr - #27104

Open
JSlomian wants to merge 2 commits into
azerothcore:masterfrom
JSlomian:refactor/classmgr
Open

refactor(Core/Player): compute playable class count and mask dynamically via ClassMgr#27104
JSlomian wants to merge 2 commits into
azerothcore:masterfrom
JSlomian:refactor/classmgr

Conversation

@JSlomian

@JSlomian JSlomian commented Aug 11, 2026

Copy link
Copy Markdown

Changes Proposed:

This PR proposes changes to:

  • Core (units, players, creatures, game systems).
  • Scripts (bosses, spell scripts, creature scripts).
  • Database (SAI, creatures, etc).

ClassMgr is added, mirroring the existing RaceMgr: it scans sChrClassesStore at boot to compute the max class id and the playable-class bitmask, instead of relying on the hardcoded MAX_CLASSES/CLASSMASK_ALL_PLAYABLE literals. Every downstream consumer of those two literals for validity/mask checks (ObjectMgr player-create/level-info loading, item AllowableClass validation, quest RequiredClasses validation, trainer class requirements, guild member data integrity checks, achievement criteria validation, class conditions, mail template conditions) now reads from ClassMgr instead.

MAX_CLASSES/CLASSMASK_ALL_PLAYABLE remain as compile-time sizing constants for the handful of fixed per-class data tables (stat caps, triggered spell tables) that still require literal per-class values — those are unrelated to class validity and out of scope here.

This is a pure refactor: behavior for all 9 existing classes is unchanged (verified — see Tests Performed).

AI-assisted Pull Requests

Important

Using AI tools to prepare pull requests is allowed, but it must be disclosed and it must follow our AC guidelines for AI Agentic Engineering (link below).

You are expected to fully understand the changes you submit and to be able to explain and justify them when maintainers ask.

  • AI tools (e.g. Claude, ChatGPT, or similar) were used entirely or partially to prepare this pull request. If checked, specify which tools and models below.
    • Tools/models used: Claude Code (Claude Sonnet 5)
  • I have read and understood the AC guidelines for AI Agentic Engineering

Issues Addressed:

  • Closes N/A

SOURCE:

The changes have been validated through:

  • Live research (checked on live servers, e.g Classic WotLK, Retail, etc.)
  • Sniffs (remember to share them with the open source community!)
  • Video evidence, knowledge databases or other public sources (e.g forums, Wowhead, etc.)
  • The changes promoted by this pull request come partially or entirely from another project (cherry-pick).

N/A — this is a structural engine refactor (generalizing a hardcoded literal into a data-driven manager), not a gameplay/behavior fix, so it isn't backed by an external behavioral source. It's structurally cloned from this codebase's own existing RaceMgr (src/server/game/Entities/Player/RaceMgr.h/.cpp), which already solves the identical problem for races.

Tests Performed:

This PR has been:

  • Tested in-game by the author.
  • Tested in-game by other community members/someone else other than the author/has been live on production servers.
  • This pull request requires further testing and may have edge cases to be tested.

Verified so far:

  • codestyle-cpp.py passes.
  • Full Docker build (docker compose up -d --build) compiles clean with zero errors/warnings.
  • Worldserver boots to ready with the new ClassMgr::LoadClasses() call in the boot sequence; LoadPlayerInfo reports the same counts as before the change (63 player create definitions, 1115 skills, 190 level stats definitions); zero occurrences of any of the migrated validation warnings (does not have any playable classes, invalid class requirement, non existing classmask, Wrong class ... ignoring) — confirming all 9 existing classes still validate exactly as before.

Not yet done: an in-game click-through by a human (see reproduction steps below).

How to Test the Changes:

  • This pull request can be tested by following the reproduction steps provided in the linked issue
  • This pull request requires further testing. Provide steps to test your changes. If it requires any specific setup e.g multiple players please specify it as well.
  1. Log in with an existing character of a couple of different classes — ideally one at each end of the class-ID range touched by the bounds-checks in this PR: a Warrior (class 1) and a Druid (class 11, the highest existing class ID).
  2. Create a new character for 2-3 different classes — exercises the full LoadPlayerInfo path this PR touches (race/class create-info, starting items, skills, spells, action bars, level stats).
  3. Open the talent pane for a couple of classes.
  4. Visit a class trainer (e.g. Warrior trainer).
  5. Pick up/equip a class-restricted item (e.g. plate on a Warrior).
  6. If convenient: a class-gated quest, and guild membership login.

Confirm all of the above behaves identically to master — this PR should be behaviorally invisible for the 9 existing classes.

Known Issues and TODO List:

  • sTalentTabPages in DBCStores.cpp still sizes/bounds off the MAX_CLASSES literal rather than ClassMgr, because LoadDBCStores() runs before ClassMgr::LoadClasses() in the boot sequence. Left as a follow-up since it requires its own fix (computing the max class id locally during LoadDBCStores()), independent of this PR's scope.

…lly via ClassMgr

Add ClassMgr, mirroring the existing RaceMgr, which scans sChrClassesStore
at boot to compute the max class id and playable class bitmask instead of
relying on the hardcoded MAX_CLASSES/CLASSMASK_ALL_PLAYABLE literals.

Migrate downstream consumers (ObjectMgr player-create/level-info loading,
item AllowableClass validation, quest RequiredClasses validation, trainer
class requirements, guild member data integrity checks, achievement
criteria validation, class conditions, and mail template conditions) to
read from ClassMgr instead of the static macros, so class validity now
tracks what is actually loaded from the DBC rather than a value a human
has to keep in sync by hand.

MAX_CLASSES/CLASSMASK_ALL_PLAYABLE remain as compile-time sizing
constants for fixed per-class data tables (stat caps, triggered spell
tables) that still require literal per-class values.
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Added ClassMgr to load the maximum class ID and playable-class mask from DBC data during world startup. Updated ObjectMgr to use runtime-sized class storage and runtime class bounds. Replaced fixed playable-class masks and class limits in achievement, condition, guild, mail, item, quest, trainer, and player-data validation.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: 82dacecc-b631-4953-948e-3a81dde4f181

📥 Commits

Reviewing files that changed from the base of the PR and between 06f1a97 and 85a32f1.

📒 Files selected for processing (9)
  • src/server/game/Achievements/AchievementMgr.cpp
  • src/server/game/Conditions/ConditionMgr.cpp
  • src/server/game/Entities/Player/ClassMgr.cpp
  • src/server/game/Entities/Player/ClassMgr.h
  • src/server/game/Globals/ObjectMgr.cpp
  • src/server/game/Globals/ObjectMgr.h
  • src/server/game/Guilds/Guild.cpp
  • src/server/game/Mails/ServerMailMgr.cpp
  • src/server/game/World/World.cpp

Comment on lines +56 to +63
uint8 classId = classEntry->ClassID;

if (GetMaxClasses() <= classId)
SetMaxClasses(classId + 1);

uint32 classBit = (1 << (classId - 1));

_playableClassMask |= classBit;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Validate ClassID before deriving its mask.

ClassID == 0 underflows the shift count, ClassID >= 33 has an invalid shift count, and ClassID == 32 shifts a signed 1 into its sign bit. Reject unsupported IDs and use uint32(1) for the shift. The runtime registry can otherwise publish an invalid mask and class limit.

Proposed fix
-        uint8 classId = classEntry->ClassID;
+        uint32 classId = classEntry->ClassID;
+
+        if (classId == 0 || classId > std::numeric_limits<uint32>::digits)
+        {
+            LOG_ERROR("server.loading", "Invalid class ID {} in ChrClasses.dbc.", classId);
+            continue;
+        }

         if (GetMaxClasses() <= classId)
-            SetMaxClasses(classId + 1);
+            SetMaxClasses(static_cast<uint8>(classId + 1));

-        uint32 classBit = (1 << (classId - 1));
+        uint32 classBit = uint32(1) << (classId - 1);
#!/bin/bash
set -euo pipefail

dbc_file="$(fd -a -i '^ChrClasses\.dbc$' . | head -n 1)"
test -n "${dbc_file}" || { echo "ChrClasses.dbc not found"; exit 1; }

python - "${dbc_file}" <<'PY'
import struct
import sys

with open(sys.argv[1], "rb") as dbc:
    magic, rows, fields, row_size, strings = struct.unpack("<4s4I", dbc.read(20))
    assert magic == b"WDBC", f"Unexpected DBC header: {magic!r}"
    class_ids = [struct.unpack("<I", dbc.read(row_size)[:4])[0] for _ in range(rows)]

print(f"class IDs: {class_ids}")
assert all(1 <= class_id <= 32 for class_id in class_ids), "Class ID is outside the uint32 mask range"
PY

…le mask

ChrClasses.dbc rows with a class ID of 0 or greater than 32 would either
underflow the shift count or shift past the width of the bitmask, both
undefined behavior. Reject out-of-range IDs and use an unsigned shift.
Also widen the local class ID variable to uint32 to match the DBC field's
type, avoiding a silent truncation for out-of-range values.
@github-actions github-actions Bot added CORE Related to the core file-cpp Used to trigger the matrix build labels Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CORE Related to the core file-cpp Used to trigger the matrix build

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant