diff --git a/src/engine/parsers/cache/CObjectIndex.java b/src/engine/parsers/cache/CObjectIndex.java new file mode 100644 index 00000000..57745be2 --- /dev/null +++ b/src/engine/parsers/cache/CObjectIndex.java @@ -0,0 +1,235 @@ +// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . +// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· +// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ +// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ +// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ +// 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 + "'"); + } +} diff --git a/src/engine/server/login/LoginServer.java b/src/engine/server/login/LoginServer.java index 2eb173e3..9707c3d8 100644 --- a/src/engine/server/login/LoginServer.java +++ b/src/engine/server/login/LoginServer.java @@ -21,6 +21,7 @@ import engine.net.client.ClientConnection; import engine.net.client.ClientConnectionManager; import engine.net.client.msg.login.*; import engine.objects.*; +import engine.parsers.cache.CObjectIndex; import engine.parsers.cache.DeedParser; import engine.parsers.cache.ItemParser; import engine.parsers.cache.RuneParser; @@ -314,6 +315,9 @@ public class LoginServer { exit(1); } + Logger.info("Indexing Cache Templates"); + CObjectIndex.build(); + Logger.info("Initializing Network"); Network.init(); diff --git a/src/engine/server/world/WorldServer.java b/src/engine/server/world/WorldServer.java index e5982a1c..ffc21748 100644 --- a/src/engine/server/world/WorldServer.java +++ b/src/engine/server/world/WorldServer.java @@ -31,6 +31,7 @@ import engine.net.client.ClientConnectionManager; import engine.net.client.msg.UpdateStateMsg; import engine.net.client.msg.chat.ChatSystemMsg; import engine.objects.*; +import engine.parsers.cache.CObjectIndex; import engine.parsers.cache.DeedParser; import engine.parsers.cache.ItemParser; import engine.parsers.cache.RuneParser; @@ -333,6 +334,9 @@ public class WorldServer { exit(1); } + Logger.info("Indexing Cache Templates"); + CObjectIndex.build(); + Logger.info("Loading PromotionClasses"); DbManager.PromotionQueries.GET_ALL_PROMOTIONS();