From 0536aea27a7792aecef78d2bf4f012cd0d64ef37 Mon Sep 17 00:00:00 2001 From: MagicBot Date: Wed, 21 Oct 2026 10:01:52 -0400 Subject: [PATCH] Build rune requirements from the runes and check them like the client RuneBase(RuneTemplate) fills the race, class and discipline maps from item_race_req / item_class_req / item_disc_req (names to rune ids through CObjectIndex; class runes with rune_is_standard_character_creation go to baseClass, the rest to promotionClass) with a restrict flag for each, and the level from item_level_req. static_rune_runereq is no longer used for cached rune bases; GET_RUNE_REQS stays until 2.11. ApplyRuneMsg follows ArcCharacter_MeetsItemReqs (0x475F40) and ArcItemRestrict_Check (0x4BAC60): restrict true = blacklist, false = whitelist. The class check passes when the base class or the promotion is listed. The discipline check is against the discs already held, using the new rune's list only. Parity (offline, scripts/p25a.py with feasible race/promotion pairs under the client rule; scripts/p25b.py over all 315 rune bases). Value changes, all decided JSON (D6, 2026-10-21): - 3001 Animator: + Irekei (4 pairs); Summoner blocks it - 3008 Commander: Saboteur blocks it - 3010 Duelist: level 20, 5 races, 7 promotions (had no DB rows; -111 pairs) - 3018 Enchanter, 3030 Conjurer: + Vampire (4 pairs each) - 3020 Summoner: - Thief/Warrior/Barbarian (28 pairs), + Centaur Druid/Priest (4); Animator blocks it - 3031 Darksworn: + Irekei Priest (2); 3032 Valkyr: + Human/Aelfborn Warrior (2) - 3037 Artillerist: + Sentinel (11); 3044 Strigoi: Belgosch blocks it - 252127 Master of Throwing: - Scout (16 pairs) - 70 more runes with no DB rows gain their JSON lists (class runes 2500-2526 except the Pet rune, creation talents and Bloodline talents); only runestones are checked - Stat chains: the DB "higher rank held" blocks leave the discipline map. Ranks 1-8 are still refused by the sub type rule; a rank-0 rune now replaces a higher rank as the client does (decided: follow the client) Co-Authored-By: Claude Opus 5.5 --- src/engine/net/client/msg/ApplyRuneMsg.java | 102 ++++++++------------ src/engine/objects/RuneBase.java | 74 ++++++++++++-- 2 files changed, 108 insertions(+), 68 deletions(-) diff --git a/src/engine/net/client/msg/ApplyRuneMsg.java b/src/engine/net/client/msg/ApplyRuneMsg.java index 526ff445..a78ca1a5 100644 --- a/src/engine/net/client/msg/ApplyRuneMsg.java +++ b/src/engine/net/client/msg/ApplyRuneMsg.java @@ -22,7 +22,11 @@ import engine.server.MBServerStatics; import org.pmw.tinylog.Logger; import java.util.ArrayList; -import java.util.concurrent.ConcurrentHashMap; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; public class ApplyRuneMsg extends ClientNetMsg { @@ -81,67 +85,28 @@ public class ApplyRuneMsg extends ClientNetMsg { return false; } - //Check race is met - ConcurrentHashMap races = rb.getRace(); - if (races.size() > 0) { - int raceID = playerCharacter.getRaceID(); - boolean valid = false; - for (int validID : races.keySet()) { - if (validID == raceID) { - valid = true; - break; - } - } - if (!valid) { - return false; - } - } - - //Check base class is met - ConcurrentHashMap baseClasses = rb.getBaseClass(); - if (baseClasses.size() > 0) { - int baseClassID = playerCharacter.getBaseClassID(); - boolean valid = false; - for (int validID : baseClasses.keySet()) { - if (validID == baseClassID) { - valid = true; - break; - } - } - if (!valid) { - return false; - } - } - - //Check promotion class is met - ConcurrentHashMap promotionClasses = rb.getPromotionClass(); - if (promotionClasses.size() > 0) { - int promotionClassID = playerCharacter.getPromotionClassID(); - boolean valid = false; - for (int validID : promotionClasses.keySet()) { - if (validID == promotionClassID) { - valid = true; - break; - } - } - if (!valid) { - return false; - } - } - - //Check disciplines are met ArrayList runes = playerCharacter.getRunes(); - ConcurrentHashMap disciplines = rb.getDiscipline(); - if (disciplines.size() > 0) { - for (CharacterRune cr : runes) { - int runeBaseID = cr.getRuneBaseID(); - for (Integer prohID : disciplines.keySet()) { - if (runeBaseID == prohID) { - return false; //Prohibited rune - } - } - } - } + HashSet classIDs = new HashSet<>(); + HashSet heldDiscIDs = new HashSet<>(); + + // Client ArcCharacter_MeetsItemReqs (0x475F40) + if (!meetsListReq(rb.getRace().keySet(), rb.isRaceRestrict(), Collections.singleton(playerCharacter.getRaceID()))) + return false; + + // A listed base class covers its promotions: base or promotion listed is enough + classIDs.addAll(rb.getBaseClass().keySet()); + classIDs.addAll(rb.getPromotionClass().keySet()); + + if (!meetsListReq(classIDs, rb.isClassRestrict(), Arrays.asList(playerCharacter.getBaseClassID(), playerCharacter.getPromotionClassID()))) + return false; + + // Only the new rune's list is checked, not the held discs' lists + for (CharacterRune cr : runes) + if (isDiscipline(cr.getRuneBaseID())) + heldDiscIDs.add(cr.getRuneBaseID()); + + if (!meetsListReq(rb.getDiscipline().keySet(), rb.isDiscRestrict(), heldDiscIDs)) + return false; int discCount = 0; for (CharacterRune cr : runes) { @@ -351,6 +316,21 @@ public class ApplyRuneMsg extends ClientNetMsg { return runeID > 3000 && runeID < 3050; } + // Client ArcItemRestrict_Check (0x4BAC60). An empty list has no requirement; no rune has an empty whitelist. + private static boolean meetsListReq(Set listedIDs, boolean restrict, Collection heldIDs) { + + boolean listed = false; + + if (listedIDs.isEmpty()) + return true; + + for (int heldID : heldIDs) + if (listedIDs.contains(heldID)) + listed = true; + + return listed != restrict; + } + /** * Serializes the subclass specific items to the supplied NetMsgWriter. */ diff --git a/src/engine/objects/RuneBase.java b/src/engine/objects/RuneBase.java index 7048f45e..fd702ea8 100644 --- a/src/engine/objects/RuneBase.java +++ b/src/engine/objects/RuneBase.java @@ -12,6 +12,7 @@ package engine.objects; import engine.gameManager.DbManager; import engine.mbEnums; import engine.net.ByteBufferWriter; +import engine.parsers.cache.CObjectIndex; import engine.parsers.objects.RuneTemplate; import engine.server.MBServerStatics; import engine.util.Hasher; @@ -42,6 +43,9 @@ public class RuneBase extends AbstractGameObject { private mbEnums.RuneType runeType; private int subTypeToken; private int rank; + private boolean raceRestrict; + private boolean classRestrict; + private boolean discRestrict; /** * No Table ID Constructor @@ -80,6 +84,8 @@ public class RuneBase extends AbstractGameObject { this.type = rs.getInt("type"); this.subtype = rs.getByte("subtype"); + // static_rune_runereq race/class rows are whitelists, its discipline rows prohibitions + this.discRestrict = true; DbManager.RuneBaseQueries.GET_RUNE_REQS(this); this.effectsList = DbManager.MobBaseQueries.GET_RUNEBASE_EFFECTS(this.getObjectUUID()); } @@ -87,24 +93,65 @@ public class RuneBase extends AbstractGameObject { /** * Rune Template Constructor * type and subtype are server enums that don't map cleanly from rune_type/rune_sub_type, so they - * come from the static_rune_runebase row, as do the requirement maps and effects until 2.5b/2.10. + * come from the static_rune_runebase row, as do the effects until 2.10. * Talents with the same sub type token replace each other by rank (client 0x4695D0), so no overwrite list. */ public RuneBase(RuneTemplate rune, RuneBase dbRuneBase) { super(rune.template_id); + Integer classRuneID; + Integer discRuneID; + ArrayList raceRuneIDs; + RuneTemplate classRune; + this.name = rune.item_base_name; this.description = rune.rune_dsc; this.type = dbRuneBase.type; this.subtype = dbRuneBase.subtype; - this.race.putAll(dbRuneBase.race); - this.baseClass.putAll(dbRuneBase.baseClass); - this.promotionClass.putAll(dbRuneBase.promotionClass); - this.discipline.putAll(dbRuneBase.discipline); - this.levelRequired = dbRuneBase.levelRequired; this.effectsList = dbRuneBase.effectsList; this.runeType = rune.rune_type; this.rank = rune.rune_rank; + this.levelRequired = rune.item_level_req; + this.raceRestrict = rune.item_race_req_restrict; + this.classRestrict = rune.item_class_req_restrict; + this.discRestrict = rune.item_disc_req_restrict; + + for (mbEnums.MonsterType raceType : rune.item_race_req) { + raceRuneIDs = CObjectIndex.getRaceRuneIds(raceType); + + if (raceRuneIDs.isEmpty()) + Logger.warn("RuneBase " + rune.template_id + ": race " + raceType + " has no race rune"); + + for (int raceRuneID : raceRuneIDs) + this.race.put(raceRuneID, true); + } + + for (mbEnums.ClassType classType : rune.item_class_req) { + classRuneID = CObjectIndex.getClassRuneId(classType); + + if (classRuneID == null) { + Logger.warn("RuneBase " + rune.template_id + ": class " + classType + " has no class rune"); + continue; + } + + classRune = RuneTemplate._runes.get(classRuneID); + + if (classRune.rune_is_standard_character_creation) + this.baseClass.put(classRuneID, true); + else + this.promotionClass.put(classRuneID, true); + } + + for (mbEnums.DisciplineType discType : rune.item_disc_req) { + discRuneID = CObjectIndex.getDisciplineRuneId(discType); + + if (discRuneID == null) { + Logger.warn("RuneBase " + rune.template_id + ": discipline " + discType + " has no discipline rune"); + continue; + } + + this.discipline.put(discRuneID, true); + } // Same token as CObjectIndex: unresolved sub types are stored as their hash if (!rune.rune_sub_type.isEmpty()) @@ -136,7 +183,7 @@ public class RuneBase extends AbstractGameObject { } // Iterates the static_rune_runebase rows: no rune field separates the player runes from the mob - // talents, and each row still holds the server type/subtype and requirement maps. + // talents, and each row still holds the server type/subtype. public static void LoadAllRuneBases() { @@ -266,6 +313,19 @@ public class RuneBase extends AbstractGameObject { return this.rank; } + // restrict true: the list is a blacklist; false: a whitelist + public boolean isRaceRestrict() { + return this.raceRestrict; + } + + public boolean isClassRestrict() { + return this.classRestrict; + } + + public boolean isDiscRestrict() { + return this.discRestrict; + } + /* * Serializing */