From e91c803a327388f8ba50ac152d71d92c82b94323 Mon Sep 17 00:00:00 2001 From: MagicBot Date: Tue, 20 Oct 2026 10:33:08 -0400 Subject: [PATCH] Build PromotionClass from its class rune and compare every field Adds PromotionClass(RuneTemplate): health/mana/stamina from rune_health/mana/ stamina, token = SBStringHash(rune_sub_type), description from rune_dsc, and allowedRunes = the sub type tokens of the race and base class runes named in item_race_req/item_class_req (all whitelists). Skills, powers and effects still load from the DB by rune id (tasks 2.8, 2.9, 2.10). isPromotionRune selects the non-creation CLASS runes that require a base class (skips 2500-2503 and Pet). A static_rune_promotion comparator checks every field against a JSON-built PromotionClass; rows 2901+ (TALENT runes in the JSON) report as missing. Offline parity (scripts/p22.py, 33 rows + 163 promotionrunereq rows vs 22 promotion runes): 0 differences in name, token, health, stamina and allowed rune tokens. Templar 2516 manaMod is 14 in the DB and 15 in the JSON. 2901-2911 have no promotion rune. description is NULL in the DB and the rune_dsc blurb in the JSON; nothing reads PromotionClass.getDescription(). Co-Authored-By: Claude Opus 5.5 --- src/engine/objects/PromotionClass.java | 59 +++++++++++++++++++++++ src/engine/parsers/cache/ParityCheck.java | 49 +++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/src/engine/objects/PromotionClass.java b/src/engine/objects/PromotionClass.java index 3317069b..81e8b3d1 100644 --- a/src/engine/objects/PromotionClass.java +++ b/src/engine/objects/PromotionClass.java @@ -10,8 +10,12 @@ package engine.objects; import engine.gameManager.DbManager; +import engine.mbEnums; import engine.mbEnums.GameObjectType; import engine.net.ByteBufferWriter; +import engine.parsers.cache.CObjectIndex; +import engine.parsers.objects.RuneTemplate; +import engine.util.Hasher; import java.sql.ResultSet; import java.sql.SQLException; @@ -103,6 +107,61 @@ public class PromotionClass extends AbstractGameObject { } + /** + * Rune Template Constructor + */ + public PromotionClass(RuneTemplate rune) { + super(rune.template_id); + + this.name = rune.obj_name; + this.description = rune.rune_dsc; + this.token = Hasher.SBStringHash(rune.rune_sub_type); + this.healthMod = rune.rune_health; + this.manaMod = rune.rune_mana; + this.staminaMod = rune.rune_stamina; + this.allowedRunes = getAllowedRuneTokens(rune); + this.skillsGranted = DbManager.SkillReqQueries.GET_REQS_FOR_RUNE(this.getObjectUUID()); + this.powersGranted = PowerReq.getPowerReqsForRune(this.getObjectUUID()); + this.effectsGranted = DbManager.RuneBaseEffectQueries.GET_EFFECTS_FOR_RUNEBASE(this.getObjectUUID()); + this.effectsGrantedFighter = DbManager.RuneBaseEffectQueries.GET_EFFECTS_FOR_RUNEBASE((this.getObjectUUID() * 10) + 2500); + this.effectsGrantedHealer = DbManager.RuneBaseEffectQueries.GET_EFFECTS_FOR_RUNEBASE((this.getObjectUUID() * 10) + 2501); + this.effectsGrantedRogue = DbManager.RuneBaseEffectQueries.GET_EFFECTS_FOR_RUNEBASE((this.getObjectUUID() * 10) + 2502); + this.effectsGrantedMage = DbManager.RuneBaseEffectQueries.GET_EFFECTS_FOR_RUNEBASE((this.getObjectUUID() * 10) + 2503); + this.effectsList = DbManager.MobBaseQueries.GET_RUNEBASE_EFFECTS(this.getObjectUUID()); + } + + // Promotions are the CLASS runes that require a base class; this skips the creation classes and Pet 2522. + + public static boolean isPromotionRune(RuneTemplate rune) { + return rune.rune_type == mbEnums.RuneType.CLASS && !rune.rune_is_standard_character_creation + && !rune.item_class_req.isEmpty(); + } + + // Race and base class tokens are SBStringHash(rune_sub_type) of the runes named in the req lists. + // Every promotion rune's lists are whitelists (restrict false). + + private static ArrayList getAllowedRuneTokens(RuneTemplate rune) { + + ArrayList tokens = new ArrayList<>(); + Integer classRuneId; + int token; + + for (mbEnums.MonsterType race : rune.item_race_req) + for (int raceRuneId : CObjectIndex.getRaceRuneIds(race)) { + token = Hasher.SBStringHash(RuneTemplate._runes.get(raceRuneId).rune_sub_type); + if (!tokens.contains(token)) + tokens.add(token); + } + + for (mbEnums.ClassType classType : rune.item_class_req) { + classRuneId = CObjectIndex.getClassRuneId(classType); + if (classRuneId != null) + tokens.add(Hasher.SBStringHash(RuneTemplate._runes.get(classRuneId).rune_sub_type)); + } + + return tokens; + } + public static void serializeForClientMsg(PromotionClass promotionClass, ByteBufferWriter writer) { writer.putInt(3); // For BaseClass writer.putInt(0); // Pad diff --git a/src/engine/parsers/cache/ParityCheck.java b/src/engine/parsers/cache/ParityCheck.java index 4d861267..aaca0e3c 100644 --- a/src/engine/parsers/cache/ParityCheck.java +++ b/src/engine/parsers/cache/ParityCheck.java @@ -9,10 +9,17 @@ package engine.parsers.cache; import engine.gameManager.ConfigManager; +import engine.gameManager.DbManager; +import engine.mbEnums; +import engine.objects.AbstractGameObject; +import engine.objects.PromotionClass; +import engine.parsers.objects.RuneTemplate; import org.pmw.tinylog.Logger; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; // Dev-only check that compares DB-built objects with the cache JSON before a loader is switched. @@ -28,6 +35,10 @@ public class ParityCheck { private int differenceCount = 0; private int missingCount = 0; + static { + register("static_rune_promotion", ParityCheck::comparePromotionClasses); + } + private ParityCheck(String table) { this.table = table; } @@ -81,4 +92,42 @@ public class ParityCheck { differenceCount++; Logger.warn("Parity: " + table + "/" + id + "/" + field + ": db=" + dbValue + " json=" + jsonValue); } + + private static void comparePromotionClasses(ParityCheck check) { + + ConcurrentHashMap cache; + PromotionClass promotionClass; + PromotionClass jsonClass; + RuneTemplate rune; + int id; + + cache = DbManager.getMap(mbEnums.GameObjectType.PromotionClass); + + if (cache == null) { + Logger.warn("Parity: static_rune_promotion: no PromotionClass objects loaded"); + return; + } + + for (AbstractGameObject cached : cache.values()) { + promotionClass = (PromotionClass) cached; + id = promotionClass.getObjectUUID(); + rune = RuneTemplate._runes.get(id); + + // The CSR/admin rows 2901+ are TALENT runes in the JSON, not promotions + if (rune != null && !PromotionClass.isPromotionRune(rune)) + rune = null; + + if (!check.compareRow(id, rune)) + continue; + + jsonClass = new PromotionClass(rune); + + check.compareField(id, "name", promotionClass.getName(), jsonClass.getName()); + check.compareField(id, "token", promotionClass.getToken(), jsonClass.getToken()); + check.compareField(id, "healthMod", promotionClass.getHealthMod(), jsonClass.getHealthMod()); + check.compareField(id, "manaMod", promotionClass.getManaMod(), jsonClass.getManaMod()); + check.compareField(id, "staminaMod", promotionClass.getStaminaMod(), jsonClass.getStaminaMod()); + check.compareField(id, "allowedRunes", new HashSet<>(promotionClass.getRuneList()), new HashSet<>(jsonClass.getRuneList())); + } + } }