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; + } +}