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 */