// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . // ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· // ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ // ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ // ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ // Magicbane Emulator Project © 2013 - 2026 // https://www.magicbane.com package engine.parsers.cache; import engine.mbEnums; import engine.parsers.objects.CObjectTemplate; import engine.parsers.objects.DeedTemplate; import engine.parsers.objects.ItemTemplate; import engine.parsers.objects.PowerGrant; import engine.parsers.objects.RuneTemplate; import engine.parsers.objects.UserPowerAction; import engine.util.Hasher; import org.pmw.tinylog.Logger; import java.util.ArrayList; import java.util.EnumMap; import java.util.HashMap; import java.util.Map; // Lookups across the ITEM, DEED and RUNE cache templates. // The JSON references runes, skills and powers by name; the server and DB use ids and tokens. // Build once after all three parsers have loaded. Read only afterwards. public class CObjectIndex { private static final EnumMap> raceRuneIds = new EnumMap<>(mbEnums.MonsterType.class); private static final EnumMap classRuneIds = new EnumMap<>(mbEnums.ClassType.class); private static final EnumMap disciplineRuneIds = new EnumMap<>(mbEnums.DisciplineType.class); private static final HashMap> runesBySubTypeToken = new HashMap<>(); private static final HashMap skillTokens = new HashMap<>(); private static final HashMap skillNames = new HashMap<>(); private static final HashMap powerTokens = new HashMap<>(); private static final HashMap powerNames = new HashMap<>(); // Same order as the client's id space checks (JSON-Refactor-Plan D1). public static CObjectTemplate lookup(int templateId) { CObjectTemplate template; template = ItemTemplate._items.get(templateId); if (template == null) template = DeedTemplate._deeds.get(templateId); if (template == null) template = RuneTemplate._runes.get(templateId); return template; } // Male and female runes share a race, so most races return two ids. public static ArrayList getRaceRuneIds(mbEnums.MonsterType race) { return raceRuneIds.getOrDefault(race, new ArrayList<>()); } public static Integer getClassRuneId(mbEnums.ClassType classType) { return classRuneIds.get(classType); } public static Integer getDisciplineRuneId(mbEnums.DisciplineType discipline) { return disciplineRuneIds.get(discipline); } // DB race and class tokens are SBStringHash(rune_sub_type). Mob and talent runes share sub types. public static ArrayList getRunesBySubTypeToken(int token) { return runesBySubTypeToken.getOrDefault(token, new ArrayList<>()); } public static Integer getSkillToken(String skillName) { return skillTokens.get(skillName); } public static String getSkillName(int token) { return skillNames.get(token); } public static Integer getPowerToken(String powerName) { return powerTokens.get(powerName); } public static String getPowerName(int token) { return powerNames.get(token); } public static void build() { int overlapCount = 0; for (Integer templateId : RuneTemplate._runes.keySet()) if (ItemTemplate._items.containsKey(templateId) || DeedTemplate._deeds.containsKey(templateId)) overlapCount++; for (Integer templateId : DeedTemplate._deeds.keySet()) if (ItemTemplate._items.containsKey(templateId)) overlapCount++; if (overlapCount > 0) Logger.warn(overlapCount + " template ids exist in more than one of ITEM/DEED/RUNE"); for (RuneTemplate rune : RuneTemplate._runes.values()) { indexRuneName(rune); indexSubTypeToken(rune); indexRuneNames(rune); } for (ItemTemplate item : ItemTemplate._items.values()) { addSkill(item.item_skill_used); addSkill(item.item_skill_mastery_used); indexItemFields(item.item_skill_req, item.item_user_power_action, item.item_power_grant, item.item_power_action); } for (DeedTemplate deed : DeedTemplate._deeds.values()) { addSkill(deed.item_skill_used); addSkill(deed.item_skill_mastery_used); indexItemFields(deed.item_skill_req, deed.item_user_power_action, deed.item_power_grant, deed.item_power_action); } Logger.info("Indexed " + raceRuneIds.size() + " races, " + classRuneIds.size() + " classes, " + disciplineRuneIds.size() + " disciplines, " + runesBySubTypeToken.size() + " rune sub type tokens, " + skillTokens.size() + " skills, " + powerTokens.size() + " powers"); } private static void indexRuneName(RuneTemplate rune) { Integer previousId; if (rune.rune_type == null || rune.rune_sub_type.isEmpty()) return; try { switch (rune.rune_type) { case RACE: // Mob runes reuse race sub types; only creation races map to player race ids if (rune.rune_is_standard_character_creation) raceRuneIds.computeIfAbsent(CacheParserUtil.readEnum(mbEnums.MonsterType.class, rune.rune_sub_type, rune.template_id, "rune_sub_type"), k -> new ArrayList<>()).add(rune.template_id); break; case CLASS: previousId = classRuneIds.put(CacheParserUtil.readEnum(mbEnums.ClassType.class, rune.rune_sub_type, rune.template_id, "rune_sub_type"), rune.template_id); if (previousId != null) Logger.warn(rune.template_id + ": class " + rune.rune_sub_type + " already indexed by " + previousId); break; case DISCIPLINE: previousId = disciplineRuneIds.put(CacheParserUtil.readEnum(mbEnums.DisciplineType.class, rune.rune_sub_type, rune.template_id, "rune_sub_type"), rune.template_id); if (previousId != null) Logger.warn(rune.template_id + ": discipline " + rune.rune_sub_type + " already indexed by " + previousId); break; default: break; } } catch (IllegalArgumentException e) { // The Pet class rune has no ClassType Logger.warn(e.getMessage()); } } private static void indexSubTypeToken(RuneTemplate rune) { int token; if (!rune.rune_sub_type.isEmpty()) token = Hasher.SBStringHash(rune.rune_sub_type); else if (rune.rune_sub_type_hash != 0) token = rune.rune_sub_type_hash; else return; runesBySubTypeToken.computeIfAbsent(token, k -> new ArrayList<>()).add(rune); } private static void indexRuneNames(RuneTemplate rune) { for (RuneTemplate.SkillGrant grant : rune.rune_skill_grant) { addSkill(grant.skill_type); grant.skill_granted_skills.keySet().forEach(CObjectIndex::addSkill); grant.skill_granted_powers.keySet().forEach(CObjectIndex::addPower); } rune.rune_skill_adj.keySet().forEach(CObjectIndex::addSkill); addSkill(rune.item_skill_used); addSkill(rune.item_skill_mastery_used); indexItemFields(rune.item_skill_req, rune.item_user_power_action, rune.item_power_grant, rune.item_power_action); } private static void indexItemFields(Map skillReq, ArrayList userPowerActions, ArrayList powerGrants, Map> powerActions) { skillReq.keySet().forEach(CObjectIndex::addSkill); for (UserPowerAction action : userPowerActions) addPower(action.power); for (PowerGrant grant : powerGrants) { addPower(grant.power_type); grant.power_granted_skills.keySet().forEach(CObjectIndex::addSkill); grant.power_granted_powers.keySet().forEach(CObjectIndex::addPower); } powerActions.keySet().forEach(CObjectIndex::addPower); } private static void addSkill(String skillName) { addName(skillName, skillTokens, skillNames, "skill"); } private static void addPower(String powerName) { addName(powerName, powerTokens, powerNames, "power"); } private static void addName(String name, HashMap tokens, HashMap names, String kind) { int token; String previousName; // readHashLabel keeps unresolved hashes as decimal strings; those are not names if (name == null || name.isEmpty() || name.matches("-?\\d+") || tokens.containsKey(name)) return; token = Hasher.SBStringHash(name); tokens.put(name, token); previousName = names.putIfAbsent(token, name); if (previousName != null) Logger.warn(kind + " token " + token + " shared by '" + previousName + "' and '" + name + "'"); } }