From cf976a9363fbb27f12c202ae77bb052293f0ee45 Mon Sep 17 00:00:00 2001 From: MagicBot Date: Sat, 24 Oct 2026 10:55:28 -0400 Subject: [PATCH] Load, save and export hireling, pet and minion mobiles ArcHirelingInfo_ctorFromStream (sb.exe 0xC65A90) reads product, stock, minion and service element lists that were created but never loaded, and Hireling/Pet/Minion had no save or JSON methods. Sub-records follow the client loaders (Product 0xC60E50, MinionElement 0xC63410, ServiceElement 0xC64350); int32 values are read signed, product prefix/suffix ids are hashed. Unnamed fields are numbered unknownN. Co-Authored-By: Claude Opus 5.5 --- .../arcane/zones/mobile/HirelingInfo.py | 424 +++++++++++++++--- .../arcane/zones/mobile/MinionInfo.py | 15 +- 2 files changed, 387 insertions(+), 52 deletions(-) diff --git a/mbEditorPro2.0/arcane/zones/mobile/HirelingInfo.py b/mbEditorPro2.0/arcane/zones/mobile/HirelingInfo.py index 31e45d1..618a4c8 100644 --- a/mbEditorPro2.0/arcane/zones/mobile/HirelingInfo.py +++ b/mbEditorPro2.0/arcane/zones/mobile/HirelingInfo.py @@ -6,28 +6,119 @@ # Magicbane Emulator Project © 2013 - 2022 # www.magicbane.com +from collections import OrderedDict + +from arcane.enums.hashes import hash_to_string, string_to_hash from arcane.util import ResStream from .MerchantInfo import MerchantInfo +def read_complete_time(stream: ResStream): + return [stream.read_long() for _ in range(6)] + + +def write_complete_time(stream: ResStream, values): + for value in values: + stream.write_long(value) + + +def load_objects(stream: ResStream, object_class): + count = stream.read_dword() + objects = [object_class() for _ in range(count)] + for obj in objects: + obj.load_binary(stream) + return objects + + +def save_objects(stream: ResStream, objects): + stream.write_dword(len(objects)) + for obj in objects: + obj.save_binary(stream) + + +def objects_from_json(data, object_class): + objects = [] + for obj_data in data: + obj = object_class() + obj.load_json(obj_data) + objects.append(obj) + return objects + + class Product: def load_binary(self, stream: ResStream): - self.product_cost = stream.read_dword() - self.product_base_time = stream.read_dword() - self.product_min_rank = stream.read_dword() + self.product_cost = stream.read_long() + self.product_base_time = stream.read_long() + self.product_min_rank = stream.read_long() self.product_is_complete = stream.read_bool() self.product_is_pending = stream.read_bool() - self.product_order_num = stream.read_dword() - self.product_time_mod = stream.read_dword() - self.product_enchantment = stream.read_dword() + self.product_order_num = stream.read_long() + self.product_time_mod = stream.read_long() + self.product_enchantment = stream.read_long() self.product_action_prefix_id = stream.read_dword() self.product_action_suffix_id = stream.read_dword() - self.product_u = stream.read_dword() - self.product_u = stream.read_bool() + self.product_unknown1 = stream.read_dword() + self.product_unknown2 = stream.read_bool() self.product_id = stream.read_qword() self.product_pending_id = stream.read_qword() self.product_item_type_id = stream.read_qword() - self.product_next_complete = [stream.read_dword() for _ in range(6)] + self.product_next_complete = read_complete_time(stream) + + def save_binary(self, stream: ResStream): + stream.write_long(self.product_cost) + stream.write_long(self.product_base_time) + stream.write_long(self.product_min_rank) + stream.write_bool(self.product_is_complete) + stream.write_bool(self.product_is_pending) + stream.write_long(self.product_order_num) + stream.write_long(self.product_time_mod) + stream.write_long(self.product_enchantment) + stream.write_dword(self.product_action_prefix_id) + stream.write_dword(self.product_action_suffix_id) + stream.write_dword(self.product_unknown1) + stream.write_bool(self.product_unknown2) + stream.write_qword(self.product_id) + stream.write_qword(self.product_pending_id) + stream.write_qword(self.product_item_type_id) + write_complete_time(stream, self.product_next_complete) + + def save_json(self): + data = OrderedDict() + data['product_cost'] = self.product_cost + data['product_base_time'] = self.product_base_time + data['product_min_rank'] = self.product_min_rank + data['product_is_complete'] = self.product_is_complete + data['product_is_pending'] = self.product_is_pending + data['product_order_num'] = self.product_order_num + data['product_time_mod'] = self.product_time_mod + data['product_enchantment'] = self.product_enchantment + data['product_action_prefix_id'] = hash_to_string(self.product_action_prefix_id) + data['product_action_suffix_id'] = hash_to_string(self.product_action_suffix_id) + data['product_unknown1'] = self.product_unknown1 + data['product_unknown2'] = self.product_unknown2 + data['product_id'] = self.product_id + data['product_pending_id'] = self.product_pending_id + data['product_item_type_id'] = self.product_item_type_id + data['product_next_complete'] = self.product_next_complete + return data + + def load_json(self, data): + self.product_cost = data['product_cost'] + self.product_base_time = data['product_base_time'] + self.product_min_rank = data['product_min_rank'] + self.product_is_complete = data['product_is_complete'] + self.product_is_pending = data['product_is_pending'] + self.product_order_num = data['product_order_num'] + self.product_time_mod = data['product_time_mod'] + self.product_enchantment = data['product_enchantment'] + self.product_action_prefix_id = string_to_hash(data['product_action_prefix_id']) + self.product_action_suffix_id = string_to_hash(data['product_action_suffix_id']) + self.product_unknown1 = data['product_unknown1'] + self.product_unknown2 = data['product_unknown2'] + self.product_id = data['product_id'] + self.product_pending_id = data['product_pending_id'] + self.product_item_type_id = data['product_item_type_id'] + self.product_next_complete = data['product_next_complete'] class MinionElement: @@ -35,81 +126,199 @@ class MinionElement: self.minion_element_id = stream.read_qword() self.minion_element_type = stream.read_qword() self.minion_element_lookup = stream.read_qword() - self.minion_element_base_time = stream.read_dword() - self.minion_element_mod_time = stream.read_dword() - self.minion_element_u = stream.read_dword() + self.minion_element_base_time = stream.read_long() + self.minion_element_mod_time = stream.read_long() + self.minion_element_unknown1 = stream.read_long() self.minion_element_is_complete = stream.read_bool() - self.minion_element_next_complete = [stream.read_dword() for _ in range(6)] + self.minion_element_next_complete = read_complete_time(stream) self.minion_element_name = stream.read_string() - self.minion_element_u = stream.read_string() - self.minion_element_rank = stream.read_dword() - self.minion_element_category = stream.read_dword() - self.minion_element_u = stream.read_string() - self.minion_element_u = stream.read_string() + self.minion_element_unknown2 = stream.read_string() + self.minion_element_rank = stream.read_long() + self.minion_element_category = stream.read_long() + self.minion_element_unknown3 = stream.read_string() + self.minion_element_unknown4 = stream.read_string() + + def save_binary(self, stream: ResStream): + stream.write_qword(self.minion_element_id) + stream.write_qword(self.minion_element_type) + stream.write_qword(self.minion_element_lookup) + stream.write_long(self.minion_element_base_time) + stream.write_long(self.minion_element_mod_time) + stream.write_long(self.minion_element_unknown1) + stream.write_bool(self.minion_element_is_complete) + write_complete_time(stream, self.minion_element_next_complete) + stream.write_string(self.minion_element_name) + stream.write_string(self.minion_element_unknown2) + stream.write_long(self.minion_element_rank) + stream.write_long(self.minion_element_category) + stream.write_string(self.minion_element_unknown3) + stream.write_string(self.minion_element_unknown4) + + def save_json(self): + data = OrderedDict() + data['minion_element_id'] = self.minion_element_id + data['minion_element_type'] = self.minion_element_type + data['minion_element_lookup'] = self.minion_element_lookup + data['minion_element_base_time'] = self.minion_element_base_time + data['minion_element_mod_time'] = self.minion_element_mod_time + data['minion_element_unknown1'] = self.minion_element_unknown1 + data['minion_element_is_complete'] = self.minion_element_is_complete + data['minion_element_next_complete'] = self.minion_element_next_complete + data['minion_element_name'] = self.minion_element_name + data['minion_element_unknown2'] = self.minion_element_unknown2 + data['minion_element_rank'] = self.minion_element_rank + data['minion_element_category'] = self.minion_element_category + data['minion_element_unknown3'] = self.minion_element_unknown3 + data['minion_element_unknown4'] = self.minion_element_unknown4 + return data + + def load_json(self, data): + self.minion_element_id = data['minion_element_id'] + self.minion_element_type = data['minion_element_type'] + self.minion_element_lookup = data['minion_element_lookup'] + self.minion_element_base_time = data['minion_element_base_time'] + self.minion_element_mod_time = data['minion_element_mod_time'] + self.minion_element_unknown1 = data['minion_element_unknown1'] + self.minion_element_is_complete = data['minion_element_is_complete'] + self.minion_element_next_complete = data['minion_element_next_complete'] + self.minion_element_name = data['minion_element_name'] + self.minion_element_unknown2 = data['minion_element_unknown2'] + self.minion_element_rank = data['minion_element_rank'] + self.minion_element_category = data['minion_element_category'] + self.minion_element_unknown3 = data['minion_element_unknown3'] + self.minion_element_unknown4 = data['minion_element_unknown4'] class ServiceElement: def load_binary(self, stream: ResStream): self.service_element_id = stream.read_qword() - self.service_element_keyword = stream.read_dword() - self.service_element_key_value = stream.read_dword() + self.service_element_keyword = stream.read_long() + self.service_element_key_value = stream.read_long() self.service_element_description = stream.read_string() self.service_element_unit_description = stream.read_string() - self.service_element_cost = stream.read_dword() - self.service_element_unit_value = stream.read_dword() - self.service_element_min_rank = stream.read_dword() + self.service_element_cost = stream.read_long() + self.service_element_unit_value = stream.read_long() + self.service_element_min_rank = stream.read_long() + + def save_binary(self, stream: ResStream): + stream.write_qword(self.service_element_id) + stream.write_long(self.service_element_keyword) + stream.write_long(self.service_element_key_value) + stream.write_string(self.service_element_description) + stream.write_string(self.service_element_unit_description) + stream.write_long(self.service_element_cost) + stream.write_long(self.service_element_unit_value) + stream.write_long(self.service_element_min_rank) + + def save_json(self): + data = OrderedDict() + data['service_element_id'] = self.service_element_id + data['service_element_keyword'] = self.service_element_keyword + data['service_element_key_value'] = self.service_element_key_value + data['service_element_description'] = self.service_element_description + data['service_element_unit_description'] = self.service_element_unit_description + data['service_element_cost'] = self.service_element_cost + data['service_element_unit_value'] = self.service_element_unit_value + data['service_element_min_rank'] = self.service_element_min_rank + return data + + def load_json(self, data): + self.service_element_id = data['service_element_id'] + self.service_element_keyword = data['service_element_keyword'] + self.service_element_key_value = data['service_element_key_value'] + self.service_element_description = data['service_element_description'] + self.service_element_unit_description = data['service_element_unit_description'] + self.service_element_cost = data['service_element_cost'] + self.service_element_unit_value = data['service_element_unit_value'] + self.service_element_min_rank = data['service_element_min_rank'] class HirelingMod: def load_binary(self, stream: ResStream): - self.hireling_mod_type = stream.read_dword() - self.hireling_mod_rank = stream.read_dword() - self.hireling_mod_quantity = stream.read_dword() + self.hireling_mod_type = stream.read_long() + self.hireling_mod_rank = stream.read_long() + self.hireling_mod_quantity = stream.read_long() self.hireling_mod_percent = stream.read_float() + def save_binary(self, stream: ResStream): + stream.write_long(self.hireling_mod_type) + stream.write_long(self.hireling_mod_rank) + stream.write_long(self.hireling_mod_quantity) + stream.write_float(self.hireling_mod_percent) + + def save_json(self): + data = OrderedDict() + data['hireling_mod_type'] = self.hireling_mod_type + data['hireling_mod_rank'] = self.hireling_mod_rank + data['hireling_mod_quantity'] = self.hireling_mod_quantity + data['hireling_mod_percent'] = self.hireling_mod_percent + return data + + def load_json(self, data): + self.hireling_mod_type = data['hireling_mod_type'] + self.hireling_mod_rank = data['hireling_mod_rank'] + self.hireling_mod_quantity = data['hireling_mod_quantity'] + self.hireling_mod_percent = data['hireling_mod_percent'] + class JunkerBasics: def load_binary(self, stream: ResStream): self.junker_auto_junk = stream.read_bool() self.junker_auto_junk_magic = stream.read_bool() - self.junker_auto_junk_int = stream.read_dword() - self.junker_auto_junk_magic_int = stream.read_dword() + self.junker_auto_junk_int = stream.read_long() + self.junker_auto_junk_magic_int = stream.read_long() + + def save_binary(self, stream: ResStream): + stream.write_bool(self.junker_auto_junk) + stream.write_bool(self.junker_auto_junk_magic) + stream.write_long(self.junker_auto_junk_int) + stream.write_long(self.junker_auto_junk_magic_int) + + def save_json(self): + data = OrderedDict() + data['junker_auto_junk'] = self.junker_auto_junk + data['junker_auto_junk_magic'] = self.junker_auto_junk_magic + data['junker_auto_junk_int'] = self.junker_auto_junk_int + data['junker_auto_junk_magic_int'] = self.junker_auto_junk_magic_int + return data + + def load_json(self, data): + self.junker_auto_junk = data['junker_auto_junk'] + self.junker_auto_junk_magic = data['junker_auto_junk_magic'] + self.junker_auto_junk_int = data['junker_auto_junk_int'] + self.junker_auto_junk_magic_int = data['junker_auto_junk_magic_int'] class HirelingInfo(MerchantInfo): def load_binary(self, stream: ResStream): super().load_binary(stream) self.hireling_all_items = stream.read_bool() - self.hireling_rank = stream.read_dword() + self.hireling_rank = stream.read_long() self.hireling_title = stream.read_string() - self.hireling_salary = stream.read_dword() - self.hireling_upgrade_cost = stream.read_dword() - self.hireling_upgrade_time = stream.read_dword() + self.hireling_salary = stream.read_long() + self.hireling_upgrade_cost = stream.read_long() + self.hireling_upgrade_time = stream.read_long() self.hireling_can_upgrade = stream.read_bool() self.hireling_use_specified_location = stream.read_bool() - self.hireling_category = stream.read_dword() + self.hireling_category = stream.read_long() num_npc_features = stream.read_dword() - self.hireling_npc_features = [stream.read_dword() for _ in range(num_npc_features)] + self.hireling_npc_features = [stream.read_long() for _ in range(num_npc_features)] num_item_types = stream.read_dword() - self.hireling_item_types = [stream.read_dword() for _ in range(num_item_types)] - num_products = stream.read_dword() - self.hireling_products = [Product() for _ in range(num_products)] - num_stocks = stream.read_dword() - self.hireling_stocks = [Product() for _ in range(num_stocks)] - num_minions = stream.read_dword() - self.hireling_minions = [MinionElement() for _ in range(num_minions)] - num_servies = stream.read_dword() - self.hireling_servies = [ServiceElement() for _ in range(num_servies)] + self.hireling_item_types = [stream.read_long() for _ in range(num_item_types)] + self.hireling_products = load_objects(stream, Product) + self.hireling_stocks = load_objects(stream, Product) + self.hireling_minions = load_objects(stream, MinionElement) + self.hireling_services = load_objects(stream, ServiceElement) num_orders = stream.read_dword() self.hireling_orders = [[ - stream.read_dword() for _ in range(4) + stream.read_long() for _ in range(4) ] for _ in range(num_orders)] num_starting_formulas = stream.read_dword() self.hireling_starting_formulas = [stream.read_qword() for _ in range(num_starting_formulas)] num_valid_formula_types = stream.read_dword() - self.hireling_valid_formula_types = [stream.read_dword() for _ in range(num_valid_formula_types)] + self.hireling_valid_formula_types = [stream.read_long() for _ in range(num_valid_formula_types)] num_valid_formula_categories = stream.read_dword() - self.hireling_valid_formula_category = [stream.read_dword() for _ in range(num_valid_formula_categories)] + self.hireling_valid_formula_categories = [stream.read_long() for _ in range(num_valid_formula_categories)] num_enchantment_types = stream.read_dword() self.hireling_enchantment_types = [stream.read_dword() for _ in range(num_enchantment_types)] num_valid_weapons = stream.read_dword() @@ -118,12 +327,125 @@ class HirelingInfo(MerchantInfo): self.hireling_valid_armors = [stream.read_dword() for _ in range(num_valid_armors)] num_valid_slaves = stream.read_dword() self.hireling_valid_slaves = [stream.read_dword() for _ in range(num_valid_slaves)] - num_mods = stream.read_dword() - self.hireling_mods = [HirelingMod() for _ in range(num_mods)] - for mod in self.hireling_mods: - mod.load_binary(stream) - self.hireling_max_items_stocked = stream.read_dword() + self.hireling_mods = load_objects(stream, HirelingMod) + self.hireling_max_items_stocked = stream.read_long() self.hireling_has_junker = stream.read_bool() if self.hireling_has_junker: self.hireling_junker = JunkerBasics() self.hireling_junker.load_binary(stream) + + def save_binary(self, stream: ResStream): + super().save_binary(stream) + stream.write_bool(self.hireling_all_items) + stream.write_long(self.hireling_rank) + stream.write_string(self.hireling_title) + stream.write_long(self.hireling_salary) + stream.write_long(self.hireling_upgrade_cost) + stream.write_long(self.hireling_upgrade_time) + stream.write_bool(self.hireling_can_upgrade) + stream.write_bool(self.hireling_use_specified_location) + stream.write_long(self.hireling_category) + stream.write_dword(len(self.hireling_npc_features)) + for feature in self.hireling_npc_features: + stream.write_long(feature) + stream.write_dword(len(self.hireling_item_types)) + for item_type in self.hireling_item_types: + stream.write_long(item_type) + save_objects(stream, self.hireling_products) + save_objects(stream, self.hireling_stocks) + save_objects(stream, self.hireling_minions) + save_objects(stream, self.hireling_services) + stream.write_dword(len(self.hireling_orders)) + for order in self.hireling_orders: + for value in order: + stream.write_long(value) + stream.write_dword(len(self.hireling_starting_formulas)) + for formula in self.hireling_starting_formulas: + stream.write_qword(formula) + stream.write_dword(len(self.hireling_valid_formula_types)) + for formula_type in self.hireling_valid_formula_types: + stream.write_long(formula_type) + stream.write_dword(len(self.hireling_valid_formula_categories)) + for category in self.hireling_valid_formula_categories: + stream.write_long(category) + stream.write_dword(len(self.hireling_enchantment_types)) + for enchantment_type in self.hireling_enchantment_types: + stream.write_dword(enchantment_type) + stream.write_dword(len(self.hireling_valid_weapons)) + for weapon in self.hireling_valid_weapons: + stream.write_dword(weapon) + stream.write_dword(len(self.hireling_valid_armors)) + for armor in self.hireling_valid_armors: + stream.write_dword(armor) + stream.write_dword(len(self.hireling_valid_slaves)) + for slave in self.hireling_valid_slaves: + stream.write_dword(slave) + save_objects(stream, self.hireling_mods) + stream.write_long(self.hireling_max_items_stocked) + stream.write_bool(self.hireling_has_junker) + if self.hireling_has_junker: + self.hireling_junker.save_binary(stream) + + def save_json(self): + data = super().save_json() + data['hireling_all_items'] = self.hireling_all_items + data['hireling_rank'] = self.hireling_rank + data['hireling_title'] = self.hireling_title + data['hireling_salary'] = self.hireling_salary + data['hireling_upgrade_cost'] = self.hireling_upgrade_cost + data['hireling_upgrade_time'] = self.hireling_upgrade_time + data['hireling_can_upgrade'] = self.hireling_can_upgrade + data['hireling_use_specified_location'] = self.hireling_use_specified_location + data['hireling_category'] = self.hireling_category + data['hireling_npc_features'] = self.hireling_npc_features + data['hireling_item_types'] = self.hireling_item_types + data['hireling_products'] = [product.save_json() for product in self.hireling_products] + data['hireling_stocks'] = [stock.save_json() for stock in self.hireling_stocks] + data['hireling_minions'] = [minion.save_json() for minion in self.hireling_minions] + data['hireling_services'] = [service.save_json() for service in self.hireling_services] + data['hireling_orders'] = self.hireling_orders + data['hireling_starting_formulas'] = self.hireling_starting_formulas + data['hireling_valid_formula_types'] = self.hireling_valid_formula_types + data['hireling_valid_formula_categories'] = self.hireling_valid_formula_categories + data['hireling_enchantment_types'] = self.hireling_enchantment_types + data['hireling_valid_weapons'] = self.hireling_valid_weapons + data['hireling_valid_armors'] = self.hireling_valid_armors + data['hireling_valid_slaves'] = self.hireling_valid_slaves + data['hireling_mods'] = [mod.save_json() for mod in self.hireling_mods] + data['hireling_max_items_stocked'] = self.hireling_max_items_stocked + data['hireling_has_junker'] = self.hireling_has_junker + if self.hireling_has_junker: + data['hireling_junker'] = self.hireling_junker.save_json() + return data + + def load_json(self, data): + super().load_json(data) + self.hireling_all_items = data['hireling_all_items'] + self.hireling_rank = data['hireling_rank'] + self.hireling_title = data['hireling_title'] + self.hireling_salary = data['hireling_salary'] + self.hireling_upgrade_cost = data['hireling_upgrade_cost'] + self.hireling_upgrade_time = data['hireling_upgrade_time'] + self.hireling_can_upgrade = data['hireling_can_upgrade'] + self.hireling_use_specified_location = data['hireling_use_specified_location'] + self.hireling_category = data['hireling_category'] + self.hireling_npc_features = data['hireling_npc_features'] + self.hireling_item_types = data['hireling_item_types'] + self.hireling_products = objects_from_json(data['hireling_products'], Product) + self.hireling_stocks = objects_from_json(data['hireling_stocks'], Product) + self.hireling_minions = objects_from_json(data['hireling_minions'], MinionElement) + self.hireling_services = objects_from_json(data['hireling_services'], ServiceElement) + self.hireling_orders = data['hireling_orders'] + self.hireling_starting_formulas = data['hireling_starting_formulas'] + self.hireling_valid_formula_types = data['hireling_valid_formula_types'] + self.hireling_valid_formula_categories = data['hireling_valid_formula_categories'] + self.hireling_enchantment_types = data['hireling_enchantment_types'] + self.hireling_valid_weapons = data['hireling_valid_weapons'] + self.hireling_valid_armors = data['hireling_valid_armors'] + self.hireling_valid_slaves = data['hireling_valid_slaves'] + self.hireling_mods = objects_from_json(data['hireling_mods'], HirelingMod) + self.hireling_max_items_stocked = data['hireling_max_items_stocked'] + self.hireling_has_junker = data['hireling_has_junker'] + if self.hireling_has_junker: + self.hireling_junker = JunkerBasics() + self.hireling_junker.load_json(data['hireling_junker']) diff --git a/mbEditorPro2.0/arcane/zones/mobile/MinionInfo.py b/mbEditorPro2.0/arcane/zones/mobile/MinionInfo.py index 8c39fc3..a80d569 100644 --- a/mbEditorPro2.0/arcane/zones/mobile/MinionInfo.py +++ b/mbEditorPro2.0/arcane/zones/mobile/MinionInfo.py @@ -13,4 +13,17 @@ from .HirelingInfo import HirelingInfo class MinionInfo(HirelingInfo): def load_binary(self, stream: ResStream): super().load_binary(stream) - self.minion_info_u = stream.read_string() + self.minion_info_unknown1 = stream.read_string() + + def save_binary(self, stream: ResStream): + super().save_binary(stream) + stream.write_string(self.minion_info_unknown1) + + def save_json(self): + data = super().save_json() + data['minion_info_unknown1'] = self.minion_info_unknown1 + return data + + def load_json(self, data): + super().load_json(data) + self.minion_info_unknown1 = data['minion_info_unknown1']