From fbd38732dc3eb117ccf073d8fc59e3f6a847c806 Mon Sep 17 00:00:00 2001 From: MagicBot Date: Sat, 17 Oct 2026 09:51:20 -0400 Subject: [PATCH] Add ITEM cache JSON parser ItemTemplate follows ArcItemTemplate_Load (sb.exe 0x4AABD0): weapon and armor blocks keyed by item type, sheath and stub blocks, and empty equip slot masks read as empty sets. Adds SheathSlot and readHashLabel, which keeps unresolved hashes such as item_skill_used in 195340. Co-Authored-By: Claude Opus 5.5 --- src/engine/mbEnums.java | 8 + src/engine/parsers/cache/CacheParserUtil.java | 21 ++ src/engine/parsers/cache/ItemParser.java | 248 ++++++++++++++++++ src/engine/parsers/objects/ItemTemplate.java | 165 ++++++++++++ 4 files changed, 442 insertions(+) create mode 100644 src/engine/parsers/cache/ItemParser.java create mode 100644 src/engine/parsers/objects/ItemTemplate.java diff --git a/src/engine/mbEnums.java b/src/engine/mbEnums.java index 8d085131..3466934f 100644 --- a/src/engine/mbEnums.java +++ b/src/engine/mbEnums.java @@ -2910,6 +2910,14 @@ public class mbEnums { EMPTY_NEWITEM; } + // Cache JSON names. UNKNOWN is raw 0, which the client treats as no slot (sb.exe 0x4AC810). + public enum SheathSlot { + UNKNOWN, + BACK, + WAIST, + NONE + } + public enum AttributeType { None, Strength, diff --git a/src/engine/parsers/cache/CacheParserUtil.java b/src/engine/parsers/cache/CacheParserUtil.java index a5875113..026322d6 100644 --- a/src/engine/parsers/cache/CacheParserUtil.java +++ b/src/engine/parsers/cache/CacheParserUtil.java @@ -227,6 +227,27 @@ public class CacheParserUtil { return ""; } + // Like readLabel, but a non-zero number is a hash the exporter could not + // name; it is kept as its decimal string rather than dropped. + + public static String readHashLabel(JSONObject json, String key, int templateId) { + + Object value = json.get(key); + long hash; + + if (value instanceof String) + return (String) value; + + hash = json.getLong(key); + + if (hash == 0) + return ""; + + Logger.warn(templateId + ": " + key + " has unresolved hash " + hash); + + return Long.toString(hash); + } + public static ArrayList readUserPowerActions(JSONArray actionsJson) { ArrayList userPowerActions = new ArrayList<>(); diff --git a/src/engine/parsers/cache/ItemParser.java b/src/engine/parsers/cache/ItemParser.java new file mode 100644 index 00000000..c1a44d3c --- /dev/null +++ b/src/engine/parsers/cache/ItemParser.java @@ -0,0 +1,248 @@ +// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . +// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· +// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ +// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ +// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ +// Magicbane Emulator Project © 2013 - 2026 +// https://www.magicbane.com + +package engine.parsers.cache; + +import engine.gameManager.ConfigManager; +import engine.mbEnums; +import engine.parsers.objects.ItemTemplate; +import org.json.JSONArray; +import org.json.JSONObject; +import org.pmw.tinylog.Logger; + +import java.io.File; +import java.util.HashMap; + +// Loads ITEM templates from the client cache JSON (COBJECTS/ITEM/.json). + +public class ItemParser { + + private static final String itemPath = ConfigManager.DEFAULT_DATA_DIR + "COBJECTS/ITEM/"; + + public static HashMap parseItemFolder() { + + HashMap itemTemplates = new HashMap<>(); + File[] itemFiles = CacheParserUtil.listJsonFiles(itemPath); + int templateId; + JSONObject itemJson; + int failedCount = 0; + + for (File itemFile : itemFiles) { + try { + templateId = CacheParserUtil.templateIdFromFile(itemFile); + itemJson = CacheParserUtil.readJsonFile(itemFile); + itemTemplates.put(templateId, parseItemEntry(templateId, itemJson)); + } catch (Exception e) { + Logger.error(itemFile.getName() + ": " + e); + failedCount++; + } + } + + Logger.info("Loaded " + itemTemplates.size() + " of " + itemFiles.length + " item templates"); + + // Partial item data is never usable; null tells the caller to abort + if (failedCount > 0) + return null; + + return itemTemplates; + } + + private static ItemTemplate parseItemEntry(int templateId, JSONObject itemJson) { + + ItemTemplate item = new ItemTemplate(); + JSONObject raceReq; + JSONObject classReq; + JSONObject discReq; + JSONArray itemFlags; + JSONArray useFlags; + String flag; + Object stubJson; + JSONArray stubHolder; + + item.template_id = templateId; + + // obj_ + + item.obj_name = itemJson.getString("obj_name"); + item.obj_pickable = itemJson.getBoolean("obj_pickable"); + item.obj_gravity = itemJson.getFloat("obj_gravity"); + item.obj_cull_distance = itemJson.getFloat("obj_cull_distance"); + item.obj_scale = CacheParserUtil.readVector3(itemJson.getJSONArray("obj_scale")); + item.obj_render_object = itemJson.getInt("obj_render_object"); + item.obj_double_fusion = itemJson.getBoolean("obj_double_fusion"); + item.obj_forward_vector = CacheParserUtil.readVector3(itemJson.getJSONArray("obj_forward_vector")); + item.obj_tracking_name = itemJson.getString("obj_tracking_name"); + item.obj_max_tracking_distance = itemJson.getFloat("obj_max_tracking_distance"); + item.obj_icon = itemJson.getInt("obj_icon"); + item.obj_gravity_f = itemJson.getFloat("obj_gravity_f"); + item.obj_sound_events.putAll(CacheParserUtil.readStringValueMap(itemJson.getJSONArray("obj_sound_events"), "sound_type", "sound_event")); + item.obj_arc_hardpoint_list = itemJson.getJSONArray("obj_arc_hardpoint_list"); + item.obj_sparse_data.putAll(CacheParserUtil.readSparseData(itemJson.getJSONObject("obj_sparse_data"))); + item.obj_render_object_low_detail = itemJson.getInt("obj_render_object_low_detail"); + item.obj_default_alignment = CacheParserUtil.readVector3(itemJson.getJSONArray("obj_default_alignment")); + item.obj_sound_table = CacheParserUtil.readHashLabel(itemJson, "obj_sound_table", templateId); + + // combat_ + + item.combat_health_current = itemJson.getFloat("combat_health_current"); + item.combat_health_full = itemJson.getFloat("combat_health_full"); + item.combat_attack_resist.putAll(CacheParserUtil.readEnumFloatMap(mbEnums.DamageType.class, itemJson.getJSONObject("combat_attack_resist"), + templateId, "combat_attack_resist")); + item.combat_powers = itemJson.getJSONArray("combat_powers"); + + // item_ + + item.item_type = CacheParserUtil.readEnum(mbEnums.ItemType.class, itemJson.getString("item_type"), templateId, "item_type"); + item.item_eq_slots_value = itemJson.getInt("item_eq_slots_value"); + item.item_eq_slots_type = itemJson.getBoolean("item_eq_slots_type"); + + // The exporter writes ["NONE"] for an empty slot mask + + item.item_eq_slots_or.addAll(CacheParserUtil.readEnumSet(mbEnums.EquipSlotType.class, itemJson.getJSONArray("item_eq_slots_or"), templateId, "item_eq_slots_or")); + item.item_eq_slots_or.remove(mbEnums.EquipSlotType.NONE); + item.item_eq_slots_and.addAll(CacheParserUtil.readEnumSet(mbEnums.EquipSlotType.class, itemJson.getJSONArray("item_eq_slots_and"), templateId, "item_eq_slots_and")); + item.item_eq_slots_and.remove(mbEnums.EquipSlotType.NONE); + + item.item_takeable = itemJson.getBoolean("item_takeable"); + item.item_value = itemJson.getInt("item_value"); + item.item_wt = itemJson.getInt("item_wt"); + item.item_passive_defense_mod = itemJson.getFloat("item_passive_defense_mod"); + item.item_base_name = itemJson.getString("item_base_name"); + item.item_dsc = itemJson.getString("item_dsc"); + item.item_render_object_female = itemJson.getInt("item_render_object_female"); + item.item_health_full = itemJson.getFloat("item_health_full"); + item.item_skill_used = CacheParserUtil.readHashLabel(itemJson, "item_skill_used", templateId); + item.item_skill_mastery_used = CacheParserUtil.readHashLabel(itemJson, "item_skill_mastery_used", templateId); + item.item_parry_anim_id = itemJson.getInt("item_parry_anim_id"); + item.item_offering_info.putAll(CacheParserUtil.readEnumValueMap(mbEnums.ShrineType.class, itemJson.getJSONArray("item_offering_info"), + "offering_type", "offering_value", templateId, "item_offering_info")); + + // The client reads these blocks by item type, not by key presence + + if (item.item_type == mbEnums.ItemType.WEAPON) { + item.item_weapon = parseWeapon(templateId, itemJson.getJSONObject("item_weapon")); + item.item_primary_attr = CacheParserUtil.readEnum(mbEnums.AttributeType.class, itemJson.getString("item_primary_attr"), templateId, "item_primary_attr"); + item.item_secondary_attr = CacheParserUtil.readEnum(mbEnums.AttributeType.class, itemJson.getString("item_secondary_attr"), templateId, "item_secondary_attr"); + } + + if (item.item_type == mbEnums.ItemType.ARMOR) { + item.item_bulk_factor = itemJson.getFloat("item_bulk_factor"); + item.item_defense_rating = itemJson.getInt("item_defense_rating"); + } + + // "None" is bit 0 in the client's flag tables, not a real flag + + itemFlags = itemJson.getJSONArray("item_flags"); + + for (int i = 0; i < itemFlags.length(); i++) { + flag = itemFlags.getString(i); + + if (flag.equals("None")) + continue; + + item.item_flags.add(CacheParserUtil.readEnum(mbEnums.ItemFlags.class, flag, templateId, "item_flags")); + } + + useFlags = itemJson.getJSONArray("item_use_flags"); + + for (int i = 0; i < useFlags.length(); i++) { + flag = useFlags.getString(i); + + if (flag.equals("NONE")) + continue; + + item.item_use_flags.add(CacheParserUtil.readEnum(mbEnums.ItemUseFlags.class, flag, templateId, "item_use_flags")); + } + + item.item_post_item_id = itemJson.getInt("item_post_item_id"); + item.item_initial_charges = itemJson.getInt("item_initial_charges"); + item.item_book_arcana = itemJson.getInt("item_book_arcana"); + item.item_skill_req.putAll(CacheParserUtil.readStringValueMap(itemJson.getJSONArray("item_skill_req"), "skill_type", "skill_level")); + + raceReq = itemJson.getJSONObject("item_race_req"); + item.item_race_req_restrict = raceReq.getBoolean("restrict"); + item.item_race_req.addAll(CacheParserUtil.readEnumSet(mbEnums.MonsterType.class, raceReq.getJSONArray("races"), templateId, "item_race_req")); + + classReq = itemJson.getJSONObject("item_class_req"); + item.item_class_req_restrict = classReq.getBoolean("restrict"); + item.item_class_req.addAll(CacheParserUtil.readEnumSet(mbEnums.ClassType.class, classReq.getJSONArray("classes"), templateId, "item_class_req")); + + discReq = itemJson.getJSONObject("item_disc_req"); + item.item_disc_req_restrict = discReq.getBoolean("restrict"); + item.item_disc_req.addAll(CacheParserUtil.readEnumSet(mbEnums.DisciplineType.class, discReq.getJSONArray("discs"), templateId, "item_disc_req")); + + item.item_attr_req.putAll(CacheParserUtil.readEnumValueMap(mbEnums.AttributeType.class, itemJson.getJSONArray("item_attr_req"), + "attr_type", "attr_value", templateId, "item_attr_req")); + item.item_level_req = itemJson.getInt("item_level_req"); + item.item_rank_req = itemJson.getInt("item_rank_req"); + item.item_sex_req = CacheParserUtil.readEnum(mbEnums.SexType.class, itemJson.getString("item_sex_req"), templateId, "item_sex_req"); + item.item_user_power_action.addAll(CacheParserUtil.readUserPowerActions(itemJson.getJSONArray("item_user_power_action"))); + item.item_power_grant.addAll(CacheParserUtil.readPowerGrants(itemJson.getJSONArray("item_power_grant"), templateId)); + item.item_power_action.putAll(CacheParserUtil.readPowerActions(itemJson.getJSONArray("item_power_action"))); + + item.item_sheathable = itemJson.getBoolean("item_sheathable"); + + if (item.item_sheathable) { + item.item_sheath_slot = CacheParserUtil.readEnum(mbEnums.SheathSlot.class, itemJson.getString("item_sheath_slot"), templateId, "item_sheath_slot"); + item.item_sheath_offset = CacheParserUtil.readVector3(itemJson.getJSONArray("item_sheath_offset")); + item.item_sheath_rotation = CacheParserUtil.readVector3(itemJson.getJSONArray("item_sheath_rotation")); + } + + stubJson = itemJson.get("item_has_stub"); + + if (stubJson instanceof JSONArray) { + stubHolder = (JSONArray) stubJson; + item.item_has_stub = true; + item.item_stub_flag = stubHolder.getBoolean(0); + item.item_stub_id = stubHolder.getInt(1); + } else { + item.item_has_stub = itemJson.getBoolean("item_has_stub"); + } + + item.item_offering_adjustments.putAll(CacheParserUtil.readEnumValueMap(mbEnums.ShrineType.class, itemJson.getJSONArray("item_offering_adjustments"), + "offering_type", "offering_value", templateId, "item_offering_adjustments")); + item.item_resource_costs.putAll(CacheParserUtil.readEnumValueMap(mbEnums.ResourceType.class, itemJson.getJSONArray("item_resource_costs"), + "resource_type", "resource_value", templateId, "item_resource_costs")); + item.item_bane_rank = itemJson.getInt("item_bane_rank"); + item.item_ignore_saved_actions = itemJson.getBoolean("item_ignore_saved_actions"); + + return item; + } + + private static ItemTemplate.Weapon parseWeapon(int templateId, JSONObject weaponJson) { + + ItemTemplate.Weapon weapon = new ItemTemplate.Weapon(); + JSONArray damageJson; + JSONObject damageEntry; + ItemTemplate.Damage damage; + + weapon.weapon_wepspeed = weaponJson.getFloat("weapon_wepspeed"); + weapon.weapon_max_range = weaponJson.getFloat("weapon_max_range"); + weapon.weapon_projectile_id = weaponJson.getInt("weapon_projectile_id"); + weapon.weapon_projectile_speed = weaponJson.getFloat("weapon_projectile_speed"); + weapon.weapon_combat_idle_anim = weaponJson.getInt("weapon_combat_idle_anim"); + + damageJson = weaponJson.getJSONArray("weapon_damage"); + + for (int i = 0; i < damageJson.length(); i++) { + damageEntry = damageJson.getJSONObject(i); + damage = new ItemTemplate.Damage(); + + damage.damage_type = CacheParserUtil.readEnum(mbEnums.DamageType.class, damageEntry.getString("damage_type"), templateId, "weapon_damage"); + damage.damage_min = damageEntry.getInt("damage_min"); + damage.damage_max = damageEntry.getInt("damage_max"); + + weapon.weapon_damage.add(damage); + } + + weapon.weapon_attack_anim_right.addAll(CacheParserUtil.readIntArrays(weaponJson.getJSONArray("weapon_attack_anim_right"))); + weapon.weapon_attack_anim_left.addAll(CacheParserUtil.readIntArrays(weaponJson.getJSONArray("weapon_attack_anim_left"))); + + return weapon; + } +} diff --git a/src/engine/parsers/objects/ItemTemplate.java b/src/engine/parsers/objects/ItemTemplate.java new file mode 100644 index 00000000..5207bf96 --- /dev/null +++ b/src/engine/parsers/objects/ItemTemplate.java @@ -0,0 +1,165 @@ +// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . +// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· +// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ +// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ +// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ +// Magicbane Emulator Project © 2013 - 2026 +// https://www.magicbane.com + +package engine.parsers.objects; + +import engine.math.Vector3fImmutable; +import engine.mbEnums; +import org.json.JSONArray; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.LinkedHashMap; + +// Item template as defined by the client's cache data +// (MBDATA/data/COBJECTS/ITEM). Field names match the JSON keys. +// Field layout follows ArcItemTemplate_Load (sb.exe 0x4AABD0). +// +// Values are kept as the client defines them: no req/res splitting, +// no defaults substituted. Strings with a matching enum in mbEnums +// are stored as that enum; everything else stays a String. +// +// Not to be confused with engine.objects.ItemTemplate, which is loaded +// from the database. Read only once loaded. + +public class ItemTemplate { + + public static HashMap _items = new HashMap<>(); + + public int template_id; + + // obj_ + + public String obj_name; + public boolean obj_pickable; + public float obj_gravity; + public float obj_cull_distance; + public Vector3fImmutable obj_scale; + public int obj_render_object; + public boolean obj_double_fusion; + public Vector3fImmutable obj_forward_vector; + public String obj_tracking_name; + public float obj_max_tracking_distance; + public int obj_icon; + public float obj_gravity_f; + public final LinkedHashMap obj_sound_events = new LinkedHashMap<>(); + public JSONArray obj_arc_hardpoint_list; + public final HashMap obj_sparse_data = new HashMap<>(); + public int obj_render_object_low_detail; + public Vector3fImmutable obj_default_alignment; + public String obj_sound_table; + + // combat_ + + public float combat_health_current; + public float combat_health_full; + public final EnumMap combat_attack_resist = new EnumMap<>(mbEnums.DamageType.class); + public JSONArray combat_powers; + + // item_ + + public mbEnums.ItemType item_type; + + // Index into the client's equip slot names; the leading slot of EQSLOTAND. + public int item_eq_slots_value; + + // true: any one of item_eq_slots_or; false: all of item_eq_slots_and (sb.exe 0x4AC430). + public boolean item_eq_slots_type; + public final EnumSet item_eq_slots_or = EnumSet.noneOf(mbEnums.EquipSlotType.class); + public final EnumSet item_eq_slots_and = EnumSet.noneOf(mbEnums.EquipSlotType.class); + public boolean item_takeable; + public int item_value; + public int item_wt; + public float item_passive_defense_mod; + public String item_base_name; + public String item_dsc; + public int item_render_object_female; + public float item_health_full; + public String item_skill_used; + public String item_skill_mastery_used; + public int item_parry_anim_id; + public final EnumMap item_offering_info = new EnumMap<>(mbEnums.ShrineType.class); + + // WEAPON only; null otherwise. + public Weapon item_weapon; + public mbEnums.AttributeType item_primary_attr; + public mbEnums.AttributeType item_secondary_attr; + + // ARMOR only. + public float item_bulk_factor; + public int item_defense_rating; + + public final EnumSet item_flags = EnumSet.noneOf(mbEnums.ItemFlags.class); + public final EnumSet item_use_flags = EnumSet.noneOf(mbEnums.ItemUseFlags.class); + public int item_post_item_id; + public int item_initial_charges; + + // Read as an unsigned value, possibly a hash; 0 in every ITEM file. + public int item_book_arcana; + public final LinkedHashMap item_skill_req = new LinkedHashMap<>(); + + // For race, class and disc: restrict true means the list is excluded + // (RACERESTRICT), false means it is required (RACEREQ). + public boolean item_race_req_restrict; + public final EnumSet item_race_req = EnumSet.noneOf(mbEnums.MonsterType.class); + public boolean item_class_req_restrict; + public final EnumSet item_class_req = EnumSet.noneOf(mbEnums.ClassType.class); + public boolean item_disc_req_restrict; + public final EnumSet item_disc_req = EnumSet.noneOf(mbEnums.DisciplineType.class); + public final EnumMap item_attr_req = new EnumMap<>(mbEnums.AttributeType.class); + public int item_level_req; + public int item_rank_req; + public mbEnums.SexType item_sex_req; + public final ArrayList item_user_power_action = new ArrayList<>(); + public final ArrayList item_power_grant = new ArrayList<>(); + public final LinkedHashMap> item_power_action = new LinkedHashMap<>(); + + // Sheath fields are only present when item_sheathable is true. + public boolean item_sheathable; + public mbEnums.SheathSlot item_sheath_slot; + public Vector3fImmutable item_sheath_offset; + public Vector3fImmutable item_sheath_rotation; + + // The JSON holds false, or [flag, id] when the stub is present (ArcItemStubHolder2, sb.exe 0x4A8D40). + // Meaning of the flag is not known. + public boolean item_has_stub; + public boolean item_stub_flag; + public int item_stub_id; + + public final EnumMap item_offering_adjustments = new EnumMap<>(mbEnums.ShrineType.class); + public final EnumMap item_resource_costs = new EnumMap<>(mbEnums.ResourceType.class); + public int item_bane_rank; + public boolean item_ignore_saved_actions; + + // item_weapon (ArcAttackInfo_LoadWeapon, sb.exe 0x61FA60). + + public static class Weapon { + + public float weapon_wepspeed; + public float weapon_max_range; + public int weapon_projectile_id; + public float weapon_projectile_speed; + public int weapon_combat_idle_anim; + public final ArrayList weapon_damage = new ArrayList<>(); + + // [animation, duration] pairs in file order; names from engine.objects.ItemTemplate, not the client. + public final ArrayList weapon_attack_anim_right = new ArrayList<>(); + public final ArrayList weapon_attack_anim_left = new ArrayList<>(); + } + + // One entry of weapon_damage. + + public static class Damage { + + public mbEnums.DamageType damage_type; + public int damage_min; + public int damage_max; + } +}