// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . // ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· // ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ // ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ // ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ // Magicbane Emulator Project © 2013 - 2026 // https://www.magicbane.com package engine.objects; import engine.gameManager.DbManager; import engine.mbEnums; import engine.parsers.cache.CObjectIndex; import engine.parsers.objects.RuneTemplate; import engine.server.MBServerStatics; import engine.util.Hasher; import org.pmw.tinylog.Logger; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; public class RuneBase extends AbstractGameObject { public static HashMap> AllowedBaseClassRunesMap = new HashMap<>(); public static HashMap> AllowedRaceRunesMap = new HashMap<>(); // Flagged rune_is_standard_character_creation but missing from CharCreateRuneList.cfg ATTRRUNES: // admin runes 2910/2911, unused duplicates, and Proficient with Bows (commented out in the cfg). private static final Set NON_CREATION_TALENTS = new HashSet<>(Arrays.asList(2910, 2911, 250081, 250116, 250117, 250123, 250124, 250125)); private final String name; private final String description; private final ConcurrentHashMap race = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_LOW); private final ConcurrentHashMap baseClass = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_LOW); private final ConcurrentHashMap promotionClass = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_LOW); private final ConcurrentHashMap discipline = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_LOW); private int levelRequired = 1; private ArrayList effectsList = new ArrayList<>(); private mbEnums.RuneType runeType; private int subTypeToken; private int rank; private boolean raceRestrict; private boolean classRestrict; private boolean discRestrict; /** * Rune Template Constructor * Talents with the same sub type token replace each other by rank (client 0x4695D0), so no overwrite list. */ public RuneBase(RuneTemplate rune) { super(rune.template_id); Integer classRuneID; Integer discRuneID; ArrayList raceRuneIDs; RuneTemplate classRune; this.name = rune.item_base_name; this.description = rune.rune_dsc; this.effectsList = MobBaseEffects.getEffectsForRune(rune.template_id); 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()) this.subTypeToken = Hasher.SBStringHash(rune.rune_sub_type); else this.subTypeToken = rune.rune_sub_type_hash; } public static RuneBase getRuneBase(int tableId) { if (tableId == 0) return null; return (RuneBase) DbManager.getFromCache(mbEnums.GameObjectType.RuneBase, tableId); } // Rune bases are every non-RACE rune: the classes, disciplines and talents (the 315 former static_rune_runebase rows). public static void LoadAllRuneBases() { ArrayList attributes; int loadedCount = 0; RuneBaseAttribute.runeBaseAttributeMap.clear(); for (RuneTemplate rune : RuneTemplate._runes.values()) { if (rune.rune_type == mbEnums.RuneType.RACE) continue; DbManager.addToCache(new RuneBase(rune)); attributes = RuneBaseAttribute.getAttributesForRune(rune); // getAttrs() returns null for a rune without attributes if (!attributes.isEmpty()) RuneBaseAttribute.runeBaseAttributeMap.put(rune.template_id, attributes); loadedCount++; } Logger.info("Loaded " + loadedCount + " rune bases from runes"); loadCreationTalents(); } // Every creation race and class gets an explicit list; there is no "any" entry. private static void loadCreationTalents() { ArrayList creationRaceIDs = new ArrayList<>(); ArrayList creationClassIDs = new ArrayList<>(); ArrayList listedRaceIDs; ArrayList listedClassIDs; Integer classRuneID; boolean listed; boolean allowed; int talentCount = 0; AllowedRaceRunesMap = new HashMap<>(); AllowedBaseClassRunesMap = new HashMap<>(); for (RuneTemplate rune : RuneTemplate._runes.values()) { if (!rune.rune_is_standard_character_creation) continue; if (rune.rune_type == mbEnums.RuneType.RACE) { creationRaceIDs.add(rune.template_id); AllowedRaceRunesMap.put(rune.template_id, new ArrayList<>()); } if (rune.rune_type == mbEnums.RuneType.CLASS) { creationClassIDs.add(rune.template_id); AllowedBaseClassRunesMap.put(rune.template_id, new ArrayList<>()); } } for (RuneTemplate rune : RuneTemplate._runes.values()) { if (!rune.rune_is_standard_character_creation || rune.rune_type != mbEnums.RuneType.TALENT) continue; if (NON_CREATION_TALENTS.contains(rune.template_id)) continue; listedRaceIDs = new ArrayList<>(); listedClassIDs = new ArrayList<>(); for (mbEnums.MonsterType raceType : rune.item_race_req) listedRaceIDs.addAll(CObjectIndex.getRaceRuneIds(raceType)); for (mbEnums.ClassType classType : rune.item_class_req) { classRuneID = CObjectIndex.getClassRuneId(classType); if (classRuneID != null) listedClassIDs.add(classRuneID); } // restrict true = blacklist, false = whitelist (ArcItemRestrict_Check 0x4BAC60) for (int raceRuneID : creationRaceIDs) { listed = listedRaceIDs.contains(raceRuneID); allowed = rune.item_race_req_restrict ? !listed : listed; if (allowed) AllowedRaceRunesMap.get(raceRuneID).add(rune.template_id); } for (int baseClassID : creationClassIDs) { listed = listedClassIDs.contains(baseClassID); allowed = rune.item_class_req_restrict ? !listed : listed; if (allowed) AllowedBaseClassRunesMap.get(baseClassID).add(rune.template_id); } talentCount++; } Logger.info("Loaded " + talentCount + " creation talents for " + creationRaceIDs.size() + " races and " + creationClassIDs.size() + " classes from runes"); } @Override public boolean equals(Object obj) { if (!super.equals(obj)) { return false; } if (obj instanceof RuneBase) { RuneBase rbObj = (RuneBase) obj; if (!this.name.equals(rbObj.name)) { return false; } if (!this.description.equals(rbObj.description)) { return false; } return true; } return false; } /* * Getters */ public String getName() { return name; } public String getDescription() { return description; } /** * @return the attrs */ public ArrayList getAttrs() { return RuneBaseAttribute.runeBaseAttributeMap.get(this.getObjectUUID()); } public ConcurrentHashMap getRace() { return this.race; } public ConcurrentHashMap getBaseClass() { return this.baseClass; } public ConcurrentHashMap getPromotionClass() { return this.promotionClass; } public ConcurrentHashMap getDiscipline() { return this.discipline; } public mbEnums.RuneType getRuneType() { return this.runeType; } // 0 when the rune has no sub type; talents sharing a non-zero token replace each other public int getSubTypeToken() { return this.subTypeToken; } public int getRank() { 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 */ public int getLevelRequired() { return this.levelRequired; } public void setLevelRequired(int levelRequired) { this.levelRequired = levelRequired; } @Override public void updateDatabase() { // TODO Auto-generated method stub } /** * @return the effectsList */ public ArrayList getEffectsList() { return effectsList; } }